summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorJohn Gouwar <jgouwar@gmail.com>2024-11-12 23:51:19 -0500
committerGitHub <noreply@github.com>2024-11-12 20:51:19 -0800
commitbf16f0bf7424355976470cd4ae8813b9960b16fa (patch)
treeb5432a6352790cfc5c9aed5e368cd5d7e3eafa7e /lib
parentad518d975c711c04bdc013363d8fc33a212e9194 (diff)
downloadspack-bf16f0bf7424355976470cd4ae8813b9960b16fa.tar.gz
spack-bf16f0bf7424355976470cd4ae8813b9960b16fa.tar.bz2
spack-bf16f0bf7424355976470cd4ae8813b9960b16fa.tar.xz
spack-bf16f0bf7424355976470cd4ae8813b9960b16fa.zip
Add solver capability for synthesizing splices of ABI compatible packages. (#46729)
This PR provides complementary 2 features: 1. An augmentation to the package language to express ABI compatibility relationships among packages. 2. An extension to the concretizer that can synthesize splices between ABI compatible packages. 1. The `can_splice` directive and ABI compatibility We augment the package language with a single directive: `can_splice`. Here is an example of a package `Foo` exercising the `can_splice` directive: class Foo(Package): version("1.0") version("1.1") variant("compat", default=True) variant("json", default=False) variant("pic", default=False) can_splice("foo@1.0", when="@1.1") can_splice("bar@1.0", when="@1.0+compat") can_splice("baz@1.0+compat", when="@1.0+compat", match_variants="*") can_splice("quux@1.0", when=@1.1~compat", match_variants="json") Explanations of the uses of each directive: - `can_splice("foo@1.0", when="@1.1")`: If `foo@1.0` is the dependency of an already installed spec and `foo@1.1` could be a valid dependency for the parent spec, then `foo@1.1` can be spliced in for `foo@1.0` in the parent spec. - `can_splice("bar@1.0", when="@1.0+compat")`: If `bar@1.0` is the dependency of an already installed spec and `foo@1.0+compat` could be a valid dependency for the parent spec, then `foo@1.0+compat` can be spliced in for `bar@1.0+compat` in the parent spec - `can_splice("baz@1.0", when="@1.0+compat", match_variants="*")`: If `baz@1.0+compat` is the dependency of an already installed spec and `foo@1.0+compat` could be a valid dependency for the parent spec, then `foo@1.0+compat` can be spliced in for `baz@1.0+compat` in the parent spec, provided that they have the same value for all other variants (regardless of what those values are). - `can_splice("quux@1.0", when=@1.1~compat", match_variants="json")`:If `quux@1.0` is the dependency of an already installed spec and `foo@1.1~compat` could be a valid dependency for the parent spec, then `foo@1.0~compat` can be spliced in for `quux@1.0` in the parent spec, provided that they have the same value for their `json` variant. 2. Augmenting the solver to synthesize splices ### Changes to the hash encoding in `asp.py` Previously, when including concrete specs in the solve, they would have the following form: installed_hash("foo", "xxxyyy") imposed_constraint("xxxyyy", "foo", "attr1", ...) imposed_constraint("xxxyyy", "foo", "attr2", ...) % etc. Concrete specs now have the following form: installed_hash("foo", "xxxyyy") hash_attr("xxxyyy", "foo", "attr1", ...) hash_attr("xxxyyy", "foo", "attr2", ...) This transformation allows us to control which constraints are imposed when we select a hash, to facilitate the splicing of dependencies. 2.1 Compiling `can_splice` directives in `asp.py` Consider the concrete spec: foo@2.72%gcc@11.4 arch=linux-ubuntu22.04-icelake build_system=autotools ^bar ... It will emit the following facts for reuse (below is a subset) installed_hash("foo", "xxxyyy") hash_attr("xxxyyy", "hash", "foo", "xxxyyy") hash_attr("xxxyyy", "version", "foo", "2.72") hash_attr("xxxyyy", "node_os", "ubuntu22.04") hash_attr("xxxyyy", "hash", "bar", "zzzqqq") hash_attr("xxxyyy", "depends_on", "foo", "bar", "link") Rules that derive abi_splice_conditions_hold will be generated from use of the `can_splice` directive. They will have the following form: can_splice("foo@1.0.0+a", when="@1.0.1+a", match_variants=["b"]) ---> abi_splice_conditions_hold(0, node(SID, "foo"), "foo", BaseHash) :- installed_hash("foo", BaseHash), attr("node", node(SID, SpliceName)), attr("node_version_satisfies", node(SID, "foo"), "1.0.1"), hash_attr("hash", "node_version_satisfies", "foo", "1.0.1"), attr("variant_value", node(SID, "foo"), "a", "True"), hash_attr("hash", "variant_value", "foo", "a", "True"), attr("variant_value", node(SID, "foo"), "b", VariVar0), hash_attr("hash", "variant_value", "foo", "b", VariVar0). 2.2 Synthesizing splices in `concretize.lp` and `splices.lp` The ASP solver generates "splice_at_hash" attrs to indicate that a particular node has a splice in one of its immediate dependencies. Splices can be introduced in the dependencies of concrete specs when `splices.lp` is conditionally loaded (based on the config option `concretizer:splice:True`. 2.3 Constructing spliced specs in `asp.py` The method `SpecBuilder._resolve_splices` implements a top-down memoized implementation of hybrid splicing. This is an optimization over the more general `Spec.splice`, since the solver gives a global view of exactly which specs can be shared, to ensure the minimal number of splicing operations. Misc changes to facilitate configuration and benchmarking - Added the method `Solver.solve_with_stats` to expose timers from the public interface for easier benchmarking - Added the boolean config option `concretizer:splice` to conditionally load splicing behavior Co-authored-by: Greg Becker <becker33@llnl.gov>
Diffstat (limited to 'lib')
-rw-r--r--lib/spack/docs/build_settings.rst32
-rw-r--r--lib/spack/docs/packaging_guide.rst52
-rw-r--r--lib/spack/spack/directives.py38
-rw-r--r--lib/spack/spack/package_base.py1
-rw-r--r--lib/spack/spack/schema/concretizer.py3
-rw-r--r--lib/spack/spack/solver/asp.py202
-rw-r--r--lib/spack/spack/solver/concretize.lp71
-rw-r--r--lib/spack/spack/solver/core.py13
-rw-r--r--lib/spack/spack/solver/display.lp1
-rw-r--r--lib/spack/spack/solver/splices.lp56
-rw-r--r--lib/spack/spack/spec.py24
-rw-r--r--lib/spack/spack/test/abi_splicing.py234
-rw-r--r--lib/spack/spack/test/cmd/pkg.py14
-rw-r--r--lib/spack/spack/test/spec_semantics.py4
14 files changed, 689 insertions, 56 deletions
diff --git a/lib/spack/docs/build_settings.rst b/lib/spack/docs/build_settings.rst
index 97c81bf17a..bdad8c8a51 100644
--- a/lib/spack/docs/build_settings.rst
+++ b/lib/spack/docs/build_settings.rst
@@ -237,3 +237,35 @@ is optional -- by default, splices will be transitive.
``mpich/abcdef`` instead of ``mvapich2`` as the MPI provider. Spack
will warn the user in this case, but will not fail the
concretization.
+
+.. _automatic_splicing:
+
+^^^^^^^^^^^^^^^^^^
+Automatic Splicing
+^^^^^^^^^^^^^^^^^^
+
+The Spack solver can be configured to do automatic splicing for
+ABI-compatible packages. Automatic splices are enabled in the concretizer
+config section
+
+.. code-block:: yaml
+
+ concretizer:
+ splice:
+ automatic: True
+
+Packages can include ABI-compatibility information using the
+``can_splice`` directive. See :ref:`the packaging
+guide<abi_compatibility>` for instructions on specifying ABI
+compatibility using the ``can_splice`` directive.
+
+.. note::
+
+ The ``can_splice`` directive is experimental and may be changed in
+ future versions.
+
+When automatic splicing is enabled, the concretizer will combine any
+number of ABI-compatible specs if possible to reuse installed packages
+and packages available from binary caches. The end result of these
+specs is equivalent to a series of transitive/intransitive splices,
+but the series may be non-obvious.
diff --git a/lib/spack/docs/packaging_guide.rst b/lib/spack/docs/packaging_guide.rst
index d9a37175b6..87fb184c65 100644
--- a/lib/spack/docs/packaging_guide.rst
+++ b/lib/spack/docs/packaging_guide.rst
@@ -1267,7 +1267,7 @@ Git fetching supports the following parameters to ``version``:
This feature requires ``git`` to be version ``2.25.0`` or later but is useful for
large repositories that have separate portions that can be built independently.
If paths provided are directories then all the subdirectories and associated files
- will also be cloned.
+ will also be cloned.
Only one of ``tag``, ``branch``, or ``commit`` can be used at a time.
@@ -1367,8 +1367,8 @@ Submodules
git-submodule``.
Sparse-Checkout
- You can supply ``git_sparse_paths`` at the package or version level to utilize git's
- sparse-checkout feature. This will only clone the paths that are specified in the
+ You can supply ``git_sparse_paths`` at the package or version level to utilize git's
+ sparse-checkout feature. This will only clone the paths that are specified in the
``git_sparse_paths`` attribute for the package along with the files in the top level directory.
This feature allows you to only clone what you need from a large repository.
Note that this is a newer feature in git and requries git ``2.25.0`` or greater.
@@ -2392,7 +2392,7 @@ by the ``--jobs`` option:
.. code-block:: python
:emphasize-lines: 7, 11
:linenos:
-
+
class Xios(Package):
...
def install(self, spec, prefix):
@@ -5420,7 +5420,7 @@ by build recipes. Examples of checking :ref:`variant settings <variants>` and
determine whether it needs to also set up build dependencies (see
:ref:`test-build-tests`).
-The ``MyPackage`` package below provides two basic test examples:
+The ``MyPackage`` package below provides two basic test examples:
``test_example`` and ``test_example2``. The first runs the installed
``example`` and ensures its output contains an expected string. The second
runs ``example2`` without checking output so is only concerned with confirming
@@ -5737,7 +5737,7 @@ subdirectory of the installation prefix. They are automatically copied to
the appropriate relative paths under the test stage directory prior to
executing stand-alone tests.
-.. tip::
+.. tip::
*Perform test-related conversions once when copying files.*
@@ -7113,6 +7113,46 @@ might write:
CXXFLAGS += -I$DWARF_PREFIX/include
CXXFLAGS += -L$DWARF_PREFIX/lib
+.. _abi_compatibility:
+
+----------------------------
+Specifying ABI Compatibility
+----------------------------
+
+Packages can include ABI-compatibility information using the
+``can_splice`` directive. For example, if ``Foo`` version 1.1 can
+always replace version 1.0, then the package could have:
+
+.. code-block:: python
+
+ can_splice("foo@1.0", when="@1.1")
+
+For virtual packages, packages can also specify ABI-compabitiliby with
+other packages providing the same virtual. For example, ``zlib-ng``
+could specify:
+
+.. code-block:: python
+
+ can_splice("zlib@1.3.1", when="@2.2+compat")
+
+Some packages have ABI-compatibility that is dependent on matching
+variant values, either for all variants or for some set of
+ABI-relevant variants. In those cases, it is not necessary to specify
+the full combinatorial explosion. The ``match_variants`` keyword can
+cover all single-value variants.
+
+.. code-block:: python
+
+ can_splice("foo@1.1", when="@1.2", match_variants=["bar"]) # any value for bar as long as they're the same
+ can_splice("foo@1.2", when="@1.3", match_variants="*") # any variant values if all single-value variants match
+
+The concretizer will use ABI compatibility to determine automatic
+splices when :ref:`automatic splicing<automatic_splicing>` is enabled.
+
+.. note::
+
+ The ``can_splice`` directive is experimental, and may be replaced
+ by a higher-level interface in future versions of Spack.
.. _package_class_structure:
diff --git a/lib/spack/spack/directives.py b/lib/spack/spack/directives.py
index 0d6b66780c..0e3bb522ce 100644
--- a/lib/spack/spack/directives.py
+++ b/lib/spack/spack/directives.py
@@ -77,6 +77,7 @@ __all__ = [
"build_system",
"requires",
"redistribute",
+ "can_splice",
]
_patch_order_index = 0
@@ -505,6 +506,43 @@ def provides(*specs: SpecType, when: WhenType = None):
return _execute_provides
+@directive("splice_specs")
+def can_splice(
+ target: SpecType, *, when: SpecType, match_variants: Union[None, str, List[str]] = None
+):
+ """Packages can declare whether they are ABI-compatible with another package
+ and thus can be spliced into concrete versions of that package.
+
+ Args:
+ target: The spec that the current package is ABI-compatible with.
+
+ when: An anonymous spec constraining current package for when it is
+ ABI-compatible with target.
+
+ match_variants: A list of variants that must match
+ between target spec and current package, with special value '*'
+ which matches all variants. Example: a variant is defined on both
+ packages called json, and they are ABI-compatible whenever they agree on
+ the json variant (regardless of whether it is turned on or off). Note
+ that this cannot be applied to multi-valued variants and multi-valued
+ variants will be skipped by '*'.
+ """
+
+ def _execute_can_splice(pkg: "spack.package_base.PackageBase"):
+ when_spec = _make_when_spec(when)
+ if isinstance(match_variants, str) and match_variants != "*":
+ raise ValueError(
+ "* is the only valid string for match_variants "
+ "if looking to provide a single variant, use "
+ f"[{match_variants}] instead"
+ )
+ if when_spec is None:
+ return
+ pkg.splice_specs[when_spec] = (spack.spec.Spec(target), match_variants)
+
+ return _execute_can_splice
+
+
@directive("patches")
def patch(
url_or_filename: str,
diff --git a/lib/spack/spack/package_base.py b/lib/spack/spack/package_base.py
index ef2f27cca6..943c4eb0a4 100644
--- a/lib/spack/spack/package_base.py
+++ b/lib/spack/spack/package_base.py
@@ -622,6 +622,7 @@ class PackageBase(WindowsRPath, PackageViewMixin, RedistributionMixin, metaclass
patches: Dict["spack.spec.Spec", List["spack.patch.Patch"]]
variants: Dict["spack.spec.Spec", Dict[str, "spack.variant.Variant"]]
languages: Dict["spack.spec.Spec", Set[str]]
+ splice_specs: Dict["spack.spec.Spec", Tuple["spack.spec.Spec", Union[None, str, List[str]]]]
#: By default, packages are not virtual
#: Virtual packages override this attribute
diff --git a/lib/spack/spack/schema/concretizer.py b/lib/spack/spack/schema/concretizer.py
index 86e58de258..4fba79fece 100644
--- a/lib/spack/spack/schema/concretizer.py
+++ b/lib/spack/spack/schema/concretizer.py
@@ -78,7 +78,8 @@ properties: Dict[str, Any] = {
"transitive": {"type": "boolean", "default": False},
},
},
- }
+ },
+ "automatic": {"type": "boolean"},
},
},
"duplicates": {
diff --git a/lib/spack/spack/solver/asp.py b/lib/spack/spack/solver/asp.py
index 24b7aeb4ff..32db03f5cf 100644
--- a/lib/spack/spack/solver/asp.py
+++ b/lib/spack/spack/solver/asp.py
@@ -52,6 +52,7 @@ from spack.error import SpecSyntaxError
from .core import (
AspFunction,
+ AspVar,
NodeArgument,
ast_sym,
ast_type,
@@ -524,12 +525,14 @@ class Result:
node = SpecBuilder.make_node(pkg=providers[0])
candidate = answer.get(node)
- if candidate and candidate.build_spec.satisfies(input_spec):
- if not candidate.satisfies(input_spec):
- tty.warn(
- "explicit splice configuration has caused the concretized spec"
- f" {candidate} not to satisfy the input spec {input_spec}"
- )
+ if candidate and candidate.satisfies(input_spec):
+ self._concrete_specs.append(answer[node])
+ self._concrete_specs_by_input[input_spec] = answer[node]
+ elif candidate and candidate.build_spec.satisfies(input_spec):
+ tty.warn(
+ "explicit splice configuration has caused the concretized spec"
+ f" {candidate} not to satisfy the input spec {input_spec}"
+ )
self._concrete_specs.append(answer[node])
self._concrete_specs_by_input[input_spec] = answer[node]
else:
@@ -854,6 +857,8 @@ class PyclingoDriver:
self.control.load(os.path.join(parent_dir, "libc_compatibility.lp"))
else:
self.control.load(os.path.join(parent_dir, "os_compatibility.lp"))
+ if setup.enable_splicing:
+ self.control.load(os.path.join(parent_dir, "splices.lp"))
timer.stop("load")
@@ -1166,6 +1171,9 @@ class SpackSolverSetup:
# list of unique libc specs targeted by compilers (or an educated guess if no compiler)
self.libcs: List[spack.spec.Spec] = []
+ # If true, we have to load the code for synthesizing splices
+ self.enable_splicing: bool = spack.config.CONFIG.get("concretizer:splice:automatic")
+
def pkg_version_rules(self, pkg):
"""Output declared versions of a package.
@@ -1336,6 +1344,10 @@ class SpackSolverSetup:
# dependencies
self.package_dependencies_rules(pkg)
+ # splices
+ if self.enable_splicing:
+ self.package_splice_rules(pkg)
+
# virtual preferences
self.virtual_preferences(
pkg.name,
@@ -1674,6 +1686,94 @@ class SpackSolverSetup:
self.gen.newline()
+ def _gen_match_variant_splice_constraints(
+ self,
+ pkg,
+ cond_spec: "spack.spec.Spec",
+ splice_spec: "spack.spec.Spec",
+ hash_asp_var: "AspVar",
+ splice_node,
+ match_variants: List[str],
+ ):
+ # If there are no variants to match, no constraints are needed
+ variant_constraints = []
+ for i, variant_name in enumerate(match_variants):
+ vari_defs = pkg.variant_definitions(variant_name)
+ # the spliceable config of the package always includes the variant
+ if vari_defs != [] and any(cond_spec.satisfies(s) for (s, _) in vari_defs):
+ variant = vari_defs[0][1]
+ if variant.multi:
+ continue # cannot automatically match multi-valued variants
+ value_var = AspVar(f"VariValue{i}")
+ attr_constraint = fn.attr("variant_value", splice_node, variant_name, value_var)
+ hash_attr_constraint = fn.hash_attr(
+ hash_asp_var, "variant_value", splice_spec.name, variant_name, value_var
+ )
+ variant_constraints.append(attr_constraint)
+ variant_constraints.append(hash_attr_constraint)
+ return variant_constraints
+
+ def package_splice_rules(self, pkg):
+ self.gen.h2("Splice rules")
+ for i, (cond, (spec_to_splice, match_variants)) in enumerate(
+ sorted(pkg.splice_specs.items())
+ ):
+ with named_spec(cond, pkg.name):
+ self.version_constraints.add((cond.name, cond.versions))
+ self.version_constraints.add((spec_to_splice.name, spec_to_splice.versions))
+ hash_var = AspVar("Hash")
+ splice_node = fn.node(AspVar("NID"), cond.name)
+ when_spec_attrs = [
+ fn.attr(c.args[0], splice_node, *(c.args[2:]))
+ for c in self.spec_clauses(cond, body=True, required_from=None)
+ if c.args[0] != "node"
+ ]
+ splice_spec_hash_attrs = [
+ fn.hash_attr(hash_var, *(c.args))
+ for c in self.spec_clauses(spec_to_splice, body=True, required_from=None)
+ if c.args[0] != "node"
+ ]
+ if match_variants is None:
+ variant_constraints = []
+ elif match_variants == "*":
+ filt_match_variants = set()
+ for map in pkg.variants.values():
+ for k in map:
+ filt_match_variants.add(k)
+ filt_match_variants = list(filt_match_variants)
+ variant_constraints = self._gen_match_variant_splice_constraints(
+ pkg, cond, spec_to_splice, hash_var, splice_node, filt_match_variants
+ )
+ else:
+ if any(
+ v in cond.variants or v in spec_to_splice.variants for v in match_variants
+ ):
+ raise Exception(
+ "Overlap between match_variants and explicitly set variants"
+ )
+ variant_constraints = self._gen_match_variant_splice_constraints(
+ pkg, cond, spec_to_splice, hash_var, splice_node, match_variants
+ )
+
+ rule_head = fn.abi_splice_conditions_hold(
+ i, splice_node, spec_to_splice.name, hash_var
+ )
+ rule_body_components = (
+ [
+ # splice_set_fact,
+ fn.attr("node", splice_node),
+ fn.installed_hash(spec_to_splice.name, hash_var),
+ ]
+ + when_spec_attrs
+ + splice_spec_hash_attrs
+ + variant_constraints
+ )
+ rule_body = ",\n ".join(str(r) for r in rule_body_components)
+ rule = f"{rule_head} :-\n {rule_body}."
+ self.gen.append(rule)
+
+ self.gen.newline()
+
def virtual_preferences(self, pkg_name, func):
"""Call func(vspec, provider, i) for each of pkg's provider prefs."""
config = spack.config.get("packages")
@@ -2536,8 +2636,9 @@ class SpackSolverSetup:
for h, spec in self.reusable_and_possible.explicit_items():
# this indicates that there is a spec like this installed
self.gen.fact(fn.installed_hash(spec.name, h))
- # this describes what constraints it imposes on the solve
- self.impose(h, spec, body=True)
+ # indirection layer between hash constraints and imposition to allow for splicing
+ for pred in self.spec_clauses(spec, body=True, required_from=None):
+ self.gen.fact(fn.hash_attr(h, *pred.args))
self.gen.newline()
# Declare as possible parts of specs that are not in package.py
# - Add versions to possible versions
@@ -3478,6 +3579,14 @@ class RuntimePropertyRecorder:
self._setup.effect_rules()
+# This should be a dataclass, but dataclasses don't work on Python 3.6
+class Splice:
+ def __init__(self, splice_node: NodeArgument, child_name: str, child_hash: str):
+ self.splice_node = splice_node
+ self.child_name = child_name
+ self.child_hash = child_hash
+
+
class SpecBuilder:
"""Class with actions to rebuild a spec from ASP results."""
@@ -3513,10 +3622,11 @@ class SpecBuilder:
"""
return NodeArgument(id="0", pkg=pkg)
- def __init__(
- self, specs: List[spack.spec.Spec], *, hash_lookup: Optional[ConcreteSpecsByHash] = None
- ):
+ def __init__(self, specs, hash_lookup=None):
self._specs: Dict[NodeArgument, spack.spec.Spec] = {}
+
+ # Matches parent nodes to splice node
+ self._splices: Dict[NodeArgument, List[Splice]] = {}
self._result = None
self._command_line_specs = specs
self._flag_sources: Dict[Tuple[NodeArgument, str], Set[str]] = collections.defaultdict(
@@ -3600,16 +3710,8 @@ class SpecBuilder:
def depends_on(self, parent_node, dependency_node, type):
dependency_spec = self._specs[dependency_node]
- edges = self._specs[parent_node].edges_to_dependencies(name=dependency_spec.name)
- edges = [x for x in edges if id(x.spec) == id(dependency_spec)]
depflag = dt.flag_from_string(type)
-
- if not edges:
- self._specs[parent_node].add_dependency_edge(
- self._specs[dependency_node], depflag=depflag, virtuals=()
- )
- else:
- edges[0].update_deptypes(depflag=depflag)
+ self._specs[parent_node].add_dependency_edge(dependency_spec, depflag=depflag, virtuals=())
def virtual_on_edge(self, parent_node, provider_node, virtual):
dependencies = self._specs[parent_node].edges_to_dependencies(name=(provider_node.pkg))
@@ -3726,6 +3828,48 @@ class SpecBuilder:
def deprecated(self, node: NodeArgument, version: str) -> None:
tty.warn(f'using "{node.pkg}@{version}" which is a deprecated version')
+ def splice_at_hash(
+ self,
+ parent_node: NodeArgument,
+ splice_node: NodeArgument,
+ child_name: str,
+ child_hash: str,
+ ):
+ splice = Splice(splice_node, child_name=child_name, child_hash=child_hash)
+ self._splices.setdefault(parent_node, []).append(splice)
+
+ def _resolve_automatic_splices(self):
+ """After all of the specs have been concretized, apply all immediate
+ splices in size order. This ensures that all dependencies are resolved
+ before their parents, allowing for maximal sharing and minimal copying.
+ """
+ fixed_specs = {}
+ for node, spec in sorted(self._specs.items(), key=lambda x: len(x[1])):
+ immediate = self._splices.get(node, [])
+ if not immediate and not any(
+ edge.spec in fixed_specs for edge in spec.edges_to_dependencies()
+ ):
+ continue
+ new_spec = spec.copy(deps=False)
+ new_spec.build_spec = spec
+ for edge in spec.edges_to_dependencies():
+ depflag = edge.depflag & ~dt.BUILD
+ if any(edge.spec.dag_hash() == splice.child_hash for splice in immediate):
+ splice = [s for s in immediate if s.child_hash == edge.spec.dag_hash()][0]
+ new_spec.add_dependency_edge(
+ self._specs[splice.splice_node], depflag=depflag, virtuals=edge.virtuals
+ )
+ elif edge.spec in fixed_specs:
+ new_spec.add_dependency_edge(
+ fixed_specs[edge.spec], depflag=depflag, virtuals=edge.virtuals
+ )
+ else:
+ new_spec.add_dependency_edge(
+ edge.spec, depflag=depflag, virtuals=edge.virtuals
+ )
+ self._specs[node] = new_spec
+ fixed_specs[spec] = new_spec
+
@staticmethod
def sort_fn(function_tuple) -> Tuple[int, int]:
"""Ensure attributes are evaluated in the correct order.
@@ -3755,7 +3899,6 @@ class SpecBuilder:
# them here so that directives that build objects (like node and
# node_compiler) are called in the right order.
self.function_tuples = sorted(set(function_tuples), key=self.sort_fn)
-
self._specs = {}
for name, args in self.function_tuples:
if SpecBuilder.ignored_attributes.match(name):
@@ -3785,10 +3928,14 @@ class SpecBuilder:
continue
# if we've already gotten a concrete spec for this pkg,
- # do not bother calling actions on it
+ # do not bother calling actions on it except for node_flag_source,
+ # since node_flag_source is tracking information not in the spec itself
+ # we also need to keep track of splicing information.
spec = self._specs.get(args[0])
if spec and spec.concrete:
- continue
+ do_not_ignore_attrs = ["node_flag_source", "splice_at_hash"]
+ if name not in do_not_ignore_attrs:
+ continue
action(*args)
@@ -3798,7 +3945,7 @@ class SpecBuilder:
# inject patches -- note that we' can't use set() to unique the
# roots here, because the specs aren't complete, and the hash
# function will loop forever.
- roots = [spec.root for spec in self._specs.values() if not spec.root.installed]
+ roots = [spec.root for spec in self._specs.values()]
roots = dict((id(r), r) for r in roots)
for root in roots.values():
spack.spec.Spec.inject_patches_variant(root)
@@ -3814,6 +3961,8 @@ class SpecBuilder:
for root in roots.values():
root._finalize_concretization()
+ self._resolve_automatic_splices()
+
for s in self._specs.values():
spack.spec.Spec.ensure_no_deprecated(s)
@@ -3828,7 +3977,6 @@ class SpecBuilder:
)
specs = self.execute_explicit_splices()
-
return specs
def execute_explicit_splices(self):
@@ -4165,7 +4313,6 @@ class ReusableSpecsSelector:
result = []
for reuse_source in self.reuse_sources:
result.extend(reuse_source.selected_specs())
-
# If we only want to reuse dependencies, remove the root specs
if self.reuse_strategy == ReuseStrategy.DEPENDENCIES:
result = [spec for spec in result if not any(root in spec for root in specs)]
@@ -4335,11 +4482,10 @@ class SolverError(InternalConcretizerError):
super().__init__(msg)
- self.provided = provided
-
# Add attribute expected of the superclass interface
self.required = None
self.constraint_type = None
+ self.provided = provided
class InvalidSpliceError(spack.error.SpackError):
diff --git a/lib/spack/spack/solver/concretize.lp b/lib/spack/spack/solver/concretize.lp
index f4695be9b9..63a5a71175 100644
--- a/lib/spack/spack/solver/concretize.lp
+++ b/lib/spack/spack/solver/concretize.lp
@@ -1449,25 +1449,71 @@ attr("node_flag", PackageNode, NodeFlag) :- attr("node_flag_set", PackageNode, N
%-----------------------------------------------------------------------------
-% Installed packages
+% Installed Packages
%-----------------------------------------------------------------------------
-% the solver is free to choose at most one installed hash for each package
-{ attr("hash", node(ID, Package), Hash) : installed_hash(Package, Hash) } 1
- :- attr("node", node(ID, Package)), internal_error("Package must resolve to at most one hash").
+#defined installed_hash/2.
+#defined abi_splice_conditions_hold/4.
+
+% These are the previously concretized attributes of the installed package as
+% a hash. It has the general form:
+% hash_attr(Hash, Attribute, PackageName, Args*)
+#defined hash_attr/3.
+#defined hash_attr/4.
+#defined hash_attr/5.
+#defined hash_attr/6.
+#defined hash_attr/7.
+
+{ attr("hash", node(ID, PackageName), Hash): installed_hash(PackageName, Hash) } 1 :-
+ attr("node", node(ID, PackageName)),
+ internal_error("Package must resolve to at most 1 hash").
% you can't choose an installed hash for a dev spec
:- attr("hash", PackageNode, Hash), attr("variant_value", PackageNode, "dev_path", _).
-
% You can't install a hash, if it is not installed
:- attr("hash", node(ID, Package), Hash), not installed_hash(Package, Hash).
-% This should be redundant given the constraint above
-:- attr("node", PackageNode), 2 { attr("hash", PackageNode, Hash) }.
-% if a hash is selected, we impose all the constraints that implies
-impose(Hash, PackageNode) :- attr("hash", PackageNode, Hash).
+% hash_attrs are versions, but can_splice_attr are usually node_version_satisfies
+hash_attr(Hash, "node_version_satisfies", PackageName, Constraint) :-
+ hash_attr(Hash, "version", PackageName, Version),
+ pkg_fact(PackageName, version_satisfies(Constraint, Version)).
+
+% This recovers the exact semantics for hash reuse hash and depends_on are where
+% splices are decided, and virtual_on_edge can result in name-changes, which is
+% why they are all treated separately.
+imposed_constraint(Hash, Attr, PackageName) :-
+ hash_attr(Hash, Attr, PackageName).
+imposed_constraint(Hash, Attr, PackageName, A1) :-
+ hash_attr(Hash, Attr, PackageName, A1), Attr != "hash".
+imposed_constraint(Hash, Attr, PackageName, Arg1, Arg2) :-
+ hash_attr(Hash, Attr, PackageName, Arg1, Arg2),
+ Attr != "depends_on",
+ Attr != "virtual_on_edge".
+imposed_constraint(Hash, Attr, PackageName, A1, A2, A3) :-
+ hash_attr(Hash, Attr, PackageName, A1, A2, A3).
+imposed_constraint(Hash, "hash", PackageName, Hash) :- installed_hash(PackageName, Hash).
+% Without splicing, we simply recover the exact semantics
+imposed_constraint(ParentHash, "hash", ChildName, ChildHash) :-
+ hash_attr(ParentHash, "hash", ChildName, ChildHash),
+ ChildHash != ParentHash,
+ not abi_splice_conditions_hold(_, _, ChildName, ChildHash).
+
+imposed_constraint(Hash, "depends_on", PackageName, DepName, Type) :-
+ hash_attr(Hash, "depends_on", PackageName, DepName, Type),
+ hash_attr(Hash, "hash", DepName, DepHash),
+ not attr("splice_at_hash", _, _, DepName, DepHash).
+
+imposed_constraint(Hash, "virtual_on_edge", PackageName, DepName, VirtName) :-
+ hash_attr(Hash, "virtual_on_edge", PackageName, DepName, VirtName),
+ not attr("splice_at_hash", _, _, DepName,_).
+
+% Rules pertaining to attr("splice_at_hash") and abi_splice_conditions_hold will
+% be conditionally loaded from splices.lp
+
+impose(Hash, PackageNode) :- attr("hash", PackageNode, Hash), attr("node", PackageNode).
+
+% If there is not a hash for a package, we build it.
+build(PackageNode) :- attr("node", PackageNode), not concrete(PackageNode).
-% if we haven't selected a hash for a package, we'll be building it
-build(PackageNode) :- not attr("hash", PackageNode, _), attr("node", PackageNode).
% Minimizing builds is tricky. We want a minimizing criterion
@@ -1480,6 +1526,7 @@ build(PackageNode) :- not attr("hash", PackageNode, _), attr("node", PackageNode
% criteria for built specs -- so that they take precedence over the otherwise
% topmost-priority criterion to reuse what is installed.
%
+
% The priority ranges are:
% 1000+ Optimizations for concretization errors
% 300 - 1000 Highest priority optimizations for valid solutions
@@ -1505,12 +1552,10 @@ build_priority(PackageNode, 0) :- not build(PackageNode), attr("node", Package
pkg_fact(Package, version_declared(Version, Weight, "installed")),
not optimize_for_reuse().
-#defined installed_hash/2.
% This statement, which is a hidden feature of clingo, let us avoid cycles in the DAG
#edge (A, B) : depends_on(A, B).
-
%-----------------------------------------------------------------
% Optimization to avoid errors
%-----------------------------------------------------------------
diff --git a/lib/spack/spack/solver/core.py b/lib/spack/spack/solver/core.py
index 2530981a21..ba257173a5 100644
--- a/lib/spack/spack/solver/core.py
+++ b/lib/spack/spack/solver/core.py
@@ -44,6 +44,17 @@ def _id(thing: Any) -> Union[str, AspObject]:
return f'"{str(thing)}"'
+class AspVar(AspObject):
+ """Represents a variable in an ASP rule, allows for conditionally generating
+ rules"""
+
+ def __init__(self, name: str):
+ self.name = name
+
+ def __str__(self) -> str:
+ return str(self.name)
+
+
@lang.key_ordering
class AspFunction(AspObject):
"""A term in the ASP logic program"""
@@ -88,6 +99,8 @@ class AspFunction(AspObject):
return clingo().Number(arg)
elif isinstance(arg, AspFunction):
return clingo().Function(arg.name, [self._argify(x) for x in arg.args], positive=True)
+ elif isinstance(arg, AspVar):
+ return clingo().Variable(arg.name)
return clingo().String(str(arg))
def symbol(self):
diff --git a/lib/spack/spack/solver/display.lp b/lib/spack/spack/solver/display.lp
index 675a9d17d2..61d96b25b5 100644
--- a/lib/spack/spack/solver/display.lp
+++ b/lib/spack/spack/solver/display.lp
@@ -15,7 +15,6 @@
#show attr/4.
#show attr/5.
#show attr/6.
-
% names of optimization criteria
#show opt_criterion/2.
diff --git a/lib/spack/spack/solver/splices.lp b/lib/spack/spack/solver/splices.lp
new file mode 100644
index 0000000000..96762c456c
--- /dev/null
+++ b/lib/spack/spack/solver/splices.lp
@@ -0,0 +1,56 @@
+% Copyright 2013-2024 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)
+
+%=============================================================================
+% These rules are conditionally loaded to handle the synthesis of spliced
+% packages.
+% =============================================================================
+% Consider the concrete spec:
+% foo@2.72%gcc@11.4 arch=linux-ubuntu22.04-icelake build_system=autotools ^bar ...
+% It will emit the following facts for reuse (below is a subset)
+% installed_hash("foo", "xxxyyy")
+% hash_attr("xxxyyy", "hash", "foo", "xxxyyy")
+% hash_attr("xxxyyy", "version", "foo", "2.72")
+% hash_attr("xxxyyy", "node_os", "ubuntu22.04")
+% hash_attr("xxxyyy", "hash", "bar", "zzzqqq")
+% hash_attr("xxxyyy", "depends_on", "foo", "bar", "link")
+% Rules that derive abi_splice_conditions_hold will be generated from
+% use of the `can_splice` directive. The will have the following form:
+% can_splice("foo@1.0.0+a", when="@1.0.1+a", match_variants=["b"]) --->
+% abi_splice_conditions_hold(0, node(SID, "foo"), "foo", BashHash) :-
+% installed_hash("foo", BaseHash),
+% attr("node", node(SID, SpliceName)),
+% attr("node_version_satisfies", node(SID, "foo"), "1.0.1"),
+% hash_attr("hash", "node_version_satisfies", "foo", "1.0.1"),
+% attr("variant_value", node(SID, "foo"), "a", "True"),
+% hash_attr("hash", "variant_value", "foo", "a", "True"),
+% attr("variant_value", node(SID, "foo"), "b", VariVar0),
+% hash_attr("hash", "variant_value", "foo", "b", VariVar0),
+
+% If the splice is valid (i.e. abi_splice_conditions_hold is derived) in the
+% dependency of a concrete spec the solver free to choose whether to continue
+% with the exact hash semantics by simply imposing the child hash, or introducing
+% a spliced node as the dependency instead
+{ imposed_constraint(ParentHash, "hash", ChildName, ChildHash) } :-
+ hash_attr(ParentHash, "hash", ChildName, ChildHash),
+ abi_splice_conditions_hold(_, node(SID, SpliceName), ChildName, ChildHash).
+
+attr("splice_at_hash", ParentNode, node(SID, SpliceName), ChildName, ChildHash) :-
+ attr("hash", ParentNode, ParentHash),
+ hash_attr(ParentHash, "hash", ChildName, ChildHash),
+ abi_splice_conditions_hold(_, node(SID, SpliceName), ChildName, ChildHash),
+ ParentHash != ChildHash,
+ not imposed_constraint(ParentHash, "hash", ChildName, ChildHash).
+
+% Names and virtual providers may change when a dependency is spliced in
+imposed_constraint(Hash, "dependency_holds", ParentName, SpliceName, Type) :-
+ hash_attr(Hash, "depends_on", ParentName, DepName, Type),
+ hash_attr(Hash, "hash", DepName, DepHash),
+ attr("splice_at_hash", node(ID, ParentName), node(SID, SpliceName), DepName, DepHash).
+
+imposed_constraint(Hash, "virtual_on_edge", ParentName, SpliceName, VirtName) :-
+ hash_attr(Hash, "virtual_on_edge", ParentName, DepName, VirtName),
+ attr("splice_at_hash", node(ID, ParentName), node(SID, SpliceName), DepName, DepHash).
+
diff --git a/lib/spack/spack/spec.py b/lib/spack/spack/spec.py
index d87296a3fb..03a372be4e 100644
--- a/lib/spack/spack/spec.py
+++ b/lib/spack/spack/spec.py
@@ -1431,6 +1431,8 @@ def tree(
class Spec:
#: Cache for spec's prefix, computed lazily in the corresponding property
_prefix = None
+ #: Cache for spec's length, computed lazily in the corresponding property
+ _length = None
abstract_hash = None
@staticmethod
@@ -2907,7 +2909,7 @@ class Spec:
if (not value) and s.concrete and s.installed:
continue
elif not value:
- s.clear_cached_hashes()
+ s.clear_caches()
s._mark_root_concrete(value)
def _finalize_concretization(self):
@@ -3700,6 +3702,18 @@ class Spec:
return child
+ def __len__(self):
+ if not self.concrete:
+ raise spack.error.SpecError(f"Cannot get length of abstract spec: {self}")
+
+ if not self._length:
+ self._length = 1 + sum(len(dep) for dep in self.dependencies())
+ return self._length
+
+ def __bool__(self):
+ # Need to define this so __len__ isn't used by default
+ return True
+
def __contains__(self, spec):
"""True if this spec or some dependency satisfies the spec.
@@ -4256,7 +4270,7 @@ class Spec:
for ancestor in ancestors_in_context:
# Only set it if it hasn't been spliced before
ancestor._build_spec = ancestor._build_spec or ancestor.copy()
- ancestor.clear_cached_hashes(ignore=(ht.package_hash.attr,))
+ ancestor.clear_caches(ignore=(ht.package_hash.attr,))
for edge in ancestor.edges_to_dependencies(depflag=dt.BUILD):
if edge.depflag & ~dt.BUILD:
edge.depflag &= ~dt.BUILD
@@ -4450,7 +4464,7 @@ class Spec:
return spec
- def clear_cached_hashes(self, ignore=()):
+ def clear_caches(self, ignore=()):
"""
Clears all cached hashes in a Spec, while preserving other properties.
"""
@@ -4458,7 +4472,9 @@ class Spec:
if h.attr not in ignore:
if hasattr(self, h.attr):
setattr(self, h.attr, None)
- self._dunder_hash = None
+ for attr in ("_dunder_hash", "_prefix", "_length"):
+ if attr not in ignore:
+ setattr(self, attr, None)
def __hash__(self):
# If the spec is concrete, we leverage the process hash and just use
diff --git a/lib/spack/spack/test/abi_splicing.py b/lib/spack/spack/test/abi_splicing.py
new file mode 100644
index 0000000000..97601c578a
--- /dev/null
+++ b/lib/spack/spack/test/abi_splicing.py
@@ -0,0 +1,234 @@
+# Copyright 2013-2024 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)
+""" Test ABI-based splicing of dependencies """
+
+from typing import List
+
+import pytest
+
+import spack.config
+import spack.deptypes as dt
+import spack.package_base
+import spack.paths
+import spack.repo
+import spack.solver.asp
+from spack.installer import PackageInstaller
+from spack.spec import Spec
+
+
+class CacheManager:
+ def __init__(self, specs: List[str]) -> None:
+ self.req_specs = specs
+ self.concr_specs: List[Spec]
+ self.concr_specs = []
+
+ def __enter__(self):
+ self.concr_specs = [Spec(s).concretized() for s in self.req_specs]
+ for s in self.concr_specs:
+ PackageInstaller([s.package], fake=True, explicit=True).install()
+
+ def __exit__(self, exc_type, exc_val, exc_tb):
+ for s in self.concr_specs:
+ s.package.do_uninstall()
+
+
+# MacOS and Windows only work if you pass this function pointer rather than a
+# closure
+def _mock_has_runtime_dependencies(_x):
+ return True
+
+
+def _make_specs_non_buildable(specs: List[str]):
+ output_config = {}
+ for spec in specs:
+ output_config[spec] = {"buildable": False}
+ return output_config
+
+
+@pytest.fixture
+def splicing_setup(mutable_database, mock_packages, monkeypatch):
+ spack.config.set("concretizer:reuse", True)
+ monkeypatch.setattr(
+ spack.solver.asp, "_has_runtime_dependencies", _mock_has_runtime_dependencies
+ )
+
+
+def _enable_splicing():
+ spack.config.set("concretizer:splice", {"automatic": True})
+
+
+def _has_build_dependency(spec: Spec, name: str):
+ return any(s.name == name for s in spec.dependencies(None, dt.BUILD))
+
+
+def test_simple_reuse(splicing_setup):
+ with CacheManager(["splice-z@1.0.0+compat"]):
+ spack.config.set("packages", _make_specs_non_buildable(["splice-z"]))
+ assert Spec("splice-z").concretized().satisfies(Spec("splice-z"))
+
+
+def test_simple_dep_reuse(splicing_setup):
+ with CacheManager(["splice-z@1.0.0+compat"]):
+ spack.config.set("packages", _make_specs_non_buildable(["splice-z"]))
+ assert Spec("splice-h@1").concretized().satisfies(Spec("splice-h@1"))
+
+
+def test_splice_installed_hash(splicing_setup):
+ cache = [
+ "splice-t@1 ^splice-h@1.0.0+compat ^splice-z@1.0.0",
+ "splice-h@1.0.2+compat ^splice-z@1.0.0",
+ ]
+ with CacheManager(cache):
+ packages_config = _make_specs_non_buildable(["splice-t", "splice-h"])
+ spack.config.set("packages", packages_config)
+ goal_spec = Spec("splice-t@1 ^splice-h@1.0.2+compat ^splice-z@1.0.0")
+ with pytest.raises(Exception):
+ goal_spec.concretized()
+ _enable_splicing()
+ assert goal_spec.concretized().satisfies(goal_spec)
+
+
+def test_splice_build_splice_node(splicing_setup):
+ with CacheManager(["splice-t@1 ^splice-h@1.0.0+compat ^splice-z@1.0.0+compat"]):
+ spack.config.set("packages", _make_specs_non_buildable(["splice-t"]))
+ goal_spec = Spec("splice-t@1 ^splice-h@1.0.2+compat ^splice-z@1.0.0+compat")
+ with pytest.raises(Exception):
+ goal_spec.concretized()
+ _enable_splicing()
+ assert goal_spec.concretized().satisfies(goal_spec)
+
+
+def test_double_splice(splicing_setup):
+ cache = [
+ "splice-t@1 ^splice-h@1.0.0+compat ^splice-z@1.0.0+compat",
+ "splice-h@1.0.2+compat ^splice-z@1.0.1+compat",
+ "splice-z@1.0.2+compat",
+ ]
+ with CacheManager(cache):
+ freeze_builds_config = _make_specs_non_buildable(["splice-t", "splice-h", "splice-z"])
+ spack.config.set("packages", freeze_builds_config)
+ goal_spec = Spec("splice-t@1 ^splice-h@1.0.2+compat ^splice-z@1.0.2+compat")
+ with pytest.raises(Exception):
+ goal_spec.concretized()
+ _enable_splicing()
+ assert goal_spec.concretized().satisfies(goal_spec)
+
+
+# The next two tests are mirrors of one another
+def test_virtual_multi_splices_in(splicing_setup):
+ cache = [
+ "depends-on-virtual-with-abi ^virtual-abi-1",
+ "depends-on-virtual-with-abi ^virtual-abi-2",
+ ]
+ goal_specs = [
+ "depends-on-virtual-with-abi ^virtual-abi-multi abi=one",
+ "depends-on-virtual-with-abi ^virtual-abi-multi abi=two",
+ ]
+ with CacheManager(cache):
+ spack.config.set("packages", _make_specs_non_buildable(["depends-on-virtual-with-abi"]))
+ for gs in goal_specs:
+ with pytest.raises(Exception):
+ Spec(gs).concretized()
+ _enable_splicing()
+ for gs in goal_specs:
+ assert Spec(gs).concretized().satisfies(gs)
+
+
+def test_virtual_multi_can_be_spliced(splicing_setup):
+ cache = [
+ "depends-on-virtual-with-abi ^virtual-abi-multi abi=one",
+ "depends-on-virtual-with-abi ^virtual-abi-multi abi=two",
+ ]
+ goal_specs = [
+ "depends-on-virtual-with-abi ^virtual-abi-1",
+ "depends-on-virtual-with-abi ^virtual-abi-2",
+ ]
+ with CacheManager(cache):
+ spack.config.set("packages", _make_specs_non_buildable(["depends-on-virtual-with-abi"]))
+ with pytest.raises(Exception):
+ for gs in goal_specs:
+ Spec(gs).concretized()
+ _enable_splicing()
+ for gs in goal_specs:
+ assert Spec(gs).concretized().satisfies(gs)
+
+
+def test_manyvariant_star_matching_variant_splice(splicing_setup):
+ cache = [
+ # can_splice("manyvariants@1.0.0", when="@1.0.1", match_variants="*")
+ "depends-on-manyvariants ^manyvariants@1.0.0+a+b c=v1 d=v2",
+ "depends-on-manyvariants ^manyvariants@1.0.0~a~b c=v3 d=v3",
+ ]
+ goal_specs = [
+ Spec("depends-on-manyvariants ^manyvariants@1.0.1+a+b c=v1 d=v2"),
+ Spec("depends-on-manyvariants ^manyvariants@1.0.1~a~b c=v3 d=v3"),
+ ]
+ with CacheManager(cache):
+ freeze_build_config = {"depends-on-manyvariants": {"buildable": False}}
+ spack.config.set("packages", freeze_build_config)
+ for goal in goal_specs:
+ with pytest.raises(Exception):
+ goal.concretized()
+ _enable_splicing()
+ for goal in goal_specs:
+ assert goal.concretized().satisfies(goal)
+
+
+def test_manyvariant_limited_matching(splicing_setup):
+ cache = [
+ # can_splice("manyvariants@2.0.0+a~b", when="@2.0.1~a+b", match_variants=["c", "d"])
+ "depends-on-manyvariants@2.0 ^manyvariants@2.0.0+a~b c=v3 d=v2",
+ # can_splice("manyvariants@2.0.0 c=v1 d=v1", when="@2.0.1+a+b")
+ "depends-on-manyvariants@2.0 ^manyvariants@2.0.0~a~b c=v1 d=v1",
+ ]
+ goal_specs = [
+ Spec("depends-on-manyvariants@2.0 ^manyvariants@2.0.1~a+b c=v3 d=v2"),
+ Spec("depends-on-manyvariants@2.0 ^manyvariants@2.0.1+a+b c=v3 d=v3"),
+ ]
+ with CacheManager(cache):
+ freeze_build_config = {"depends-on-manyvariants": {"buildable": False}}
+ spack.config.set("packages", freeze_build_config)
+ for s in goal_specs:
+ with pytest.raises(Exception):
+ s.concretized()
+ _enable_splicing()
+ for s in goal_specs:
+ assert s.concretized().satisfies(s)
+
+
+def test_external_splice_same_name(splicing_setup):
+ cache = [
+ "splice-h@1.0.0 ^splice-z@1.0.0+compat",
+ "splice-t@1.0 ^splice-h@1.0.1 ^splice-z@1.0.1+compat",
+ ]
+ packages_yaml = {
+ "splice-z": {"externals": [{"spec": "splice-z@1.0.2+compat", "prefix": "/usr"}]}
+ }
+ goal_specs = [
+ Spec("splice-h@1.0.0 ^splice-z@1.0.2"),
+ Spec("splice-t@1.0 ^splice-h@1.0.1 ^splice-z@1.0.2"),
+ ]
+ with CacheManager(cache):
+ spack.config.set("packages", packages_yaml)
+ _enable_splicing()
+ for s in goal_specs:
+ assert s.concretized().satisfies(s)
+
+
+def test_spliced_build_deps_only_in_build_spec(splicing_setup):
+ cache = ["splice-t@1.0 ^splice-h@1.0.1 ^splice-z@1.0.0"]
+ goal_spec = Spec("splice-t@1.0 ^splice-h@1.0.2 ^splice-z@1.0.0")
+
+ with CacheManager(cache):
+ _enable_splicing()
+ concr_goal = goal_spec.concretized()
+ build_spec = concr_goal._build_spec
+ # Spec has been spliced
+ assert build_spec is not None
+ # Build spec has spliced build dependencies
+ assert _has_build_dependency(build_spec, "splice-h")
+ assert _has_build_dependency(build_spec, "splice-z")
+ # Spliced build dependencies are removed
+ assert len(concr_goal.dependencies(None, dt.BUILD)) == 0
diff --git a/lib/spack/spack/test/cmd/pkg.py b/lib/spack/spack/test/cmd/pkg.py
index d1f0ed139e..1811b1a617 100644
--- a/lib/spack/spack/test/cmd/pkg.py
+++ b/lib/spack/spack/test/cmd/pkg.py
@@ -311,7 +311,19 @@ def test_pkg_grep(mock_packages, capfd):
output, _ = capfd.readouterr()
assert output.strip() == "\n".join(
spack.repo.PATH.get_pkg_class(name).module.__file__
- for name in ["splice-a", "splice-h", "splice-t", "splice-vh", "splice-vt", "splice-z"]
+ for name in [
+ "depends-on-manyvariants",
+ "manyvariants",
+ "splice-a",
+ "splice-h",
+ "splice-t",
+ "splice-vh",
+ "splice-vt",
+ "splice-z",
+ "virtual-abi-1",
+ "virtual-abi-2",
+ "virtual-abi-multi",
+ ]
)
# ensure that this string isn't fouhnd
diff --git a/lib/spack/spack/test/spec_semantics.py b/lib/spack/spack/test/spec_semantics.py
index 38424e951c..9f94d11a08 100644
--- a/lib/spack/spack/test/spec_semantics.py
+++ b/lib/spack/spack/test/spec_semantics.py
@@ -1763,8 +1763,8 @@ def test_package_hash_affects_dunder_and_dag_hash(mock_packages, default_mock_co
assert a1.dag_hash() == a2.dag_hash()
assert a1.process_hash() == a2.process_hash()
- a1.clear_cached_hashes()
- a2.clear_cached_hashes()
+ a1.clear_caches()
+ a2.clear_caches()
# tweak the dag hash of one of these specs
new_hash = "00000000000000000000000000000000"