mirror of https://github.com/mamba-org/mamba.git
fix: Support globs in `MatchSpec` build strings (#3735)
Signed-off-by: Julien Jerphanion <git@jjerphan.xyz> Co-authored-by: Johan Mabille <johan.mabille@gmail.com> Co-authored-by: jaimergp <jaimergp@users.noreply.github.com>
This commit is contained in:
parent
c1c9f5fc90
commit
57a2c55826
|
@ -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;
|
||||
|
||||
|
@ -61,8 +61,8 @@ namespace mamba::specs
|
|||
|
||||
private:
|
||||
|
||||
std::regex m_pattern;
|
||||
std::string m_raw_pattern;
|
||||
std::regex m_pattern;
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <sstream>
|
||||
|
||||
#include <fmt/format.h>
|
||||
|
||||
|
@ -25,12 +26,10 @@ 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)
|
||||
{
|
||||
|
@ -38,26 +37,67 @@ namespace mamba::specs
|
|||
}
|
||||
}
|
||||
|
||||
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))
|
||||
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.insert(m_raw_pattern.begin(), pattern_start);
|
||||
}
|
||||
if (!util::ends_with(m_raw_pattern, pattern_end))
|
||||
{
|
||||
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
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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"));
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -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"
|
||||
|
@ -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
|
||||
|
||||
|
|
Loading…
Reference in New Issue