summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNathan Hanford <8302958+nhanford@users.noreply.github.com>2021-02-23 13:56:00 -0800
committerGitHub <noreply@github.com>2021-02-23 13:56:00 -0800
commit8ef67e2b159e86332de2498561122f8b3ec3dd87 (patch)
tree167c5176fffb2639a51bfbd5d36b993db076d7a0
parent21349a4d25ea0e4c3022fde9fd244f1e3179ebfd (diff)
downloadspack-8ef67e2b159e86332de2498561122f8b3ec3dd87.tar.gz
spack-8ef67e2b159e86332de2498561122f8b3ec3dd87.tar.bz2
spack-8ef67e2b159e86332de2498561122f8b3ec3dd87.tar.xz
spack-8ef67e2b159e86332de2498561122f8b3ec3dd87.zip
New splice method in class Spec. (#20262)
* Spec.splice feature Construct a new spec with a dependency swapped out. Currently can only swap dependencies of the same name, and can only apply to concrete specs. This feature is not yet attached to any install functionality, but will eventually allow us to "rewire" a package to depend on a different set of dependencies. Docstring is reformatted for git below 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. Co-authored-by: Nathan Hanford <hanford1@llnl.gov>
-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')