Skip to content

Commit

Permalink
fix: Support globs in MatchSpec build strings (#3735)
Browse files Browse the repository at this point in the history
Signed-off-by: Julien Jerphanion <[email protected]>
Co-authored-by: Johan Mabille <[email protected]>
Co-authored-by: jaimergp <[email protected]>
  • Loading branch information
3 people authored Jan 27, 2025
1 parent c1c9f5f commit 57a2c55
Show file tree
Hide file tree
Showing 5 changed files with 131 additions and 19 deletions.
4 changes: 2 additions & 2 deletions libmamba/include/mamba/specs/regex_spec.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ namespace mamba::specs
[[nodiscard]] static auto parse(std::string pattern) -> expected_parse_t<RegexSpec>;

RegexSpec();
RegexSpec(std::regex pattern, std::string raw_pattern);
explicit RegexSpec(std::string raw_pattern);

[[nodiscard]] auto contains(std::string_view str) const -> bool;

Expand Down Expand Up @@ -61,8 +61,8 @@ namespace mamba::specs

private:

std::regex m_pattern;
std::string m_raw_pattern;
std::regex m_pattern;
};
}

Expand Down
74 changes: 57 additions & 17 deletions libmamba/src/specs/regex_spec.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

#include <algorithm>
#include <cassert>
#include <sstream>

#include <fmt/format.h>

Expand All @@ -25,39 +26,78 @@ namespace mamba::specs

auto RegexSpec::parse(std::string pattern) -> expected_parse_t<RegexSpec>
{
// 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 RegexSpec{ std::move(pattern) };
}
catch (const std::regex_error& e)
{
return make_unexpected_parse(e.what());
}
}

RegexSpec::RegexSpec()
: RegexSpec(std::regex(free_pattern.data(), free_pattern.size()), std::string(free_pattern))
auto regexify(std::string raw_pattern) -> std::string
{
}
// raw_pattern can be a regex or a glob pattern. We need to convert it to a regex.

RegexSpec::RegexSpec(std::regex pattern, std::string raw_pattern)
: m_pattern(std::move(pattern))
, m_raw_pattern(std::move(raw_pattern))
{
// If the string is wrapped in `^` and `$`, `conda.model.MatchSpec` considers it a regex.
// See:
// https://github.com/conda/conda/blob/52b6393d6331e8aa36b2e23ab65766a980f381d2/conda/models/match_spec.py#L134-L139.
// See:
// https://github.com/conda/conda/blob/52b6393d6331e8aa36b2e23ab65766a980f381d2/conda/models/match_spec.py#L889-L894
if (util::starts_with(raw_pattern, RegexSpec::pattern_start)
&& util::ends_with(raw_pattern, RegexSpec::pattern_end))
{
return raw_pattern;
}

// Construct the regex progressively 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 << RegexSpec::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 == RegexSpec::pattern_start)
{
continue;
}
if (it == last_character_it && *it == RegexSpec::pattern_end)
{
continue;
}
if (*it == '*' && (it == first_character_it || *(it - 1) != '.'))
{
ss << ".*";
}
else
{
ss << *it;
}
}

ss << RegexSpec::pattern_end;

return ss.str();
}

RegexSpec::RegexSpec()
: RegexSpec(std::string(free_pattern))
{
}

RegexSpec::RegexSpec(std::string raw_pattern)
: m_raw_pattern(regexify(std::move(raw_pattern)))
, m_pattern(std::regex(m_raw_pattern))
{
}

auto RegexSpec::contains(std::string_view str) const -> bool
Expand Down
19 changes: 19 additions & 0 deletions libmamba/tests/src/specs/test_match_spec.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
16 changes: 16 additions & 0 deletions libmamba/tests/src/specs/test_regex_spec.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,20 @@ namespace
REQUIRE(hash_fn(spec1) == hash_fn(spec2));
REQUIRE(hash_fn(spec1) != hash_fn(spec3));
}

TEST_CASE("RegexSpec py3.10_cuda11.8*")
{
auto spec = RegexSpec::parse("py3.10_cuda11.8*").value();
REQUIRE(spec.contains("py3.10_cuda11.8_cudnn8.7.0_0"));
}

TEST_CASE("RegexSpec * semantic")
{
auto spec = RegexSpec::parse("py3.*").value();

REQUIRE(spec.contains("py3."));
REQUIRE(spec.contains("py3.10"));
REQUIRE(spec.contains("py3.10_cuda11.8_cudnn8.7.0_0"));
}

}
37 changes: 37 additions & 0 deletions micromamba/tests/test_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -1639,6 +1639,42 @@ def test_ca_certificates(tmp_path):
assert root_prefix_ca_certificates_used or fall_back_certificates_used


def test_glob_in_build_string(monkeypatch, tmp_path):
# Non-regression test for https://github.com/mamba-org/mamba/issues/3699
env_prefix = tmp_path / "test_glob_in_build_string"

pytorch_match_spec = "pytorch=2.3.1=py3.10_cuda11.8*"

# Export CONDA_OVERRIDE_GLIBC=2.17 to force the solver to use the glibc 2.17 package
monkeypatch.setenv("CONDA_OVERRIDE_GLIBC", "2.17")

# 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",
)

# Check that a build of pytorch 2.3.1 with `py3.10_cuda11.8_cudnn8.7.0_0` as a build string is found
assert any(
package["name"] == "pytorch"
and package["version"] == "2.3.1"
and package["build_string"] == "py3.10_cuda11.8_cudnn8.7.0_0"
for package in out["actions"]["FETCH"]
)

def test_non_url_encoding(tmp_path):
# Non-regression test for https://github.com/mamba-org/mamba/issues/3737
env_prefix = tmp_path / "env-non_url_encoding"
Expand All @@ -1656,3 +1692,4 @@ 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

0 comments on commit 57a2c55

Please sign in to comment.