Add virtual information on DAG edges (#34821)

* DependencySpec: add virtuals attribute on edges

This works for both the new and the old concretizer. Also,
added type hints to involved functions.

* Improve virtual reconstruction from old format

* Reconstruct virtuals when reading from Cray manifest

* Reconstruct virtual information on test dependencies
This commit is contained in:
Massimiliano Culpo 2023-06-15 16:16:54 +02:00 committed by GitHub
parent c638311796
commit f27d012e0c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 290 additions and 136 deletions

View File

@ -223,7 +223,7 @@ def update_external_dependencies(self, extendee_spec=None):
python.external_path = self.spec.external_path python.external_path = self.spec.external_path
python._mark_concrete() python._mark_concrete()
self.spec.add_dependency_edge(python, deptypes=("build", "link", "run")) self.spec.add_dependency_edge(python, deptypes=("build", "link", "run"), virtuals=())
class PythonPackage(PythonExtension): class PythonPackage(PythonExtension):

View File

@ -164,7 +164,10 @@ def entries_to_specs(entries):
continue continue
parent_spec = spec_dict[entry["hash"]] parent_spec = spec_dict[entry["hash"]]
dep_spec = spec_dict[dep_hash] dep_spec = spec_dict[dep_hash]
parent_spec._add_dependency(dep_spec, deptypes=deptypes) parent_spec._add_dependency(dep_spec, deptypes=deptypes, virtuals=())
for spec in spec_dict.values():
spack.spec.reconstruct_virtuals_on_edges(spec)
return spec_dict return spec_dict

View File

@ -60,7 +60,7 @@
# DB version. This is stuck in the DB file to track changes in format. # DB version. This is stuck in the DB file to track changes in format.
# Increment by one when the database format changes. # Increment by one when the database format changes.
# Versions before 5 were not integers. # Versions before 5 were not integers.
_db_version = vn.Version("6") _db_version = vn.Version("7")
# For any version combinations here, skip reindex when upgrading. # For any version combinations here, skip reindex when upgrading.
# Reindexing can take considerable time and is not always necessary. # Reindexing can take considerable time and is not always necessary.
@ -72,6 +72,7 @@
# version is saved to disk the first time the DB is written. # version is saved to disk the first time the DB is written.
(vn.Version("0.9.3"), vn.Version("5")), (vn.Version("0.9.3"), vn.Version("5")),
(vn.Version("5"), vn.Version("6")), (vn.Version("5"), vn.Version("6")),
(vn.Version("6"), vn.Version("7")),
] ]
# Default timeout for spack database locks in seconds or None (no timeout). # Default timeout for spack database locks in seconds or None (no timeout).
@ -105,7 +106,11 @@
def reader(version): def reader(version):
reader_cls = {vn.Version("5"): spack.spec.SpecfileV1, vn.Version("6"): spack.spec.SpecfileV3} reader_cls = {
vn.Version("5"): spack.spec.SpecfileV1,
vn.Version("6"): spack.spec.SpecfileV3,
vn.Version("7"): spack.spec.SpecfileV4,
}
return reader_cls[version] return reader_cls[version]
@ -743,7 +748,9 @@ def _assign_dependencies(self, spec_reader, hash_key, installs, data):
spec_node_dict = spec_node_dict[spec.name] spec_node_dict = spec_node_dict[spec.name]
if "dependencies" in spec_node_dict: if "dependencies" in spec_node_dict:
yaml_deps = spec_node_dict["dependencies"] yaml_deps = spec_node_dict["dependencies"]
for dname, dhash, dtypes, _ in spec_reader.read_specfile_dep_specs(yaml_deps): for dname, dhash, dtypes, _, virtuals in spec_reader.read_specfile_dep_specs(
yaml_deps
):
# It is important that we always check upstream installations # It is important that we always check upstream installations
# in the same order, and that we always check the local # in the same order, and that we always check the local
# installation first: if a downstream Spack installs a package # installation first: if a downstream Spack installs a package
@ -766,7 +773,7 @@ def _assign_dependencies(self, spec_reader, hash_key, installs, data):
tty.warn(msg) tty.warn(msg)
continue continue
spec._add_dependency(child, deptypes=dtypes) spec._add_dependency(child, deptypes=dtypes, virtuals=virtuals)
def _read_from_file(self, filename): def _read_from_file(self, filename):
"""Fill database from file, do not maintain old data. """Fill database from file, do not maintain old data.
@ -1172,7 +1179,7 @@ def _add(
for dep in spec.edges_to_dependencies(deptype=_tracked_deps): for dep in spec.edges_to_dependencies(deptype=_tracked_deps):
dkey = dep.spec.dag_hash() dkey = dep.spec.dag_hash()
upstream, record = self.query_by_spec_hash(dkey) upstream, record = self.query_by_spec_hash(dkey)
new_spec._add_dependency(record.spec, deptypes=dep.deptypes) new_spec._add_dependency(record.spec, deptypes=dep.deptypes, virtuals=dep.virtuals)
if not upstream: if not upstream:
record.ref_count += 1 record.ref_count += 1

View File

@ -125,7 +125,7 @@ def default_manifest_yaml():
valid_environment_name_re = r"^\w[\w-]*$" valid_environment_name_re = r"^\w[\w-]*$"
#: version of the lockfile format. Must increase monotonically. #: version of the lockfile format. Must increase monotonically.
lockfile_format_version = 4 lockfile_format_version = 5
READER_CLS = { READER_CLS = {
@ -133,6 +133,7 @@ def default_manifest_yaml():
2: spack.spec.SpecfileV1, 2: spack.spec.SpecfileV1,
3: spack.spec.SpecfileV2, 3: spack.spec.SpecfileV2,
4: spack.spec.SpecfileV3, 4: spack.spec.SpecfileV3,
5: spack.spec.SpecfileV4,
} }
@ -1548,12 +1549,13 @@ def _concretize_separately(self, tests=False):
for h in self.specs_by_hash: for h in self.specs_by_hash:
current_spec, computed_spec = self.specs_by_hash[h], by_hash[h] current_spec, computed_spec = self.specs_by_hash[h], by_hash[h]
for node in computed_spec.traverse(): for node in computed_spec.traverse():
test_deps = node.dependencies(deptype="test") test_edges = node.edges_to_dependencies(deptype="test")
for test_dependency in test_deps: for current_edge in test_edges:
test_dependency = current_edge.spec
if test_dependency in current_spec[node.name]: if test_dependency in current_spec[node.name]:
continue continue
current_spec[node.name].add_dependency_edge( current_spec[node.name].add_dependency_edge(
test_dependency.copy(), deptypes="test" test_dependency.copy(), deptypes="test", virtuals=current_edge.virtuals
) )
results = [ results = [
@ -2184,9 +2186,9 @@ def _read_lockfile_dict(self, d):
# and add them to the spec # and add them to the spec
for lockfile_key, node_dict in json_specs_by_hash.items(): for lockfile_key, node_dict in json_specs_by_hash.items():
name, data = reader.name_and_data(node_dict) name, data = reader.name_and_data(node_dict)
for _, dep_hash, deptypes, _ in reader.dependencies_from_node_dict(data): for _, dep_hash, deptypes, _, virtuals in reader.dependencies_from_node_dict(data):
specs_by_hash[lockfile_key]._add_dependency( specs_by_hash[lockfile_key]._add_dependency(
specs_by_hash[dep_hash], deptypes=deptypes specs_by_hash[dep_hash], deptypes=deptypes, virtuals=virtuals
) )
# Traverse the root specs one at a time in the order they appear. # Traverse the root specs one at a time in the order they appear.

View File

@ -544,6 +544,7 @@ def _static_edges(specs, deptype):
spack.spec.Spec(parent_name), spack.spec.Spec(parent_name),
spack.spec.Spec(dependency_name), spack.spec.Spec(dependency_name),
deptypes=deptype, deptypes=deptype,
virtuals=(),
) )

View File

@ -231,7 +231,9 @@ def _packages_needed_to_bootstrap_compiler(compiler, architecture, pkgs):
dep.concretize() dep.concretize()
# mark compiler as depended-on by the packages that use it # mark compiler as depended-on by the packages that use it
for pkg in pkgs: for pkg in pkgs:
dep._dependents.add(spack.spec.DependencySpec(pkg.spec, dep, deptypes=("build",))) dep._dependents.add(
spack.spec.DependencySpec(pkg.spec, dep, deptypes=("build",), virtuals=())
)
packages = [(s.package, False) for s in dep.traverse(order="post", root=False)] packages = [(s.package, False) for s in dep.traverse(order="post", root=False)]
packages.append((dep.package, True)) packages.append((dep.package, True))

View File

@ -291,7 +291,7 @@ def next_spec(
if root_spec.concrete: if root_spec.concrete:
raise spack.spec.RedundantSpecError(root_spec, "^" + str(dependency)) raise spack.spec.RedundantSpecError(root_spec, "^" + str(dependency))
root_spec._add_dependency(dependency, deptypes=()) root_spec._add_dependency(dependency, deptypes=(), virtuals=())
else: else:
break break

View File

@ -292,8 +292,8 @@ def from_json(stream, repository):
index.providers = _transform( index.providers = _transform(
providers, providers,
lambda vpkg, plist: ( lambda vpkg, plist: (
spack.spec.SpecfileV3.from_node_dict(vpkg), spack.spec.SpecfileV4.from_node_dict(vpkg),
set(spack.spec.SpecfileV3.from_node_dict(p) for p in plist), set(spack.spec.SpecfileV4.from_node_dict(p) for p in plist),
), ),
) )
return index return index

View File

@ -2500,10 +2500,15 @@ def depends_on(self, pkg, dep, type):
assert len(dependencies) < 2, msg assert len(dependencies) < 2, msg
if not dependencies: if not dependencies:
self._specs[pkg].add_dependency_edge(self._specs[dep], deptypes=(type,)) self._specs[pkg].add_dependency_edge(self._specs[dep], deptypes=(type,), virtuals=())
else: else:
# TODO: This assumes that each solve unifies dependencies # TODO: This assumes that each solve unifies dependencies
dependencies[0].add_type(type) dependencies[0].update_deptypes(deptypes=(type,))
def virtual_on_edge(self, pkg, provider, virtual):
dependencies = self._specs[pkg].edges_to_dependencies(name=provider)
assert len(dependencies) == 1
dependencies[0].update_virtuals((virtual,))
def reorder_flags(self): def reorder_flags(self):
"""Order compiler flags on specs in predefined order. """Order compiler flags on specs in predefined order.
@ -2581,6 +2586,8 @@ def sort_fn(function_tuple):
return (-2, 0) return (-2, 0)
elif name == "external_spec_selected": elif name == "external_spec_selected":
return (0, 0) # note out of order so this goes last return (0, 0) # note out of order so this goes last
elif name == "virtual_on_edge":
return (1, 0)
else: else:
return (-1, 0) return (-1, 0)

View File

@ -300,6 +300,11 @@ attr("depends_on", Package, Provider, Type)
provider(Provider, Virtual), provider(Provider, Virtual),
not external(Package). not external(Package).
attr("virtual_on_edge", Package, Provider, Virtual)
:- dependency_holds(Package, Virtual, Type),
provider(Provider, Virtual),
not external(Package).
% dependencies on virtuals also imply that the virtual is a virtual node % dependencies on virtuals also imply that the virtual is a virtual node
attr("virtual_node", Virtual) attr("virtual_node", Virtual)
:- dependency_holds(Package, Virtual, Type), :- dependency_holds(Package, Virtual, Type),

View File

@ -170,7 +170,7 @@
) )
#: specfile format version. Must increase monotonically #: specfile format version. Must increase monotonically
SPECFILE_FORMAT_VERSION = 3 SPECFILE_FORMAT_VERSION = 4
def colorize_spec(spec): def colorize_spec(spec):
@ -714,47 +714,81 @@ class DependencySpec:
parent: starting node of the edge parent: starting node of the edge
spec: ending node of the edge. spec: ending node of the edge.
deptypes: list of strings, representing dependency relationships. deptypes: list of strings, representing dependency relationships.
virtuals: virtual packages provided from child to parent node.
""" """
__slots__ = "parent", "spec", "deptypes" __slots__ = "parent", "spec", "parameters"
def __init__(self, parent: "Spec", spec: "Spec", *, deptypes: dp.DependencyArgument): def __init__(
self,
parent: "Spec",
spec: "Spec",
*,
deptypes: dp.DependencyArgument,
virtuals: Tuple[str, ...],
):
self.parent = parent self.parent = parent
self.spec = spec self.spec = spec
self.deptypes = dp.canonical_deptype(deptypes) self.parameters = {
"deptypes": dp.canonical_deptype(deptypes),
"virtuals": tuple(sorted(set(virtuals))),
}
def update_deptypes(self, deptypes: dp.DependencyArgument) -> bool: @property
deptypes = set(deptypes) def deptypes(self) -> Tuple[str, ...]:
deptypes.update(self.deptypes) return self.parameters["deptypes"]
deptypes = tuple(sorted(deptypes))
changed = self.deptypes != deptypes
self.deptypes = deptypes @property
return changed def virtuals(self) -> Tuple[str, ...]:
return self.parameters["virtuals"]
def _update_edge_multivalued_property(
self, property_name: str, value: Tuple[str, ...]
) -> bool:
current = self.parameters[property_name]
update = set(current) | set(value)
update = tuple(sorted(update))
changed = current != update
if not changed:
return False
self.parameters[property_name] = update
return True
def update_deptypes(self, deptypes: Tuple[str, ...]) -> bool:
"""Update the current dependency types"""
return self._update_edge_multivalued_property("deptypes", deptypes)
def update_virtuals(self, virtuals: Tuple[str, ...]) -> bool:
"""Update the list of provided virtuals"""
return self._update_edge_multivalued_property("virtuals", virtuals)
def copy(self) -> "DependencySpec": def copy(self) -> "DependencySpec":
return DependencySpec(self.parent, self.spec, deptypes=self.deptypes) """Return a copy of this edge"""
return DependencySpec(
def add_type(self, type: dp.DependencyArgument): self.parent, self.spec, deptypes=self.deptypes, virtuals=self.virtuals
self.deptypes = dp.canonical_deptype(self.deptypes + dp.canonical_deptype(type)) )
def _cmp_iter(self): def _cmp_iter(self):
yield self.parent.name if self.parent else None yield self.parent.name if self.parent else None
yield self.spec.name if self.spec else None yield self.spec.name if self.spec else None
yield self.deptypes yield self.deptypes
yield self.virtuals
def __str__(self) -> str: def __str__(self) -> str:
return "%s %s--> %s" % ( parent = self.parent.name if self.parent else None
self.parent.name if self.parent else None, child = self.spec.name if self.spec else None
self.deptypes, return f"{parent} {self.deptypes}[virtuals={','.join(self.virtuals)}] --> {child}"
self.spec.name if self.spec else None,
)
def canonical(self) -> Tuple[str, str, Tuple[str, ...]]: def canonical(self) -> Tuple[str, str, Tuple[str, ...], Tuple[str, ...]]:
return self.parent.dag_hash(), self.spec.dag_hash(), self.deptypes return self.parent.dag_hash(), self.spec.dag_hash(), self.deptypes, self.virtuals
def flip(self) -> "DependencySpec": def flip(self) -> "DependencySpec":
return DependencySpec(parent=self.spec, spec=self.parent, deptypes=self.deptypes) """Flip the dependency, and drop virtual information"""
return DependencySpec(
parent=self.spec, spec=self.parent, deptypes=self.deptypes, virtuals=()
)
class CompilerFlag(str): class CompilerFlag(str):
@ -1575,10 +1609,12 @@ def _set_compiler(self, compiler):
) )
self.compiler = compiler self.compiler = compiler
def _add_dependency(self, spec: "Spec", *, deptypes: dp.DependencyArgument): def _add_dependency(
self, spec: "Spec", *, deptypes: dp.DependencyArgument, virtuals: Tuple[str, ...]
):
"""Called by the parser to add another spec as a dependency.""" """Called by the parser to add another spec as a dependency."""
if spec.name not in self._dependencies or not spec.name: if spec.name not in self._dependencies or not spec.name:
self.add_dependency_edge(spec, deptypes=deptypes) self.add_dependency_edge(spec, deptypes=deptypes, virtuals=virtuals)
return return
# Keep the intersection of constraints when a dependency is added # Keep the intersection of constraints when a dependency is added
@ -1596,34 +1632,58 @@ def _add_dependency(self, spec: "Spec", *, deptypes: dp.DependencyArgument):
"Cannot depend on incompatible specs '%s' and '%s'" % (dspec.spec, spec) "Cannot depend on incompatible specs '%s' and '%s'" % (dspec.spec, spec)
) )
def add_dependency_edge(self, dependency_spec: "Spec", *, deptypes: dp.DependencyArgument): def add_dependency_edge(
self,
dependency_spec: "Spec",
*,
deptypes: dp.DependencyArgument,
virtuals: Tuple[str, ...],
):
"""Add a dependency edge to this spec. """Add a dependency edge to this spec.
Args: Args:
dependency_spec: spec of the dependency dependency_spec: spec of the dependency
deptypes: dependency types for this edge deptypes: dependency types for this edge
virtuals: virtuals provided by this edge
""" """
deptypes = dp.canonical_deptype(deptypes) deptypes = dp.canonical_deptype(deptypes)
# Check if we need to update edges that are already present # Check if we need to update edges that are already present
selected = self._dependencies.select(child=dependency_spec.name) selected = self._dependencies.select(child=dependency_spec.name)
for edge in selected: for edge in selected:
has_errors, details = False, []
msg = f"cannot update the edge from {edge.parent.name} to {edge.spec.name}"
if any(d in edge.deptypes for d in deptypes): if any(d in edge.deptypes for d in deptypes):
msg = ( has_errors = True
'cannot add a dependency on "{0.spec}" of {1} type ' details.append(
'when the "{0.parent}" has the edge {0!s} already' (
f"{edge.parent.name} has already an edge matching any"
f" of these types {str(deptypes)}"
) )
raise spack.error.SpecError(msg.format(edge, deptypes)) )
if any(v in edge.virtuals for v in virtuals):
has_errors = True
details.append(
(
f"{edge.parent.name} has already an edge matching any"
f" of these virtuals {str(virtuals)}"
)
)
if has_errors:
raise spack.error.SpecError(msg, "\n".join(details))
for edge in selected: for edge in selected:
if id(dependency_spec) == id(edge.spec): if id(dependency_spec) == id(edge.spec):
# If we are here, it means the edge object was previously added to # If we are here, it means the edge object was previously added to
# both the parent and the child. When we update this object they'll # both the parent and the child. When we update this object they'll
# both see the deptype modification. # both see the deptype modification.
edge.add_type(deptypes) edge.update_deptypes(deptypes=deptypes)
edge.update_virtuals(virtuals=virtuals)
return return
edge = DependencySpec(self, dependency_spec, deptypes=deptypes) edge = DependencySpec(self, dependency_spec, deptypes=deptypes, virtuals=virtuals)
self._dependencies.add(edge) self._dependencies.add(edge)
dependency_spec._dependents.add(edge) dependency_spec._dependents.add(edge)
@ -1896,12 +1956,12 @@ def lookup_hash(self):
for node in self.traverse(root=False): for node in self.traverse(root=False):
if node.abstract_hash: if node.abstract_hash:
new = node._lookup_hash() new = node._lookup_hash()
spec._add_dependency(new, deptypes=()) spec._add_dependency(new, deptypes=(), virtuals=())
# reattach nodes that were not otherwise satisfied by new dependencies # reattach nodes that were not otherwise satisfied by new dependencies
for node in self.traverse(root=False): for node in self.traverse(root=False):
if not any(n._satisfies(node) for n in spec.traverse()): if not any(n._satisfies(node) for n in spec.traverse()):
spec._add_dependency(node.copy(), deptypes=()) spec._add_dependency(node.copy(), deptypes=(), virtuals=())
return spec return spec
@ -2036,8 +2096,14 @@ def to_node_dict(self, hash=ht.dag_hash):
name_tuple = ("name", name) name_tuple = ("name", name)
for dspec in edges_for_name: for dspec in edges_for_name:
hash_tuple = (hash.name, dspec.spec._cached_hash(hash)) hash_tuple = (hash.name, dspec.spec._cached_hash(hash))
type_tuple = ("type", sorted(str(s) for s in dspec.deptypes)) parameters_tuple = (
deps_list.append(syaml.syaml_dict([name_tuple, hash_tuple, type_tuple])) "parameters",
syaml.syaml_dict(
(key, dspec.parameters[key]) for key in sorted(dspec.parameters)
),
)
ordered_entries = [name_tuple, hash_tuple, parameters_tuple]
deps_list.append(syaml.syaml_dict(ordered_entries))
d["dependencies"] = deps_list d["dependencies"] = deps_list
# Name is included in case this is replacing a virtual. # Name is included in case this is replacing a virtual.
@ -2361,7 +2427,7 @@ def spec_and_dependency_types(s):
dag_node, dependency_types = spec_and_dependency_types(s) dag_node, dependency_types = spec_and_dependency_types(s)
dependency_spec = spec_builder({dag_node: s_dependencies}) dependency_spec = spec_builder({dag_node: s_dependencies})
spec._add_dependency(dependency_spec, deptypes=dependency_types) spec._add_dependency(dependency_spec, deptypes=dependency_types, virtuals=())
return spec return spec
@ -2379,8 +2445,10 @@ def from_dict(data):
spec = SpecfileV1.load(data) spec = SpecfileV1.load(data)
elif int(data["spec"]["_meta"]["version"]) == 2: elif int(data["spec"]["_meta"]["version"]) == 2:
spec = SpecfileV2.load(data) spec = SpecfileV2.load(data)
else: elif int(data["spec"]["_meta"]["version"]) == 3:
spec = SpecfileV3.load(data) spec = SpecfileV3.load(data)
else:
spec = SpecfileV4.load(data)
# Any git version should # Any git version should
for s in spec.traverse(): for s in spec.traverse():
@ -2529,6 +2597,7 @@ def _concretize_helper(self, concretizer, presets=None, visited=None):
def _replace_with(self, concrete): def _replace_with(self, concrete):
"""Replace this virtual spec with a concrete spec.""" """Replace this virtual spec with a concrete spec."""
assert self.virtual assert self.virtual
virtuals = (self.name,)
for dep_spec in itertools.chain.from_iterable(self._dependents.values()): for dep_spec in itertools.chain.from_iterable(self._dependents.values()):
dependent = dep_spec.parent dependent = dep_spec.parent
deptypes = dep_spec.deptypes deptypes = dep_spec.deptypes
@ -2539,7 +2608,11 @@ def _replace_with(self, concrete):
# add the replacement, unless it is already a dep of dependent. # add the replacement, unless it is already a dep of dependent.
if concrete.name not in dependent._dependencies: if concrete.name not in dependent._dependencies:
dependent._add_dependency(concrete, deptypes=deptypes) dependent._add_dependency(concrete, deptypes=deptypes, virtuals=virtuals)
else:
dependent.edges_to_dependencies(name=concrete.name)[0].update_virtuals(
virtuals=virtuals
)
def _expand_virtual_packages(self, concretizer): def _expand_virtual_packages(self, concretizer):
"""Find virtual packages in this spec, replace them with providers, """Find virtual packages in this spec, replace them with providers,
@ -3180,7 +3253,9 @@ def _merge_dependency(self, dependency, visited, spec_deps, provider_index, test
# If it's a virtual dependency, try to find an existing # If it's a virtual dependency, try to find an existing
# provider in the spec, and merge that. # provider in the spec, and merge that.
virtuals = ()
if spack.repo.path.is_virtual_safe(dep.name): if spack.repo.path.is_virtual_safe(dep.name):
virtuals = (dep.name,)
visited.add(dep.name) visited.add(dep.name)
provider = self._find_provider(dep, provider_index) provider = self._find_provider(dep, provider_index)
if provider: if provider:
@ -3236,7 +3311,7 @@ def _merge_dependency(self, dependency, visited, spec_deps, provider_index, test
# Add merged spec to my deps and recurse # Add merged spec to my deps and recurse
spec_dependency = spec_deps[dep.name] spec_dependency = spec_deps[dep.name]
if dep.name not in self._dependencies: if dep.name not in self._dependencies:
self._add_dependency(spec_dependency, deptypes=dependency.type) self._add_dependency(spec_dependency, deptypes=dependency.type, virtuals=virtuals)
changed |= spec_dependency._normalize_helper(visited, spec_deps, provider_index, tests) changed |= spec_dependency._normalize_helper(visited, spec_deps, provider_index, tests)
return changed return changed
@ -3573,15 +3648,20 @@ def _constrain_dependencies(self, other):
changed |= edges_from_name[0].update_deptypes( changed |= edges_from_name[0].update_deptypes(
other._dependencies[name][0].deptypes other._dependencies[name][0].deptypes
) )
changed |= edges_from_name[0].update_virtuals(
other._dependencies[name][0].virtuals
)
# Update with additional constraints from other spec # Update with additional constraints from other spec
# operate on direct dependencies only, because a concrete dep # operate on direct dependencies only, because a concrete dep
# represented by hash may have structure that needs to be preserved # represented by hash may have structure that needs to be preserved
for name in other.direct_dep_difference(self): for name in other.direct_dep_difference(self):
dep_spec_copy = other._get_dependency(name) dep_spec_copy = other._get_dependency(name)
dep_copy = dep_spec_copy.spec self._add_dependency(
deptypes = dep_spec_copy.deptypes dep_spec_copy.spec.copy(),
self._add_dependency(dep_copy.copy(), deptypes=deptypes) deptypes=dep_spec_copy.deptypes,
virtuals=dep_spec_copy.virtuals,
)
changed = True changed = True
return changed return changed
@ -3965,7 +4045,7 @@ def spid(spec):
new_specs[spid(edge.spec)] = edge.spec.copy(deps=False) new_specs[spid(edge.spec)] = edge.spec.copy(deps=False)
new_specs[spid(edge.parent)].add_dependency_edge( new_specs[spid(edge.parent)].add_dependency_edge(
new_specs[spid(edge.spec)], deptypes=edge.deptypes new_specs[spid(edge.spec)], deptypes=edge.deptypes, virtuals=edge.virtuals
) )
def copy(self, deps=True, **kwargs): def copy(self, deps=True, **kwargs):
@ -4635,12 +4715,16 @@ def from_self(name, transitive):
if name in self_nodes: if name in self_nodes:
for edge in self[name].edges_to_dependencies(): for edge in self[name].edges_to_dependencies():
dep_name = deps_to_replace.get(edge.spec, edge.spec).name dep_name = deps_to_replace.get(edge.spec, edge.spec).name
nodes[name].add_dependency_edge(nodes[dep_name], deptypes=edge.deptypes) nodes[name].add_dependency_edge(
nodes[dep_name], deptypes=edge.deptypes, virtuals=edge.virtuals
)
if any(dep not in self_nodes for dep in self[name]._dependencies): if any(dep not in self_nodes for dep in self[name]._dependencies):
nodes[name].build_spec = self[name].build_spec nodes[name].build_spec = self[name].build_spec
else: else:
for edge in other[name].edges_to_dependencies(): for edge in other[name].edges_to_dependencies():
nodes[name].add_dependency_edge(nodes[edge.spec.name], deptypes=edge.deptypes) nodes[name].add_dependency_edge(
nodes[edge.spec.name], deptypes=edge.deptypes, virtuals=edge.virtuals
)
if any(dep not in other_nodes for dep in other[name]._dependencies): if any(dep not in other_nodes for dep in other[name]._dependencies):
nodes[name].build_spec = other[name].build_spec nodes[name].build_spec = other[name].build_spec
@ -4730,11 +4814,40 @@ def merge_abstract_anonymous_specs(*abstract_specs: Spec):
# Update with additional constraints from other spec # Update with additional constraints from other spec
for name in current_spec_constraint.direct_dep_difference(merged_spec): for name in current_spec_constraint.direct_dep_difference(merged_spec):
edge = next(iter(current_spec_constraint.edges_to_dependencies(name))) edge = next(iter(current_spec_constraint.edges_to_dependencies(name)))
merged_spec._add_dependency(edge.spec.copy(), deptypes=edge.deptypes) merged_spec._add_dependency(
edge.spec.copy(), deptypes=edge.deptypes, virtuals=edge.virtuals
)
return merged_spec return merged_spec
def reconstruct_virtuals_on_edges(spec):
"""Reconstruct virtuals on edges. Used to read from old DB and reindex.
Args:
spec: spec on which we want to reconstruct virtuals
"""
# Collect all possible virtuals
possible_virtuals = set()
for node in spec.traverse():
try:
possible_virtuals.update({x for x in node.package.dependencies if Spec(x).virtual})
except Exception as e:
warnings.warn(f"cannot reconstruct virtual dependencies on package {node.name}: {e}")
continue
# Assume all incoming edges to provider are marked with virtuals=
for vspec in possible_virtuals:
try:
provider = spec[vspec]
except KeyError:
# Virtual not in the DAG
continue
for edge in provider.edges_from_dependents():
edge.update_virtuals([vspec])
class SpecfileReaderBase: class SpecfileReaderBase:
@classmethod @classmethod
def from_node_dict(cls, node): def from_node_dict(cls, node):
@ -4818,7 +4931,7 @@ def _load(cls, data):
# Pass 0: Determine hash type # Pass 0: Determine hash type
for node in nodes: for node in nodes:
for _, _, _, dhash_type in cls.dependencies_from_node_dict(node): for _, _, _, dhash_type, _ in cls.dependencies_from_node_dict(node):
any_deps = True any_deps = True
if dhash_type: if dhash_type:
hash_type = dhash_type hash_type = dhash_type
@ -4849,8 +4962,10 @@ def _load(cls, data):
# Pass 2: Finish construction of all DAG edges (including build specs) # Pass 2: Finish construction of all DAG edges (including build specs)
for node_hash, node in hash_dict.items(): for node_hash, node in hash_dict.items():
node_spec = node["node_spec"] node_spec = node["node_spec"]
for _, dhash, dtypes, _ in cls.dependencies_from_node_dict(node): for _, dhash, dtypes, _, virtuals in cls.dependencies_from_node_dict(node):
node_spec._add_dependency(hash_dict[dhash]["node_spec"], deptypes=dtypes) node_spec._add_dependency(
hash_dict[dhash]["node_spec"], deptypes=dtypes, virtuals=virtuals
)
if "build_spec" in node.keys(): if "build_spec" in node.keys():
_, bhash, _ = cls.build_spec_from_node_dict(node, hash_type=hash_type) _, bhash, _ = cls.build_spec_from_node_dict(node, hash_type=hash_type)
node_spec._build_spec = hash_dict[bhash]["node_spec"] node_spec._build_spec = hash_dict[bhash]["node_spec"]
@ -4884,9 +4999,10 @@ def load(cls, data):
for node in nodes: for node in nodes:
# get dependency dict from the node. # get dependency dict from the node.
name, data = cls.name_and_data(node) name, data = cls.name_and_data(node)
for dname, _, dtypes, _ in cls.dependencies_from_node_dict(data): for dname, _, dtypes, _, virtuals in cls.dependencies_from_node_dict(data):
deps[name]._add_dependency(deps[dname], deptypes=dtypes) deps[name]._add_dependency(deps[dname], deptypes=dtypes, virtuals=virtuals)
reconstruct_virtuals_on_edges(result)
return result return result
@classmethod @classmethod
@ -4915,18 +5031,20 @@ def read_specfile_dep_specs(cls, deps, hash_type=ht.dag_hash.name):
if h.name in elt: if h.name in elt:
dep_hash, deptypes = elt[h.name], elt["type"] dep_hash, deptypes = elt[h.name], elt["type"]
hash_type = h.name hash_type = h.name
virtuals = []
break break
else: # We never determined a hash type... else: # We never determined a hash type...
raise spack.error.SpecError("Couldn't parse dependency spec.") raise spack.error.SpecError("Couldn't parse dependency spec.")
else: else:
raise spack.error.SpecError("Couldn't parse dependency types in spec.") raise spack.error.SpecError("Couldn't parse dependency types in spec.")
yield dep_name, dep_hash, list(deptypes), hash_type yield dep_name, dep_hash, list(deptypes), hash_type, list(virtuals)
class SpecfileV2(SpecfileReaderBase): class SpecfileV2(SpecfileReaderBase):
@classmethod @classmethod
def load(cls, data): def load(cls, data):
result = cls._load(data) result = cls._load(data)
reconstruct_virtuals_on_edges(result)
return result return result
@classmethod @classmethod
@ -4960,7 +5078,7 @@ def read_specfile_dep_specs(cls, deps, hash_type=ht.dag_hash.name):
raise spack.error.SpecError("Couldn't parse dependency spec.") raise spack.error.SpecError("Couldn't parse dependency spec.")
else: else:
raise spack.error.SpecError("Couldn't parse dependency types in spec.") raise spack.error.SpecError("Couldn't parse dependency types in spec.")
result.append((dep_name, dep_hash, list(deptypes), hash_type)) result.append((dep_name, dep_hash, list(deptypes), hash_type, list(virtuals)))
return result return result
@classmethod @classmethod
@ -4980,6 +5098,20 @@ class SpecfileV3(SpecfileV2):
pass pass
class SpecfileV4(SpecfileV2):
@classmethod
def extract_info_from_dep(cls, elt, hash):
dep_hash = elt[hash.name]
deptypes = elt["parameters"]["deptypes"]
hash_type = hash.name
virtuals = elt["parameters"]["virtuals"]
return dep_hash, deptypes, hash_type, virtuals
@classmethod
def load(cls, data):
return cls._load(data)
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
the corresponding value ``Spec(spec_like``. the corresponding value ``Spec(spec_like``.

View File

@ -2170,3 +2170,14 @@ def test_concretization_with_compilers_supporting_target_any(self):
with spack.config.override("compilers", compiler_configuration): with spack.config.override("compilers", compiler_configuration):
s = spack.spec.Spec("a").concretized() s = spack.spec.Spec("a").concretized()
assert s.satisfies("%gcc@12.1.0") assert s.satisfies("%gcc@12.1.0")
@pytest.mark.parametrize("spec_str", ["mpileaks", "mpileaks ^mpich"])
def test_virtuals_are_annotated_on_edges(self, spec_str, default_mock_concretization):
"""Tests that information on virtuals is annotated on DAG edges"""
spec = default_mock_concretization(spec_str)
mpi_provider = spec["mpi"].name
edges = spec.edges_to_dependencies(name=mpi_provider)
assert len(edges) == 1 and edges[0].virtuals == ("mpi",)
edges = spec.edges_to_dependencies(name="callpath")
assert len(edges) == 1 and edges[0].virtuals == ()

Binary file not shown.

View File

@ -125,7 +125,7 @@ def _mock_installed(self):
# use the installed C. It should *not* force A to use the installed D # use the installed C. It should *not* force A to use the installed D
# *if* we're doing a fresh installation. # *if* we're doing a fresh installation.
a_spec = Spec(a) a_spec = Spec(a)
a_spec._add_dependency(c_spec, deptypes=("build", "link")) a_spec._add_dependency(c_spec, deptypes=("build", "link"), virtuals=())
a_spec.concretize() a_spec.concretize()
assert spack.version.Version("2") == a_spec[c][d].version assert spack.version.Version("2") == a_spec[c][d].version
assert spack.version.Version("2") == a_spec[e].version assert spack.version.Version("2") == a_spec[e].version
@ -148,7 +148,7 @@ def test_specify_preinstalled_dep(tmpdir, monkeypatch):
monkeypatch.setattr(Spec, "installed", property(lambda x: x.name != "a")) monkeypatch.setattr(Spec, "installed", property(lambda x: x.name != "a"))
a_spec = Spec("a") a_spec = Spec("a")
a_spec._add_dependency(b_spec, deptypes=("build", "link")) a_spec._add_dependency(b_spec, deptypes=("build", "link"), virtuals=())
a_spec.concretize() a_spec.concretize()
assert set(x.name for x in a_spec.traverse()) == set(["a", "b", "c"]) assert set(x.name for x in a_spec.traverse()) == set(["a", "b", "c"])
@ -989,9 +989,9 @@ def test_synthetic_construction_of_split_dependencies_from_same_package(mock_pac
link_run_spec = Spec("c@=1.0").concretized() link_run_spec = Spec("c@=1.0").concretized()
build_spec = Spec("c@=2.0").concretized() build_spec = Spec("c@=2.0").concretized()
root.add_dependency_edge(link_run_spec, deptypes="link") root.add_dependency_edge(link_run_spec, deptypes="link", virtuals=())
root.add_dependency_edge(link_run_spec, deptypes="run") root.add_dependency_edge(link_run_spec, deptypes="run", virtuals=())
root.add_dependency_edge(build_spec, deptypes="build") root.add_dependency_edge(build_spec, deptypes="build", virtuals=())
# Check dependencies from the perspective of root # Check dependencies from the perspective of root
assert len(root.dependencies()) == 2 assert len(root.dependencies()) == 2
@ -1017,7 +1017,7 @@ def test_synthetic_construction_bootstrapping(mock_packages, config):
root = Spec("b@=2.0").concretized() root = Spec("b@=2.0").concretized()
bootstrap = Spec("b@=1.0").concretized() bootstrap = Spec("b@=1.0").concretized()
root.add_dependency_edge(bootstrap, deptypes="build") root.add_dependency_edge(bootstrap, deptypes="build", virtuals=())
assert len(root.dependencies()) == 1 assert len(root.dependencies()) == 1
assert root.dependencies()[0].name == "b" assert root.dependencies()[0].name == "b"
@ -1036,7 +1036,7 @@ def test_addition_of_different_deptypes_in_multiple_calls(mock_packages, config)
bootstrap = Spec("b@=1.0").concretized() bootstrap = Spec("b@=1.0").concretized()
for current_deptype in ("build", "link", "run"): for current_deptype in ("build", "link", "run"):
root.add_dependency_edge(bootstrap, deptypes=current_deptype) root.add_dependency_edge(bootstrap, deptypes=current_deptype, virtuals=())
# Check edges in dependencies # Check edges in dependencies
assert len(root.edges_to_dependencies()) == 1 assert len(root.edges_to_dependencies()) == 1
@ -1063,9 +1063,9 @@ def test_adding_same_deptype_with_the_same_name_raises(
c1 = Spec("b@=1.0").concretized() c1 = Spec("b@=1.0").concretized()
c2 = Spec("b@=2.0").concretized() c2 = Spec("b@=2.0").concretized()
p.add_dependency_edge(c1, deptypes=c1_deptypes) p.add_dependency_edge(c1, deptypes=c1_deptypes, virtuals=())
with pytest.raises(spack.error.SpackError): with pytest.raises(spack.error.SpackError):
p.add_dependency_edge(c2, deptypes=c2_deptypes) p.add_dependency_edge(c2, deptypes=c2_deptypes, virtuals=())
@pytest.mark.regression("33499") @pytest.mark.regression("33499")
@ -1084,16 +1084,16 @@ def test_indexing_prefers_direct_or_transitive_link_deps():
z3_flavor_1 = Spec("z3 +through_a1") z3_flavor_1 = Spec("z3 +through_a1")
z3_flavor_2 = Spec("z3 +through_z1") z3_flavor_2 = Spec("z3 +through_z1")
root.add_dependency_edge(a1, deptypes=("build", "run", "test")) root.add_dependency_edge(a1, deptypes=("build", "run", "test"), virtuals=())
# unique package as a dep of a build/run/test type dep. # unique package as a dep of a build/run/test type dep.
a1.add_dependency_edge(a2, deptypes="all") a1.add_dependency_edge(a2, deptypes="all", virtuals=())
a1.add_dependency_edge(z3_flavor_1, deptypes="all") a1.add_dependency_edge(z3_flavor_1, deptypes="all", virtuals=())
# chain of link type deps root -> z1 -> z2 -> z3 # chain of link type deps root -> z1 -> z2 -> z3
root.add_dependency_edge(z1, deptypes="link") root.add_dependency_edge(z1, deptypes="link", virtuals=())
z1.add_dependency_edge(z2, deptypes="link") z1.add_dependency_edge(z2, deptypes="link", virtuals=())
z2.add_dependency_edge(z3_flavor_2, deptypes="link") z2.add_dependency_edge(z3_flavor_2, deptypes="link", virtuals=())
# Indexing should prefer the link-type dep. # Indexing should prefer the link-type dep.
assert "through_z1" in root["z3"].variants assert "through_z1" in root["z3"].variants

View File

@ -971,7 +971,7 @@ def test_error_message_unknown_variant(self):
def test_satisfies_dependencies_ordered(self): def test_satisfies_dependencies_ordered(self):
d = Spec("zmpi ^fake") d = Spec("zmpi ^fake")
s = Spec("mpileaks") s = Spec("mpileaks")
s._add_dependency(d, deptypes=()) s._add_dependency(d, deptypes=(), virtuals=())
assert s.satisfies("mpileaks ^zmpi ^fake") assert s.satisfies("mpileaks ^zmpi ^fake")
@pytest.mark.parametrize("transitive", [True, False]) @pytest.mark.parametrize("transitive", [True, False])
@ -1018,6 +1018,7 @@ def test_is_extension_after_round_trip_to_dict(config, mock_packages, spec_str):
def test_malformed_spec_dict(): def test_malformed_spec_dict():
# FIXME: This test was really testing the specific implementation with an ad-hoc test
with pytest.raises(SpecError, match="malformed"): with pytest.raises(SpecError, match="malformed"):
Spec.from_dict( Spec.from_dict(
{"spec": {"_meta": {"version": 2}, "nodes": [{"dependencies": {"name": "foo"}}]}} {"spec": {"_meta": {"version": 2}, "nodes": [{"dependencies": {"name": "foo"}}]}}
@ -1025,6 +1026,7 @@ def test_malformed_spec_dict():
def test_spec_dict_hashless_dep(): def test_spec_dict_hashless_dep():
# FIXME: This test was really testing the specific implementation with an ad-hoc test
with pytest.raises(SpecError, match="Couldn't parse"): with pytest.raises(SpecError, match="Couldn't parse"):
Spec.from_dict( Spec.from_dict(
{ {
@ -1118,7 +1120,7 @@ def test_concretize_partial_old_dag_hash_spec(mock_packages, config):
# add it to an abstract spec as a dependency # add it to an abstract spec as a dependency
top = Spec("dt-diamond") top = Spec("dt-diamond")
top.add_dependency_edge(bottom, deptypes=()) top.add_dependency_edge(bottom, deptypes=(), virtuals=())
# concretize with the already-concrete dependency # concretize with the already-concrete dependency
top.concretize() top.concretize()

View File

@ -43,12 +43,6 @@ def check_json_round_trip(spec):
assert spec.eq_dag(spec_from_json) assert spec.eq_dag(spec_from_json)
def test_simple_spec():
spec = Spec("mpileaks")
check_yaml_round_trip(spec)
check_json_round_trip(spec)
def test_read_spec_from_signed_json(): def test_read_spec_from_signed_json():
spec_dir = os.path.join(spack.paths.test_path, "data", "mirrors", "signed_json") spec_dir = os.path.join(spack.paths.test_path, "data", "mirrors", "signed_json")
file_name = ( file_name = (
@ -70,13 +64,6 @@ def check_spec(spec_to_check):
check_spec(s) check_spec(s)
def test_normal_spec(mock_packages):
spec = Spec("mpileaks+debug~opt")
spec.normalize()
check_yaml_round_trip(spec)
check_json_round_trip(spec)
@pytest.mark.parametrize( @pytest.mark.parametrize(
"invalid_yaml", ["playing_playlist: {{ action }} playlist {{ playlist_name }}"] "invalid_yaml", ["playing_playlist: {{ action }} playlist {{ playlist_name }}"]
) )
@ -95,37 +82,28 @@ def test_invalid_json_spec(invalid_json, error_message):
assert error_message in exc_msg assert error_message in exc_msg
def test_external_spec(config, mock_packages): @pytest.mark.parametrize(
spec = Spec("externaltool") "abstract_spec",
spec.concretize() [
check_yaml_round_trip(spec) # Externals
check_json_round_trip(spec) "externaltool",
"externaltest",
spec = Spec("externaltest") # Ambiguous version spec
spec.concretize() "mpileaks@1.0:5.0,6.1,7.3+debug~opt",
check_yaml_round_trip(spec) # Variants
check_json_round_trip(spec) "mpileaks+debug~opt",
'multivalue-variant foo="bar,baz"',
# Virtuals on edges
def test_ambiguous_version_spec(mock_packages): "callpath",
spec = Spec("mpileaks@1.0:5.0,6.1,7.3+debug~opt") "mpileaks",
spec.normalize() ],
check_yaml_round_trip(spec) )
check_json_round_trip(spec) def test_roundtrip_concrete_specs(abstract_spec, default_mock_concretization):
check_yaml_round_trip(Spec(abstract_spec))
check_json_round_trip(Spec(abstract_spec))
def test_concrete_spec(config, mock_packages): concrete_spec = default_mock_concretization(abstract_spec)
spec = Spec("mpileaks+debug~opt") check_yaml_round_trip(concrete_spec)
spec.concretize() check_json_round_trip(concrete_spec)
check_yaml_round_trip(spec)
check_json_round_trip(spec)
def test_yaml_multivalue(config, mock_packages):
spec = Spec('multivalue-variant foo="bar,baz"')
spec.concretize()
check_yaml_round_trip(spec)
check_json_round_trip(spec)
def test_yaml_subdag(config, mock_packages): def test_yaml_subdag(config, mock_packages):
@ -506,6 +484,8 @@ def test_legacy_yaml(tmpdir, install_mockery, mock_packages):
("specfiles/hdf5.v017.json.gz", "xqh5iyjjtrp2jw632cchacn3l7vqzf3m", spack.spec.SpecfileV2), ("specfiles/hdf5.v017.json.gz", "xqh5iyjjtrp2jw632cchacn3l7vqzf3m", spack.spec.SpecfileV2),
# Use "full hash" everywhere, see https://github.com/spack/spack/pull/28504 # Use "full hash" everywhere, see https://github.com/spack/spack/pull/28504
("specfiles/hdf5.v019.json.gz", "iulacrbz7o5v5sbj7njbkyank3juh6d3", spack.spec.SpecfileV3), ("specfiles/hdf5.v019.json.gz", "iulacrbz7o5v5sbj7njbkyank3juh6d3", spack.spec.SpecfileV3),
# Add properties on edges, see https://github.com/spack/spack/pull/34821
("specfiles/hdf5.v020.json.gz", "vlirlcgazhvsvtundz4kug75xkkqqgou", spack.spec.SpecfileV4),
], ],
) )
def test_load_json_specfiles(specfile, expected_hash, reader_cls): def test_load_json_specfiles(specfile, expected_hash, reader_cls):

View File

@ -19,7 +19,7 @@ def create_dag(nodes, edges):
""" """
specs = {name: Spec(name) for name in nodes} specs = {name: Spec(name) for name in nodes}
for parent, child, deptypes in edges: for parent, child, deptypes in edges:
specs[parent].add_dependency_edge(specs[child], deptypes=deptypes) specs[parent].add_dependency_edge(specs[child], deptypes=deptypes, virtuals=())
return specs return specs

View File

@ -211,7 +211,9 @@ def get_visitor_from_args(cover, direction, deptype, key=id, visited=None, visit
def with_artificial_edges(specs): def with_artificial_edges(specs):
"""Initialize a list of edges from an imaginary root node to the root specs.""" """Initialize a list of edges from an imaginary root node to the root specs."""
return [ return [
EdgeAndDepth(edge=spack.spec.DependencySpec(parent=None, spec=s, deptypes=()), depth=0) EdgeAndDepth(
edge=spack.spec.DependencySpec(parent=None, spec=s, deptypes=(), virtuals=()), depth=0
)
for s in specs for s in specs
] ]