Stable splice-topo order for resolving splicing (#48605)

Co-authored-by: Massimiliano Culpo <massimiliano.culpo@gmail.com>
This commit is contained in:
John Gouwar 2025-01-17 10:27:16 -05:00 committed by GitHub
parent bb43fa5444
commit 9f7cff1780
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 91 additions and 56 deletions

View File

@ -37,6 +37,7 @@
import spack.package_prefs import spack.package_prefs
import spack.platforms import spack.platforms
import spack.repo import spack.repo
import spack.solver.splicing
import spack.spec import spack.spec
import spack.store import spack.store
import spack.util.crypto import spack.util.crypto
@ -1740,7 +1741,7 @@ def package_splice_rules(self, pkg):
if any( if any(
v in cond.variants or v in spec_to_splice.variants for v in match_variants v in cond.variants or v in spec_to_splice.variants for v in match_variants
): ):
raise Exception( raise spack.error.PackageError(
"Overlap between match_variants and explicitly set variants" "Overlap between match_variants and explicitly set variants"
) )
variant_constraints = self._gen_match_variant_splice_constraints( variant_constraints = self._gen_match_variant_splice_constraints(
@ -3374,14 +3375,6 @@ def consume_facts(self):
self._setup.effect_rules() 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 SpecBuilder:
"""Class with actions to rebuild a spec from ASP results.""" """Class with actions to rebuild a spec from ASP results."""
@ -3421,7 +3414,7 @@ def __init__(self, specs, hash_lookup=None):
self._specs: Dict[NodeArgument, spack.spec.Spec] = {} self._specs: Dict[NodeArgument, spack.spec.Spec] = {}
# Matches parent nodes to splice node # Matches parent nodes to splice node
self._splices: Dict[NodeArgument, List[Splice]] = {} self._splices: Dict[spack.spec.Spec, List[spack.solver.splicing.Splice]] = {}
self._result = None self._result = None
self._command_line_specs = specs self._command_line_specs = specs
self._flag_sources: Dict[Tuple[NodeArgument, str], Set[str]] = collections.defaultdict( self._flag_sources: Dict[Tuple[NodeArgument, str], Set[str]] = collections.defaultdict(
@ -3628,50 +3621,12 @@ def splice_at_hash(
child_name: str, child_name: str,
child_hash: str, child_hash: str,
): ):
splice = Splice(splice_node, child_name=child_name, child_hash=child_hash) parent_spec = self._specs[parent_node]
self._splices.setdefault(parent_node, []).append(splice) splice_spec = self._specs[splice_node]
splice = spack.solver.splicing.Splice(
def _resolve_automatic_splices(self): splice_spec, child_name=child_name, child_hash=child_hash
"""After all of the specs have been concretized, apply all immediate splices.
Use reverse topological order to ensure that all dependencies are resolved
before their parents, allowing for maximal sharing and minimal copying.
"""
fixed_specs = {}
# create a mapping from dag hash to an integer representing position in reverse topo order.
specs = self._specs.values()
topo_order = list(traverse.traverse_nodes(specs, order="topo", key=traverse.by_dag_hash))
topo_lookup = {spec.dag_hash(): index for index, spec in enumerate(reversed(topo_order))}
# iterate over specs, children before parents
for node, spec in sorted(self._specs.items(), key=lambda x: topo_lookup[x[1].dag_hash()]):
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.clear_caches(ignore=("package_hash",))
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: self._splices.setdefault(parent_spec, []).append(splice)
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 @staticmethod
def sort_fn(function_tuple) -> Tuple[int, int]: def sort_fn(function_tuple) -> Tuple[int, int]:
@ -3764,7 +3719,15 @@ def build_specs(self, function_tuples):
for root in roots.values(): for root in roots.values():
root._finalize_concretization() root._finalize_concretization()
self._resolve_automatic_splices() # Only attempt to resolve automatic splices if the solver produced any
if self._splices:
resolved_splices = spack.solver.splicing._resolve_collected_splices(
list(self._specs.values()), self._splices
)
new_specs = {}
for node, spec in self._specs.items():
new_specs[node] = resolved_splices.get(spec, spec)
self._specs = new_specs
for s in self._specs.values(): for s in self._specs.values():
spack.spec.Spec.ensure_no_deprecated(s) spack.spec.Spec.ensure_no_deprecated(s)

View File

@ -0,0 +1,73 @@
# Copyright Spack Project Developers. See COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
from functools import cmp_to_key
from typing import Dict, List, NamedTuple
import spack.deptypes as dt
from spack.spec import Spec
from spack.traverse import by_dag_hash, traverse_nodes
class Splice(NamedTuple):
#: The spec being spliced into a parent
splice_spec: Spec
#: The name of the child that splice spec is replacing
child_name: str
#: The hash of the child that `splice_spec` is replacing
child_hash: str
def _resolve_collected_splices(
specs: List[Spec], splices: Dict[Spec, List[Splice]]
) -> Dict[Spec, Spec]:
"""After all of the specs have been concretized, apply all immediate splices.
Returns a dict mapping original specs to their resolved counterparts
"""
def splice_cmp(s1: Spec, s2: Spec):
"""This function can be used to sort a list of specs such that that any
spec which will be spliced into a parent comes after the parent it will
be spliced into. This order ensures that transitive splices will be
executed in the correct order.
"""
s1_splices = splices.get(s1, [])
s2_splices = splices.get(s2, [])
if any([s2.dag_hash() == splice.splice_spec.dag_hash() for splice in s1_splices]):
return -1
elif any([s1.dag_hash() == splice.splice_spec.dag_hash() for splice in s2_splices]):
return 1
else:
return 0
splice_order = sorted(specs, key=cmp_to_key(splice_cmp))
reverse_topo_order = reversed(
[x for x in traverse_nodes(splice_order, order="topo", key=by_dag_hash) if x in specs]
)
already_resolved: Dict[Spec, Spec] = {}
for spec in reverse_topo_order:
immediate = splices.get(spec, [])
if not immediate and not any(
edge.spec in already_resolved for edge in spec.edges_to_dependencies()
):
continue
new_spec = spec.copy(deps=False)
new_spec.clear_caches(ignore=("package_hash",))
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]
# If the spec being splice in is also spliced
splice_spec = already_resolved.get(splice.splice_spec, splice.splice_spec)
new_spec.add_dependency_edge(splice_spec, depflag=depflag, virtuals=edge.virtuals)
elif edge.spec in already_resolved:
new_spec.add_dependency_edge(
already_resolved[edge.spec], depflag=depflag, virtuals=edge.virtuals
)
else:
new_spec.add_dependency_edge(edge.spec, depflag=depflag, virtuals=edge.virtuals)
already_resolved[spec] = new_spec
return already_resolved

View File

@ -103,7 +103,6 @@ def test_splice_build_splice_node(install_specs, mutable_config):
assert concrete["splice-h"].build_spec.dag_hash() == concrete["splice-h"].dag_hash() assert concrete["splice-h"].build_spec.dag_hash() == concrete["splice-h"].dag_hash()
@pytest.mark.xfail(reason="the spliced splice-h has sometimes the original splice-h hash")
def test_double_splice(install_specs, mutable_config): def test_double_splice(install_specs, mutable_config):
"""Tests splicing two dependencies of an installed spec, for other installed specs""" """Tests splicing two dependencies of an installed spec, for other installed specs"""
splice_t, splice_h, splice_z = install_specs( splice_t, splice_h, splice_z = install_specs(