From 3ef58f79b53eb283a0ec4b3faf6e1f3071c59880 Mon Sep 17 00:00:00 2001 From: Julien Jerphanion Date: Wed, 8 Jan 2025 17:15:50 +0100 Subject: [PATCH] fix: Support globs in build strings Signed-off-by: Julien Jerphanion --- libmamba/include/mamba/specs/regex_spec.hpp | 2 +- libmamba/src/specs/regex_spec.cpp | 51 ++++++++++----- libmamba/tests/src/specs/test_match_spec.cpp | 19 ++++++ libmamba/tests/src/specs/test_regex_spec.cpp | 6 ++ micromamba/tests/test_create.py | 65 ++++++++++++++++++++ 5 files changed, 128 insertions(+), 15 deletions(-) diff --git a/libmamba/include/mamba/specs/regex_spec.hpp b/libmamba/include/mamba/specs/regex_spec.hpp index 57353e44c5..3ea6fe3d91 100644 --- a/libmamba/include/mamba/specs/regex_spec.hpp +++ b/libmamba/include/mamba/specs/regex_spec.hpp @@ -31,7 +31,7 @@ namespace mamba::specs [[nodiscard]] static auto parse(std::string pattern) -> expected_parse_t; RegexSpec(); - RegexSpec(std::regex pattern, std::string raw_pattern); + RegexSpec(std::string raw_pattern); [[nodiscard]] auto contains(std::string_view str) const -> bool; diff --git a/libmamba/src/specs/regex_spec.cpp b/libmamba/src/specs/regex_spec.cpp index 2f970aa588..17277c579a 100644 --- a/libmamba/src/specs/regex_spec.cpp +++ b/libmamba/src/specs/regex_spec.cpp @@ -6,6 +6,8 @@ #include #include +#include +#include #include @@ -25,12 +27,10 @@ namespace mamba::specs auto RegexSpec::parse(std::string pattern) -> expected_parse_t { - // No other mean of getting parse result with ``std::regex``, but parse error need - // to be handled by ``tl::expected`` to be managed down the road. + // Parse error need to be handled by ``tl::expected`` to be managed down the road. try { - auto regex = std::regex(pattern); - return { { std::move(regex), std::move(pattern) } }; + return { std::move(pattern) }; } catch (const std::regex_error& e) { @@ -39,25 +39,48 @@ namespace mamba::specs } RegexSpec::RegexSpec() - : RegexSpec(std::regex(free_pattern.data(), free_pattern.size()), std::string(free_pattern)) + : RegexSpec(std::string(free_pattern)) { } - RegexSpec::RegexSpec(std::regex pattern, std::string raw_pattern) - : m_pattern(std::move(pattern)) - , m_raw_pattern(std::move(raw_pattern)) + RegexSpec::RegexSpec(std::string raw_pattern) { + // Construct ss from raw_pattern, in particular make sure to replace all `*` by `.*` + // in the pattern if they are not preceded by a `.`. // We force regex to start with `^` and end with `$` to simplify the multiple // possible representations, and because this is the safest way we can make sure it is // not a glob when serializing it. - if (!util::starts_with(m_raw_pattern, pattern_start)) - { - m_raw_pattern.insert(m_raw_pattern.begin(), pattern_start); - } - if (!util::ends_with(m_raw_pattern, pattern_end)) + std::ostringstream ss; + ss << pattern_start; + + auto first_character_it = raw_pattern.cbegin(); + auto last_character_it = raw_pattern.cend() - 1; + + for (auto it = first_character_it; it != raw_pattern.cend(); ++it) { - m_raw_pattern.push_back(pattern_end); + if (it == first_character_it && *it == pattern_start) + { + continue; + } + if (it == last_character_it && *it == pattern_end) + { + continue; + } + if (*it == '*' && (it == first_character_it || *(it - 1) != '.')) + { + ss << ".*"; + } + else + { + ss << *it; + } } + + ss << pattern_end; + + m_raw_pattern = ss.str(); + + m_pattern = std::regex(m_raw_pattern); } auto RegexSpec::contains(std::string_view str) const -> bool diff --git a/libmamba/tests/src/specs/test_match_spec.cpp b/libmamba/tests/src/specs/test_match_spec.cpp index 0e0cd90c33..a5ee3a42c2 100644 --- a/libmamba/tests/src/specs/test_match_spec.cpp +++ b/libmamba/tests/src/specs/test_match_spec.cpp @@ -981,6 +981,25 @@ namespace /* .track_features =*/{ "openssl", "mkl" }, })); } + + SECTION("pytorch=2.3.1=py3.10_cuda11.8*") + { + // Check that it contains `pytorch=2.3.1=py3.10_cuda11.8_cudnn8.7.0_0` + + 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 =*/{}, + })); + } } TEST_CASE("MatchSpec comparability and hashability") diff --git a/libmamba/tests/src/specs/test_regex_spec.cpp b/libmamba/tests/src/specs/test_regex_spec.cpp index 2258b5f34a..9c13dca916 100644 --- a/libmamba/tests/src/specs/test_regex_spec.cpp +++ b/libmamba/tests/src/specs/test_regex_spec.cpp @@ -80,4 +80,10 @@ namespace REQUIRE(hash_fn(spec1) == hash_fn(spec2)); REQUIRE(hash_fn(spec1) != hash_fn(spec3)); } + + TEST_CASE("^py3.10_cuda11.8*$") + { + auto spec = RegexSpec::parse("^py3.10_cuda11.8*$").value(); + REQUIRE(spec.contains("py3.10_cuda11.8_cudnn8.7.0_0")); + } } diff --git a/micromamba/tests/test_create.py b/micromamba/tests/test_create.py index e560891459..a81f2b970a 100644 --- a/micromamba/tests/test_create.py +++ b/micromamba/tests/test_create.py @@ -1575,3 +1575,68 @@ def test_update_spec_list(tmp_path): out = helpers.create("-p", env_prefix, "-f", env_spec_file, "--dry-run") assert update_specs_list in out.replace("\r", "") + + +def test_glob_in_build_string(tmp_path): + # Non-regression test for https://github.com/mamba-org/mamba/issues/3699 + env_prefix = tmp_path / "test_glob_in_build_string" + + # Export CONDA_OVERRIDE_GLIBC=2.17 to force the solver to use the glibc 2.17 package + os.environ["CONDA_OVERRIDE_GLIBC"] = "2.17" + + pytorch_match_spec = "pytorch=2.3.1=py3.10_cuda11.8*" + + pytorch_build_package_info = { + "build": "py3.10_cuda11.8_cudnn8.7.0_0", + "build_number": 0, + "build_string": "py3.10_cuda11.8_cudnn8.7.0_0", + "channel": "pytorch", + "constrains": ["cpuonly <0"], + "depends": [ + "typing_extensions", + "filelock", + "jinja2", + "networkx", + "sympy", + "pyyaml", + "pytorch-mutex 1.0 cuda", + "mkl >=2018", + "blas * mkl", + "python >=3.10,<3.11.0a0", + "llvm-openmp <16", + "pytorch-cuda >=11.8,<11.9", + "torchtriton 2.3.1", + ], + "fn": "pytorch-2.3.1-py3.10_cuda11.8_cudnn8.7.0_0.tar.bz2", + "license": "BSD 3-Clause", + "md5": "eb00b4790822b082179cc5cee67d934f", + "name": "pytorch", + "sha256": "3b49061de3e481fc684d463d68aae93d87aff35e715730c72431249d877c1ec9", + "size": 1673590592, + "subdir": "linux-64", + "timestamp": 1716914789, + "track_features": "", + "url": "https://conda.anaconda.org/pytorch/linux-64/pytorch-2.3.1-py3.10_cuda11.8_cudnn8.7.0_0.tar.bz2", + "version": "2.3.1", + } + + # Should run without error + out = helpers.create( + "-p", + env_prefix, + pytorch_match_spec, + "-c", + "pytorch", + "-c", + "nvidia/label/cuda-11.8.0", + "-c", + "nvidia", + "-c", + "conda-forge", + "--platform", + "linux-64", + "--dry-run", + "--json", + ) + + assert pytorch_build_package_info in out["actions"]["FETCH"]