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.py318
1 files changed, 217 insertions, 101 deletions
diff --git a/lib/spack/spack/spec.py b/lib/spack/spack/spec.py
index edb2c94300..609b20e238 100644
--- a/lib/spack/spack/spec.py
+++ b/lib/spack/spack/spec.py
@@ -1,4 +1,4 @@
-# Copyright 2013-2020 Lawrence Livermore National Security, LLC and other
+# Copyright 2013-2021 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)
@@ -76,10 +76,8 @@ thing. Spack uses ~variant in directory names and in the canonical form of
specs to avoid ambiguity. Both are provided because ~ can cause shell
expansion when it is the first character in an id typed on the command line.
"""
-import base64
import sys
import collections
-import hashlib
import itertools
import operator
import os
@@ -108,6 +106,7 @@ import spack.solver
import spack.store
import spack.util.crypto
import spack.util.executable
+import spack.util.hash
import spack.util.module_cmd as md
import spack.util.prefix
import spack.util.spack_json as sjson
@@ -203,7 +202,7 @@ def colorize_spec(spec):
return clr.colorize(re.sub(_separators, insert_color(), str(spec)) + '@.')
-@lang.key_ordering
+@lang.lazy_lexicographic_ordering
class ArchSpec(object):
def __init__(self, spec_or_platform_tuple=(None, None, None)):
""" Architecture specification a package should be built with.
@@ -252,8 +251,10 @@ class ArchSpec(object):
return spec_like
return ArchSpec(spec_like)
- def _cmp_key(self):
- return self.platform, self.os, self.target
+ def _cmp_iter(self):
+ yield self.platform
+ yield self.os
+ yield self.target
def _dup(self, other):
self.platform = other.platform
@@ -534,7 +535,7 @@ class ArchSpec(object):
return string in str(self) or string in self.target
-@lang.key_ordering
+@lang.lazy_lexicographic_ordering
class CompilerSpec(object):
"""The CompilerSpec field represents the compiler or range of compiler
versions that a package should be built with. CompilerSpecs have a
@@ -623,8 +624,9 @@ class CompilerSpec(object):
clone.versions = self.versions.copy()
return clone
- def _cmp_key(self):
- return (self.name, self.versions)
+ def _cmp_iter(self):
+ yield self.name
+ yield self.versions
def to_dict(self):
d = syaml.syaml_dict([('name', self.name)])
@@ -648,7 +650,7 @@ class CompilerSpec(object):
return str(self)
-@lang.key_ordering
+@lang.lazy_lexicographic_ordering
class DependencySpec(object):
"""DependencySpecs connect two nodes in the DAG, and contain deptypes.
@@ -686,10 +688,10 @@ class DependencySpec(object):
self.deptypes + dp.canonical_deptype(type)
)
- def _cmp_key(self):
- return (self.parent.name if self.parent else None,
- self.spec.name if self.spec else None,
- self.deptypes)
+ def _cmp_iter(self):
+ yield self.parent.name if self.parent else None
+ yield self.spec.name if self.spec else None
+ yield self.deptypes
def __str__(self):
return "%s %s--> %s" % (self.parent.name if self.parent else None,
@@ -747,8 +749,15 @@ class FlagMap(lang.HashableMap):
clone[name] = value
return clone
- def _cmp_key(self):
- return tuple((k, tuple(v)) for k, v in sorted(six.iteritems(self)))
+ def _cmp_iter(self):
+ for k, v in sorted(self.items()):
+ yield k
+
+ def flags():
+ for flag in v:
+ yield flag
+
+ yield flags
def __str__(self):
sorted_keys = [k for k in sorted(self.keys()) if self[k] != []]
@@ -1016,7 +1025,7 @@ class SpecBuildInterface(lang.ObjectWrapper):
)
-@lang.key_ordering
+@lang.lazy_lexicographic_ordering(set_hash=False)
class Spec(object):
#: Cache for spec's prefix, computed lazily in the corresponding property
@@ -1060,7 +1069,7 @@ class Spec(object):
self._hash = None
self._build_hash = None
- self._cmp_key_cache = None
+ self._dunder_hash = None
self._package = None
# Most of these are internal implementation details that can be
@@ -1088,6 +1097,12 @@ class Spec(object):
# external specs. None signal that it was not set yet.
self.extra_attributes = None
+ # This attribute holds the original build copy of the spec if it is
+ # deployed differently than it was built. None signals that the spec
+ # is deployed "as built."
+ # Build spec should be the actual build spec unless marked dirty.
+ self._build_spec = None
+
if isinstance(spec_like, six.string_types):
spec_list = SpecParser(self).parse(spec_like)
if len(spec_list) > 1:
@@ -1302,6 +1317,13 @@ class Spec(object):
"""
return self._concrete
+ @property
+ def spliced(self):
+ """Returns whether or not this Spec is being deployed as built i.e.
+ whether or not this Spec has ever been spliced.
+ """
+ return any(s.build_spec is not s for s in self.traverse(root=True))
+
def traverse(self, **kwargs):
direction = kwargs.get('direction', 'children')
depth = kwargs.get('depth', False)
@@ -1369,7 +1391,16 @@ class Spec(object):
cover = kwargs.get('cover', 'nodes')
direction = kwargs.get('direction', 'children')
order = kwargs.get('order', 'pre')
- deptype = dp.canonical_deptype(deptype)
+
+ # we don't want to run canonical_deptype every time through
+ # traverse, because it is somewhat expensive. This ensures we
+ # canonicalize only once.
+ canonical_deptype = kwargs.get("canonical_deptype", None)
+ if canonical_deptype is None:
+ deptype = dp.canonical_deptype(deptype)
+ kwargs["canonical_deptype"] = deptype
+ else:
+ deptype = canonical_deptype
# Make sure kwargs have legal values; raise ValueError if not.
def validate(name, val, allowed_values):
@@ -1473,13 +1504,7 @@ class Spec(object):
# this when we move to using package hashing on all specs.
node_dict = self.to_node_dict(hash=hash)
yaml_text = syaml.dump(node_dict, default_flow_style=True)
- sha = hashlib.sha1(yaml_text.encode('utf-8'))
- b32_hash = base64.b32encode(sha.digest()).lower()
-
- if sys.version_info[0] >= 3:
- b32_hash = b32_hash.decode('utf-8')
-
- return b32_hash
+ return spack.util.hash.b32_hash(yaml_text)
def _cached_hash(self, hash, length=None):
"""Helper function for storing a cached hash on the spec.
@@ -1535,7 +1560,7 @@ class Spec(object):
def dag_hash_bit_prefix(self, bits):
"""Get the first <bits> bits of the DAG hash as an integer type."""
- return base32_prefix_bits(self.dag_hash(), bits)
+ return spack.util.hash.base32_prefix_bits(self.dag_hash(), bits)
def to_node_dict(self, hash=ht.dag_hash):
"""Create a dictionary representing the state of this Spec.
@@ -1629,7 +1654,13 @@ class Spec(object):
d['patches'] = variant._patches_in_order_of_appearance
if hash.package_hash:
- d['package_hash'] = self.package.content_hash()
+ package_hash = self.package.content_hash()
+
+ # Full hashes are in bytes
+ if (not isinstance(package_hash, six.text_type)
+ and isinstance(package_hash, six.binary_type)):
+ package_hash = package_hash.decode('utf-8')
+ d['package_hash'] = package_hash
deps = self.dependencies_dict(deptype=hash.deptype)
if deps:
@@ -1786,10 +1817,12 @@ class Spec(object):
name = next(iter(node))
node = node[name]
- spec = Spec(name, full_hash=node.get('full_hash', None))
+ spec = Spec()
+ spec.name = name
spec.namespace = node.get('namespace', None)
spec._hash = node.get('hash', None)
spec._build_hash = node.get('build_hash', None)
+ spec._full_hash = node.get('full_hash', None)
if 'version' in node or 'versions' in node:
spec.versions = vn.VersionList.from_dict(node)
@@ -2263,6 +2296,8 @@ class Spec(object):
# If replacement is external then trim the dependencies
if replacement.external:
if (spec._dependencies):
+ for dep in spec.dependencies():
+ del dep._dependents[spec.name]
changed = True
spec._dependencies = DependencyMap()
replacement._dependencies = DependencyMap()
@@ -2536,17 +2571,27 @@ class Spec(object):
else:
self._old_concretize(tests)
+ def _mark_root_concrete(self, value=True):
+ """Mark just this spec (not dependencies) concrete."""
+ if (not value) and self.concrete and self.package.installed:
+ return
+ self._normal = value
+ self._concrete = value
+
def _mark_concrete(self, value=True):
"""Mark this spec and its dependencies as concrete.
Only for internal use -- client code should use "concretize"
unless there is a need to force a spec to be concrete.
"""
+ # if set to false, clear out all hashes (set to None or remove attr)
+ # may need to change references to respect None
for s in self.traverse():
if (not value) and s.concrete and s.package.installed:
continue
- s._normal = value
- s._concrete = value
+ elif not value:
+ s.clear_cached_hashes()
+ s._mark_root_concrete(value)
def concretized(self, tests=False):
"""This is a non-destructive version of concretize().
@@ -3274,7 +3319,7 @@ class Spec(object):
"""Return list of any virtual deps in this spec."""
return [spec for spec in self.traverse() if spec.virtual]
- @property
+ @property # type: ignore[misc] # decorated prop not supported in mypy
@lang.memoized
def patches(self):
"""Return patch objects for any patch sha256 sums on this Spec.
@@ -3320,7 +3365,7 @@ class Spec(object):
before possibly copying the dependencies of ``other`` onto
``self``
caches (bool or None): preserve cached fields such as
- ``_normal``, ``_hash``, and ``_cmp_key_cache``. By
+ ``_normal``, ``_hash``, and ``_dunder_hash``. By
default this is ``False`` if DAG structure would be
changed by the copy, ``True`` if it's an exact copy.
@@ -3357,6 +3402,7 @@ class Spec(object):
self.compiler_flags = other.compiler_flags.copy()
self.compiler_flags.spec = self
self.variants = other.variants.copy()
+ self._build_spec = other._build_spec
# FIXME: we manage _patches_in_order_of_appearance specially here
# to keep it from leaking out of spec.py, but we should figure
@@ -3393,13 +3439,13 @@ class Spec(object):
if caches:
self._hash = other._hash
self._build_hash = other._build_hash
- self._cmp_key_cache = other._cmp_key_cache
+ self._dunder_hash = other._dunder_hash
self._normal = other._normal
self._full_hash = other._full_hash
else:
self._hash = None
self._build_hash = None
- self._cmp_key_cache = None
+ self._dunder_hash = None
self._normal = False
self._full_hash = None
@@ -3518,18 +3564,17 @@ class Spec(object):
else:
return any(s.satisfies(spec) for s in self.traverse(root=False))
- def sorted_deps(self):
- """Return a list of all dependencies sorted by name."""
- deps = self.flat_dependencies()
- return tuple(deps[name] for name in sorted(deps))
+ def eq_dag(self, other, deptypes=True, vs=None, vo=None):
+ """True if the full dependency DAGs of specs are equal."""
+ if vs is None:
+ vs = set()
+ if vo is None:
+ vo = set()
- def _eq_dag(self, other, vs, vo, deptypes):
- """Recursive helper for eq_dag and ne_dag. Does the actual DAG
- traversal."""
vs.add(id(self))
vo.add(id(other))
- if self.ne_node(other):
+ if not self.eq_node(other):
return False
if len(self._dependencies) != len(other._dependencies):
@@ -3557,58 +3602,38 @@ class Spec(object):
continue
# Recursive check for equality
- if not s._eq_dag(o, vs, vo, deptypes):
+ if not s.eq_dag(o, deptypes, vs, vo):
return False
return True
- def eq_dag(self, other, deptypes=True):
- """True if the full dependency DAGs of specs are equal."""
- return self._eq_dag(other, set(), set(), deptypes)
-
- def ne_dag(self, other, deptypes=True):
- """True if the full dependency DAGs of specs are not equal."""
- return not self.eq_dag(other, set(), set(), deptypes)
-
def _cmp_node(self):
- """Comparison key for just *this node* and not its deps."""
- # Name or namespace None will lead to invalid comparisons for abstract
- # specs. Replace them with the empty string, which is not a valid spec
- # name nor namespace so it will not create spurious equalities.
- return (self.name or '',
- self.namespace or '',
- tuple(self.versions),
- self.variants,
- self.architecture,
- self.compiler,
- self.compiler_flags)
+ """Yield comparable elements of just *this node* and not its deps."""
+ yield self.name
+ yield self.namespace
+ yield self.versions
+ yield self.variants
+ yield self.compiler
+ yield self.compiler_flags
+ yield self.architecture
def eq_node(self, other):
"""Equality with another spec, not including dependencies."""
- return self._cmp_node() == other._cmp_node()
-
- def ne_node(self, other):
- """Inequality with another spec, not including dependencies."""
- return self._cmp_node() != other._cmp_node()
-
- def _cmp_key(self):
- """This returns a key for the spec *including* DAG structure.
-
- The key is the concatenation of:
- 1. A tuple describing this node in the DAG.
- 2. The hash of each of this node's dependencies' cmp_keys.
- """
- if self._cmp_key_cache:
- return self._cmp_key_cache
+ return (other is not None) and lang.lazy_eq(
+ self._cmp_node, other._cmp_node
+ )
- dep_tuple = tuple(
- (d.spec.name, hash(d.spec), tuple(sorted(d.deptypes)))
- for name, d in sorted(self._dependencies.items()))
+ def _cmp_iter(self):
+ """Lazily yield components of self for comparison."""
+ for item in self._cmp_node():
+ yield item
- key = (self._cmp_node(), dep_tuple)
- if self._concrete:
- self._cmp_key_cache = key
- return key
+ def deps():
+ for _, dep in sorted(self._dependencies.items()):
+ yield dep.spec.name
+ yield tuple(sorted(dep.deptypes))
+ yield hash(dep.spec)
+ yield deps
def colorized(self):
return colorize_spec(self)
@@ -3848,7 +3873,9 @@ class Spec(object):
'Format string terminated while reading attribute.'
'Missing terminating }.'
)
- return out.getvalue()
+
+ formatted_spec = out.getvalue()
+ return formatted_spec.strip()
def old_format(self, format_string='$_$@$%@+$+$=', **kwargs):
"""
@@ -4104,12 +4131,12 @@ class Spec(object):
kwargs.setdefault('color', None)
return self.format(*args, **kwargs)
- def dep_string(self):
- return ''.join(" ^" + dep.format() for dep in self.sorted_deps())
-
def __str__(self):
- ret = self.format() + self.dep_string()
- return ret.strip()
+ sorted_nodes = [self] + sorted(
+ self.traverse(root=False), key=lambda x: x.name
+ )
+ spec_str = " ^".join(d.format() for d in sorted_nodes)
+ return spec_str.strip()
def install_status(self):
"""Helper for tree to print DB install status."""
@@ -4217,6 +4244,105 @@ class Spec(object):
# to give to the attribute the appropriate comparison semantic
return self.architecture.target.microarchitecture
+ @property
+ def build_spec(self):
+ return self._build_spec or self
+
+ @build_spec.setter
+ def build_spec(self, value):
+ self._build_spec = value
+
+ def splice(self, other, transitive):
+ """Splices dependency "other" into this ("target") Spec, and return the
+ result as a concrete Spec.
+ If transitive, then other and its dependencies will be extrapolated to
+ a list of Specs and spliced in accordingly.
+ For example, let there exist a dependency graph as follows:
+ T
+ | \
+ Z<-H
+ In this example, Spec T depends on H and Z, and H also depends on Z.
+ Suppose, however, that we wish to use a differently-built H, known as
+ H'. This function will splice in the new H' in one of two ways:
+ 1. transitively, where H' depends on the Z' it was built with, and the
+ new T* also directly depends on this new Z', or
+ 2. intransitively, where the new T* and H' both depend on the original
+ Z.
+ Since the Spec returned by this splicing function is no longer deployed
+ the same way it was built, any such changes are tracked by setting the
+ build_spec to point to the corresponding dependency from the original
+ Spec.
+ TODO: Extend this for non-concrete Specs.
+ """
+ assert self.concrete
+ assert other.concrete
+ assert other.name in self
+
+ # Multiple unique specs with the same name will collide, so the
+ # _dependents of these specs should not be trusted.
+ # Variants may also be ignored here for now...
+
+ if transitive:
+ self_nodes = dict((s.name, s.copy(deps=False))
+ for s in self.traverse(root=True)
+ if s.name not in other)
+ other_nodes = dict((s.name, s.copy(deps=False))
+ for s in other.traverse(root=True))
+ else:
+ # If we're not doing a transitive splice, then we only want the
+ # root of other.
+ self_nodes = dict((s.name, s.copy(deps=False))
+ for s in self.traverse(root=True)
+ if s.name != other.name)
+ other_nodes = {other.name: other.copy(deps=False)}
+
+ nodes = other_nodes.copy()
+ nodes.update(self_nodes)
+
+ for name in nodes:
+ if name in self_nodes:
+ dependencies = self[name]._dependencies
+ for dep in dependencies:
+ nodes[name]._add_dependency(nodes[dep],
+ dependencies[dep].deptypes)
+ if any(dep not in self_nodes for dep in dependencies):
+ nodes[name].build_spec = self[name].build_spec
+ else:
+ dependencies = other[name]._dependencies
+ for dep in dependencies:
+ nodes[name]._add_dependency(nodes[dep],
+ dependencies[dep].deptypes)
+ if any(dep not in other_nodes for dep in dependencies):
+ nodes[name].build_spec = other[name].build_spec
+
+ # Clear cached hashes
+ nodes[self.name].clear_cached_hashes()
+ return nodes[self.name]
+
+ def clear_cached_hashes(self):
+ """
+ Clears all cached hashes in a Spec, while preserving other properties.
+ """
+ for attr in ht.SpecHashDescriptor.hash_types:
+ if hasattr(self, attr):
+ setattr(self, attr, None)
+
+ def __hash__(self):
+ # If the spec is concrete, we leverage the DAG hash and just use
+ # a 64-bit prefix of it. The DAG hash has the advantage that it's
+ # computed once per concrete spec, and it's saved -- so if we
+ # read concrete specs we don't need to recompute the whole hash.
+ # This is good for large, unchanging specs.
+ if self.concrete:
+ if not self._dunder_hash:
+ self._dunder_hash = self.dag_hash_bit_prefix(64)
+ return self._dunder_hash
+
+ # This is the normal hash for lazy_lexicographic_ordering. It's
+ # slow for large specs because it traverses the whole spec graph,
+ # so we hope it only runs on abstract specs, which are small.
+ return hash(lang.tuplify(self._cmp_iter))
+
class LazySpecCache(collections.defaultdict):
"""Cache for Specs that uses a spec_like as key, and computes lazily
@@ -4627,16 +4753,6 @@ def save_dependency_spec_yamls(
fd.write(dep_spec.to_yaml(hash=ht.build_hash))
-def base32_prefix_bits(hash_string, bits):
- """Return the first <bits> bits of a base32 string as an integer."""
- if bits > len(hash_string) * 5:
- raise ValueError("Too many bits! Requested %d bit prefix of '%s'."
- % (bits, hash_string))
-
- hash_bytes = base64.b32decode(hash_string, casefold=True)
- return spack.util.crypto.prefix_bits(hash_bytes, bits)
-
-
class SpecParseError(spack.error.SpecError):
"""Wrapper for ParseError for when we're parsing specs."""
def __init__(self, parse_error):