summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/spack/spack/hash_types.py3
-rw-r--r--lib/spack/spack/spec.py103
-rw-r--r--lib/spack/spack/test/spec_semantics.py68
-rw-r--r--var/spack/repos/builtin.mock/packages/splice-h/package.py22
-rw-r--r--var/spack/repos/builtin.mock/packages/splice-t/package.py18
-rw-r--r--var/spack/repos/builtin.mock/packages/splice-z/package.py18
6 files changed, 232 insertions, 0 deletions
diff --git a/lib/spack/spack/hash_types.py b/lib/spack/spack/hash_types.py
index 0ad321dec6..554665e24a 100644
--- a/lib/spack/spack/hash_types.py
+++ b/lib/spack/spack/hash_types.py
@@ -18,6 +18,9 @@ class SpecHashDescriptor(object):
We currently use different hashes for different use cases.
"""
+
+ hash_types = ('_dag_hash', '_build_hash', '_full_hash')
+
def __init__(self, deptype=('link', 'run'), package_hash=False, attr=None):
self.deptype = dp.canonical_deptype(deptype)
self.package_hash = package_hash
diff --git a/lib/spack/spack/spec.py b/lib/spack/spack/spec.py
index 5986cf8e18..42be765a71 100644
--- a/lib/spack/spack/spec.py
+++ b/lib/spack/spack/spec.py
@@ -1088,6 +1088,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 +1308,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)
@@ -2551,7 +2564,13 @@ class Spec(object):
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
+ elif not value:
+ s.clear_cached_hashes()
s._mark_root_concrete(value)
def concretized(self, tests=False):
@@ -3365,6 +3384,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
@@ -4225,6 +4245,89 @@ 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)
+
class LazySpecCache(collections.defaultdict):
"""Cache for Specs that uses a spec_like as key, and computes lazily
diff --git a/lib/spack/spack/test/spec_semantics.py b/lib/spack/spack/test/spec_semantics.py
index 6aa3068da0..9fb5eaf2b6 100644
--- a/lib/spack/spack/test/spec_semantics.py
+++ b/lib/spack/spack/test/spec_semantics.py
@@ -985,6 +985,74 @@ class TestSpecSematics(object):
assert 'avx512' not in spec.target
assert spec.target < 'broadwell'
+ @pytest.mark.parametrize('transitive', [True, False])
+ def test_splice(self, transitive):
+ # Tests the new splice function in Spec using a somewhat simple case
+ # with a variant with a conditional dependency.
+ # TODO: Test being able to splice in different provider for a virtual.
+ # Example: mvapich for mpich.
+ spec = Spec('splice-t')
+ dep = Spec('splice-h+foo')
+ spec.concretize()
+ dep.concretize()
+ # Sanity checking that these are not the same thing.
+ assert dep.dag_hash() != spec['splice-h'].dag_hash()
+ assert dep.build_hash() != spec['splice-h'].build_hash()
+ # Do the splice.
+ out = spec.splice(dep, transitive)
+ # Returned spec should still be concrete.
+ assert out.concrete
+ # Traverse the spec and assert that all dependencies are accounted for.
+ for node in spec.traverse():
+ assert node.name in out
+ # If the splice worked, then the full hash of the spliced dep should
+ # now match the full hash of the build spec of the dependency from the
+ # returned spec.
+ out_h_build = out['splice-h'].build_spec
+ assert out_h_build.full_hash() == dep.full_hash()
+ # Transitivity should determine whether the transitive dependency was
+ # changed.
+ expected_z = dep['splice-z'] if transitive else spec['splice-z']
+ assert out['splice-z'].full_hash() == expected_z.full_hash()
+ # Sanity check build spec of out should be the original spec.
+ assert (out['splice-t'].build_spec.full_hash() ==
+ spec['splice-t'].full_hash())
+ # Finally, the spec should know it's been spliced:
+ assert out.spliced
+
+ @pytest.mark.parametrize('transitive', [True, False])
+ def test_splice_input_unchanged(self, transitive):
+ spec = Spec('splice-t').concretized()
+ dep = Spec('splice-h+foo').concretized()
+ orig_spec_hash = spec.full_hash()
+ orig_dep_hash = dep.full_hash()
+ spec.splice(dep, transitive)
+ # Post-splice, dag hash should still be different; no changes should be
+ # made to these specs.
+ assert spec.full_hash() == orig_spec_hash
+ assert dep.full_hash() == orig_dep_hash
+
+ @pytest.mark.parametrize('transitive', [True, False])
+ def test_splice_subsequent(self, transitive):
+ spec = Spec('splice-t')
+ dep = Spec('splice-h+foo')
+ spec.concretize()
+ dep.concretize()
+ out = spec.splice(dep, transitive)
+ # Now we attempt a second splice.
+ dep = Spec('splice-z+bar')
+ dep.concretize()
+ # Transitivity shouldn't matter since Splice Z has no dependencies.
+ out2 = out.splice(dep, transitive)
+ assert out2.concrete
+ assert out2['splice-z'].build_hash() != spec['splice-z'].build_hash()
+ assert out2['splice-z'].build_hash() != out['splice-z'].build_hash()
+ assert out2['splice-z'].full_hash() != spec['splice-z'].full_hash()
+ assert out2['splice-z'].full_hash() != out['splice-z'].full_hash()
+ assert (out2['splice-t'].build_spec.full_hash() ==
+ spec['splice-t'].full_hash())
+ assert out2.spliced
+
@pytest.mark.parametrize('spec,constraint,expected_result', [
('libelf target=haswell', 'target=broadwell', False),
('libelf target=haswell', 'target=haswell', True),
diff --git a/var/spack/repos/builtin.mock/packages/splice-h/package.py b/var/spack/repos/builtin.mock/packages/splice-h/package.py
new file mode 100644
index 0000000000..79b91bc963
--- /dev/null
+++ b/var/spack/repos/builtin.mock/packages/splice-h/package.py
@@ -0,0 +1,22 @@
+# 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)
+
+from spack import *
+
+
+class SpliceH(AutotoolsPackage):
+ """Simple package with one optional dependency"""
+
+ homepage = "http://www.example.com"
+ url = "http://www.example.com/splice-h-1.0.tar.gz"
+
+ version('1.0', '0123456789abcdef0123456789abcdef')
+
+ variant('foo', default=False, description='nope')
+ variant('bar', default=False, description='nope')
+ variant('baz', default=False, description='nope')
+
+ depends_on('splice-z')
+ depends_on('splice-z+foo', when='+foo')
diff --git a/var/spack/repos/builtin.mock/packages/splice-t/package.py b/var/spack/repos/builtin.mock/packages/splice-t/package.py
new file mode 100644
index 0000000000..ec27fd28b6
--- /dev/null
+++ b/var/spack/repos/builtin.mock/packages/splice-t/package.py
@@ -0,0 +1,18 @@
+# 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)
+
+from spack import *
+
+
+class SpliceT(AutotoolsPackage):
+ """Simple package with one optional dependency"""
+
+ homepage = "http://www.example.com"
+ url = "http://www.example.com/splice-t-1.0.tar.gz"
+
+ version('1.0', '0123456789abcdef0123456789abcdef')
+
+ depends_on('splice-h')
+ depends_on('splice-z')
diff --git a/var/spack/repos/builtin.mock/packages/splice-z/package.py b/var/spack/repos/builtin.mock/packages/splice-z/package.py
new file mode 100644
index 0000000000..e28d359b66
--- /dev/null
+++ b/var/spack/repos/builtin.mock/packages/splice-z/package.py
@@ -0,0 +1,18 @@
+# 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)
+
+from spack import *
+
+
+class SpliceZ(AutotoolsPackage):
+ """Simple package with one optional dependency"""
+
+ homepage = "http://www.example.com"
+ url = "http://www.example.com/splice-z-1.0.tar.gz"
+
+ version('1.0', '0123456789abcdef0123456789abcdef')
+
+ variant('foo', default=False, description='nope')
+ variant('bar', default=False, description='nope')