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>
This commit is contained in:
parent
21349a4d25
commit
8ef67e2b15
6 changed files with 232 additions and 0 deletions
|
@ -18,6 +18,9 @@ class SpecHashDescriptor(object):
|
||||||
|
|
||||||
We currently use different hashes for different use cases.
|
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):
|
def __init__(self, deptype=('link', 'run'), package_hash=False, attr=None):
|
||||||
self.deptype = dp.canonical_deptype(deptype)
|
self.deptype = dp.canonical_deptype(deptype)
|
||||||
self.package_hash = package_hash
|
self.package_hash = package_hash
|
||||||
|
|
|
@ -1088,6 +1088,12 @@ def __init__(self, spec_like=None,
|
||||||
# external specs. None signal that it was not set yet.
|
# external specs. None signal that it was not set yet.
|
||||||
self.extra_attributes = None
|
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):
|
if isinstance(spec_like, six.string_types):
|
||||||
spec_list = SpecParser(self).parse(spec_like)
|
spec_list = SpecParser(self).parse(spec_like)
|
||||||
if len(spec_list) > 1:
|
if len(spec_list) > 1:
|
||||||
|
@ -1302,6 +1308,13 @@ def concrete(self):
|
||||||
"""
|
"""
|
||||||
return self._concrete
|
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):
|
def traverse(self, **kwargs):
|
||||||
direction = kwargs.get('direction', 'children')
|
direction = kwargs.get('direction', 'children')
|
||||||
depth = kwargs.get('depth', False)
|
depth = kwargs.get('depth', False)
|
||||||
|
@ -2551,7 +2564,13 @@ def _mark_concrete(self, value=True):
|
||||||
Only for internal use -- client code should use "concretize"
|
Only for internal use -- client code should use "concretize"
|
||||||
unless there is a need to force a spec to be concrete.
|
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():
|
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)
|
s._mark_root_concrete(value)
|
||||||
|
|
||||||
def concretized(self, tests=False):
|
def concretized(self, tests=False):
|
||||||
|
@ -3365,6 +3384,7 @@ def _dup(self, other, deps=True, cleardeps=True, caches=None):
|
||||||
self.compiler_flags = other.compiler_flags.copy()
|
self.compiler_flags = other.compiler_flags.copy()
|
||||||
self.compiler_flags.spec = self
|
self.compiler_flags.spec = self
|
||||||
self.variants = other.variants.copy()
|
self.variants = other.variants.copy()
|
||||||
|
self._build_spec = other._build_spec
|
||||||
|
|
||||||
# FIXME: we manage _patches_in_order_of_appearance specially here
|
# FIXME: we manage _patches_in_order_of_appearance specially here
|
||||||
# to keep it from leaking out of spec.py, but we should figure
|
# to keep it from leaking out of spec.py, but we should figure
|
||||||
|
@ -4225,6 +4245,89 @@ def target(self):
|
||||||
# to give to the attribute the appropriate comparison semantic
|
# to give to the attribute the appropriate comparison semantic
|
||||||
return self.architecture.target.microarchitecture
|
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):
|
class LazySpecCache(collections.defaultdict):
|
||||||
"""Cache for Specs that uses a spec_like as key, and computes lazily
|
"""Cache for Specs that uses a spec_like as key, and computes lazily
|
||||||
|
|
|
@ -985,6 +985,74 @@ def test_forwarding_of_architecture_attributes(self):
|
||||||
assert 'avx512' not in spec.target
|
assert 'avx512' not in spec.target
|
||||||
assert spec.target < 'broadwell'
|
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', [
|
@pytest.mark.parametrize('spec,constraint,expected_result', [
|
||||||
('libelf target=haswell', 'target=broadwell', False),
|
('libelf target=haswell', 'target=broadwell', False),
|
||||||
('libelf target=haswell', 'target=haswell', True),
|
('libelf target=haswell', 'target=haswell', True),
|
||||||
|
|
22
var/spack/repos/builtin.mock/packages/splice-h/package.py
Normal file
22
var/spack/repos/builtin.mock/packages/splice-h/package.py
Normal file
|
@ -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')
|
18
var/spack/repos/builtin.mock/packages/splice-t/package.py
Normal file
18
var/spack/repos/builtin.mock/packages/splice-t/package.py
Normal file
|
@ -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')
|
18
var/spack/repos/builtin.mock/packages/splice-z/package.py
Normal file
18
var/spack/repos/builtin.mock/packages/splice-z/package.py
Normal file
|
@ -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')
|
Loading…
Reference in a new issue