diff options
Diffstat (limited to 'lib/spack/spack/spec.py')
-rw-r--r-- | lib/spack/spack/spec.py | 142 |
1 files changed, 122 insertions, 20 deletions
diff --git a/lib/spack/spack/spec.py b/lib/spack/spack/spec.py index 97ae6b020a..a16efd336c 100644 --- a/lib/spack/spack/spec.py +++ b/lib/spack/spack/spec.py @@ -110,7 +110,6 @@ __all__ = [ "UnsatisfiableDependencySpecError", "AmbiguousHashError", "InvalidHashError", - "NoSuchHashError", "RedundantSpecError", "SpecDeprecatedError", ] @@ -147,7 +146,7 @@ _separators = "[\\%s]" % "\\".join(color_formats.keys()) default_format = "{name}{@versions}" default_format += "{%compiler.name}{@compiler.versions}{compiler_flags}" -default_format += "{variants}{arch=architecture}" +default_format += "{variants}{arch=architecture}{/abstract_hash}" #: Regular expression to pull spec contents out of clearsigned signature #: file. @@ -1249,6 +1248,7 @@ class SpecBuildInterface(lang.ObjectWrapper): class Spec(object): #: Cache for spec's prefix, computed lazily in the corresponding property _prefix = None + abstract_hash = None @staticmethod def default_arch(): @@ -1556,7 +1556,7 @@ class Spec(object): def _add_dependency(self, spec: "Spec", *, deptypes: dp.DependencyArgument): """Called by the parser to add another spec as a dependency.""" - if spec.name not in self._dependencies: + if spec.name not in self._dependencies or not spec.name: self.add_dependency_edge(spec, deptypes=deptypes) return @@ -1618,6 +1618,10 @@ class Spec(object): ) @property + def anonymous(self): + return not self.name and not self.abstract_hash + + @property def root(self): """Follow dependent links and find the root of this spec's DAG. @@ -1825,6 +1829,73 @@ class Spec(object): """Get the first <bits> bits of the DAG hash as an integer type.""" return spack.util.hash.base32_prefix_bits(self.process_hash(), bits) + def _lookup_hash(self): + """Lookup just one spec with an abstract hash, returning a spec from the the environment, + store, or finally, binary caches.""" + import spack.environment + + matches = [] + active_env = spack.environment.active_environment() + + if active_env: + env_matches = active_env.get_by_hash(self.abstract_hash) or [] + matches = [m for m in env_matches if m._satisfies(self)] + if not matches: + db_matches = spack.store.db.get_by_hash(self.abstract_hash) or [] + matches = [m for m in db_matches if m._satisfies(self)] + if not matches: + query = spack.binary_distribution.BinaryCacheQuery(True) + remote_matches = query("/" + self.abstract_hash) or [] + matches = [m for m in remote_matches if m._satisfies(self)] + if not matches: + raise InvalidHashError(self, self.abstract_hash) + + if len(matches) != 1: + raise spack.spec.AmbiguousHashError( + f"Multiple packages specify hash beginning '{self.abstract_hash}'.", *matches + ) + + return matches[0] + + def lookup_hash(self): + """Given a spec with an abstract hash, return a copy of the spec with all properties and + dependencies by looking up the hash in the environment, store, or finally, binary caches. + This is non-destructive.""" + if self.concrete or not any(node.abstract_hash for node in self.traverse()): + return self + + spec = self.copy(deps=False) + # root spec is replaced + if spec.abstract_hash: + new = self._lookup_hash() + spec._dup(new) + return spec + + # Get dependencies that need to be replaced + for node in self.traverse(root=False): + if node.abstract_hash: + new = node._lookup_hash() + spec._add_dependency(new, deptypes=()) + + # reattach nodes that were not otherwise satisfied by new dependencies + for node in self.traverse(root=False): + if not any(n._satisfies(node) for n in spec.traverse()): + spec._add_dependency(node.copy(), deptypes=()) + + return spec + + def replace_hash(self): + """Given a spec with an abstract hash, attempt to populate all properties and dependencies + by looking up the hash in the environment, store, or finally, binary caches. + This is destructive.""" + + if not any(node for node in self.traverse(order="post") if node.abstract_hash): + return + + spec_by_hash = self.lookup_hash() + + self._dup(spec_by_hash) + def to_node_dict(self, hash=ht.dag_hash): """Create a dictionary representing the state of this Spec. @@ -2583,6 +2654,8 @@ class Spec(object): ) warnings.warn(msg) + self.replace_hash() + if not self.name: raise spack.error.SpecError("Attempting to concretize anonymous spec") @@ -2781,8 +2854,13 @@ class Spec(object): def _new_concretize(self, tests=False): import spack.solver.asp - if not self.name: - raise spack.error.SpecError("Spec has no name; cannot concretize an anonymous spec") + self.replace_hash() + + for node in self.traverse(): + if not node.name: + raise spack.error.SpecError( + f"Spec {node} has no name; cannot concretize an anonymous spec" + ) if self._concrete: return @@ -3365,6 +3443,11 @@ class Spec(object): raise spack.error.UnsatisfiableSpecError(self, other, "constrain a concrete spec") other = self._autospec(other) + if other.abstract_hash: + if not self.abstract_hash or other.abstract_hash.startswith(self.abstract_hash): + self.abstract_hash = other.abstract_hash + elif not self.abstract_hash.startswith(other.abstract_hash): + raise InvalidHashError(self, other.abstract_hash) if not (self.name == other.name or (not self.name) or (not other.name)): raise UnsatisfiableSpecNameError(self.name, other.name) @@ -3523,6 +3606,12 @@ class Spec(object): """ other = self._autospec(other) + lhs = self.lookup_hash() or self + rhs = other.lookup_hash() or other + + return lhs._intersects(rhs, deps) + + def _intersects(self, other: "Spec", deps: bool = True) -> bool: if other.concrete and self.concrete: return self.dag_hash() == other.dag_hash() @@ -3588,9 +3677,18 @@ class Spec(object): else: return True - def _intersects_dependencies(self, other): + def satisfies(self, other, deps=True): + """ + This checks constraints on common dependencies against each other. + """ other = self._autospec(other) + lhs = self.lookup_hash() or self + rhs = other.lookup_hash() or other + + return lhs._satisfies(rhs, deps=deps) + + def _intersects_dependencies(self, other): if not other._dependencies or not self._dependencies: # one spec *could* eventually satisfy the other return True @@ -3625,7 +3723,7 @@ class Spec(object): return True - def satisfies(self, other: "Spec", deps: bool = True) -> bool: + def _satisfies(self, other: "Spec", deps: bool = True) -> bool: """Return True if all concrete specs matching self also match other, otherwise False. Args: @@ -3770,6 +3868,7 @@ class Spec(object): and self.external_path != other.external_path and self.external_modules != other.external_modules and self.compiler_flags != other.compiler_flags + and self.abstract_hash != other.abstract_hash ) self._package = None @@ -3812,6 +3911,8 @@ class Spec(object): self._concrete = other._concrete + self.abstract_hash = other.abstract_hash + if self._concrete: self._dunder_hash = other._dunder_hash self._normal = other._normal @@ -4001,6 +4102,7 @@ class Spec(object): yield self.compiler yield self.compiler_flags yield self.architecture + yield self.abstract_hash # this is not present on older specs yield getattr(self, "_package_hash", None) @@ -4011,7 +4113,10 @@ class Spec(object): def _cmp_iter(self): """Lazily yield components of self for comparison.""" - for item in self._cmp_node(): + + cmp_spec = self.lookup_hash() or self + + for item in cmp_spec._cmp_node(): yield item # This needs to be in _cmp_iter so that no specs with different process hashes @@ -4022,10 +4127,10 @@ class Spec(object): # TODO: they exist for speed. We should benchmark whether it's really worth # TODO: having two types of hashing now that we use `json` instead of `yaml` for # TODO: spec hashing. - yield self.process_hash() if self.concrete else None + yield cmp_spec.process_hash() if cmp_spec.concrete else None def deps(): - for dep in sorted(itertools.chain.from_iterable(self._dependencies.values())): + for dep in sorted(itertools.chain.from_iterable(cmp_spec._dependencies.values())): yield dep.spec.name yield tuple(sorted(dep.deptypes)) yield hash(dep.spec) @@ -4146,7 +4251,7 @@ class Spec(object): raise SpecFormatSigilError(sig, "versions", attribute) elif sig == "%" and attribute not in ("compiler", "compiler.name"): raise SpecFormatSigilError(sig, "compilers", attribute) - elif sig == "/" and not re.match(r"hash(:\d+)?$", attribute): + elif sig == "/" and not re.match(r"(abstract_)?hash(:\d+)?$", attribute): raise SpecFormatSigilError(sig, "DAG hashes", attribute) elif sig == " arch=" and attribute not in ("architecture", "arch"): raise SpecFormatSigilError(sig, "the architecture", attribute) @@ -4266,7 +4371,9 @@ class Spec(object): return self.format(*args, **kwargs) def __str__(self): - sorted_nodes = [self] + sorted(self.traverse(root=False), key=lambda x: x.name) + sorted_nodes = [self] + sorted( + self.traverse(root=False), key=lambda x: x.name or x.abstract_hash + ) spec_str = " ^".join(d.format() for d in sorted_nodes) return spec_str.strip() @@ -5066,14 +5173,9 @@ class AmbiguousHashError(spack.error.SpecError): class InvalidHashError(spack.error.SpecError): def __init__(self, spec, hash): - super(InvalidHashError, self).__init__( - "The spec specified by %s does not match provided spec %s" % (hash, spec) - ) - - -class NoSuchHashError(spack.error.SpecError): - def __init__(self, hash): - super(NoSuchHashError, self).__init__("No installed spec matches the hash: '%s'" % hash) + msg = f"No spec with hash {hash} could be found to match {spec}." + msg += " Either the hash does not exist, or it does not match other spec constraints." + super(InvalidHashError, self).__init__(msg) class SpecFilenameError(spack.error.SpecError): |