Requirements and preferences should not define (non-git) versions (#37687)

Ensure that requirements `packages:*:require:@x` and preferences `packages:*:version:[x]`
fail concretization when no version defined in the package satisfies `x`. This always holds
except for git versions -- they are defined on the fly.
This commit is contained in:
Peter Scheibel 2023-05-16 06:45:11 -07:00 committed by GitHub
parent a0e7ca94b2
commit 7bc5b26c52
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 177 additions and 11 deletions

View file

@ -1759,13 +1759,30 @@ def key_fn(item):
# All the preferred version from packages.yaml, versions in external # All the preferred version from packages.yaml, versions in external
# specs will be computed later # specs will be computed later
version_preferences = packages_yaml.get(pkg_name, {}).get("version", []) version_preferences = packages_yaml.get(pkg_name, {}).get("version", [])
for idx, v in enumerate(version_preferences): version_defs = []
# v can be a string so force it into an actual version for comparisons pkg_class = spack.repo.path.get_pkg_class(pkg_name)
ver = vn.Version(v) for vstr in version_preferences:
v = vn.ver(vstr)
if isinstance(v, vn.GitVersion):
version_defs.append(v)
else:
satisfying_versions = list(x for x in pkg_class.versions if x.satisfies(v))
if not satisfying_versions:
raise spack.config.ConfigError(
"Preference for version {0} does not match any version "
" defined in {1}".format(str(v), pkg_name)
)
# Amongst all defined versions satisfying this specific
# preference, the highest-numbered version is the
# most-preferred: therefore sort satisfying versions
# from greatest to least
version_defs.extend(sorted(satisfying_versions, reverse=True))
for weight, vdef in enumerate(llnl.util.lang.dedupe(version_defs)):
self.declared_versions[pkg_name].append( self.declared_versions[pkg_name].append(
DeclaredVersion(version=ver, idx=idx, origin=Provenance.PACKAGES_YAML) DeclaredVersion(version=vdef, idx=weight, origin=Provenance.PACKAGES_YAML)
) )
self.possible_versions[pkg_name].add(ver) self.possible_versions[pkg_name].add(vdef)
def add_concrete_versions_from_specs(self, specs, origin): def add_concrete_versions_from_specs(self, specs, origin):
"""Add concrete versions to possible versions from lists of CLI/dev specs.""" """Add concrete versions to possible versions from lists of CLI/dev specs."""
@ -2296,6 +2313,11 @@ def _get_versioned_specs_from_pkg_requirements():
def _specs_from_requires(pkg_name, section): def _specs_from_requires(pkg_name, section):
"""Collect specs from requirements which define versions (i.e. those that
have a concrete version). Requirements can define *new* versions if
they are included as part of an equivalence (hash=number) but not
otherwise.
"""
if isinstance(section, str): if isinstance(section, str):
spec = spack.spec.Spec(section) spec = spack.spec.Spec(section)
if not spec.name: if not spec.name:
@ -2324,7 +2346,30 @@ def _specs_from_requires(pkg_name, section):
spec.name = pkg_name spec.name = pkg_name
extracted_specs.append(spec) extracted_specs.append(spec)
version_specs = [x for x in extracted_specs if x.versions.concrete] version_specs = []
for spec in extracted_specs:
if spec.versions.concrete:
# Note: this includes git versions
version_specs.append(spec)
continue
# Prefer spec's name if it exists, in case the spec is
# requiring a specific implementation inside of a virtual section
# e.g. packages:mpi:require:openmpi@4.0.1
pkg_class = spack.repo.path.get_pkg_class(spec.name or pkg_name)
satisfying_versions = list(v for v in pkg_class.versions if v.satisfies(spec.versions))
if not satisfying_versions:
raise spack.config.ConfigError(
"{0} assigns a version that is not defined in"
" the associated package.py".format(str(spec))
)
# Version ranges ("@1.3" without the "=", "@1.2:1.4") and lists
# will end up here
ordered_satisfying_versions = sorted(satisfying_versions, reverse=True)
vspecs = list(spack.spec.Spec("@{0}".format(x)) for x in ordered_satisfying_versions)
version_specs.extend(vspecs)
for spec in version_specs: for spec in version_specs:
spec.attach_git_version_lookup() spec.attach_git_version_lookup()
return version_specs return version_specs

View file

@ -906,7 +906,7 @@ def test_env_config_precedence(environment_from_manifest):
mpileaks: mpileaks:
version: ["2.2"] version: ["2.2"]
libelf: libelf:
version: ["0.8.11"] version: ["0.8.10"]
""" """
) )

View file

@ -152,7 +152,9 @@ def test_preferred_versions(self):
assert spec.version == Version("2.2") assert spec.version == Version("2.2")
def test_preferred_versions_mixed_version_types(self): def test_preferred_versions_mixed_version_types(self):
update_packages("mixedversions", "version", ["2.0"]) if spack.config.get("config:concretizer") == "original":
pytest.skip("This behavior is not enforced for the old concretizer")
update_packages("mixedversions", "version", ["=2.0"])
spec = concretize("mixedversions") spec = concretize("mixedversions")
assert spec.version == Version("2.0") assert spec.version == Version("2.0")
@ -228,6 +230,29 @@ def test_preferred(self):
spec.concretize() spec.concretize()
assert spec.version == Version("3.5.0") assert spec.version == Version("3.5.0")
def test_preferred_undefined_raises(self):
"""Preference should not specify an undefined version"""
if spack.config.get("config:concretizer") == "original":
pytest.xfail("This behavior is not enforced for the old concretizer")
update_packages("python", "version", ["3.5.0.1"])
spec = Spec("python")
with pytest.raises(spack.config.ConfigError):
spec.concretize()
def test_preferred_truncated(self):
"""Versions without "=" are treated as version ranges: if there is
a satisfying version defined in the package.py, we should use that
(don't define a new version).
"""
if spack.config.get("config:concretizer") == "original":
pytest.skip("This behavior is not enforced for the old concretizer")
update_packages("python", "version", ["3.5"])
spec = Spec("python")
spec.concretize()
assert spec.satisfies("@3.5.1")
def test_develop(self): def test_develop(self):
"""Test concretization with develop-like versions""" """Test concretization with develop-like versions"""
spec = Spec("develop-test") spec = Spec("develop-test")

View file

@ -66,6 +66,28 @@ class V(Package):
) )
_pkgt = (
"t",
"""\
class T(Package):
version('2.1')
version('2.0')
depends_on('u', when='@2.1:')
""",
)
_pkgu = (
"u",
"""\
class U(Package):
version('1.1')
version('1.0')
""",
)
@pytest.fixture @pytest.fixture
def create_test_repo(tmpdir, mutable_config): def create_test_repo(tmpdir, mutable_config):
repo_path = str(tmpdir) repo_path = str(tmpdir)
@ -79,7 +101,7 @@ def create_test_repo(tmpdir, mutable_config):
) )
packages_dir = tmpdir.join("packages") packages_dir = tmpdir.join("packages")
for pkg_name, pkg_str in [_pkgx, _pkgy, _pkgv]: for pkg_name, pkg_str in [_pkgx, _pkgy, _pkgv, _pkgt, _pkgu]:
pkg_dir = packages_dir.ensure(pkg_name, dir=True) pkg_dir = packages_dir.ensure(pkg_name, dir=True)
pkg_file = pkg_dir.join("package.py") pkg_file = pkg_dir.join("package.py")
with open(str(pkg_file), "w") as f: with open(str(pkg_file), "w") as f:
@ -144,6 +166,45 @@ def test_requirement_isnt_optional(concretize_scope, test_repo):
Spec("x@1.1").concretize() Spec("x@1.1").concretize()
def test_require_undefined_version(concretize_scope, test_repo):
"""If a requirement specifies a numbered version that isn't in
the associated package.py and isn't part of a Git hash
equivalence (hash=number), then Spack should raise an error
(it is assumed this is a typo, and raising the error here
avoids a likely error when Spack attempts to fetch the version).
"""
if spack.config.get("config:concretizer") == "original":
pytest.skip("Original concretizer does not support configuration requirements")
conf_str = """\
packages:
x:
require: "@1.2"
"""
update_packages_config(conf_str)
with pytest.raises(spack.config.ConfigError):
Spec("x").concretize()
def test_require_truncated(concretize_scope, test_repo):
"""A requirement specifies a version range, with satisfying
versions defined in the package.py. Make sure we choose one
of the defined versions (vs. allowing the requirement to
define a new version).
"""
if spack.config.get("config:concretizer") == "original":
pytest.skip("Original concretizer does not support configuration requirements")
conf_str = """\
packages:
x:
require: "@1"
"""
update_packages_config(conf_str)
xspec = Spec("x").concretized()
assert xspec.satisfies("@1.1")
def test_git_user_supplied_reference_satisfaction( def test_git_user_supplied_reference_satisfaction(
concretize_scope, test_repo, mock_git_version_info, monkeypatch concretize_scope, test_repo, mock_git_version_info, monkeypatch
): ):
@ -220,6 +281,40 @@ def test_requirement_adds_new_version(
assert s1.version.ref == a_commit_hash assert s1.version.ref == a_commit_hash
def test_requirement_adds_version_satisfies(
concretize_scope, test_repo, mock_git_version_info, monkeypatch
):
"""Make sure that new versions added by requirements are factored into
conditions. In this case create a new version that satisfies a
depends_on condition and make sure it is triggered (i.e. the
dependency is added).
"""
if spack.config.get("config:concretizer") == "original":
pytest.skip("Original concretizer does not support configuration" " requirements")
repo_path, filename, commits = mock_git_version_info
monkeypatch.setattr(
spack.package_base.PackageBase, "git", path_to_file_url(repo_path), raising=False
)
# Sanity check: early version of T does not include U
s0 = Spec("t@2.0").concretized()
assert not ("u" in s0)
conf_str = """\
packages:
t:
require: "@{0}=2.2"
""".format(
commits[0]
)
update_packages_config(conf_str)
s1 = Spec("t").concretized()
assert "u" in s1
assert s1.satisfies("@2.2")
def test_requirement_adds_git_hash_version( def test_requirement_adds_git_hash_version(
concretize_scope, test_repo, mock_git_version_info, monkeypatch concretize_scope, test_repo, mock_git_version_info, monkeypatch
): ):

View file

@ -6,9 +6,9 @@ spack:
mesa: mesa:
require: "+glx +osmesa +opengl ~opengles +llvm" require: "+glx +osmesa +opengl ~opengles +llvm"
libosmesa: libosmesa:
require: ^mesa +osmesa require: "mesa +osmesa"
libglx: libglx:
require: ^mesa +glx require: "mesa +glx"
ospray: ospray:
require: "@2.8.0 +denoiser +mpi" require: "@2.8.0 +denoiser +mpi"
llvm: llvm:

View file

@ -14,6 +14,7 @@ class Python(Package):
extendable = True extendable = True
version("3.7.1", md5="aaabbbcccdddeeefffaaabbbcccddd12")
version("3.5.1", md5="be78e48cdfc1a7ad90efff146dce6cfe") version("3.5.1", md5="be78e48cdfc1a7ad90efff146dce6cfe")
version("3.5.0", md5="a56c0c0b45d75a0ec9c6dee933c41c36") version("3.5.0", md5="a56c0c0b45d75a0ec9c6dee933c41c36")
version("2.7.11", md5="6b6076ec9e93f05dd63e47eb9c15728b", preferred=True) version("2.7.11", md5="6b6076ec9e93f05dd63e47eb9c15728b", preferred=True)