From 89abba3df07ce7ff4b01a2ab3252da37947f54d4 Mon Sep 17 00:00:00 2001 From: Johan Mabille Date: Tue, 11 Feb 2025 15:05:41 +0100 Subject: [PATCH 1/4] warning as error default to OFF and enabled in CI (#3814) --- .github/workflows/unix_impl.yml | 1 + CMakeLists.txt | 7 ++----- libmambapy/setup.py | 12 +++++++++--- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/.github/workflows/unix_impl.yml b/.github/workflows/unix_impl.yml index fff6bdc1cb..ee5b749d33 100644 --- a/.github/workflows/unix_impl.yml +++ b/.github/workflows/unix_impl.yml @@ -40,6 +40,7 @@ jobs: --preset mamba-unix-shared-${{ inputs.build_type }} \ -D CMAKE_CXX_COMPILER_LAUNCHER=sccache \ -D CMAKE_C_COMPILER_LAUNCHER=sccache \ + -D MAMBA_WARNING_AS_ERROR=ON \ -D BUILD_LIBMAMBAPY=OFF \ -D ENABLE_MAMBA_ROOT_PREFIX_FALLBACK=OFF cmake --build build/ --parallel diff --git a/CMakeLists.txt b/CMakeLists.txt index 9786aad45a..21e1b0c7b3 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,11 +19,8 @@ option(BUILD_LIBMAMBA_TESTS "Build libmamba C++ tests" OFF) option(BUILD_MAMBA "Build mamba" OFF) option(BUILD_MICROMAMBA "Build micromamba" OFF) option(BUILD_MAMBA_PACKAGE "Build mamba package utility" OFF) -if(MSVC) - option(MAMBA_WARNING_AS_ERROR "Treat compiler warnings as errors" OFF) -else() - option(MAMBA_WARNING_AS_ERROR "Treat compiler warnings as errors" ON) -endif() +option(MAMBA_WARNING_AS_ERROR "Treat compiler warnings as errors" OFF) + set( MAMBA_LTO "Default" diff --git a/libmambapy/setup.py b/libmambapy/setup.py index 0bd50878ec..b8bc28281e 100644 --- a/libmambapy/setup.py +++ b/libmambapy/setup.py @@ -1,6 +1,7 @@ import importlib.util import os import pathlib +import sys import skbuild import skbuild.constants @@ -23,6 +24,13 @@ def libmambapy_version(): return ver.__version__ +def get_cmake_args(): + cmake_args = [f"-DMAMBA_INSTALL_PYTHON_EXT_LIBDIR={CMAKE_INSTALL_DIR()}/src/libmambapy"] + if sys.platform != "win32" and sys.platform != "cygwin": + cmake_args += ["-DMAMBA_WARNING_AS_ERROR=ON"] + return cmake_args + + skbuild.setup( version=libmambapy_version(), packages=["libmambapy", "libmambapy.bindings", "libmambapy.solver"], @@ -31,7 +39,5 @@ def libmambapy_version(): cmake_languages=["CXX"], cmake_minimum_required_version="3.17", cmake_install_dir="src/libmambapy", # Must match package_dir layout - cmake_args=[ - f"-DMAMBA_INSTALL_PYTHON_EXT_LIBDIR={CMAKE_INSTALL_DIR()}/src/libmambapy", - ], + cmake_args=get_cmake_args(), ) From 402b2d474b5afe1164b6ad79f18e228487ce452b Mon Sep 17 00:00:00 2001 From: Julien Jerphanion Date: Thu, 13 Feb 2025 16:41:25 +0200 Subject: [PATCH 2/4] fix: Adaptive level for compatible Version formatting (#3818) Signed-off-by: Julien Jerphanion --- libmamba/src/specs/match_spec.cpp | 39 ---- libmamba/src/specs/version_spec.cpp | 5 +- libmamba/tests/src/specs/test_match_spec.cpp | 219 +++++++++++++++++- .../tests/src/specs/test_version_spec.cpp | 21 ++ micromamba/tests/test_create.py | 11 + 5 files changed, 248 insertions(+), 47 deletions(-) diff --git a/libmamba/src/specs/match_spec.cpp b/libmamba/src/specs/match_spec.cpp index 09d763af8c..fde3956109 100644 --- a/libmamba/src/specs/match_spec.cpp +++ b/libmamba/src/specs/match_spec.cpp @@ -545,45 +545,6 @@ namespace mamba::specs } } - // Handle PEP 440 "Compatible release" specification - // See: https://peps.python.org/pep-0440/#compatible-release - // - // Find a general replacement of the encoding of `~=` with `>=,.*` to be able to parse it - // properly. - // - // For instance: - // - // "~=x.y" must be replaced to ">=x.y,x.*" where `x` and `y` are positive integers. - // - // This solution must handle the case where the version is encoded with `~=` within the - // specification for instance: - // - // ">1.8,<2|==1.7,!=1.9,~=1.7.1 py34_0" - // - // must be replaced with: - // - // ">1.8,<2|==1.7,!=1.9,>=1.7.1,1.7.* py34_0" - // - while (raw_match_spec_str.find("~=") != std::string::npos) - { - // Extract the string before the `~=` operator (">1.8,<2|==1.7,!=1.9," for the above - // example) - const auto before = raw_match_spec_str.substr(0, str.find("~=")); - // Extract the string after the `~=` operator (include `~=` in it) and the next operator - // space or end of the string ("~=1.7.1 py34_0" for the above example) - const auto after = raw_match_spec_str.substr(str.find("~=")); - // Extract the version part after the `~=` operator ("1.7.1" for the above example) - const auto version = after.substr(2, after.find_first_of(" ,") - 2); - // Extract the version part without the last segment ("1.7" for the above example) - const auto version_without_last_segment = version.substr(0, version.find_last_of('.')); - // Extract the build part after the version part (" py34_0" for the above example) if - // present - const auto build = after.find(" ") != std::string::npos ? after.substr(after.find(" ")) - : ""; - raw_match_spec_str = before + ">=" + version + "," + version_without_last_segment + ".*" - + build; - } - auto parse_error = [&raw_match_spec_str](std::string_view err) -> tl::unexpected { return tl::make_unexpected(ParseError( diff --git a/libmamba/src/specs/version_spec.cpp b/libmamba/src/specs/version_spec.cpp index 8f784a26fb..f62d796a07 100644 --- a/libmamba/src/specs/version_spec.cpp +++ b/libmamba/src/specs/version_spec.cpp @@ -264,11 +264,14 @@ fmt::formatter::format( } if constexpr (std::is_same_v) { + // Make sure to print the version without loosing information. + auto version_level = pred.m_version.version().size(); + auto format_level = std::max(op.level, version_level); out = fmt::format_to( out, "{}{}", VersionSpec::compatible_str, - pred.m_version.str(op.level) + pred.m_version.str(format_level) ); } }, diff --git a/libmamba/tests/src/specs/test_match_spec.cpp b/libmamba/tests/src/specs/test_match_spec.cpp index a5ee3a42c2..fc903e629f 100644 --- a/libmamba/tests/src/specs/test_match_spec.cpp +++ b/libmamba/tests/src/specs/test_match_spec.cpp @@ -128,6 +128,20 @@ namespace REQUIRE(ms.str() == "abc>3"); } + SECTION("numpy~=1.26.0") + { + auto ms = MatchSpec::parse("numpy~=1.26.0").value(); + REQUIRE(ms.name().str() == "numpy"); + REQUIRE(ms.version().str() == "~=1.26.0"); + REQUIRE(ms.build_string().is_explicitly_free()); + REQUIRE(ms.build_number().is_explicitly_free()); + REQUIRE(ms.str() == "numpy~=1.26.0"); + + // TODO: test this assumption for many more cases + auto ms2 = MatchSpec::parse(ms.str()).value(); + REQUIRE(ms2 == ms); + } + // Invalid case from `inform2w64-sysroot_win-64-v12.0.0.r2.ggc561118da-h707e725_0.conda` // which is currently supported but which must not. SECTION("mingw-w64-ucrt-x86_64-crt-git v12.0.0.r2.ggc561118da h707e725_0") @@ -475,11 +489,10 @@ namespace { auto ms = MatchSpec::parse(R"(numpy >1.8,<2|==1.7,!=1.9,~=1.7.1 py34_0)").value(); REQUIRE(ms.name().str() == "numpy"); - REQUIRE(ms.version().str() == ">1.8,((<2|==1.7),(!=1.9,(>=1.7.1,=1.7)))"); + REQUIRE(ms.version().str() == ">1.8,((<2|==1.7),(!=1.9,~=1.7.1))"); REQUIRE(ms.build_string().str() == "py34_0"); REQUIRE( - ms.str() - == R"ms(numpy[version=">1.8,((<2|==1.7),(!=1.9,(>=1.7.1,=1.7)))",build="py34_0"])ms" + ms.str() == R"ms(numpy[version=">1.8,((<2|==1.7),(!=1.9,~=1.7.1))",build="py34_0"])ms" ); } @@ -487,16 +500,25 @@ namespace { auto ms = MatchSpec::parse("python-graphviz~=0.20").value(); REQUIRE(ms.name().str() == "python-graphviz"); - REQUIRE(ms.version().str() == ">=0.20,=0"); - REQUIRE(ms.str() == R"ms(python-graphviz[version=">=0.20,=0"])ms"); + REQUIRE(ms.version().str() == "~=0.20"); + REQUIRE(ms.str() == R"ms(python-graphviz~=0.20)ms"); } SECTION("python-graphviz ~= 0.20") { auto ms = MatchSpec::parse("python-graphviz ~= 0.20").value(); REQUIRE(ms.name().str() == "python-graphviz"); - REQUIRE(ms.version().str() == ">=0.20,=0"); - REQUIRE(ms.str() == R"ms(python-graphviz[version=">=0.20,=0"])ms"); + REQUIRE(ms.version().str() == "~=0.20"); + REQUIRE(ms.str() == R"ms(python-graphviz~=0.20)ms"); + } + + SECTION("python[version='~=3.11.0',build=*_cpython]") + { + auto ms = MatchSpec::parse("python[version='~=3.11.0',build=*_cpython]").value(); + REQUIRE(ms.name().str() == "python"); + REQUIRE(ms.version().str() == "~=3.11.0"); + REQUIRE(ms.build_string().str() == "*_cpython"); + REQUIRE(ms.str() == R"ms(python[version="~=3.11.0",build="*_cpython"])ms"); } SECTION("*[md5=fewjaflknd]") @@ -1000,6 +1022,189 @@ namespace /* .track_features =*/{}, })); } + + SECTION("pytorch~=2.3.1=py3.10_cuda11.8*") + { + const auto ms = "pytorch~=2.3.1=py3.10_cuda11.8*"_ms; + + REQUIRE(ms.contains_except_channel(Pkg{ + /* .name= */ "pytorch", + /* .version= */ "2.3.1"_v, + /* .build_string= */ "py3.10_cuda11.8_cudnn8.7.0_0", + /* .build_number= */ 0, + /* .md5= */ "lemd5", + /* .sha256= */ "somesha256", + /* .license= */ "GPL", + /* .platform= */ "linux-64", + /* .track_features =*/{}, + })); + + REQUIRE(ms.contains_except_channel(Pkg{ + /* .name= */ "pytorch", + /* .version= */ "2.3.2"_v, + /* .build_string= */ "py3.10_cuda11.8_cudnn8.7.0_0", + /* .build_number= */ 0, + /* .md5= */ "lemd5", + /* .sha256= */ "somesha256", + /* .license= */ "GPL", + /* .platform= */ "linux-64", + /* .track_features =*/{}, + })); + + REQUIRE_FALSE(ms.contains_except_channel(Pkg{ + /* .name= */ "pytorch", + /* .version= */ "2.4.0"_v, + /* .build_string= */ "py3.10_cuda11.8_cudnn8.7.0_0", + /* .build_number= */ 0, + /* .md5= */ "lemd5", + /* .sha256= */ "somesha256", + /* .license= */ "GPL", + /* .platform= */ "linux-64", + /* .track_features =*/{}, + })); + + REQUIRE_FALSE(ms.contains_except_channel(Pkg{ + /* .name= */ "pytorch", + /* .version= */ "3.0"_v, + /* .build_string= */ "py3.10_cuda11.8_cudnn8.7.0_0", + /* .build_number= */ 0, + /* .md5= */ "lemd5", + /* .sha256= */ "somesha256", + /* .license= */ "GPL", + /* .platform= */ "linux-64", + /* .track_features =*/{}, + })); + + REQUIRE_FALSE(ms.contains_except_channel(Pkg{ + /* .name= */ "pytorch", + /* .version= */ "2.3.0"_v, + /* .build_string= */ "py3.10_cuda11.8_cudnn8.7.0_0", + /* .build_number= */ 0, + /* .md5= */ "lemd5", + /* .sha256= */ "somesha256", + /* .license= */ "GPL", + /* .platform= */ "linux-64", + /* .track_features =*/{}, + })); + } + + SECTION("numpy~=1.26.0") + { + const auto ms = "numpy~=1.26.0"_ms; + + REQUIRE(ms.contains_except_channel(Pkg{ + /* .name= */ "numpy", + /* .version= */ "1.26.0"_v, + /* .build_string= */ "py310h1d0b8b9_0", + /* .build_number= */ 0, + /* .md5= */ "lemd5", + /* .sha256= */ "somesha256", + /* .license= */ "GPL", + /* .platform= */ "linux-64", + /* .track_features =*/{}, + })); + + REQUIRE(ms.contains_except_channel(Pkg{ + /* .name= */ "numpy", + /* .version= */ "1.26.1"_v, + /* .build_string= */ "py310h1d0b8b9_0", + /* .build_number= */ 0, + /* .md5= */ "lemd5", + /* .sha256= */ "somesha256", + /* .license= */ "GPL", + /* .platform= */ "linux-64", + /* .track_features =*/{}, + })); + + REQUIRE_FALSE(ms.contains_except_channel(Pkg{ + /* .name= */ "numpy", + /* .version= */ "1.27"_v, + /* .build_string= */ "py310h1d0b8b9_0", + /* .build_number= */ 0, + /* .md5= */ "lemd5", + /* .sha256= */ "somesha256", + /* .license= */ "GPL", + /* .platform= */ "linux-64", + /* .track_features =*/{}, + })); + + REQUIRE_FALSE(ms.contains_except_channel(Pkg{ + /* .name= */ "numpy", + /* .version= */ "2.0.0"_v, + /* .build_string= */ "py310h1d0b8b9_1", + /* .build_number= */ 1, + /* .md5= */ "lemd5", + /* .sha256= */ "somesha256", + /* .license= */ "GPL", + /* .platform= */ "linux-64", + /* .track_features =*/{}, + })); + + REQUIRE_FALSE(ms.contains_except_channel(Pkg{ + /* .name= */ "numpy", + /* .version= */ "1.25.0"_v, + /* .build_string= */ "py310h1d0b8b9_0", + /* .build_number= */ 0, + /* .md5= */ "lemd5", + /* .sha256= */ "somesha256", + /* .license= */ "GPL", + /* .platform= */ "linux-64", + /* .track_features =*/{}, + })); + } + + SECTION("numpy~=1.26") + { + const auto ms = "numpy~=1.26"_ms; + + REQUIRE(ms.contains_except_channel(Pkg{ + /* .name= */ "numpy", + /* .version= */ "1.26.0"_v, + /* .build_string= */ "py310h1d0b8b9_0", + /* .build_number= */ 0, + /* .md5= */ "lemd5", + /* .sha256= */ "somesha256", + /* .license= */ "GPL", + /* .platform= */ "linux-64", + /* .track_features =*/{}, + })); + + REQUIRE(ms.contains_except_channel(Pkg{ + /* .name= */ "numpy", + /* .version= */ "1.26.1"_v, + /* .build_string= */ "py310h1d0b8b9_0", + /* .build_number= */ 0, + /* .md5= */ "lemd5", + /* .sha256= */ "somesha256", + /* .license= */ "GPL", + /* .platform= */ "linux-64", + /* .track_features =*/{}, + })); + + REQUIRE(ms.contains_except_channel(Pkg{ + /* .name= */ "numpy", + /* .version= */ "1.27"_v, + /* .build_string= */ "py310h1d0b8b9_0", + /* .build_number= */ 0, + /* .md5= */ "lemd5", + /* .sha256= */ "somesha256", + /* .license= */ "GPL", + /* .platform= */ "linux-64", + /* .track_features =*/{}, + })); + + REQUIRE_FALSE(ms.contains_except_channel(Pkg{ + /* .name= */ "numpy", + /* .version= */ "2.0.0"_v, + /* .build_string= */ "py310h1d0b8b9_1", + /* .build_number= */ 1, + /* .md5= */ "lemd5", + /* .sha256= */ "somesha256", + /* .license= */ "GPL", + /* .platform= */ "linux-64", + /* .track_features =*/{}, + })); + } } TEST_CASE("MatchSpec comparability and hashability") diff --git a/libmamba/tests/src/specs/test_version_spec.cpp b/libmamba/tests/src/specs/test_version_spec.cpp index f71ef36c6f..b48bf52ef5 100644 --- a/libmamba/tests/src/specs/test_version_spec.cpp +++ b/libmamba/tests/src/specs/test_version_spec.cpp @@ -422,6 +422,27 @@ namespace REQUIRE(vs.str() == "=2.3,<3.0"); REQUIRE(vs.str_conda_build() == "2.3.*,<3.0"); } + + SECTION("~=1") + { + auto vs = VersionSpec::parse("~=1").value(); + REQUIRE(vs.str() == "~=1"); + REQUIRE(vs.str_conda_build() == "~=1"); + } + + SECTION("~=1.8") + { + auto vs = VersionSpec::parse("~=1.8").value(); + REQUIRE(vs.str() == "~=1.8"); + REQUIRE(vs.str_conda_build() == "~=1.8"); + } + + SECTION("~=1.8.0") + { + auto vs = VersionSpec::parse("~=1.8.0").value(); + REQUIRE(vs.str() == "~=1.8.0"); + REQUIRE(vs.str_conda_build() == "~=1.8.0"); + } } TEST_CASE("VersionSpec::is_explicitly_free") diff --git a/micromamba/tests/test_create.py b/micromamba/tests/test_create.py index 56b0089c4d..5ad63975ae 100644 --- a/micromamba/tests/test_create.py +++ b/micromamba/tests/test_create.py @@ -3,6 +3,7 @@ import shutil import subprocess from pathlib import Path +from packaging.version import Version import pytest import yaml @@ -1693,3 +1694,13 @@ def test_non_url_encoding(tmp_path): non_encoded_url_start = "https://conda.anaconda.org/conda-forge/linux-64/x264-1!" out = helpers.run_env("export", "-p", env_prefix, "--explicit") assert non_encoded_url_start in out + + +def test_compatible_release(tmp_path): + # Non-regression test for: https://github.com/mamba-org/mamba/issues/3472 + env_prefix = tmp_path / "env-compatible-release" + + out = helpers.create("--json", "jupyterlab~=4.3", "-p", env_prefix, "--dry-run") + + jupyterlab_package = next(pkg for pkg in out["actions"]["LINK"] if pkg["name"] == "jupyterlab") + assert Version(jupyterlab_package["version"]) >= Version("4.3.0") From fce50fbb6ef865f40afaa272c98d42790cfcd2e6 Mon Sep 17 00:00:00 2001 From: Kingsley Collie <101559596+k-collie@users.noreply.github.com> Date: Tue, 18 Feb 2025 09:54:30 +0000 Subject: [PATCH 3/4] Print explanation when micromamba env update can't solve --- libmamba/src/api/update.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/libmamba/src/api/update.cpp b/libmamba/src/api/update.cpp index 98b8d084c3..42e22d653c 100644 --- a/libmamba/src/api/update.cpp +++ b/libmamba/src/api/update.cpp @@ -200,6 +200,14 @@ namespace mamba auto outcome = solver::libsolv::Solver().solve(db, request).value(); if (auto* unsolvable = std::get_if(&outcome)) { + unsolvable->explain_problems_to( + db, + LOG_ERROR, + { + /* .unavailable= */ ctx.graphics_params.palette.failure, + /* .available= */ ctx.graphics_params.palette.success, + } + ); if (ctx.output_params.json) { Console::instance().json_write({ { "success", false }, From 86e377daefff090077baccac89f03d5a8a22c90b Mon Sep 17 00:00:00 2001 From: Kingsley Collie <101559596+k-collie@users.noreply.github.com> Date: Tue, 18 Feb 2025 18:18:07 +0000 Subject: [PATCH 4/4] Add regression test for micromamba update unsolvable explanation --- micromamba/tests/test_update.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/micromamba/tests/test_update.py b/micromamba/tests/test_update.py index a2ef63034e..5d9577f156 100644 --- a/micromamba/tests/test_update.py +++ b/micromamba/tests/test_update.py @@ -216,6 +216,19 @@ def test_update_check_logs(self, env_created, output_flag): else: assert "To activate this environment, use:" not in res + def test_update_explains_problems(self, env_created): + # Non-regression test for: https://github.com/mamba-org/mamba/issues/3828 + with pytest.raises(helpers.subprocess.CalledProcessError) as e: + helpers.update("-n", TestUpdate.env_name, "xtensor=0.24.5", "xtensor=0.25.0") + err_string = str(e.value.stderr.decode("utf-8")) + assert "The following packages are incompatible" in err_string + + def test_update_explains_problems_json(self, env_created): + with pytest.raises(helpers.subprocess.CalledProcessError) as e: + helpers.update("-n", TestUpdate.env_name, "xtensor=0.24.5", "xtensor=0.25.0", "--json") + out_string = str(e.value.stdout.decode("utf-8")) + assert "cannot install both" in out_string + class TestUpdateConfig: current_root_prefix = os.environ["MAMBA_ROOT_PREFIX"]