Split satisfies(..., strict=True/False) into two functions (#35681)

This commit formalizes `satisfies(lhs, rhs, strict=True/False)`
and splits it into two functions: `satisfies(lhs, rhs)` and
`intersects(lhs, rhs)`.

- `satisfies(lhs, rhs)` means: all concrete specs matching the
   left hand side also match the right hand side
- `intersects(lhs, rhs)` means: there exist concrete specs
   matching both lhs and rhs.

`intersects` now has the property that it's commutative,
which previously was not guaranteed.

For abstract specs, `intersects(lhs, rhs)` implies that
`constrain(lhs, rhs)` works.

What's *not* done in this PR is ensuring that
`intersects(concrete, abstract)` returns false when the
abstract spec has additional properties not present in the
concrete spec, but `constrain(concrete, abstract)` will
raise an error.

To accomplish this, some semantics have changed, as well
as bugfixes to ArchSpec:
- GitVersion is now interpreted as a more constrained
  version
- Compiler flags are interpreted as strings since their
  order is important
- Abstract specs respect variant type (bool / multivalued)
This commit is contained in:
Massimiliano Culpo 2023-03-08 13:00:53 +01:00 committed by GitHub
parent 39adb65dc7
commit d54611af2c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
39 changed files with 1057 additions and 859 deletions

View File

@ -25,7 +25,7 @@ def architecture_compatible(self, target, constraint):
return (
not target.architecture
or not constraint.architecture
or target.architecture.satisfies(constraint.architecture)
or target.architecture.intersects(constraint.architecture)
)
@memoized
@ -104,7 +104,7 @@ def compiler_compatible(self, parent, child, **kwargs):
for cversion in child.compiler.versions:
# For a few compilers use specialized comparisons.
# Otherwise match on version match.
if pversion.satisfies(cversion):
if pversion.intersects(cversion):
return True
elif parent.compiler.name == "gcc" and self._gcc_compiler_compare(
pversion, cversion

View File

@ -721,7 +721,7 @@ def _version_constraints_are_satisfiable_by_some_version_in_repo(pkgs, error_cls
dependency_pkg_cls = None
try:
dependency_pkg_cls = spack.repo.path.get_pkg_class(s.name)
assert any(v.satisfies(s.versions) for v in list(dependency_pkg_cls.versions))
assert any(v.intersects(s.versions) for v in list(dependency_pkg_cls.versions))
except Exception:
summary = (
"{0}: dependency on {1} cannot be satisfied " "by known versions of {1.name}"

View File

@ -208,7 +208,7 @@ def _install_and_test(self, abstract_spec, bincache_platform, bincache_data, tes
# This will be None for things that don't depend on python
python_spec = item.get("python", None)
# Skip specs which are not compatible
if not abstract_spec.satisfies(candidate_spec):
if not abstract_spec.intersects(candidate_spec):
continue
if python_spec is not None and python_spec not in abstract_spec:

View File

@ -361,7 +361,7 @@ def append_dep(s, d):
def _spec_matches(spec, match_string):
return spec.satisfies(match_string)
return spec.intersects(match_string)
def _remove_attributes(src_dict, dest_dict):
@ -938,7 +938,7 @@ def generate_gitlab_ci_yaml(
bs_arch = c_spec.architecture
bs_arch_family = bs_arch.target.microarchitecture.family
if (
c_spec.satisfies(compiler_pkg_spec)
c_spec.intersects(compiler_pkg_spec)
and bs_arch_family == spec_arch_family
):
# We found the bootstrap compiler this release spec

View File

@ -498,11 +498,11 @@ def list_fn(args):
if not args.allarch:
arch = spack.spec.Spec.default_arch()
specs = [s for s in specs if s.satisfies(arch)]
specs = [s for s in specs if s.intersects(arch)]
if args.specs:
constraints = set(args.specs)
specs = [s for s in specs if any(s.satisfies(c) for c in constraints)]
specs = [s for s in specs if any(s.intersects(c) for c in constraints)]
if sys.stdout.isatty():
builds = len(specs)
tty.msg("%s." % plural(builds, "cached build"))

View File

@ -283,7 +283,7 @@ def print_tests(pkg):
c_names = ("gcc", "intel", "intel-parallel-studio", "pgi")
if pkg.name in c_names:
v_names.extend(["c", "cxx", "fortran"])
if pkg.spec.satisfies("llvm+clang"):
if pkg.spec.intersects("llvm+clang"):
v_names.extend(["c", "cxx"])
# TODO Refactor END

View File

@ -335,7 +335,7 @@ def not_excluded_fn(args):
exclude_specs.extend(spack.cmd.parse_specs(str(args.exclude_specs).split()))
def not_excluded(x):
return not any(x.satisfies(y, strict=True) for y in exclude_specs)
return not any(x.satisfies(y) for y in exclude_specs)
return not_excluded

View File

@ -134,7 +134,7 @@ def _valid_virtuals_and_externals(self, spec):
externals = spec_externals(cspec)
for ext in externals:
if ext.satisfies(spec):
if ext.intersects(spec):
usable.append(ext)
# If nothing is in the usable list now, it's because we aren't
@ -200,7 +200,7 @@ def concretize_version(self, spec):
# List of versions we could consider, in sorted order
pkg_versions = spec.package_class.versions
usable = [v for v in pkg_versions if any(v.satisfies(sv) for sv in spec.versions)]
usable = [v for v in pkg_versions if any(v.intersects(sv) for sv in spec.versions)]
yaml_prefs = PackagePrefs(spec.name, "version")
@ -344,7 +344,7 @@ def concretize_architecture(self, spec):
new_target_arch = spack.spec.ArchSpec((None, None, str(new_target)))
curr_target_arch = spack.spec.ArchSpec((None, None, str(curr_target)))
if not new_target_arch.satisfies(curr_target_arch):
if not new_target_arch.intersects(curr_target_arch):
# new_target is an incorrect guess based on preferences
# and/or default
valid_target_ranges = str(curr_target).split(",")

View File

@ -1525,7 +1525,7 @@ def _query(
if not (start_date < inst_date < end_date):
continue
if query_spec is any or rec.spec.satisfies(query_spec, strict=True):
if query_spec is any or rec.spec.satisfies(query_spec):
results.append(rec.spec)
return results

View File

@ -349,7 +349,8 @@ def _is_dev_spec_and_has_changed(spec):
def _spec_needs_overwrite(spec, changed_dev_specs):
"""Check whether the current spec needs to be overwritten because either it has
changed itself or one of its dependencies have changed"""
changed itself or one of its dependencies have changed
"""
# if it's not installed, we don't need to overwrite it
if not spec.installed:
return False
@ -2313,7 +2314,7 @@ def _concretize_from_constraints(spec_constraints, tests=False):
invalid_deps = [
c
for c in spec_constraints
if any(c.satisfies(invd, strict=True) for invd in invalid_deps_string)
if any(c.satisfies(invd) for invd in invalid_deps_string)
]
if len(invalid_deps) != len(invalid_deps_string):
raise e

View File

@ -1501,7 +1501,7 @@ def _from_merged_attrs(fetcher, pkg, version):
return fetcher(**attrs)
def for_package_version(pkg, version):
def for_package_version(pkg, version=None):
"""Determine a fetch strategy based on the arguments supplied to
version() in the package description."""
@ -1512,8 +1512,18 @@ def for_package_version(pkg, version):
check_pkg_attributes(pkg)
if not isinstance(version, spack.version.VersionBase):
version = spack.version.Version(version)
if version is not None:
assert not pkg.spec.concrete, "concrete specs should not pass the 'version=' argument"
# Specs are initialized with the universe range, if no version information is given,
# so here we make sure we always match the version passed as argument
if not isinstance(version, spack.version.VersionBase):
version = spack.version.Version(version)
version_list = spack.version.VersionList()
version_list.add(version)
pkg.spec.versions = version_list
else:
version = pkg.version
# if it's a commit, we must use a GitFetchStrategy
if isinstance(version, spack.version.GitVersion):

View File

@ -492,7 +492,7 @@ def get_matching_versions(specs, num_versions=1):
break
# Generate only versions that satisfy the spec.
if spec.concrete or v.satisfies(spec.versions):
if spec.concrete or v.intersects(spec.versions):
s = spack.spec.Spec(pkg.name)
s.versions = VersionList([v])
s.variants = spec.variants.copy()

View File

@ -207,7 +207,7 @@ def merge_config_rules(configuration, spec):
# evaluated in order of appearance in the module file
spec_configuration = module_specific_configuration.pop("all", {})
for constraint, action in module_specific_configuration.items():
if spec.satisfies(constraint, strict=True):
if spec.satisfies(constraint):
if hasattr(constraint, "override") and constraint.override:
spec_configuration = {}
update_dictionary_extending_lists(spec_configuration, action)

View File

@ -1197,7 +1197,7 @@ def _make_fetcher(self):
# one element (the root package). In case there are resources
# associated with the package, append their fetcher to the
# composite.
root_fetcher = fs.for_package_version(self, self.version)
root_fetcher = fs.for_package_version(self)
fetcher = fs.FetchStrategyComposite() # Composite fetcher
fetcher.append(root_fetcher) # Root fetcher is always present
resources = self._get_needed_resources()
@ -1308,7 +1308,7 @@ def provides(self, vpkg_name):
True if this package provides a virtual package with the specified name
"""
return any(
any(self.spec.satisfies(c) for c in constraints)
any(self.spec.intersects(c) for c in constraints)
for s, constraints in self.provided.items()
if s.name == vpkg_name
)
@ -1614,7 +1614,7 @@ def content_hash(self, content=None):
# TODO: resources
if self.spec.versions.concrete:
try:
source_id = fs.for_package_version(self, self.version).source_id()
source_id = fs.for_package_version(self).source_id()
except (fs.ExtrapolationError, fs.InvalidArgsError):
# ExtrapolationError happens if the package has no fetchers defined.
# InvalidArgsError happens when there are version directives with args,
@ -1777,7 +1777,7 @@ def _get_needed_resources(self):
# conflict with the spec, so we need to invoke
# when_spec.satisfies(self.spec) vs.
# self.spec.satisfies(when_spec)
if when_spec.satisfies(self.spec, strict=False):
if when_spec.intersects(self.spec):
resources.extend(resource_list)
# Sorts the resources by the length of the string representing their
# destination. Since any nested resource must contain another

View File

@ -73,7 +73,7 @@ def __call__(self, spec):
# integer is the index of the first spec in order that satisfies
# spec, or it's a number larger than any position in the order.
match_index = next(
(i for i, s in enumerate(spec_order) if spec.satisfies(s)), len(spec_order)
(i for i, s in enumerate(spec_order) if spec.intersects(s)), len(spec_order)
)
if match_index < len(spec_order) and spec_order[match_index] == spec:
# If this is called with multiple specs that all satisfy the same
@ -185,7 +185,7 @@ def _package(maybe_abstract_spec):
),
extra_attributes=entry.get("extra_attributes", {}),
)
if external_spec.satisfies(spec):
if external_spec.intersects(spec):
external_specs.append(external_spec)
# Defensively copy returned specs

View File

@ -10,7 +10,7 @@ def get_projection(projections, spec):
"""
all_projection = None
for spec_like, projection in projections.items():
if spec.satisfies(spec_like, strict=True):
if spec.satisfies(spec_like):
return projection
elif spec_like == "all":
all_projection = projection

View File

@ -72,7 +72,7 @@ def providers_for(self, virtual_spec):
# Add all the providers that satisfy the vpkg spec.
if virtual_spec.name in self.providers:
for p_spec, spec_set in self.providers[virtual_spec.name].items():
if p_spec.satisfies(virtual_spec, deps=False):
if p_spec.intersects(virtual_spec, deps=False):
result.update(spec_set)
# Return providers in order. Defensively copy.
@ -186,7 +186,7 @@ def update(self, spec):
provider_spec = provider_spec_readonly.copy()
provider_spec.compiler_flags = spec.compiler_flags.copy()
if spec.satisfies(provider_spec, deps=False):
if spec.intersects(provider_spec, deps=False):
provided_name = provided_spec.name
provider_map = self.providers.setdefault(provided_name, {})

View File

@ -501,7 +501,7 @@ def _compute_specs_from_answer_set(self):
key = providers[0]
candidate = answer.get(key)
if candidate and candidate.satisfies(input_spec):
if candidate and candidate.intersects(input_spec):
self._concrete_specs.append(answer[key])
self._concrete_specs_by_input[input_spec] = answer[key]
else:
@ -1878,7 +1878,7 @@ def define_version_constraints(self):
for pkg_name, versions in sorted(self.version_constraints):
# version must be *one* of the ones the spec allows.
allowed_versions = [
v for v in sorted(self.possible_versions[pkg_name]) if v.satisfies(versions)
v for v in sorted(self.possible_versions[pkg_name]) if v.intersects(versions)
]
# This is needed to account for a variable number of

View File

@ -191,9 +191,7 @@ def __call__(self, match):
@lang.lazy_lexicographic_ordering
class ArchSpec(object):
"""Aggregate the target platform, the operating system and the target
microarchitecture into an architecture spec..
"""
"""Aggregate the target platform, the operating system and the target microarchitecture."""
@staticmethod
def _return_arch(os_tag, target_tag):
@ -362,17 +360,11 @@ def target_or_none(t):
self._target = value
def satisfies(self, other, strict=False):
"""Predicate to check if this spec satisfies a constraint.
def satisfies(self, other: "ArchSpec") -> bool:
"""Return True if all concrete specs matching self also match other, otherwise False.
Args:
other (ArchSpec or str): constraint on the current instance
strict (bool): if ``False`` the function checks if the current
instance *might* eventually satisfy the constraint. If
``True`` it check if the constraint is satisfied right now.
Returns:
True if the constraint is satisfied, False otherwise.
other: spec to be satisfied
"""
other = self._autospec(other)
@ -380,47 +372,69 @@ def satisfies(self, other, strict=False):
for attribute in ("platform", "os"):
other_attribute = getattr(other, attribute)
self_attribute = getattr(self, attribute)
if strict or self.concrete:
if other_attribute and self_attribute != other_attribute:
return False
else:
if other_attribute and self_attribute and self_attribute != other_attribute:
return False
if other_attribute and self_attribute != other_attribute:
return False
# Check target
return self.target_satisfies(other, strict=strict)
return self._target_satisfies(other, strict=True)
def target_satisfies(self, other, strict):
need_to_check = (
bool(other.target) if strict or self.concrete else bool(other.target and self.target)
)
def intersects(self, other: "ArchSpec") -> bool:
"""Return True if there exists at least one concrete spec that matches both
self and other, otherwise False.
This operation is commutative, and if two specs intersect it means that one
can constrain the other.
Args:
other: spec to be checked for compatibility
"""
other = self._autospec(other)
# Check platform and os
for attribute in ("platform", "os"):
other_attribute = getattr(other, attribute)
self_attribute = getattr(self, attribute)
if other_attribute and self_attribute and self_attribute != other_attribute:
return False
return self._target_satisfies(other, strict=False)
def _target_satisfies(self, other: "ArchSpec", strict: bool) -> bool:
if strict is True:
need_to_check = bool(other.target)
else:
need_to_check = bool(other.target and self.target)
# If there's no need to check we are fine
if not need_to_check:
return True
# self is not concrete, but other_target is there and strict=True
# other_target is there and strict=True
if self.target is None:
return False
return bool(self.target_intersection(other))
return bool(self._target_intersection(other))
def target_constrain(self, other):
if not other.target_satisfies(self, strict=False):
def _target_constrain(self, other: "ArchSpec") -> bool:
if not other._target_satisfies(self, strict=False):
raise UnsatisfiableArchitectureSpecError(self, other)
if self.target_concrete:
return False
elif other.target_concrete:
self.target = other.target
return True
# Compute the intersection of every combination of ranges in the lists
results = self.target_intersection(other)
# Do we need to dedupe here?
self.target = ",".join(results)
results = self._target_intersection(other)
attribute_str = ",".join(results)
def target_intersection(self, other):
if self.target == attribute_str:
return False
self.target = attribute_str
return True
def _target_intersection(self, other):
results = []
if not self.target or not other.target:
@ -464,7 +478,7 @@ def target_intersection(self, other):
results.append("%s:%s" % (n_min, n_max))
return results
def constrain(self, other):
def constrain(self, other: "ArchSpec") -> bool:
"""Projects all architecture fields that are specified in the given
spec onto the instance spec if they're missing from the instance
spec.
@ -479,7 +493,7 @@ def constrain(self, other):
"""
other = self._autospec(other)
if not other.satisfies(self):
if not other.intersects(self):
raise UnsatisfiableArchitectureSpecError(other, self)
constrained = False
@ -489,7 +503,7 @@ def constrain(self, other):
setattr(self, attr, ovalue)
constrained = True
self.target_constrain(other)
constrained |= self._target_constrain(other)
return constrained
@ -505,7 +519,9 @@ def concrete(self):
@property
def target_concrete(self):
"""True if the target is not a range or list."""
return ":" not in str(self.target) and "," not in str(self.target)
return (
self.target is not None and ":" not in str(self.target) and "," not in str(self.target)
)
def to_dict(self):
d = syaml.syaml_dict(
@ -591,11 +607,31 @@ def _autospec(self, compiler_spec_like):
return compiler_spec_like
return CompilerSpec(compiler_spec_like)
def satisfies(self, other, strict=False):
other = self._autospec(other)
return self.name == other.name and self.versions.satisfies(other.versions, strict=strict)
def intersects(self, other: "CompilerSpec") -> bool:
"""Return True if all concrete specs matching self also match other, otherwise False.
def constrain(self, other):
For compiler specs this means that the name of the compiler must be the same for
self and other, and that the versions ranges should intersect.
Args:
other: spec to be satisfied
"""
other = self._autospec(other)
return self.name == other.name and self.versions.intersects(other.versions)
def satisfies(self, other: "CompilerSpec") -> bool:
"""Return True if all concrete specs matching self also match other, otherwise False.
For compiler specs this means that the name of the compiler must be the same for
self and other, and that the version range of self is a subset of that of other.
Args:
other: spec to be satisfied
"""
other = self._autospec(other)
return self.name == other.name and self.versions.satisfies(other.versions)
def constrain(self, other: "CompilerSpec") -> bool:
"""Intersect self's versions with other.
Return whether the CompilerSpec changed.
@ -603,7 +639,7 @@ def constrain(self, other):
other = self._autospec(other)
# ensure that other will actually constrain this spec.
if not other.satisfies(self):
if not other.intersects(self):
raise UnsatisfiableCompilerSpecError(other, self)
return self.versions.intersect(other.versions)
@ -736,24 +772,25 @@ def __init__(self, spec):
super(FlagMap, self).__init__()
self.spec = spec
def satisfies(self, other, strict=False):
if strict or (self.spec and self.spec._concrete):
return all(f in self and set(self[f]) == set(other[f]) for f in other)
else:
if not all(
set(self[f]) == set(other[f]) for f in other if (other[f] != [] and f in self)
):
def satisfies(self, other):
return all(f in self and self[f] == other[f] for f in other)
def intersects(self, other):
common_types = set(self) & set(other)
for flag_type in common_types:
if not self[flag_type] or not other[flag_type]:
# At least one of the two is empty
continue
if self[flag_type] != other[flag_type]:
return False
# Check that the propagation values match
for flag_type in other:
if not all(
other[flag_type][i].propagate == self[flag_type][i].propagate
for i in range(len(other[flag_type]))
if flag_type in self
):
return False
return True
if not all(
f1.propagate == f2.propagate for f1, f2 in zip(self[flag_type], other[flag_type])
):
# At least one propagation flag didn't match
return False
return True
def constrain(self, other):
"""Add all flags in other that aren't in self to self.
@ -2611,9 +2648,9 @@ def _old_concretize(self, tests=False, deprecation_warning=True):
# it's possible to build that configuration with Spack
continue
for conflict_spec, when_list in x.package_class.conflicts.items():
if x.satisfies(conflict_spec, strict=True):
if x.satisfies(conflict_spec):
for when_spec, msg in when_list:
if x.satisfies(when_spec, strict=True):
if x.satisfies(when_spec):
when = when_spec.copy()
when.name = x.name
matches.append((x, conflict_spec, when, msg))
@ -2665,7 +2702,7 @@ def inject_patches_variant(root):
# Add any patches from the package to the spec.
patches = []
for cond, patch_list in s.package_class.patches.items():
if s.satisfies(cond, strict=True):
if s.satisfies(cond):
for patch in patch_list:
patches.append(patch)
if patches:
@ -2683,7 +2720,7 @@ def inject_patches_variant(root):
patches = []
for cond, dependency in pkg_deps[dspec.spec.name].items():
for pcond, patch_list in dependency.patches.items():
if dspec.parent.satisfies(cond, strict=True) and dspec.spec.satisfies(pcond):
if dspec.parent.satisfies(cond) and dspec.spec.satisfies(pcond):
patches.extend(patch_list)
if patches:
all_patches = spec_to_patches.setdefault(id(dspec.spec), [])
@ -2941,7 +2978,7 @@ def _evaluate_dependency_conditions(self, name):
# evaluate when specs to figure out constraints on the dependency.
dep = None
for when_spec, dependency in conditions.items():
if self.satisfies(when_spec, strict=True):
if self.satisfies(when_spec):
if dep is None:
dep = dp.Dependency(self.name, Spec(name), type=())
try:
@ -2976,7 +3013,7 @@ def _find_provider(self, vdep, provider_index):
# result.
for provider in providers:
for spec in providers:
if spec is not provider and provider.satisfies(spec):
if spec is not provider and provider.intersects(spec):
providers.remove(spec)
# Can't have multiple providers for the same thing in one spec.
if len(providers) > 1:
@ -3293,9 +3330,15 @@ def update_variant_validate(self, variant_name, values):
pkg_variant.validate_or_raise(self.variants[variant_name], pkg_cls)
def constrain(self, other, deps=True):
"""Merge the constraints of other with self.
"""Intersect self with other in-place. Return True if self changed, False otherwise.
Returns True if the spec changed as a result, False if not.
Args:
other: constraint to be added to self
deps: if False, constrain only the root node, otherwise constrain dependencies
as well.
Raises:
spack.error.UnsatisfiableSpecError: when self cannot be constrained
"""
# If we are trying to constrain a concrete spec, either the spec
# already satisfies the constraint (and the method returns False)
@ -3375,6 +3418,9 @@ def constrain(self, other, deps=True):
if deps:
changed |= self._constrain_dependencies(other)
if other.concrete and not self.concrete and other.satisfies(self):
self._finalize_concretization()
return changed
def _constrain_dependencies(self, other):
@ -3387,7 +3433,7 @@ def _constrain_dependencies(self, other):
# TODO: might want more detail than this, e.g. specific deps
# in violation. if this becomes a priority get rid of this
# check and be more specific about what's wrong.
if not other.satisfies_dependencies(self):
if not other._intersects_dependencies(self):
raise UnsatisfiableDependencySpecError(other, self)
if any(not d.name for d in other.traverse(root=False)):
@ -3449,58 +3495,49 @@ def _autospec(self, spec_like):
return spec_like
return Spec(spec_like)
def satisfies(self, other, deps=True, strict=False):
"""Determine if this spec satisfies all constraints of another.
def intersects(self, other: "Spec", deps: bool = True) -> bool:
"""Return True if there exists at least one concrete spec that matches both
self and other, otherwise False.
There are two senses for satisfies, depending on the ``strict``
argument.
This operation is commutative, and if two specs intersect it means that one
can constrain the other.
* ``strict=False``: the left-hand side and right-hand side have
non-empty intersection. For example ``zlib`` satisfies
``zlib@1.2.3`` and ``zlib@1.2.3`` satisfies ``zlib``. In this
sense satisfies is a commutative operation: ``x.satisfies(y)``
if and only if ``y.satisfies(x)``.
* ``strict=True``: the left-hand side is a subset of the right-hand
side. For example ``zlib@1.2.3`` satisfies ``zlib``, but ``zlib``
does not satisfy ``zlib@1.2.3``. In this sense satisfies is not
commutative: the left-hand side should be at least as constrained
as the right-hand side.
Args:
other: spec to be checked for compatibility
deps: if True check compatibility of dependency nodes too, if False only check root
"""
other = self._autospec(other)
# Optimizations for right-hand side concrete:
# 1. For subset (strict=True) tests this means the left-hand side must
# be the same singleton with identical hash. Notice that package hashes
# can be different for otherwise indistinguishable concrete Spec objects.
# 2. For non-empty intersection (strict=False) we only have a fast path
# when the left-hand side is also concrete.
if other.concrete:
if strict:
return self.concrete and self.dag_hash() == other.dag_hash()
elif self.concrete:
return self.dag_hash() == other.dag_hash()
if other.concrete and self.concrete:
return self.dag_hash() == other.dag_hash()
# If the names are different, we need to consider virtuals
if self.name != other.name and self.name and other.name:
# A concrete provider can satisfy a virtual dependency.
if not self.virtual and other.virtual:
if self.virtual and other.virtual:
# Two virtual specs intersect only if there are providers for both
lhs = spack.repo.path.providers_for(str(self))
rhs = spack.repo.path.providers_for(str(other))
intersection = [s for s in lhs if any(s.intersects(z) for z in rhs)]
return bool(intersection)
# A provider can satisfy a virtual dependency.
elif self.virtual or other.virtual:
virtual_spec, non_virtual_spec = (self, other) if self.virtual else (other, self)
try:
# Here we might get an abstract spec
pkg_cls = spack.repo.path.get_pkg_class(self.fullname)
pkg = pkg_cls(self)
pkg_cls = spack.repo.path.get_pkg_class(non_virtual_spec.fullname)
pkg = pkg_cls(non_virtual_spec)
except spack.repo.UnknownEntityError:
# If we can't get package info on this spec, don't treat
# it as a provider of this vdep.
return False
if pkg.provides(other.name):
if pkg.provides(virtual_spec.name):
for provided, when_specs in pkg.provided.items():
if any(
self.satisfies(when, deps=False, strict=strict) for when in when_specs
non_virtual_spec.intersects(when, deps=False) for when in when_specs
):
if provided.satisfies(other):
if provided.intersects(virtual_spec):
return True
return False
@ -3511,75 +3548,41 @@ def satisfies(self, other, deps=True, strict=False):
and self.namespace != other.namespace
):
return False
if self.versions and other.versions:
if not self.versions.satisfies(other.versions, strict=strict):
if not self.versions.intersects(other.versions):
return False
elif strict and (self.versions or other.versions):
return False
# None indicates no constraints when not strict.
if self.compiler and other.compiler:
if not self.compiler.satisfies(other.compiler, strict=strict):
if not self.compiler.intersects(other.compiler):
return False
elif strict and (other.compiler and not self.compiler):
if not self.variants.intersects(other.variants):
return False
var_strict = strict
if (not self.name) or (not other.name):
var_strict = True
if not self.variants.satisfies(other.variants, strict=var_strict):
return False
# Architecture satisfaction is currently just string equality.
# If not strict, None means unconstrained.
if self.architecture and other.architecture:
if not self.architecture.satisfies(other.architecture, strict):
if not self.architecture.intersects(other.architecture):
return False
elif strict and (other.architecture and not self.architecture):
return False
if not self.compiler_flags.satisfies(other.compiler_flags, strict=strict):
if not self.compiler_flags.intersects(other.compiler_flags):
return False
# If we need to descend into dependencies, do it, otherwise we're done.
if deps:
deps_strict = strict
if self._concrete and not other.name:
# We're dealing with existing specs
deps_strict = True
return self.satisfies_dependencies(other, strict=deps_strict)
return self._intersects_dependencies(other)
else:
return True
def satisfies_dependencies(self, other, strict=False):
"""
This checks constraints on common dependencies against each other.
"""
def _intersects_dependencies(self, other):
other = self._autospec(other)
# If there are no constraints to satisfy, we're done.
if not other._dependencies:
return True
if strict:
# if we have no dependencies, we can't satisfy any constraints.
if not self._dependencies:
return False
# use list to prevent double-iteration
selfdeps = list(self.traverse(root=False))
otherdeps = list(other.traverse(root=False))
if not all(any(d.satisfies(dep, strict=True) for d in selfdeps) for dep in otherdeps):
return False
elif not self._dependencies:
# if not strict, this spec *could* eventually satisfy the
# constraints on other.
if not other._dependencies or not self._dependencies:
# one spec *could* eventually satisfy the other
return True
# Handle first-order constraints directly
for name in self.common_dependencies(other):
if not self[name].satisfies(other[name], deps=False):
if not self[name].intersects(other[name], deps=False):
return False
# For virtual dependencies, we need to dig a little deeper.
@ -3607,6 +3610,89 @@ def satisfies_dependencies(self, other, strict=False):
return True
def satisfies(self, other: "Spec", deps: bool = True) -> bool:
"""Return True if all concrete specs matching self also match other, otherwise False.
Args:
other: spec to be satisfied
deps: if True descend to dependencies, otherwise only check root node
"""
other = self._autospec(other)
if other.concrete:
# The left-hand side must be the same singleton with identical hash. Notice that
# package hashes can be different for otherwise indistinguishable concrete Spec
# objects.
return self.concrete and self.dag_hash() == other.dag_hash()
# If the names are different, we need to consider virtuals
if self.name != other.name and self.name and other.name:
# A concrete provider can satisfy a virtual dependency.
if not self.virtual and other.virtual:
try:
# Here we might get an abstract spec
pkg_cls = spack.repo.path.get_pkg_class(self.fullname)
pkg = pkg_cls(self)
except spack.repo.UnknownEntityError:
# If we can't get package info on this spec, don't treat
# it as a provider of this vdep.
return False
if pkg.provides(other.name):
for provided, when_specs in pkg.provided.items():
if any(self.satisfies(when, deps=False) for when in when_specs):
if provided.intersects(other):
return True
return False
# namespaces either match, or other doesn't require one.
if (
other.namespace is not None
and self.namespace is not None
and self.namespace != other.namespace
):
return False
if not self.versions.satisfies(other.versions):
return False
if self.compiler and other.compiler:
if not self.compiler.satisfies(other.compiler):
return False
elif other.compiler and not self.compiler:
return False
if not self.variants.satisfies(other.variants):
return False
if self.architecture and other.architecture:
if not self.architecture.satisfies(other.architecture):
return False
elif other.architecture and not self.architecture:
return False
if not self.compiler_flags.satisfies(other.compiler_flags):
return False
# If we need to descend into dependencies, do it, otherwise we're done.
if not deps:
return True
# If there are no constraints to satisfy, we're done.
if not other._dependencies:
return True
# If we have no dependencies, we can't satisfy any constraints.
if not self._dependencies:
return False
# If we arrived here, then rhs is abstract. At the moment we don't care about the edge
# structure of an abstract DAG - hence the deps=False parameter.
return all(
any(lhs.satisfies(rhs, deps=False) for lhs in self.traverse(root=False))
for rhs in other.traverse(root=False)
)
def virtual_dependencies(self):
"""Return list of any virtual deps in this spec."""
return [spec for spec in self.traverse() if spec.virtual]

View File

@ -183,7 +183,7 @@ def test_optimization_flags_with_custom_versions(
def test_satisfy_strict_constraint_when_not_concrete(architecture_tuple, constraint_tuple):
architecture = spack.spec.ArchSpec(architecture_tuple)
constraint = spack.spec.ArchSpec(constraint_tuple)
assert not architecture.satisfies(constraint, strict=True)
assert not architecture.satisfies(constraint)
@pytest.mark.parametrize(

View File

@ -82,8 +82,8 @@ def test_change_match_spec():
change("--match-spec", "mpileaks@2.2", "mpileaks@2.3")
assert not any(x.satisfies("mpileaks@2.2") for x in e.user_specs)
assert any(x.satisfies("mpileaks@2.3") for x in e.user_specs)
assert not any(x.intersects("mpileaks@2.2") for x in e.user_specs)
assert any(x.intersects("mpileaks@2.3") for x in e.user_specs)
def test_change_multiple_matches():
@ -97,8 +97,8 @@ def test_change_multiple_matches():
change("--match-spec", "mpileaks", "-a", "mpileaks%gcc")
assert all(x.satisfies("%gcc") for x in e.user_specs if x.name == "mpileaks")
assert any(x.satisfies("%clang") for x in e.user_specs if x.name == "libelf")
assert all(x.intersects("%gcc") for x in e.user_specs if x.name == "mpileaks")
assert any(x.intersects("%clang") for x in e.user_specs if x.name == "libelf")
def test_env_add_virtual():
@ -111,7 +111,7 @@ def test_env_add_virtual():
hashes = e.concretized_order
assert len(hashes) == 1
spec = e.specs_by_hash[hashes[0]]
assert spec.satisfies("mpi")
assert spec.intersects("mpi")
def test_env_add_nonexistant_fails():
@ -687,7 +687,7 @@ def test_env_with_config():
with e:
e.concretize()
assert any(x.satisfies("mpileaks@2.2") for x in e._get_environment_specs())
assert any(x.intersects("mpileaks@2.2") for x in e._get_environment_specs())
def test_with_config_bad_include():
@ -1630,9 +1630,9 @@ def test_stack_concretize_extraneous_deps(tmpdir, config, mock_packages):
assert concrete.concrete
assert not user.concrete
if user.name == "libelf":
assert not concrete.satisfies("^mpi", strict=True)
assert not concrete.satisfies("^mpi")
elif user.name == "mpileaks":
assert concrete.satisfies("^mpi", strict=True)
assert concrete.satisfies("^mpi")
def test_stack_concretize_extraneous_variants(tmpdir, config, mock_packages):

View File

@ -276,7 +276,7 @@ def test_install_commit(mock_git_version_info, install_mockery, mock_packages, m
assert filename in installed
with open(spec.prefix.bin.join(filename), "r") as f:
content = f.read().strip()
assert content == "[]" # contents are weird for another test
assert content == "[0]" # contents are weird for another test
def test_install_overwrite_multiple(

View File

@ -293,11 +293,11 @@ def test_concretize_with_provides_when(self):
we ask for some advanced version.
"""
repo = spack.repo.path
assert not any(s.satisfies("mpich2@:1.0") for s in repo.providers_for("mpi@2.1"))
assert not any(s.satisfies("mpich2@:1.1") for s in repo.providers_for("mpi@2.2"))
assert not any(s.satisfies("mpich@:1") for s in repo.providers_for("mpi@2"))
assert not any(s.satisfies("mpich@:1") for s in repo.providers_for("mpi@3"))
assert not any(s.satisfies("mpich2") for s in repo.providers_for("mpi@3"))
assert not any(s.intersects("mpich2@:1.0") for s in repo.providers_for("mpi@2.1"))
assert not any(s.intersects("mpich2@:1.1") for s in repo.providers_for("mpi@2.2"))
assert not any(s.intersects("mpich@:1") for s in repo.providers_for("mpi@2"))
assert not any(s.intersects("mpich@:1") for s in repo.providers_for("mpi@3"))
assert not any(s.intersects("mpich2") for s in repo.providers_for("mpi@3"))
def test_provides_handles_multiple_providers_of_same_version(self):
""" """
@ -1462,7 +1462,7 @@ def test_concrete_specs_are_not_modified_on_reuse(
with spack.config.override("concretizer:reuse", True):
s = spack.spec.Spec(spec_str).concretized()
assert s.installed is expect_installed
assert s.satisfies(spec_str, strict=True)
assert s.satisfies(spec_str)
@pytest.mark.regression("26721,19736")
def test_sticky_variant_in_package(self):

View File

@ -157,7 +157,9 @@ def latest_commit():
return git("rev-list", "-n1", "HEAD", output=str, error=str).strip()
# Add two commits on main branch
write_file(filename, "[]")
# A commit without a previous version counts as "0"
write_file(filename, "[0]")
git("add", filename)
commit("first commit")
commits.append(latest_commit())

View File

@ -86,13 +86,13 @@ def test_env_change_spec_in_definition(tmpdir, mock_packages, config, mutable_mo
e.concretize()
e.write()
assert any(x.satisfies("mpileaks@2.1%gcc") for x in e.user_specs)
assert any(x.intersects("mpileaks@2.1%gcc") for x in e.user_specs)
e.change_existing_spec(spack.spec.Spec("mpileaks@2.2"), list_name="desired_specs")
e.write()
assert any(x.satisfies("mpileaks@2.2%gcc") for x in e.user_specs)
assert not any(x.satisfies("mpileaks@2.1%gcc") for x in e.user_specs)
assert any(x.intersects("mpileaks@2.2%gcc") for x in e.user_specs)
assert not any(x.intersects("mpileaks@2.1%gcc") for x in e.user_specs)
def test_env_change_spec_in_matrix_raises_error(

View File

@ -16,6 +16,12 @@
from spack.version import VersionChecksumError
def pkg_factory(name):
"""Return a package object tied to an abstract spec"""
pkg_cls = spack.repo.path.get_pkg_class(name)
return pkg_cls(Spec(name))
@pytest.mark.usefixtures("config", "mock_packages")
class TestPackage(object):
def test_load_package(self):
@ -184,8 +190,7 @@ def test_url_for_version_with_only_overrides_with_gaps(mock_packages, config):
)
def test_fetcher_url(spec_str, expected_type, expected_url):
"""Ensure that top-level git attribute can be used as a default."""
s = Spec(spec_str).concretized()
fetcher = spack.fetch_strategy.for_package_version(s.package, "1.0")
fetcher = spack.fetch_strategy.for_package_version(pkg_factory(spec_str), "1.0")
assert isinstance(fetcher, expected_type)
assert fetcher.url == expected_url
@ -204,8 +209,7 @@ def test_fetcher_url(spec_str, expected_type, expected_url):
def test_fetcher_errors(spec_str, version_str, exception_type):
"""Verify that we can't extrapolate versions for non-URL packages."""
with pytest.raises(exception_type):
s = Spec(spec_str).concretized()
spack.fetch_strategy.for_package_version(s.package, version_str)
spack.fetch_strategy.for_package_version(pkg_factory(spec_str), version_str)
@pytest.mark.usefixtures("mock_packages", "config")
@ -220,11 +224,12 @@ def test_fetcher_errors(spec_str, version_str, exception_type):
)
def test_git_url_top_level_url_versions(version_str, expected_url, digest):
"""Test URL fetch strategy inference when url is specified with git."""
s = Spec("git-url-top-level").concretized()
# leading 62 zeros of sha256 hash
leading_zeros = "0" * 62
fetcher = spack.fetch_strategy.for_package_version(s.package, version_str)
fetcher = spack.fetch_strategy.for_package_version(
pkg_factory("git-url-top-level"), version_str
)
assert isinstance(fetcher, spack.fetch_strategy.URLFetchStrategy)
assert fetcher.url == expected_url
assert fetcher.digest == leading_zeros + digest
@ -245,9 +250,9 @@ def test_git_url_top_level_url_versions(version_str, expected_url, digest):
)
def test_git_url_top_level_git_versions(version_str, tag, commit, branch):
"""Test git fetch strategy inference when url is specified with git."""
s = Spec("git-url-top-level").concretized()
fetcher = spack.fetch_strategy.for_package_version(s.package, version_str)
fetcher = spack.fetch_strategy.for_package_version(
pkg_factory("git-url-top-level"), version_str
)
assert isinstance(fetcher, spack.fetch_strategy.GitFetchStrategy)
assert fetcher.url == "https://example.com/some/git/repo"
assert fetcher.tag == tag
@ -259,9 +264,8 @@ def test_git_url_top_level_git_versions(version_str, tag, commit, branch):
@pytest.mark.parametrize("version_str", ["1.0", "1.1", "1.2", "1.3"])
def test_git_url_top_level_conflicts(version_str):
"""Test git fetch strategy inference when url is specified with git."""
s = Spec("git-url-top-level").concretized()
with pytest.raises(spack.fetch_strategy.FetcherConflict):
spack.fetch_strategy.for_package_version(s.package, version_str)
spack.fetch_strategy.for_package_version(pkg_factory("git-url-top-level"), version_str)
def test_rpath_args(mutable_database):
@ -301,9 +305,8 @@ def test_bundle_patch_directive(mock_directive_bundle, clear_directive_functions
)
def test_fetch_options(version_str, digest_end, extra_options):
"""Test fetch options inference."""
s = Spec("fetch-options").concretized()
leading_zeros = "000000000000000000000000000000"
fetcher = spack.fetch_strategy.for_package_version(s.package, version_str)
fetcher = spack.fetch_strategy.for_package_version(pkg_factory("fetch-options"), version_str)
assert isinstance(fetcher, spack.fetch_strategy.URLFetchStrategy)
assert fetcher.digest == leading_zeros + digest_end
assert fetcher.extra_options == extra_options

File diff suppressed because it is too large Load Diff

View File

@ -1040,18 +1040,44 @@ def test_compare_abstract_specs():
assert a <= b or b < a
def test_git_ref_spec_equivalences(mock_packages):
spec_hash_fmt = "develop-branch-version@git.{hash}=develop"
s1 = SpecParser(spec_hash_fmt.format(hash="a" * 40)).next_spec()
s2 = SpecParser(spec_hash_fmt.format(hash="b" * 40)).next_spec()
s3 = SpecParser("develop-branch-version@git.0.2.15=develop").next_spec()
s_no_git = SpecParser("develop-branch-version@develop").next_spec()
@pytest.mark.parametrize(
"lhs_str,rhs_str,expected",
[
# Git shasum vs generic develop
(
f"develop-branch-version@git.{'a' * 40}=develop",
"develop-branch-version@develop",
(True, True, False),
),
# Two different shasums
(
f"develop-branch-version@git.{'a' * 40}=develop",
f"develop-branch-version@git.{'b' * 40}=develop",
(False, False, False),
),
# Git shasum vs. git tag
(
f"develop-branch-version@git.{'a' * 40}=develop",
"develop-branch-version@git.0.2.15=develop",
(False, False, False),
),
# Git tag vs. generic develop
(
"develop-branch-version@git.0.2.15=develop",
"develop-branch-version@develop",
(True, True, False),
),
],
)
def test_git_ref_spec_equivalences(mock_packages, lhs_str, rhs_str, expected):
lhs = SpecParser(lhs_str).next_spec()
rhs = SpecParser(rhs_str).next_spec()
intersect, lhs_sat_rhs, rhs_sat_lhs = expected
assert s1.satisfies(s_no_git)
assert s2.satisfies(s_no_git)
assert not s_no_git.satisfies(s1)
assert not s2.satisfies(s1)
assert not s3.satisfies(s1)
assert lhs.intersects(rhs) is intersect
assert rhs.intersects(lhs) is intersect
assert lhs.satisfies(rhs) is lhs_sat_rhs
assert rhs.satisfies(lhs) is rhs_sat_lhs
@pytest.mark.regression("32471")

View File

@ -638,11 +638,11 @@ def test_satisfies_and_constrain(self):
b["foobar"] = SingleValuedVariant("foobar", "fee")
b["shared"] = BoolValuedVariant("shared", True)
assert not a.satisfies(b)
assert b.satisfies(a)
assert a.intersects(b)
assert b.intersects(a)
assert not a.satisfies(b, strict=True)
assert not b.satisfies(a, strict=True)
assert not a.satisfies(b)
assert not b.satisfies(a)
# foo=bar,baz foobar=fee feebar=foo shared=True
c = VariantMap(None)

View File

@ -600,6 +600,7 @@ def test_versions_from_git(git, mock_git_version_info, monkeypatch, mock_package
with working_dir(repo_path):
git("checkout", commit)
with open(os.path.join(repo_path, filename), "r") as f:
expected = f.read()
@ -607,30 +608,38 @@ def test_versions_from_git(git, mock_git_version_info, monkeypatch, mock_package
@pytest.mark.skipif(sys.platform == "win32", reason="Not supported on Windows (yet)")
def test_git_hash_comparisons(mock_git_version_info, install_mockery, mock_packages, monkeypatch):
@pytest.mark.parametrize(
"commit_idx,expected_satisfies,expected_not_satisfies",
[
# Spec based on earliest commit
(-1, ("@:0",), ("@1.0",)),
# Spec based on second commit (same as version 1.0)
(-2, ("@1.0",), ("@1.1:",)),
# Spec based on 4th commit (in timestamp order)
(-4, ("@1.1", "@1.0:1.2"), tuple()),
],
)
def test_git_hash_comparisons(
mock_git_version_info,
install_mockery,
mock_packages,
monkeypatch,
commit_idx,
expected_satisfies,
expected_not_satisfies,
):
"""Check that hashes compare properly to versions"""
repo_path, filename, commits = mock_git_version_info
monkeypatch.setattr(
spack.package_base.PackageBase, "git", "file://%s" % repo_path, raising=False
)
# Spec based on earliest commit
spec0 = spack.spec.Spec("git-test-commit@%s" % commits[-1])
spec0.concretize()
assert spec0.satisfies("@:0")
assert not spec0.satisfies("@1.0")
spec = spack.spec.Spec(f"git-test-commit@{commits[commit_idx]}").concretized()
for item in expected_satisfies:
assert spec.satisfies(item)
# Spec based on second commit (same as version 1.0)
spec1 = spack.spec.Spec("git-test-commit@%s" % commits[-2])
spec1.concretize()
assert spec1.satisfies("@1.0")
assert not spec1.satisfies("@1.1:")
# Spec based on 4th commit (in timestamp order)
spec4 = spack.spec.Spec("git-test-commit@%s" % commits[-4])
spec4.concretize()
assert spec4.satisfies("@1.1")
assert spec4.satisfies("@1.0:1.2")
for item in expected_not_satisfies:
assert not spec.satisfies(item)
@pytest.mark.skipif(sys.platform == "win32", reason="Not supported on Windows (yet)")
@ -738,3 +747,27 @@ def test_git_ref_can_be_assigned_a_version(vstring, eq_vstring, is_commit):
assert v.is_ref
assert not v._ref_lookup
assert v_equivalent.version == v.ref_version
@pytest.mark.parametrize(
"lhs_str,rhs_str,expected",
[
# VersionBase
("4.7.3", "4.7.3", (True, True, True)),
("4.7.3", "4.7", (True, True, False)),
("4.7.3", "4", (True, True, False)),
("4.7.3", "4.8", (False, False, False)),
# GitVersion
(f"git.{'a' * 40}=develop", "develop", (True, True, False)),
(f"git.{'a' * 40}=develop", f"git.{'a' * 40}=develop", (True, True, True)),
(f"git.{'a' * 40}=develop", f"git.{'b' * 40}=develop", (False, False, False)),
],
)
def test_version_intersects_satisfies_semantic(lhs_str, rhs_str, expected):
lhs, rhs = ver(lhs_str), ver(rhs_str)
intersect, lhs_sat_rhs, rhs_sat_lhs = expected
assert lhs.intersects(rhs) is intersect
assert lhs.intersects(rhs) is rhs.intersects(lhs)
assert lhs.satisfies(rhs) is lhs_sat_rhs
assert rhs.satisfies(lhs) is rhs_sat_lhs

View File

@ -178,7 +178,7 @@ def visit_FunctionDef(self, func):
conditions.append(None)
else:
# Check statically whether spec satisfies the condition
conditions.append(self.spec.satisfies(cond_spec, strict=True))
conditions.append(self.spec.satisfies(cond_spec))
except AttributeError:
# In this case the condition for the 'when' decorator is

View File

@ -203,8 +203,7 @@ def implicit_variant_conversion(method):
@functools.wraps(method)
def convert(self, other):
# We don't care if types are different as long as I can convert
# other to type(self)
# We don't care if types are different as long as I can convert other to type(self)
try:
other = type(self)(other.name, other._original_value)
except (error.SpecError, ValueError):
@ -349,7 +348,12 @@ def satisfies(self, other):
# (`foo=bar` will never satisfy `baz=bar`)
return other.name == self.name
@implicit_variant_conversion
def intersects(self, other):
"""Returns True if there are variant matching both self and other, False otherwise."""
if isinstance(other, (SingleValuedVariant, BoolValuedVariant)):
return other.intersects(self)
return other.name == self.name
def compatible(self, other):
"""Returns True if self and other are compatible, False otherwise.
@ -364,7 +368,7 @@ def compatible(self, other):
"""
# If names are different then `self` is not compatible with `other`
# (`foo=bar` is incompatible with `baz=bar`)
return other.name == self.name
return self.intersects(other)
@implicit_variant_conversion
def constrain(self, other):
@ -475,6 +479,9 @@ def satisfies(self, other):
self.value == other.value or other.value == "*" or self.value == "*"
)
def intersects(self, other):
return self.satisfies(other)
def compatible(self, other):
return self.satisfies(other)
@ -575,29 +582,11 @@ def substitute(self, vspec):
# Set the item
super(VariantMap, self).__setitem__(vspec.name, vspec)
def satisfies(self, other, strict=False):
"""Returns True if this VariantMap is more constrained than other,
False otherwise.
def satisfies(self, other):
return all(k in self and self[k].satisfies(other[k]) for k in other)
Args:
other (VariantMap): VariantMap instance to satisfy
strict (bool): if True return False if a key is in other and
not in self, otherwise discard that key and proceed with
evaluation
Returns:
bool: True or False
"""
to_be_checked = [k for k in other]
strict_or_concrete = strict
if self.spec is not None:
strict_or_concrete |= self.spec._concrete
if not strict_or_concrete:
to_be_checked = filter(lambda x: x in self, to_be_checked)
return all(k in self and self[k].satisfies(other[k]) for k in to_be_checked)
def intersects(self, other):
return all(self[k].intersects(other[k]) for k in other if k in self)
def constrain(self, other):
"""Add all variants in other that aren't in self to self. Also

View File

@ -133,6 +133,9 @@ def __hash__(self):
def __str__(self):
return self.data
def __repr__(self):
return f"VersionStrComponent('{self.data}')"
def __eq__(self, other):
if isinstance(other, VersionStrComponent):
return self.data == other.data
@ -242,9 +245,9 @@ def __init__(self, string: str) -> None:
if string and not VALID_VERSION.match(string):
raise ValueError("Bad characters in version string: %s" % string)
self.separators, self.version = self._generate_seperators_and_components(string)
self.separators, self.version = self._generate_separators_and_components(string)
def _generate_seperators_and_components(self, string):
def _generate_separators_and_components(self, string):
segments = SEGMENT_REGEX.findall(string)
components = tuple(int(m[0]) if m[0] else VersionStrComponent(m[1]) for m in segments)
separators = tuple(m[2] for m in segments)
@ -348,11 +351,26 @@ def isdevelop(self):
return False
@coerced
def satisfies(self, other):
"""A Version 'satisfies' another if it is at least as specific and has
a common prefix. e.g., we want gcc@4.7.3 to satisfy a request for
gcc@4.7 so that when a user asks to build with gcc@4.7, we can find
a suitable compiler.
def intersects(self, other: "VersionBase") -> bool:
"""Return True if self intersects with other, False otherwise.
Two versions intersect if one can be constrained by the other. For instance
@4.7 and @4.7.3 intersect (the intersection being @4.7.3).
Arg:
other: version to be checked for intersection
"""
n = min(len(self.version), len(other.version))
return self.version[:n] == other.version[:n]
@coerced
def satisfies(self, other: "VersionBase") -> bool:
"""Return True if self is at least as specific and share a common prefix with other.
For instance, @4.7.3 satisfies @4.7 but not vice-versa.
Arg:
other: version to be checked for intersection
"""
nself = len(self.version)
nother = len(other.version)
@ -466,9 +484,8 @@ def is_predecessor(self, other):
def is_successor(self, other):
return other.is_predecessor(self)
@coerced
def overlaps(self, other):
return self in other or other in self
return self.intersects(other)
@coerced
def union(self, other):
@ -548,7 +565,7 @@ def __init__(self, string):
if "=" in pruned_string:
self.ref, self.ref_version_str = pruned_string.split("=")
_, self.ref_version = self._generate_seperators_and_components(self.ref_version_str)
_, self.ref_version = self._generate_separators_and_components(self.ref_version_str)
self.user_supplied_reference = True
else:
self.ref = pruned_string
@ -578,6 +595,9 @@ def _cmp(self, other_lookups=None):
if ref_info:
prev_version, distance = ref_info
if prev_version is None:
prev_version = "0"
# Extend previous version by empty component and distance
# If commit is exactly a known version, no distance suffix
prev_tuple = VersionBase(prev_version).version if prev_version else ()
@ -587,14 +607,22 @@ def _cmp(self, other_lookups=None):
return self.version
@coerced
def intersects(self, other):
# If they are both references, they must match exactly
if self.is_ref and other.is_ref:
return self.version == other.version
# Otherwise the ref_version of the reference must intersect with the version of the other
v1 = self.ref_version if self.is_ref else self.version
v2 = other.ref_version if other.is_ref else other.version
n = min(len(v1), len(v2))
return v1[:n] == v2[:n]
@coerced
def satisfies(self, other):
"""A Version 'satisfies' another if it is at least as specific and has
a common prefix. e.g., we want gcc@4.7.3 to satisfy a request for
gcc@4.7 so that when a user asks to build with gcc@4.7, we can find
a suitable compiler. In the case of two GitVersions we require the ref_versions
to satisfy one another and the versions to be an exact match.
"""
# In the case of two GitVersions we require the ref_versions
# to satisfy one another and the versions to be an exact match.
self_cmp = self._cmp(other.ref_lookup)
other_cmp = other._cmp(self.ref_lookup)
@ -731,7 +759,7 @@ def __init__(self, start, end):
# means the range [1.2.3, 1.3), which is non-empty.
min_len = min(len(start), len(end))
if end.up_to(min_len) < start.up_to(min_len):
raise ValueError("Invalid Version range: %s" % self)
raise ValueError(f"Invalid Version range: {self}")
def lowest(self):
return self.start
@ -805,26 +833,32 @@ def __contains__(self, other):
)
return in_upper
@coerced
def satisfies(self, other):
"""
x.satisfies(y) in general means that x and y have a
non-zero intersection. For VersionRange this means they overlap.
def intersects(self, other) -> bool:
"""Return two if two version ranges overlap with each other, False otherwise.
`satisfies` is a commutative binary operator, meaning that
x.satisfies(y) if and only if y.satisfies(x).
This is a commutative operation.
Note: in some cases we have the keyword x.satisfies(y, strict=True)
to mean strict set inclusion, which is not commutative. However, this
lacks in VersionRange for unknown reasons.
Examples
Examples:
- 1:3 satisfies 2:4, as their intersection is 2:3.
- 1:2 does not satisfy 3:4, as their intersection is empty.
- 4.5:4.7 satisfies 4.7.2:4.8, as their intersection is 4.7.2:4.7
Args:
other: version range to be checked for intersection
"""
return self.overlaps(other)
@coerced
def satisfies(self, other):
"""A version range satisfies another if it is a subset of the other.
Examples:
- 1:2 does not satisfy 3:4, as their intersection is empty.
- 1:3 does not satisfy 2:4, as they overlap but neither is a subset of the other
- 1:3 satisfies 1:4.
"""
return self.intersection(other) == self
@coerced
def overlaps(self, other):
return (
@ -882,34 +916,33 @@ def union(self, other):
@coerced
def intersection(self, other):
if self.overlaps(other):
if self.start is None:
start = other.start
else:
start = self.start
if other.start is not None:
if other.start > start or other.start in start:
start = other.start
if self.end is None:
end = other.end
else:
end = self.end
# TODO: does this make sense?
# This is tricky:
# 1.6.5 in 1.6 = True (1.6.5 is more specific)
# 1.6 < 1.6.5 = True (lexicographic)
# Should 1.6 NOT be less than 1.6.5? Hmm.
# Here we test (not end in other.end) first to avoid paradox.
if other.end is not None and end not in other.end:
if other.end < end or other.end in end:
end = other.end
return VersionRange(start, end)
else:
if not self.overlaps(other):
return VersionList()
if self.start is None:
start = other.start
else:
start = self.start
if other.start is not None:
if other.start > start or other.start in start:
start = other.start
if self.end is None:
end = other.end
else:
end = self.end
# TODO: does this make sense?
# This is tricky:
# 1.6.5 in 1.6 = True (1.6.5 is more specific)
# 1.6 < 1.6.5 = True (lexicographic)
# Should 1.6 NOT be less than 1.6.5? Hmm.
# Here we test (not end in other.end) first to avoid paradox.
if other.end is not None and end not in other.end:
if other.end < end or other.end in end:
end = other.end
return VersionRange(start, end)
def __hash__(self):
return hash((self.start, self.end))
@ -1022,6 +1055,9 @@ def overlaps(self, other):
o += 1
return False
def intersects(self, other):
return self.overlaps(other)
def to_dict(self):
"""Generate human-readable dict for YAML."""
if self.concrete:
@ -1040,31 +1076,10 @@ def from_dict(dictionary):
raise ValueError("Dict must have 'version' or 'versions' in it.")
@coerced
def satisfies(self, other, strict=False):
"""A VersionList satisfies another if some version in the list
would satisfy some version in the other list. This uses
essentially the same algorithm as overlaps() does for
VersionList, but it calls satisfies() on member Versions
and VersionRanges.
If strict is specified, this version list must lie entirely
*within* the other in order to satisfy it.
"""
if not other or not self:
return False
if strict:
return self in other
s = o = 0
while s < len(self) and o < len(other):
if self[s].satisfies(other[o]):
return True
elif self[s] < other[o]:
s += 1
else:
o += 1
return False
def satisfies(self, other) -> bool:
# This exploits the fact that version lists are "reduced" and normalized, so we can
# never have a list like [1:3, 2:4] since that would be normalized to [1:4]
return all(any(lhs.satisfies(rhs) for rhs in other) for lhs in self)
@coerced
def update(self, other):

View File

@ -0,0 +1,18 @@
# Copyright 2013-2023 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
from spack.package import *
class V1Consumer(Package):
"""Mimic the real netlib-lapack, that may be built on top of an
optimized blas.
"""
homepage = "https://dev.null"
version("1.0")
depends_on("v2")
depends_on("v1")

View File

@ -575,7 +575,7 @@ def validate_detected_spec(cls, spec, extra_attributes):
"languages=d": "d",
"languages=fortran": "fortran",
}.items():
if spec.satisfies(constraint, strict=True):
if spec.satisfies(constraint):
msg = "{0} not in {1}"
assert key in compilers, msg.format(key, spec)

View File

@ -200,7 +200,7 @@ def install_pkgconfig(self):
# pkg-config generation is introduced in May 5, 2021.
# It must not be overwritten by spack-generated tbb.pc.
# https://github.com/oneapi-src/oneTBB/commit/478de5b1887c928e52f029d706af6ea640a877be
if self.spec.satisfies("@:2021.2.0", strict=True):
if self.spec.satisfies("@:2021.2.0"):
mkdirp(self.prefix.lib.pkgconfig)
with open(join_path(self.prefix.lib.pkgconfig, "tbb.pc"), "w") as f:
@ -296,7 +296,7 @@ def install(self, pkg, spec, prefix):
# install debug libs if they exist
install(join_path("build", "*debug", lib_name + "_debug.*"), prefix.lib)
if spec.satisfies("@2017.8,2018.1:", strict=True):
if spec.satisfies("@2017.8,2018.1:"):
# Generate and install the CMake Config file.
cmake_args = (
"-DTBB_ROOT={0}".format(prefix),

View File

@ -21,7 +21,7 @@ class Macsio(CMakePackage):
version("1.0", sha256="1dd0df28f9f31510329d5874c1519c745b5c6bec12e102cea3e9f4b05e5d3072")
variant("mpi", default=True, description="Build MPI plugin")
variant("silo", default=True, description="Build with SILO plugin")
variant("silo", default=False, description="Build with SILO plugin")
# TODO: multi-level variants for hdf5
variant("hdf5", default=False, description="Build HDF5 plugin")
variant("zfp", default=False, description="Build HDF5 with ZFP compression")

View File

@ -954,7 +954,7 @@ def configure_args(self):
config_args.extend(self.with_or_without("schedulers"))
config_args.extend(self.enable_or_disable("memchecker"))
if spec.satisfies("+memchecker", strict=True):
if spec.satisfies("+memchecker"):
config_args.extend(["--enable-debug"])
# Package dependencies

View File

@ -352,7 +352,7 @@ def flag_handler(self, name, flags):
# Fix for following issues for python with aocc%3.2.0:
# https://github.com/spack/spack/issues/29115
# https://github.com/spack/spack/pull/28708
if self.spec.satisfies("%aocc@3.2.0", strict=True):
if self.spec.satisfies("%aocc@3.2.0"):
if name == "cflags":
flags.extend(["-mllvm", "-disable-indvar-simplify=true"])
@ -473,7 +473,7 @@ def configure_args(self):
config_args.append("--with-lto")
config_args.append("--with-computed-gotos")
if spec.satisfies("@3.7 %intel", strict=True):
if spec.satisfies("@3.7 %intel"):
config_args.append("--with-icc={0}".format(spack_cc))
if "+debug" in spec: