summaryrefslogtreecommitdiff
path: root/lib/spack/spack/spec.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/spack/spack/spec.py')
-rw-r--r--lib/spack/spack/spec.py142
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):