concretizer: reduce search space with static analysis (#48729)
Currently, when we setup the ASP problem for `clingo`, we don't take into account the configuration. This results in setting up ASP problems that are larger than necessary, with possibly redundant information, and higher concretization times. This PR tries to improve things by adding an opt-in feature that computes the _possible dependencies_ of a solve taking also into account the current configuration, and avoids adding possible dependencies that we are certain can't be in the final solution. The feature can be activated with: ```yaml concretizer: static_analysis: true ``` Examples of simple rules to discard dependencies are: - Dependencies that are not buildable, and for which no binary is present (e.g. `cray-mpich` etc. on non Cray systems) - Dependencies that are not for the current platform (e.g. `msmpi` on non Windows platforms) - Conditional dependencies that cannot be activated, because of some user requirement (e.g. `cuda` etc. if the user requires `~cuda` in configuration) - Virtual providers that cannot be used, because of a requirement on a virtual The speed-up these rules seem to give depends on the use case at hand, but if the configuration is updated properly, they are noticeable. Since in cases where there is no rule to exclude packages upfront, reuse is active, and this option is activated, it's possible to see some minor slow down, the feature has been added as opt-in, so it's turned off by default.
This commit is contained in:
parent
1a71bb046e
commit
54210270c8
@ -63,3 +63,7 @@ concretizer:
|
||||
# Setting this to false yields unreproducible results, so we advise to use that value only
|
||||
# for debugging purposes (e.g. check which constraints can help Spack concretize faster).
|
||||
error_on_timeout: true
|
||||
|
||||
# Static analysis may reduce the concretization time by generating smaller ASP problems, in
|
||||
# cases where there are requirements that prevent part of the search space to be explored.
|
||||
static_analysis: false
|
||||
|
@ -9,9 +9,9 @@
|
||||
|
||||
import spack.cmd
|
||||
import spack.environment as ev
|
||||
import spack.package_base
|
||||
import spack.store
|
||||
from spack.cmd.common import arguments
|
||||
from spack.solver.input_analysis import create_graph_analyzer
|
||||
|
||||
description = "show dependencies of a package"
|
||||
section = "basic"
|
||||
@ -68,15 +68,17 @@ def dependencies(parser, args):
|
||||
|
||||
else:
|
||||
spec = specs[0]
|
||||
dependencies = spack.package_base.possible_dependencies(
|
||||
dependencies, virtuals, _ = create_graph_analyzer().possible_dependencies(
|
||||
spec,
|
||||
transitive=args.transitive,
|
||||
expand_virtuals=args.expand_virtuals,
|
||||
depflag=args.deptype,
|
||||
allowed_deps=args.deptype,
|
||||
)
|
||||
if not args.expand_virtuals:
|
||||
dependencies.update(virtuals)
|
||||
|
||||
if spec.name in dependencies:
|
||||
del dependencies[spec.name]
|
||||
dependencies.remove(spec.name)
|
||||
|
||||
if dependencies:
|
||||
colify(sorted(dependencies))
|
||||
|
@ -42,10 +42,10 @@
|
||||
import llnl.util.tty.color
|
||||
|
||||
import spack.deptypes as dt
|
||||
import spack.repo
|
||||
import spack.spec
|
||||
import spack.tengine
|
||||
import spack.traverse
|
||||
from spack.solver.input_analysis import create_graph_analyzer
|
||||
|
||||
|
||||
def find(seq, predicate):
|
||||
@ -537,10 +537,11 @@ def edge_entry(self, edge):
|
||||
|
||||
def _static_edges(specs, depflag):
|
||||
for spec in specs:
|
||||
pkg_cls = spack.repo.PATH.get_pkg_class(spec.name)
|
||||
possible = pkg_cls.possible_dependencies(expand_virtuals=True, depflag=depflag)
|
||||
*_, edges = create_graph_analyzer().possible_dependencies(
|
||||
spec.name, expand_virtuals=True, allowed_deps=depflag
|
||||
)
|
||||
|
||||
for parent_name, dependencies in possible.items():
|
||||
for parent_name, dependencies in edges.items():
|
||||
for dependency_name in dependencies:
|
||||
yield spack.spec.DependencySpec(
|
||||
spack.spec.Spec(parent_name),
|
||||
|
@ -22,7 +22,6 @@
|
||||
import textwrap
|
||||
import time
|
||||
import traceback
|
||||
import typing
|
||||
from typing import Any, Callable, Dict, Iterable, List, Optional, Set, Tuple, Type, TypeVar, Union
|
||||
|
||||
from typing_extensions import Literal
|
||||
@ -822,104 +821,6 @@ def get_variant(self, name: str) -> spack.variant.Variant:
|
||||
except StopIteration:
|
||||
raise ValueError(f"No variant '{name}' on spec: {self.spec}")
|
||||
|
||||
@classmethod
|
||||
def possible_dependencies(
|
||||
cls,
|
||||
transitive: bool = True,
|
||||
expand_virtuals: bool = True,
|
||||
depflag: dt.DepFlag = dt.ALL,
|
||||
visited: Optional[dict] = None,
|
||||
missing: Optional[dict] = None,
|
||||
virtuals: Optional[set] = None,
|
||||
) -> Dict[str, Set[str]]:
|
||||
"""Return dict of possible dependencies of this package.
|
||||
|
||||
Args:
|
||||
transitive (bool or None): return all transitive dependencies if
|
||||
True, only direct dependencies if False (default True)..
|
||||
expand_virtuals (bool or None): expand virtual dependencies into
|
||||
all possible implementations (default True)
|
||||
depflag: dependency types to consider
|
||||
visited (dict or None): dict of names of dependencies visited so
|
||||
far, mapped to their immediate dependencies' names.
|
||||
missing (dict or None): dict to populate with packages and their
|
||||
*missing* dependencies.
|
||||
virtuals (set): if provided, populate with virtuals seen so far.
|
||||
|
||||
Returns:
|
||||
(dict): dictionary mapping dependency names to *their*
|
||||
immediate dependencies
|
||||
|
||||
Each item in the returned dictionary maps a (potentially
|
||||
transitive) dependency of this package to its possible
|
||||
*immediate* dependencies. If ``expand_virtuals`` is ``False``,
|
||||
virtual package names wil be inserted as keys mapped to empty
|
||||
sets of dependencies. Virtuals, if not expanded, are treated as
|
||||
though they have no immediate dependencies.
|
||||
|
||||
Missing dependencies by default are ignored, but if a
|
||||
missing dict is provided, it will be populated with package names
|
||||
mapped to any dependencies they have that are in no
|
||||
repositories. This is only populated if transitive is True.
|
||||
|
||||
Note: the returned dict *includes* the package itself.
|
||||
|
||||
"""
|
||||
visited = {} if visited is None else visited
|
||||
missing = {} if missing is None else missing
|
||||
|
||||
visited.setdefault(cls.name, set())
|
||||
|
||||
for name, conditions in cls.dependencies_by_name(when=True).items():
|
||||
# check whether this dependency could be of the type asked for
|
||||
depflag_union = 0
|
||||
for deplist in conditions.values():
|
||||
for dep in deplist:
|
||||
depflag_union |= dep.depflag
|
||||
if not (depflag & depflag_union):
|
||||
continue
|
||||
|
||||
# expand virtuals if enabled, otherwise just stop at virtuals
|
||||
if spack.repo.PATH.is_virtual(name):
|
||||
if virtuals is not None:
|
||||
virtuals.add(name)
|
||||
if expand_virtuals:
|
||||
providers = spack.repo.PATH.providers_for(name)
|
||||
dep_names = [spec.name for spec in providers]
|
||||
else:
|
||||
visited.setdefault(cls.name, set()).add(name)
|
||||
visited.setdefault(name, set())
|
||||
continue
|
||||
else:
|
||||
dep_names = [name]
|
||||
|
||||
# add the dependency names to the visited dict
|
||||
visited.setdefault(cls.name, set()).update(set(dep_names))
|
||||
|
||||
# recursively traverse dependencies
|
||||
for dep_name in dep_names:
|
||||
if dep_name in visited:
|
||||
continue
|
||||
|
||||
visited.setdefault(dep_name, set())
|
||||
|
||||
# skip the rest if not transitive
|
||||
if not transitive:
|
||||
continue
|
||||
|
||||
try:
|
||||
dep_cls = spack.repo.PATH.get_pkg_class(dep_name)
|
||||
except spack.repo.UnknownPackageError:
|
||||
# log unknown packages
|
||||
missing.setdefault(cls.name, set()).add(dep_name)
|
||||
continue
|
||||
|
||||
dep_cls.possible_dependencies(
|
||||
transitive, expand_virtuals, depflag, visited, missing, virtuals
|
||||
)
|
||||
|
||||
return visited
|
||||
|
||||
@classproperty
|
||||
def package_dir(cls):
|
||||
"""Directory where the package.py file lives."""
|
||||
@ -2284,47 +2185,6 @@ def rpath_args(self):
|
||||
build_system_flags = PackageBase.build_system_flags
|
||||
|
||||
|
||||
def possible_dependencies(
|
||||
*pkg_or_spec: Union[str, spack.spec.Spec, typing.Type[PackageBase]],
|
||||
transitive: bool = True,
|
||||
expand_virtuals: bool = True,
|
||||
depflag: dt.DepFlag = dt.ALL,
|
||||
missing: Optional[dict] = None,
|
||||
virtuals: Optional[set] = None,
|
||||
) -> Dict[str, Set[str]]:
|
||||
"""Get the possible dependencies of a number of packages.
|
||||
|
||||
See ``PackageBase.possible_dependencies`` for details.
|
||||
"""
|
||||
packages = []
|
||||
for pos in pkg_or_spec:
|
||||
if isinstance(pos, PackageMeta) and issubclass(pos, PackageBase):
|
||||
packages.append(pos)
|
||||
continue
|
||||
|
||||
if not isinstance(pos, spack.spec.Spec):
|
||||
pos = spack.spec.Spec(pos)
|
||||
|
||||
if spack.repo.PATH.is_virtual(pos.name):
|
||||
packages.extend(p.package_class for p in spack.repo.PATH.providers_for(pos.name))
|
||||
continue
|
||||
else:
|
||||
packages.append(pos.package_class)
|
||||
|
||||
visited: Dict[str, Set[str]] = {}
|
||||
for pkg in packages:
|
||||
pkg.possible_dependencies(
|
||||
visited=visited,
|
||||
transitive=transitive,
|
||||
expand_virtuals=expand_virtuals,
|
||||
depflag=depflag,
|
||||
missing=missing,
|
||||
virtuals=virtuals,
|
||||
)
|
||||
|
||||
return visited
|
||||
|
||||
|
||||
def deprecated_version(pkg: PackageBase, version: Union[str, StandardVersion]) -> bool:
|
||||
"""Return True iff the version is deprecated.
|
||||
|
||||
|
@ -87,6 +87,7 @@
|
||||
"strategy": {"type": "string", "enum": ["none", "minimal", "full"]}
|
||||
},
|
||||
},
|
||||
"static_analysis": {"type": "boolean"},
|
||||
"timeout": {"type": "integer", "minimum": 0},
|
||||
"error_on_timeout": {"type": "boolean"},
|
||||
"os_compatible": {"type": "object", "additionalProperties": {"type": "array"}},
|
||||
|
@ -62,7 +62,7 @@
|
||||
parse_files,
|
||||
parse_term,
|
||||
)
|
||||
from .counter import FullDuplicatesCounter, MinimalDuplicatesCounter, NoDuplicatesCounter
|
||||
from .input_analysis import create_counter, create_graph_analyzer
|
||||
from .requirements import RequirementKind, RequirementParser, RequirementRule
|
||||
from .version_order import concretization_version_order
|
||||
|
||||
@ -271,15 +271,6 @@ def remove_node(spec: spack.spec.Spec, facts: List[AspFunction]) -> List[AspFunc
|
||||
return list(filter(lambda x: x.args[0] not in ("node", "virtual_node"), facts))
|
||||
|
||||
|
||||
def _create_counter(specs: List[spack.spec.Spec], tests: bool):
|
||||
strategy = spack.config.CONFIG.get("concretizer:duplicates:strategy", "none")
|
||||
if strategy == "full":
|
||||
return FullDuplicatesCounter(specs, tests=tests)
|
||||
if strategy == "minimal":
|
||||
return MinimalDuplicatesCounter(specs, tests=tests)
|
||||
return NoDuplicatesCounter(specs, tests=tests)
|
||||
|
||||
|
||||
def all_libcs() -> Set[spack.spec.Spec]:
|
||||
"""Return a set of all libc specs targeted by any configured compiler. If none, fall back to
|
||||
libc determined from the current Python process if dynamically linked."""
|
||||
@ -1121,6 +1112,8 @@ class SpackSolverSetup:
|
||||
"""Class to set up and run a Spack concretization solve."""
|
||||
|
||||
def __init__(self, tests: bool = False):
|
||||
self.possible_graph = create_graph_analyzer()
|
||||
|
||||
# these are all initialized in setup()
|
||||
self.gen: "ProblemInstanceBuilder" = ProblemInstanceBuilder()
|
||||
self.requirement_parser = RequirementParser(spack.config.CONFIG)
|
||||
@ -2397,38 +2390,20 @@ def keyfun(os):
|
||||
|
||||
def target_defaults(self, specs):
|
||||
"""Add facts about targets and target compatibility."""
|
||||
self.gen.h2("Default target")
|
||||
|
||||
platform = spack.platforms.host()
|
||||
uarch = archspec.cpu.TARGETS.get(platform.default)
|
||||
|
||||
self.gen.h2("Target compatibility")
|
||||
|
||||
# Construct the list of targets which are compatible with the host
|
||||
candidate_targets = [uarch] + uarch.ancestors
|
||||
|
||||
# Get configuration options
|
||||
granularity = spack.config.get("concretizer:targets:granularity")
|
||||
host_compatible = spack.config.get("concretizer:targets:host_compatible")
|
||||
|
||||
# Add targets which are not compatible with the current host
|
||||
if not host_compatible:
|
||||
additional_targets_in_family = sorted(
|
||||
[
|
||||
t
|
||||
for t in archspec.cpu.TARGETS.values()
|
||||
if (t.family.name == uarch.family.name and t not in candidate_targets)
|
||||
],
|
||||
key=lambda x: len(x.ancestors),
|
||||
reverse=True,
|
||||
)
|
||||
candidate_targets += additional_targets_in_family
|
||||
|
||||
# Check if we want only generic architecture
|
||||
if granularity == "generic":
|
||||
candidate_targets = [t for t in candidate_targets if t.vendor == "generic"]
|
||||
|
||||
# Add targets explicitly requested from specs
|
||||
candidate_targets = []
|
||||
for x in self.possible_graph.candidate_targets():
|
||||
if all(
|
||||
self.possible_graph.unreachable(pkg_name=pkg_name, when_spec=f"target={x}")
|
||||
for pkg_name in self.pkgs
|
||||
):
|
||||
tty.debug(f"[{__name__}] excluding target={x}, cause no package can use it")
|
||||
continue
|
||||
candidate_targets.append(x)
|
||||
|
||||
host_compatible = spack.config.CONFIG.get("concretizer:targets:host_compatible")
|
||||
for spec in specs:
|
||||
if not spec.architecture or not spec.architecture.target:
|
||||
continue
|
||||
@ -2444,6 +2419,8 @@ def target_defaults(self, specs):
|
||||
if ancestor not in candidate_targets:
|
||||
candidate_targets.append(ancestor)
|
||||
|
||||
platform = spack.platforms.host()
|
||||
uarch = archspec.cpu.TARGETS.get(platform.default)
|
||||
best_targets = {uarch.family.name}
|
||||
for compiler_id, known_compiler in enumerate(self.possible_compilers):
|
||||
if not known_compiler.available:
|
||||
@ -2501,7 +2478,6 @@ def target_defaults(self, specs):
|
||||
self.gen.newline()
|
||||
|
||||
self.default_targets = list(sorted(set(self.default_targets)))
|
||||
|
||||
self.target_preferences()
|
||||
|
||||
def virtual_providers(self):
|
||||
@ -2605,7 +2581,14 @@ def define_variant_values(self):
|
||||
# Tell the concretizer about possible values from specs seen in spec_clauses().
|
||||
# We might want to order these facts by pkg and name if we are debugging.
|
||||
for pkg_name, variant_def_id, value in self.variant_values_from_specs:
|
||||
vid = self.variant_ids_by_def_id[variant_def_id]
|
||||
try:
|
||||
vid = self.variant_ids_by_def_id[variant_def_id]
|
||||
except KeyError:
|
||||
tty.debug(
|
||||
f"[{__name__}] cannot retrieve id of the {value} variant from {pkg_name}"
|
||||
)
|
||||
continue
|
||||
|
||||
self.gen.fact(fn.pkg_fact(pkg_name, fn.variant_possible_value(vid, value)))
|
||||
|
||||
def register_concrete_spec(self, spec, possible):
|
||||
@ -2676,7 +2659,7 @@ def setup(
|
||||
"""
|
||||
check_packages_exist(specs)
|
||||
|
||||
node_counter = _create_counter(specs, tests=self.tests)
|
||||
node_counter = create_counter(specs, tests=self.tests, possible_graph=self.possible_graph)
|
||||
self.possible_virtuals = node_counter.possible_virtuals()
|
||||
self.pkgs = node_counter.possible_dependencies()
|
||||
self.libcs = sorted(all_libcs()) # type: ignore[type-var]
|
||||
|
@ -1,179 +0,0 @@
|
||||
# Copyright Spack Project Developers. See COPYRIGHT file for details.
|
||||
#
|
||||
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
|
||||
import collections
|
||||
from typing import List, Set
|
||||
|
||||
from llnl.util import lang
|
||||
|
||||
import spack.deptypes as dt
|
||||
import spack.package_base
|
||||
import spack.repo
|
||||
import spack.spec
|
||||
|
||||
PossibleDependencies = Set[str]
|
||||
|
||||
|
||||
class Counter:
|
||||
"""Computes the possible packages and the maximum number of duplicates
|
||||
allowed for each of them.
|
||||
|
||||
Args:
|
||||
specs: abstract specs to concretize
|
||||
tests: if True, add test dependencies to the list of possible packages
|
||||
"""
|
||||
|
||||
def __init__(self, specs: List["spack.spec.Spec"], tests: bool) -> None:
|
||||
runtime_pkgs = spack.repo.PATH.packages_with_tags("runtime")
|
||||
runtime_virtuals = set()
|
||||
for x in runtime_pkgs:
|
||||
pkg_class = spack.repo.PATH.get_pkg_class(x)
|
||||
runtime_virtuals.update(pkg_class.provided_virtual_names())
|
||||
|
||||
self.specs = specs + [spack.spec.Spec(x) for x in runtime_pkgs]
|
||||
|
||||
self.link_run_types: dt.DepFlag = dt.LINK | dt.RUN | dt.TEST
|
||||
self.all_types: dt.DepFlag = dt.ALL
|
||||
if not tests:
|
||||
self.link_run_types = dt.LINK | dt.RUN
|
||||
self.all_types = dt.LINK | dt.RUN | dt.BUILD
|
||||
|
||||
self._possible_dependencies: PossibleDependencies = set()
|
||||
self._possible_virtuals: Set[str] = (
|
||||
set(x.name for x in specs if x.virtual) | runtime_virtuals
|
||||
)
|
||||
|
||||
def possible_dependencies(self) -> PossibleDependencies:
|
||||
"""Returns the list of possible dependencies"""
|
||||
self.ensure_cache_values()
|
||||
return self._possible_dependencies
|
||||
|
||||
def possible_virtuals(self) -> Set[str]:
|
||||
"""Returns the list of possible virtuals"""
|
||||
self.ensure_cache_values()
|
||||
return self._possible_virtuals
|
||||
|
||||
def ensure_cache_values(self) -> None:
|
||||
"""Ensure the cache values have been computed"""
|
||||
if self._possible_dependencies:
|
||||
return
|
||||
self._compute_cache_values()
|
||||
|
||||
def possible_packages_facts(self, gen: "spack.solver.asp.PyclingoDriver", fn) -> None:
|
||||
"""Emit facts associated with the possible packages"""
|
||||
raise NotImplementedError("must be implemented by derived classes")
|
||||
|
||||
def _compute_cache_values(self):
|
||||
raise NotImplementedError("must be implemented by derived classes")
|
||||
|
||||
|
||||
class NoDuplicatesCounter(Counter):
|
||||
def _compute_cache_values(self):
|
||||
result = spack.package_base.possible_dependencies(
|
||||
*self.specs, virtuals=self._possible_virtuals, depflag=self.all_types
|
||||
)
|
||||
self._possible_dependencies = set(result)
|
||||
|
||||
def possible_packages_facts(self, gen, fn):
|
||||
gen.h2("Maximum number of nodes (packages)")
|
||||
for package_name in sorted(self.possible_dependencies()):
|
||||
gen.fact(fn.max_dupes(package_name, 1))
|
||||
gen.newline()
|
||||
gen.h2("Maximum number of nodes (virtual packages)")
|
||||
for package_name in sorted(self.possible_virtuals()):
|
||||
gen.fact(fn.max_dupes(package_name, 1))
|
||||
gen.newline()
|
||||
gen.h2("Possible package in link-run subDAG")
|
||||
for name in sorted(self.possible_dependencies()):
|
||||
gen.fact(fn.possible_in_link_run(name))
|
||||
gen.newline()
|
||||
|
||||
|
||||
class MinimalDuplicatesCounter(NoDuplicatesCounter):
|
||||
def __init__(self, specs, tests):
|
||||
super().__init__(specs, tests)
|
||||
self._link_run: PossibleDependencies = set()
|
||||
self._direct_build: PossibleDependencies = set()
|
||||
self._total_build: PossibleDependencies = set()
|
||||
self._link_run_virtuals: Set[str] = set()
|
||||
|
||||
def _compute_cache_values(self):
|
||||
self._link_run = set(
|
||||
spack.package_base.possible_dependencies(
|
||||
*self.specs, virtuals=self._possible_virtuals, depflag=self.link_run_types
|
||||
)
|
||||
)
|
||||
self._link_run_virtuals.update(self._possible_virtuals)
|
||||
for x in self._link_run:
|
||||
build_dependencies = spack.repo.PATH.get_pkg_class(x).dependencies_of_type(dt.BUILD)
|
||||
virtuals, reals = lang.stable_partition(
|
||||
build_dependencies, spack.repo.PATH.is_virtual_safe
|
||||
)
|
||||
|
||||
self._possible_virtuals.update(virtuals)
|
||||
for virtual_dep in virtuals:
|
||||
providers = spack.repo.PATH.providers_for(virtual_dep)
|
||||
self._direct_build.update(str(x) for x in providers)
|
||||
|
||||
self._direct_build.update(reals)
|
||||
|
||||
self._total_build = set(
|
||||
spack.package_base.possible_dependencies(
|
||||
*self._direct_build, virtuals=self._possible_virtuals, depflag=self.all_types
|
||||
)
|
||||
)
|
||||
self._possible_dependencies = set(self._link_run) | set(self._total_build)
|
||||
|
||||
def possible_packages_facts(self, gen, fn):
|
||||
build_tools = spack.repo.PATH.packages_with_tags("build-tools")
|
||||
gen.h2("Packages with at most a single node")
|
||||
for package_name in sorted(self.possible_dependencies() - build_tools):
|
||||
gen.fact(fn.max_dupes(package_name, 1))
|
||||
gen.newline()
|
||||
|
||||
gen.h2("Packages with at multiple possible nodes (build-tools)")
|
||||
for package_name in sorted(self.possible_dependencies() & build_tools):
|
||||
gen.fact(fn.max_dupes(package_name, 2))
|
||||
gen.fact(fn.multiple_unification_sets(package_name))
|
||||
gen.newline()
|
||||
|
||||
gen.h2("Maximum number of nodes (virtual packages)")
|
||||
for package_name in sorted(self.possible_virtuals()):
|
||||
gen.fact(fn.max_dupes(package_name, 1))
|
||||
gen.newline()
|
||||
|
||||
gen.h2("Possible package in link-run subDAG")
|
||||
for name in sorted(self._link_run):
|
||||
gen.fact(fn.possible_in_link_run(name))
|
||||
gen.newline()
|
||||
|
||||
|
||||
class FullDuplicatesCounter(MinimalDuplicatesCounter):
|
||||
def possible_packages_facts(self, gen, fn):
|
||||
build_tools = spack.repo.PATH.packages_with_tags("build-tools")
|
||||
counter = collections.Counter(
|
||||
list(self._link_run) + list(self._total_build) + list(self._direct_build)
|
||||
)
|
||||
gen.h2("Maximum number of nodes")
|
||||
for pkg, count in sorted(counter.items(), key=lambda x: (x[1], x[0])):
|
||||
count = min(count, 2)
|
||||
gen.fact(fn.max_dupes(pkg, count))
|
||||
gen.newline()
|
||||
|
||||
gen.h2("Build unification sets ")
|
||||
for name in sorted(self.possible_dependencies() & build_tools):
|
||||
gen.fact(fn.multiple_unification_sets(name))
|
||||
gen.newline()
|
||||
|
||||
gen.h2("Possible package in link-run subDAG")
|
||||
for name in sorted(self._link_run):
|
||||
gen.fact(fn.possible_in_link_run(name))
|
||||
gen.newline()
|
||||
|
||||
counter = collections.Counter(
|
||||
list(self._link_run_virtuals) + list(self._possible_virtuals)
|
||||
)
|
||||
gen.h2("Maximum number of virtual nodes")
|
||||
for pkg, count in sorted(counter.items(), key=lambda x: (x[1], x[0])):
|
||||
gen.fact(fn.max_dupes(pkg, count))
|
||||
gen.newline()
|
524
lib/spack/spack/solver/input_analysis.py
Normal file
524
lib/spack/spack/solver/input_analysis.py
Normal file
@ -0,0 +1,524 @@
|
||||
# Copyright Spack Project Developers. See COPYRIGHT file for details.
|
||||
#
|
||||
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
|
||||
"""Classes to analyze the input of a solve, and provide information to set up the ASP problem"""
|
||||
import collections
|
||||
from typing import Dict, List, NamedTuple, Set, Tuple, Union
|
||||
|
||||
import archspec.cpu
|
||||
|
||||
from llnl.util import lang, tty
|
||||
|
||||
import spack.binary_distribution
|
||||
import spack.config
|
||||
import spack.deptypes as dt
|
||||
import spack.platforms
|
||||
import spack.repo
|
||||
import spack.spec
|
||||
import spack.store
|
||||
from spack.error import SpackError
|
||||
|
||||
RUNTIME_TAG = "runtime"
|
||||
|
||||
|
||||
class PossibleGraph(NamedTuple):
|
||||
real_pkgs: Set[str]
|
||||
virtuals: Set[str]
|
||||
edges: Dict[str, Set[str]]
|
||||
|
||||
|
||||
class PossibleDependencyGraph:
|
||||
"""Returns information needed to set up an ASP problem"""
|
||||
|
||||
def unreachable(self, *, pkg_name: str, when_spec: spack.spec.Spec) -> bool:
|
||||
"""Returns true if the context can determine that the condition cannot ever
|
||||
be met on pkg_name.
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
def candidate_targets(self) -> List[archspec.cpu.Microarchitecture]:
|
||||
"""Returns a list of targets that are candidate for concretization"""
|
||||
raise NotImplementedError
|
||||
|
||||
def possible_dependencies(
|
||||
self,
|
||||
*specs: Union[spack.spec.Spec, str],
|
||||
allowed_deps: dt.DepFlag,
|
||||
transitive: bool = True,
|
||||
strict_depflag: bool = False,
|
||||
expand_virtuals: bool = True,
|
||||
) -> PossibleGraph:
|
||||
"""Returns the set of possible dependencies, and the set of possible virtuals.
|
||||
|
||||
Both sets always include runtime packages, which may be injected by compilers.
|
||||
|
||||
Args:
|
||||
transitive: return transitive dependencies if True, only direct dependencies if False
|
||||
allowed_deps: dependency types to consider
|
||||
strict_depflag: if True, only the specific dep type is considered, if False any
|
||||
deptype that intersects with allowed deptype is considered
|
||||
expand_virtuals: expand virtual dependencies into all possible implementations
|
||||
"""
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class NoStaticAnalysis(PossibleDependencyGraph):
|
||||
"""Implementation that tries to minimize the setup time (i.e. defaults to give fast
|
||||
answers), rather than trying to reduce the ASP problem size with more complex analysis.
|
||||
"""
|
||||
|
||||
def __init__(self, *, configuration: spack.config.Configuration, repo: spack.repo.RepoPath):
|
||||
self.configuration = configuration
|
||||
self.repo = repo
|
||||
self.runtime_pkgs = set(self.repo.packages_with_tags(RUNTIME_TAG))
|
||||
self.runtime_virtuals = set()
|
||||
for x in self.runtime_pkgs:
|
||||
pkg_class = self.repo.get_pkg_class(x)
|
||||
self.runtime_virtuals.update(pkg_class.provided_virtual_names())
|
||||
|
||||
try:
|
||||
self.libc_pkgs = [x.name for x in self.providers_for("libc")]
|
||||
except spack.repo.UnknownPackageError:
|
||||
self.libc_pkgs = []
|
||||
|
||||
def is_virtual(self, name: str) -> bool:
|
||||
return self.repo.is_virtual(name)
|
||||
|
||||
@lang.memoized
|
||||
def is_allowed_on_this_platform(self, *, pkg_name: str) -> bool:
|
||||
"""Returns true if a package is allowed on the current host"""
|
||||
pkg_cls = self.repo.get_pkg_class(pkg_name)
|
||||
platform_condition = (
|
||||
f"platform={spack.platforms.host()} target={archspec.cpu.host().family}:"
|
||||
)
|
||||
for when_spec, conditions in pkg_cls.requirements.items():
|
||||
if not when_spec.intersects(platform_condition):
|
||||
continue
|
||||
for requirements, _, _ in conditions:
|
||||
if not any(x.intersects(platform_condition) for x in requirements):
|
||||
tty.debug(f"[{__name__}] {pkg_name} is not for this platform")
|
||||
return False
|
||||
return True
|
||||
|
||||
def providers_for(self, virtual_str: str) -> List[spack.spec.Spec]:
|
||||
"""Returns a list of possible providers for the virtual string in input."""
|
||||
return self.repo.providers_for(virtual_str)
|
||||
|
||||
def can_be_installed(self, *, pkg_name) -> bool:
|
||||
"""Returns True if a package can be installed, False otherwise."""
|
||||
return True
|
||||
|
||||
def unreachable(self, *, pkg_name: str, when_spec: spack.spec.Spec) -> bool:
|
||||
"""Returns true if the context can determine that the condition cannot ever
|
||||
be met on pkg_name.
|
||||
"""
|
||||
return False
|
||||
|
||||
def candidate_targets(self) -> List[archspec.cpu.Microarchitecture]:
|
||||
"""Returns a list of targets that are candidate for concretization"""
|
||||
platform = spack.platforms.host()
|
||||
default_target = archspec.cpu.TARGETS[platform.default]
|
||||
|
||||
# Construct the list of targets which are compatible with the host
|
||||
candidate_targets = [default_target] + default_target.ancestors
|
||||
granularity = self.configuration.get("concretizer:targets:granularity")
|
||||
host_compatible = self.configuration.get("concretizer:targets:host_compatible")
|
||||
|
||||
# Add targets which are not compatible with the current host
|
||||
if not host_compatible:
|
||||
additional_targets_in_family = sorted(
|
||||
[
|
||||
t
|
||||
for t in archspec.cpu.TARGETS.values()
|
||||
if (t.family.name == default_target.family.name and t not in candidate_targets)
|
||||
],
|
||||
key=lambda x: len(x.ancestors),
|
||||
reverse=True,
|
||||
)
|
||||
candidate_targets += additional_targets_in_family
|
||||
|
||||
# Check if we want only generic architecture
|
||||
if granularity == "generic":
|
||||
candidate_targets = [t for t in candidate_targets if t.vendor == "generic"]
|
||||
|
||||
return candidate_targets
|
||||
|
||||
def possible_dependencies(
|
||||
self,
|
||||
*specs: Union[spack.spec.Spec, str],
|
||||
allowed_deps: dt.DepFlag,
|
||||
transitive: bool = True,
|
||||
strict_depflag: bool = False,
|
||||
expand_virtuals: bool = True,
|
||||
) -> PossibleGraph:
|
||||
stack = [x for x in self._package_list(specs)]
|
||||
virtuals: Set[str] = set()
|
||||
edges: Dict[str, Set[str]] = {}
|
||||
|
||||
while stack:
|
||||
pkg_name = stack.pop()
|
||||
|
||||
if pkg_name in edges:
|
||||
continue
|
||||
|
||||
edges[pkg_name] = set()
|
||||
|
||||
# Since libc is not buildable, there is no need to extend the
|
||||
# search space with libc dependencies.
|
||||
if pkg_name in self.libc_pkgs:
|
||||
continue
|
||||
|
||||
pkg_cls = self.repo.get_pkg_class(pkg_name=pkg_name)
|
||||
for name, conditions in pkg_cls.dependencies_by_name(when=True).items():
|
||||
if all(self.unreachable(pkg_name=pkg_name, when_spec=x) for x in conditions):
|
||||
tty.debug(
|
||||
f"[{__name__}] Not adding {name} as a dep of {pkg_name}, because "
|
||||
f"conditions cannot be met"
|
||||
)
|
||||
continue
|
||||
|
||||
if not self._has_deptypes(
|
||||
conditions, allowed_deps=allowed_deps, strict=strict_depflag
|
||||
):
|
||||
continue
|
||||
|
||||
if name in virtuals:
|
||||
continue
|
||||
|
||||
dep_names = set()
|
||||
if self.is_virtual(name):
|
||||
virtuals.add(name)
|
||||
if expand_virtuals:
|
||||
providers = self.providers_for(name)
|
||||
dep_names = {spec.name for spec in providers}
|
||||
else:
|
||||
dep_names = {name}
|
||||
|
||||
edges[pkg_name].update(dep_names)
|
||||
|
||||
if not transitive:
|
||||
continue
|
||||
|
||||
for dep_name in dep_names:
|
||||
if dep_name in edges:
|
||||
continue
|
||||
|
||||
if not self._is_possible(pkg_name=dep_name):
|
||||
continue
|
||||
|
||||
stack.append(dep_name)
|
||||
|
||||
real_packages = set(edges)
|
||||
if not transitive:
|
||||
# We exit early, so add children from the edges information
|
||||
for root, children in edges.items():
|
||||
real_packages.update(x for x in children if self._is_possible(pkg_name=x))
|
||||
|
||||
virtuals.update(self.runtime_virtuals)
|
||||
real_packages = real_packages | self.runtime_pkgs
|
||||
return PossibleGraph(real_pkgs=real_packages, virtuals=virtuals, edges=edges)
|
||||
|
||||
def _package_list(self, specs: Tuple[Union[spack.spec.Spec, str], ...]) -> List[str]:
|
||||
stack = []
|
||||
for current_spec in specs:
|
||||
if isinstance(current_spec, str):
|
||||
current_spec = spack.spec.Spec(current_spec)
|
||||
|
||||
if self.repo.is_virtual(current_spec.name):
|
||||
stack.extend([p.name for p in self.providers_for(current_spec.name)])
|
||||
continue
|
||||
|
||||
stack.append(current_spec.name)
|
||||
return sorted(set(stack))
|
||||
|
||||
def _has_deptypes(self, dependencies, *, allowed_deps: dt.DepFlag, strict: bool) -> bool:
|
||||
if strict is True:
|
||||
return any(
|
||||
dep.depflag == allowed_deps for deplist in dependencies.values() for dep in deplist
|
||||
)
|
||||
return any(
|
||||
dep.depflag & allowed_deps for deplist in dependencies.values() for dep in deplist
|
||||
)
|
||||
|
||||
def _is_possible(self, *, pkg_name):
|
||||
try:
|
||||
return self.is_allowed_on_this_platform(pkg_name=pkg_name) and self.can_be_installed(
|
||||
pkg_name=pkg_name
|
||||
)
|
||||
except spack.repo.UnknownPackageError:
|
||||
return False
|
||||
|
||||
|
||||
class StaticAnalysis(NoStaticAnalysis):
|
||||
"""Performs some static analysis of the configuration, store, etc. to provide more precise
|
||||
answers on whether some packages can be installed, or used as a provider.
|
||||
|
||||
It increases the setup time, but might decrease the grounding and solve time considerably,
|
||||
especially when requirements restrict the possible choices for providers.
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
configuration: spack.config.Configuration,
|
||||
repo: spack.repo.RepoPath,
|
||||
store: spack.store.Store,
|
||||
binary_index: spack.binary_distribution.BinaryCacheIndex,
|
||||
):
|
||||
super().__init__(configuration=configuration, repo=repo)
|
||||
self.store = store
|
||||
self.binary_index = binary_index
|
||||
|
||||
@lang.memoized
|
||||
def providers_for(self, virtual_str: str) -> List[spack.spec.Spec]:
|
||||
candidates = super().providers_for(virtual_str)
|
||||
result = []
|
||||
for spec in candidates:
|
||||
if not self._is_provider_candidate(pkg_name=spec.name, virtual=virtual_str):
|
||||
continue
|
||||
result.append(spec)
|
||||
return result
|
||||
|
||||
@lang.memoized
|
||||
def buildcache_specs(self) -> List[spack.spec.Spec]:
|
||||
self.binary_index.update()
|
||||
return self.binary_index.get_all_built_specs()
|
||||
|
||||
@lang.memoized
|
||||
def can_be_installed(self, *, pkg_name) -> bool:
|
||||
if self.configuration.get(f"packages:{pkg_name}:buildable", True):
|
||||
return True
|
||||
|
||||
if self.configuration.get(f"packages:{pkg_name}:externals", []):
|
||||
return True
|
||||
|
||||
reuse = self.configuration.get("concretizer:reuse")
|
||||
if reuse is not False and self.store.db.query(pkg_name):
|
||||
return True
|
||||
|
||||
if reuse is not False and any(x.name == pkg_name for x in self.buildcache_specs()):
|
||||
return True
|
||||
|
||||
tty.debug(f"[{__name__}] {pkg_name} cannot be installed")
|
||||
return False
|
||||
|
||||
@lang.memoized
|
||||
def _is_provider_candidate(self, *, pkg_name: str, virtual: str) -> bool:
|
||||
if not self.is_allowed_on_this_platform(pkg_name=pkg_name):
|
||||
return False
|
||||
|
||||
if not self.can_be_installed(pkg_name=pkg_name):
|
||||
return False
|
||||
|
||||
virtual_spec = spack.spec.Spec(virtual)
|
||||
if self.unreachable(pkg_name=virtual_spec.name, when_spec=pkg_name):
|
||||
tty.debug(f"[{__name__}] {pkg_name} cannot be a provider for {virtual}")
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
@lang.memoized
|
||||
def unreachable(self, *, pkg_name: str, when_spec: spack.spec.Spec) -> bool:
|
||||
"""Returns true if the context can determine that the condition cannot ever
|
||||
be met on pkg_name.
|
||||
"""
|
||||
candidates = self.configuration.get(f"packages:{pkg_name}:require", [])
|
||||
if not candidates and pkg_name != "all":
|
||||
return self.unreachable(pkg_name="all", when_spec=when_spec)
|
||||
|
||||
if not candidates:
|
||||
return False
|
||||
|
||||
if isinstance(candidates, str):
|
||||
candidates = [candidates]
|
||||
|
||||
union_requirement = spack.spec.Spec()
|
||||
for c in candidates:
|
||||
if not isinstance(c, str):
|
||||
continue
|
||||
try:
|
||||
union_requirement.constrain(c)
|
||||
except SpackError:
|
||||
# Less optimized, but shouldn't fail
|
||||
pass
|
||||
|
||||
if not union_requirement.intersects(when_spec):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def create_graph_analyzer() -> PossibleDependencyGraph:
|
||||
static_analysis = spack.config.CONFIG.get("concretizer:static_analysis", False)
|
||||
if static_analysis:
|
||||
return StaticAnalysis(
|
||||
configuration=spack.config.CONFIG,
|
||||
repo=spack.repo.PATH,
|
||||
store=spack.store.STORE,
|
||||
binary_index=spack.binary_distribution.BINARY_INDEX,
|
||||
)
|
||||
return NoStaticAnalysis(configuration=spack.config.CONFIG, repo=spack.repo.PATH)
|
||||
|
||||
|
||||
class Counter:
|
||||
"""Computes the possible packages and the maximum number of duplicates
|
||||
allowed for each of them.
|
||||
|
||||
Args:
|
||||
specs: abstract specs to concretize
|
||||
tests: if True, add test dependencies to the list of possible packages
|
||||
"""
|
||||
|
||||
def __init__(
|
||||
self, specs: List["spack.spec.Spec"], tests: bool, possible_graph: PossibleDependencyGraph
|
||||
) -> None:
|
||||
self.possible_graph = possible_graph
|
||||
self.specs = specs
|
||||
self.link_run_types: dt.DepFlag = dt.LINK | dt.RUN | dt.TEST
|
||||
self.all_types: dt.DepFlag = dt.ALL
|
||||
if not tests:
|
||||
self.link_run_types = dt.LINK | dt.RUN
|
||||
self.all_types = dt.LINK | dt.RUN | dt.BUILD
|
||||
|
||||
self._possible_dependencies: Set[str] = set()
|
||||
self._possible_virtuals: Set[str] = set(x.name for x in specs if x.virtual)
|
||||
|
||||
def possible_dependencies(self) -> Set[str]:
|
||||
"""Returns the list of possible dependencies"""
|
||||
self.ensure_cache_values()
|
||||
return self._possible_dependencies
|
||||
|
||||
def possible_virtuals(self) -> Set[str]:
|
||||
"""Returns the list of possible virtuals"""
|
||||
self.ensure_cache_values()
|
||||
return self._possible_virtuals
|
||||
|
||||
def ensure_cache_values(self) -> None:
|
||||
"""Ensure the cache values have been computed"""
|
||||
if self._possible_dependencies:
|
||||
return
|
||||
self._compute_cache_values()
|
||||
|
||||
def possible_packages_facts(self, gen: "spack.solver.asp.ProblemInstanceBuilder", fn) -> None:
|
||||
"""Emit facts associated with the possible packages"""
|
||||
raise NotImplementedError("must be implemented by derived classes")
|
||||
|
||||
def _compute_cache_values(self) -> None:
|
||||
raise NotImplementedError("must be implemented by derived classes")
|
||||
|
||||
|
||||
class NoDuplicatesCounter(Counter):
|
||||
def _compute_cache_values(self) -> None:
|
||||
self._possible_dependencies, virtuals, _ = self.possible_graph.possible_dependencies(
|
||||
*self.specs, allowed_deps=self.all_types
|
||||
)
|
||||
self._possible_virtuals.update(virtuals)
|
||||
|
||||
def possible_packages_facts(self, gen: "spack.solver.asp.ProblemInstanceBuilder", fn) -> None:
|
||||
gen.h2("Maximum number of nodes (packages)")
|
||||
for package_name in sorted(self.possible_dependencies()):
|
||||
gen.fact(fn.max_dupes(package_name, 1))
|
||||
gen.newline()
|
||||
gen.h2("Maximum number of nodes (virtual packages)")
|
||||
for package_name in sorted(self.possible_virtuals()):
|
||||
gen.fact(fn.max_dupes(package_name, 1))
|
||||
gen.newline()
|
||||
gen.h2("Possible package in link-run subDAG")
|
||||
for name in sorted(self.possible_dependencies()):
|
||||
gen.fact(fn.possible_in_link_run(name))
|
||||
gen.newline()
|
||||
|
||||
|
||||
class MinimalDuplicatesCounter(NoDuplicatesCounter):
|
||||
def __init__(
|
||||
self, specs: List["spack.spec.Spec"], tests: bool, possible_graph: PossibleDependencyGraph
|
||||
) -> None:
|
||||
super().__init__(specs, tests, possible_graph)
|
||||
self._link_run: Set[str] = set()
|
||||
self._direct_build: Set[str] = set()
|
||||
self._total_build: Set[str] = set()
|
||||
self._link_run_virtuals: Set[str] = set()
|
||||
|
||||
def _compute_cache_values(self) -> None:
|
||||
self._link_run, virtuals, _ = self.possible_graph.possible_dependencies(
|
||||
*self.specs, allowed_deps=self.link_run_types
|
||||
)
|
||||
self._possible_virtuals.update(virtuals)
|
||||
self._link_run_virtuals.update(virtuals)
|
||||
for x in self._link_run:
|
||||
reals, virtuals, _ = self.possible_graph.possible_dependencies(
|
||||
x, allowed_deps=dt.BUILD, transitive=False, strict_depflag=True
|
||||
)
|
||||
self._possible_virtuals.update(virtuals)
|
||||
self._direct_build.update(reals)
|
||||
|
||||
self._total_build, virtuals, _ = self.possible_graph.possible_dependencies(
|
||||
*self._direct_build, allowed_deps=self.all_types
|
||||
)
|
||||
self._possible_virtuals.update(virtuals)
|
||||
self._possible_dependencies = set(self._link_run) | set(self._total_build)
|
||||
|
||||
def possible_packages_facts(self, gen, fn):
|
||||
build_tools = spack.repo.PATH.packages_with_tags("build-tools")
|
||||
gen.h2("Packages with at most a single node")
|
||||
for package_name in sorted(self.possible_dependencies() - build_tools):
|
||||
gen.fact(fn.max_dupes(package_name, 1))
|
||||
gen.newline()
|
||||
|
||||
gen.h2("Packages with at multiple possible nodes (build-tools)")
|
||||
for package_name in sorted(self.possible_dependencies() & build_tools):
|
||||
gen.fact(fn.max_dupes(package_name, 2))
|
||||
gen.fact(fn.multiple_unification_sets(package_name))
|
||||
gen.newline()
|
||||
|
||||
gen.h2("Maximum number of nodes (virtual packages)")
|
||||
for package_name in sorted(self.possible_virtuals()):
|
||||
gen.fact(fn.max_dupes(package_name, 1))
|
||||
gen.newline()
|
||||
|
||||
gen.h2("Possible package in link-run subDAG")
|
||||
for name in sorted(self._link_run):
|
||||
gen.fact(fn.possible_in_link_run(name))
|
||||
gen.newline()
|
||||
|
||||
|
||||
class FullDuplicatesCounter(MinimalDuplicatesCounter):
|
||||
def possible_packages_facts(self, gen, fn):
|
||||
build_tools = spack.repo.PATH.packages_with_tags("build-tools")
|
||||
counter = collections.Counter(
|
||||
list(self._link_run) + list(self._total_build) + list(self._direct_build)
|
||||
)
|
||||
gen.h2("Maximum number of nodes")
|
||||
for pkg, count in sorted(counter.items(), key=lambda x: (x[1], x[0])):
|
||||
count = min(count, 2)
|
||||
gen.fact(fn.max_dupes(pkg, count))
|
||||
gen.newline()
|
||||
|
||||
gen.h2("Build unification sets ")
|
||||
for name in sorted(self.possible_dependencies() & build_tools):
|
||||
gen.fact(fn.multiple_unification_sets(name))
|
||||
gen.newline()
|
||||
|
||||
gen.h2("Possible package in link-run subDAG")
|
||||
for name in sorted(self._link_run):
|
||||
gen.fact(fn.possible_in_link_run(name))
|
||||
gen.newline()
|
||||
|
||||
counter = collections.Counter(
|
||||
list(self._link_run_virtuals) + list(self._possible_virtuals)
|
||||
)
|
||||
gen.h2("Maximum number of virtual nodes")
|
||||
for pkg, count in sorted(counter.items(), key=lambda x: (x[1], x[0])):
|
||||
gen.fact(fn.max_dupes(pkg, count))
|
||||
gen.newline()
|
||||
|
||||
|
||||
def create_counter(
|
||||
specs: List[spack.spec.Spec], tests: bool, possible_graph: PossibleDependencyGraph
|
||||
) -> Counter:
|
||||
strategy = spack.config.CONFIG.get("concretizer:duplicates:strategy", "none")
|
||||
if strategy == "full":
|
||||
return FullDuplicatesCounter(specs, tests=tests, possible_graph=possible_graph)
|
||||
if strategy == "minimal":
|
||||
return MinimalDuplicatesCounter(specs, tests=tests, possible_graph=possible_graph)
|
||||
return NoDuplicatesCounter(specs, tests=tests, possible_graph=possible_graph)
|
@ -24,32 +24,24 @@
|
||||
mpi_deps = ["fake"]
|
||||
|
||||
|
||||
def test_direct_dependencies(mock_packages):
|
||||
out = dependencies("mpileaks")
|
||||
actual = set(re.split(r"\s+", out.strip()))
|
||||
expected = set(["callpath"] + mpis)
|
||||
assert expected == actual
|
||||
|
||||
|
||||
def test_transitive_dependencies(mock_packages):
|
||||
out = dependencies("--transitive", "mpileaks")
|
||||
actual = set(re.split(r"\s+", out.strip()))
|
||||
expected = set(["callpath", "dyninst", "libdwarf", "libelf"] + mpis + mpi_deps)
|
||||
assert expected == actual
|
||||
|
||||
|
||||
def test_transitive_dependencies_with_deptypes(mock_packages):
|
||||
out = dependencies("--transitive", "--deptype=link,run", "dtbuild1")
|
||||
deps = set(re.split(r"\s+", out.strip()))
|
||||
assert set(["dtlink2", "dtrun2"]) == deps
|
||||
|
||||
out = dependencies("--transitive", "--deptype=build", "dtbuild1")
|
||||
deps = set(re.split(r"\s+", out.strip()))
|
||||
assert set(["dtbuild2", "dtlink2"]) == deps
|
||||
|
||||
out = dependencies("--transitive", "--deptype=link", "dtbuild1")
|
||||
deps = set(re.split(r"\s+", out.strip()))
|
||||
assert set(["dtlink2"]) == deps
|
||||
@pytest.mark.parametrize(
|
||||
"cli_args,expected",
|
||||
[
|
||||
(["mpileaks"], set(["callpath"] + mpis)),
|
||||
(
|
||||
["--transitive", "mpileaks"],
|
||||
set(["callpath", "dyninst", "libdwarf", "libelf"] + mpis + mpi_deps),
|
||||
),
|
||||
(["--transitive", "--deptype=link,run", "dtbuild1"], {"dtlink2", "dtrun2"}),
|
||||
(["--transitive", "--deptype=build", "dtbuild1"], {"dtbuild2", "dtlink2"}),
|
||||
(["--transitive", "--deptype=link", "dtbuild1"], {"dtlink2"}),
|
||||
],
|
||||
)
|
||||
def test_direct_dependencies(cli_args, expected, mock_runtimes):
|
||||
out = dependencies(*cli_args)
|
||||
result = set(re.split(r"\s+", out.strip()))
|
||||
expected.update(mock_runtimes)
|
||||
assert expected == result
|
||||
|
||||
|
||||
@pytest.mark.db
|
||||
|
@ -1,7 +1,6 @@
|
||||
# Copyright Spack Project Developers. See COPYRIGHT file for details.
|
||||
#
|
||||
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
|
||||
import os
|
||||
import pathlib
|
||||
|
||||
import pytest
|
||||
@ -200,11 +199,11 @@ def test_requirement_adds_version_satisfies(
|
||||
|
||||
@pytest.mark.parametrize("require_checksum", (True, False))
|
||||
def test_requirement_adds_git_hash_version(
|
||||
require_checksum, concretize_scope, test_repo, mock_git_version_info, monkeypatch, working_env
|
||||
require_checksum, concretize_scope, test_repo, mock_git_version_info, monkeypatch
|
||||
):
|
||||
# A full commit sha is a checksummed version, so this test should pass in both cases
|
||||
if require_checksum:
|
||||
os.environ["SPACK_CONCRETIZER_REQUIRE_CHECKSUM"] = "yes"
|
||||
monkeypatch.setenv("SPACK_CONCRETIZER_REQUIRE_CHECKSUM", "yes")
|
||||
|
||||
repo_path, filename, commits = mock_git_version_info
|
||||
monkeypatch.setattr(
|
||||
|
@ -2171,3 +2171,8 @@ def getcode(self):
|
||||
|
||||
def info(self):
|
||||
return self.headers
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
def mock_runtimes(config, mock_packages):
|
||||
return mock_packages.packages_with_tags("runtime")
|
||||
|
@ -1,7 +1,6 @@
|
||||
# Copyright Spack Project Developers. See COPYRIGHT file for details.
|
||||
#
|
||||
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
|
||||
|
||||
"""Test class methods on Package objects.
|
||||
|
||||
This doesn't include methods on package *instances* (like do_patch(),
|
||||
@ -16,6 +15,7 @@
|
||||
|
||||
import llnl.util.filesystem as fs
|
||||
|
||||
import spack.binary_distribution
|
||||
import spack.compilers
|
||||
import spack.concretize
|
||||
import spack.deptypes as dt
|
||||
@ -23,15 +23,11 @@
|
||||
import spack.install_test
|
||||
import spack.package
|
||||
import spack.package_base
|
||||
import spack.repo
|
||||
import spack.spec
|
||||
import spack.store
|
||||
from spack.build_systems.generic import Package
|
||||
from spack.error import InstallError
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def mpi_names(mock_repo_path):
|
||||
return [spec.name for spec in mock_repo_path.providers_for("mpi")]
|
||||
from spack.solver.input_analysis import NoStaticAnalysis, StaticAnalysis
|
||||
|
||||
|
||||
@pytest.fixture()
|
||||
@ -53,78 +49,94 @@ def mpileaks_possible_deps(mock_packages, mpi_names):
|
||||
return possible
|
||||
|
||||
|
||||
def test_possible_dependencies(mock_packages, mpileaks_possible_deps):
|
||||
pkg_cls = spack.repo.PATH.get_pkg_class("mpileaks")
|
||||
expanded_possible_deps = pkg_cls.possible_dependencies(expand_virtuals=True)
|
||||
assert mpileaks_possible_deps == expanded_possible_deps
|
||||
assert {
|
||||
"callpath": {"dyninst", "mpi"},
|
||||
"dyninst": {"libdwarf", "libelf"},
|
||||
"libdwarf": {"libelf"},
|
||||
"libelf": set(),
|
||||
"mpi": set(),
|
||||
"mpileaks": {"callpath", "mpi"},
|
||||
} == pkg_cls.possible_dependencies(expand_virtuals=False)
|
||||
|
||||
|
||||
def test_possible_direct_dependencies(mock_packages, mpileaks_possible_deps):
|
||||
pkg_cls = spack.repo.PATH.get_pkg_class("mpileaks")
|
||||
deps = pkg_cls.possible_dependencies(transitive=False, expand_virtuals=False)
|
||||
assert {"callpath": set(), "mpi": set(), "mpileaks": {"callpath", "mpi"}} == deps
|
||||
|
||||
|
||||
def test_possible_dependencies_virtual(mock_packages, mpi_names):
|
||||
expected = dict(
|
||||
(name, set(dep for dep in spack.repo.PATH.get_pkg_class(name).dependencies_by_name()))
|
||||
for name in mpi_names
|
||||
)
|
||||
|
||||
# only one mock MPI has a dependency
|
||||
expected["fake"] = set()
|
||||
|
||||
assert expected == spack.package_base.possible_dependencies("mpi", transitive=False)
|
||||
|
||||
|
||||
def test_possible_dependencies_missing(mock_packages):
|
||||
pkg_cls = spack.repo.PATH.get_pkg_class("missing-dependency")
|
||||
missing = {}
|
||||
pkg_cls.possible_dependencies(transitive=True, missing=missing)
|
||||
assert {"this-is-a-missing-dependency"} == missing["missing-dependency"]
|
||||
|
||||
|
||||
def test_possible_dependencies_with_deptypes(mock_packages):
|
||||
dtbuild1 = spack.repo.PATH.get_pkg_class("dtbuild1")
|
||||
|
||||
assert {
|
||||
"dtbuild1": {"dtrun2", "dtlink2"},
|
||||
"dtlink2": set(),
|
||||
"dtrun2": set(),
|
||||
} == dtbuild1.possible_dependencies(depflag=dt.LINK | dt.RUN)
|
||||
|
||||
assert {
|
||||
"dtbuild1": {"dtbuild2", "dtlink2"},
|
||||
"dtbuild2": set(),
|
||||
"dtlink2": set(),
|
||||
} == dtbuild1.possible_dependencies(depflag=dt.BUILD)
|
||||
|
||||
assert {"dtbuild1": {"dtlink2"}, "dtlink2": set()} == dtbuild1.possible_dependencies(
|
||||
depflag=dt.LINK
|
||||
@pytest.fixture(params=[NoStaticAnalysis, StaticAnalysis])
|
||||
def mock_inspector(config, mock_packages, request):
|
||||
inspector_cls = request.param
|
||||
if inspector_cls is NoStaticAnalysis:
|
||||
return inspector_cls(configuration=config, repo=mock_packages)
|
||||
return inspector_cls(
|
||||
configuration=config,
|
||||
repo=mock_packages,
|
||||
store=spack.store.STORE,
|
||||
binary_index=spack.binary_distribution.BINARY_INDEX,
|
||||
)
|
||||
|
||||
|
||||
def test_possible_dependencies_with_multiple_classes(mock_packages, mpileaks_possible_deps):
|
||||
@pytest.fixture
|
||||
def mpi_names(mock_inspector):
|
||||
return [spec.name for spec in mock_inspector.providers_for("mpi")]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"pkg_name,fn_kwargs,expected",
|
||||
[
|
||||
(
|
||||
"mpileaks",
|
||||
{"expand_virtuals": True, "allowed_deps": dt.ALL},
|
||||
{
|
||||
"fake",
|
||||
"mpileaks",
|
||||
"multi-provider-mpi",
|
||||
"callpath",
|
||||
"dyninst",
|
||||
"mpich2",
|
||||
"libdwarf",
|
||||
"zmpi",
|
||||
"low-priority-provider",
|
||||
"intel-parallel-studio",
|
||||
"mpich",
|
||||
"libelf",
|
||||
},
|
||||
),
|
||||
(
|
||||
"mpileaks",
|
||||
{"expand_virtuals": False, "allowed_deps": dt.ALL},
|
||||
{"callpath", "dyninst", "libdwarf", "libelf", "mpileaks"},
|
||||
),
|
||||
(
|
||||
"mpileaks",
|
||||
{"expand_virtuals": False, "allowed_deps": dt.ALL, "transitive": False},
|
||||
{"callpath", "mpileaks"},
|
||||
),
|
||||
("dtbuild1", {"allowed_deps": dt.LINK | dt.RUN}, {"dtbuild1", "dtrun2", "dtlink2"}),
|
||||
("dtbuild1", {"allowed_deps": dt.BUILD}, {"dtbuild1", "dtbuild2", "dtlink2"}),
|
||||
("dtbuild1", {"allowed_deps": dt.LINK}, {"dtbuild1", "dtlink2"}),
|
||||
],
|
||||
)
|
||||
def test_possible_dependencies(pkg_name, fn_kwargs, expected, mock_runtimes, mock_inspector):
|
||||
"""Tests possible nodes of mpileaks, under different scenarios."""
|
||||
expected.update(mock_runtimes)
|
||||
result, *_ = mock_inspector.possible_dependencies(pkg_name, **fn_kwargs)
|
||||
assert expected == result
|
||||
|
||||
|
||||
def test_possible_dependencies_virtual(mock_inspector, mock_packages, mock_runtimes, mpi_names):
|
||||
expected = set(mpi_names)
|
||||
for name in mpi_names:
|
||||
expected.update(dep for dep in mock_packages.get_pkg_class(name).dependencies_by_name())
|
||||
expected.update(mock_runtimes)
|
||||
|
||||
real_pkgs, *_ = mock_inspector.possible_dependencies(
|
||||
"mpi", transitive=False, allowed_deps=dt.ALL
|
||||
)
|
||||
assert expected == real_pkgs
|
||||
|
||||
|
||||
def test_possible_dependencies_missing(mock_inspector):
|
||||
result, *_ = mock_inspector.possible_dependencies("missing-dependency", allowed_deps=dt.ALL)
|
||||
assert "this-is-a-missing-dependency" not in result
|
||||
|
||||
|
||||
def test_possible_dependencies_with_multiple_classes(
|
||||
mock_inspector, mock_packages, mpileaks_possible_deps
|
||||
):
|
||||
pkgs = ["dt-diamond", "mpileaks"]
|
||||
expected = mpileaks_possible_deps.copy()
|
||||
expected.update(
|
||||
{
|
||||
"dt-diamond": set(["dt-diamond-left", "dt-diamond-right"]),
|
||||
"dt-diamond-left": set(["dt-diamond-bottom"]),
|
||||
"dt-diamond-right": set(["dt-diamond-bottom"]),
|
||||
"dt-diamond-bottom": set(),
|
||||
}
|
||||
)
|
||||
expected = set(mpileaks_possible_deps)
|
||||
expected.update({"dt-diamond", "dt-diamond-left", "dt-diamond-right", "dt-diamond-bottom"})
|
||||
expected.update(mock_packages.packages_with_tags("runtime"))
|
||||
|
||||
assert expected == spack.package_base.possible_dependencies(*pkgs)
|
||||
real_pkgs, *_ = mock_inspector.possible_dependencies(*pkgs, allowed_deps=dt.ALL)
|
||||
assert set(expected) == real_pkgs
|
||||
|
||||
|
||||
def setup_install_test(source_paths, test_root):
|
||||
|
@ -2,10 +2,16 @@ spack:
|
||||
view: false
|
||||
packages:
|
||||
all:
|
||||
require: target=x86_64_v3
|
||||
require:
|
||||
- target=x86_64_v3
|
||||
- ~cuda
|
||||
- ~rocm
|
||||
|
||||
concretizer:
|
||||
unify: true
|
||||
reuse: false
|
||||
static_analysis: true
|
||||
|
||||
definitions:
|
||||
- default_specs:
|
||||
# editors
|
||||
|
@ -4,16 +4,32 @@ spack:
|
||||
concretizer:
|
||||
reuse: false
|
||||
unify: false
|
||||
static_analysis: true
|
||||
|
||||
packages:
|
||||
all:
|
||||
require: '%gcc target=x86_64_v3'
|
||||
providers:
|
||||
blas: [openblas]
|
||||
mpi: [mpich]
|
||||
require:
|
||||
- '%gcc target=x86_64_v3'
|
||||
variants: +mpi
|
||||
mpi:
|
||||
require:
|
||||
- mpich
|
||||
blas:
|
||||
require:
|
||||
- openblas
|
||||
lapack:
|
||||
require:
|
||||
- openblas
|
||||
binutils:
|
||||
variants: +ld +gold +headers +libiberty ~nls
|
||||
cmake:
|
||||
require:
|
||||
- "~qtgui"
|
||||
- '%gcc target=x86_64_v3'
|
||||
gmake:
|
||||
require:
|
||||
- "~guile"
|
||||
- '%gcc target=x86_64_v3'
|
||||
hdf5:
|
||||
variants: +fortran +hl +shared
|
||||
libfabric:
|
||||
@ -27,19 +43,25 @@ spack:
|
||||
+ifpack +ifpack2 +intrepid +intrepid2 +isorropia +kokkos +ml +minitensor +muelu
|
||||
+nox +piro +phalanx +rol +rythmos +sacado +stk +shards +shylu +stokhos +stratimikos
|
||||
+teko +tempus +tpetra +trilinoscouplings +zoltan +zoltan2 +superlu-dist gotype=long_long
|
||||
mpi:
|
||||
require: mpich
|
||||
mpich:
|
||||
require: '~wrapperrpath ~hwloc target=x86_64_v3'
|
||||
require:
|
||||
- '~wrapperrpath ~hwloc'
|
||||
- '%gcc target=x86_64_v3'
|
||||
tbb:
|
||||
require: intel-tbb
|
||||
require:
|
||||
- intel-tbb
|
||||
vtk-m:
|
||||
require: "+examples target=x86_64_v3"
|
||||
require:
|
||||
- "+examples"
|
||||
- '%gcc target=x86_64_v3'
|
||||
visit:
|
||||
require: "~gui target=x86_64_v3"
|
||||
require:
|
||||
- "~gui target=x86_64_v3"
|
||||
paraview:
|
||||
# Don't build GUI support or GLX rendering for HPC/container deployments
|
||||
require: "+examples ~qt ^[virtuals=gl] osmesa target=x86_64_v3"
|
||||
require:
|
||||
- "+examples ~qt ^[virtuals=gl] osmesa target=x86_64_v3"
|
||||
- '%gcc target=x86_64_v3'
|
||||
|
||||
specs:
|
||||
# CPU
|
||||
|
Loading…
Reference in New Issue
Block a user