diff --git a/src/datalad_installer.py b/src/datalad_installer.py index 4ae6e27..02474ff 100755 --- a/src/datalad_installer.py +++ b/src/datalad_installer.py @@ -1963,17 +1963,36 @@ def install_package( i = 0 while True: try: - runcmd(*cmd) + result = runcmd(*cmd, stderr=subprocess.PIPE, universal_newlines=True) + if result.stderr: + log.error("conda install stderr: %s", result.stderr) except subprocess.CalledProcessError as e: - if i < 3: - log.error( - "Command failed with exit status %d; sleeping and retrying", - e.returncode, - ) - i += 1 - sleep(5) + log.error( + "Command failed with exit status %d and stderr:\n%s", + e.returncode, + e.stderr, + ) + # Custom workaround for https://github.com/datalad/datalad-installer/issues/206 + if solver_offered := maybe_offered_solver(e.stderr): + if "--solver" in cmd: + log.info( + "--solver was already in the command, not retrying with offered %s", + solver_offered, + ) + # on this type of error, not point of retrying + raise + else: + cmd += ["--solver", solver_offered] else: - raise + if i < 3: + log.error( + "Sleeping and retrying", + e.returncode, + ) + i += 1 + sleep(5) + else: + raise else: break binpath = conda.bindir @@ -2987,6 +3006,33 @@ def parse_links(html: str, base_url: Optional[str] = None) -> list[Link]: return links +def maybe_offered_solver(e_stderr: Any) -> Optional[str]: + # May be would not be needed in the future. + solver_offered = None + if e_stderr and ( + solvers_match := re.search( + r"CondaValueError: You have chosen a non-default solver backend \(\S*\) .*" + "Choose one of: (?P.*)", + str(e_stderr), + ) + ): + solvers_offered = solvers_match.groupdict()["solvers"] + solvers_offered = [c.strip() for c in re.split("[ \t,]", solvers_offered)] + if len(solvers_offered): + solver_offered = solvers_offered[0] + if len(solvers_offered) > 1: + # TODO: might add a logic in case of multiple to try them one by one + log.info( + "More than once choice given (%s), will use the first one.", + solvers_offered, + ) + else: + log.info("Will use solver %s", solver_offered) + else: + log.info("No solver choices offered, continuing as is") + return solver_offered + + @dataclass class Link: """A hyperlink extracted from an HTML page""" diff --git a/test/test_install.py b/test/test_install.py index 3354320..3f155e0 100644 --- a/test/test_install.py +++ b/test/test_install.py @@ -61,40 +61,30 @@ def test_install_miniconda(tmp_path: Path) -> None: @pytest.mark.miniconda -def test_install_miniconda_python_match(tmp_path: Path) -> None: - miniconda_path = tmp_path / "conda" - if sys.version_info[:2] == (3, 7): - component = "miniconda=py37_23.1.0-1" - else: - component = "miniconda" - r = main( - [ - "datalad_installer.py", - component, - "--batch", - "--path", - str(miniconda_path), - "--python-match", - "minor", - ] - ) - assert r == 0 - if ON_WINDOWS: - pypath = miniconda_path / "python.exe" - else: - pypath = miniconda_path / "bin" / "python" - assert pypath.exists() - assert subprocess.run( - [str(pypath), "-c", "import sys; print(sys.version_info[:2])"], - stdout=subprocess.PIPE, - universal_newlines=True, - check=True, - ).stdout.strip() == repr(sys.version_info[:2]) - - -@pytest.mark.skipif(sys.version_info[:2] != (3, 12), reason="Only run on Python 3.12") -@pytest.mark.miniconda -def test_install_miniconda_python_match_conda_forge(tmp_path: Path) -> None: +@pytest.mark.parametrize( + "extra_opts,extra_spec", + [ + pytest.param( + [], [], marks=pytest.mark.skipif(ON_LINUX, reason="non-Linux only") + ), + pytest.param(["-c", "conda-forge"], []), + # to save testing time we will also bundle with handling of the case when + # it might fail to find solver, + # ref: https://github.com/datalad/datalad-installer/issues/206 + pytest.param( + [], + [ + "git-annex", + "-m", + "conda", + ], + marks=pytest.mark.skipif(not ON_LINUX, reason="Linux only"), + ), + ], +) +def test_install_miniconda_python_match( + extra_opts: list[str], extra_spec: list[str], tmp_path: Path +) -> None: miniconda_path = tmp_path / "conda" r = main( [ @@ -105,9 +95,9 @@ def test_install_miniconda_python_match_conda_forge(tmp_path: Path) -> None: str(miniconda_path), "--python-match", "minor", - "-c", - "conda-forge", ] + + extra_opts + + extra_spec, ) assert r == 0 if ON_WINDOWS: @@ -122,6 +112,10 @@ def test_install_miniconda_python_match_conda_forge(tmp_path: Path) -> None: check=True, ).stdout.strip() == repr(sys.version_info[:2]) + if extra_spec and extra_spec[0] == "git-annex": + # for the git-annex component + assert (miniconda_path / "bin" / "git-annex").exists() + @pytest.mark.miniconda def test_install_miniconda_autogen_path(monkeypatch: pytest.MonkeyPatch) -> None: