summaryrefslogtreecommitdiff
path: root/lib/spack/spack/spec.py
diff options
context:
space:
mode:
Diffstat (limited to 'lib/spack/spack/spec.py')
-rw-r--r--lib/spack/spack/spec.py644
1 files changed, 7 insertions, 637 deletions
diff --git a/lib/spack/spack/spec.py b/lib/spack/spack/spec.py
index 413cb22f5f..ccdc498214 100644
--- a/lib/spack/spack/spec.py
+++ b/lib/spack/spack/spec.py
@@ -70,7 +70,6 @@ import llnl.util.tty.color as clr
import spack.compiler
import spack.compilers
import spack.config
-import spack.dependency as dp
import spack.deptypes as dt
import spack.error
import spack.hash_types as ht
@@ -2615,294 +2614,6 @@ class Spec:
validate_fn = getattr(pkg_cls, "validate_detected_spec", lambda x, y: None)
validate_fn(self, self.extra_attributes)
- def _concretize_helper(self, concretizer, presets=None, visited=None):
- """Recursive helper function for concretize().
- This concretizes everything bottom-up. As things are
- concretized, they're added to the presets, and ancestors
- will prefer the settings of their children.
- """
- if presets is None:
- presets = {}
- if visited is None:
- visited = set()
-
- if self.name in visited:
- return False
-
- if self.concrete:
- visited.add(self.name)
- return False
-
- changed = False
-
- # Concretize deps first -- this is a bottom-up process.
- for name in sorted(self._dependencies):
- # WARNING: This function is an implementation detail of the
- # WARNING: original concretizer. Since with that greedy
- # WARNING: algorithm we don't allow multiple nodes from
- # WARNING: the same package in a DAG, here we hard-code
- # WARNING: using index 0 i.e. we assume that we have only
- # WARNING: one edge from package "name"
- changed |= self._dependencies[name][0].spec._concretize_helper(
- concretizer, presets, visited
- )
-
- if self.name in presets:
- changed |= self.constrain(presets[self.name])
- else:
- # Concretize virtual dependencies last. Because they're added
- # to presets below, their constraints will all be merged, but we'll
- # still need to select a concrete package later.
- if not self.virtual:
- changed |= any(
- (
- concretizer.concretize_develop(self), # special variant
- concretizer.concretize_architecture(self),
- concretizer.concretize_compiler(self),
- concretizer.adjust_target(self),
- # flags must be concretized after compiler
- concretizer.concretize_compiler_flags(self),
- concretizer.concretize_version(self),
- concretizer.concretize_variants(self),
- )
- )
- presets[self.name] = self
-
- visited.add(self.name)
- return changed
-
- def _replace_with(self, concrete):
- """Replace this virtual spec with a concrete spec."""
- assert self.virtual
- virtuals = (self.name,)
- for dep_spec in itertools.chain.from_iterable(self._dependents.values()):
- dependent = dep_spec.parent
- depflag = dep_spec.depflag
-
- # remove self from all dependents, unless it is already removed
- if self.name in dependent._dependencies:
- del dependent._dependencies.edges[self.name]
-
- # add the replacement, unless it is already a dep of dependent.
- if concrete.name not in dependent._dependencies:
- dependent._add_dependency(concrete, depflag=depflag, virtuals=virtuals)
- else:
- dependent.edges_to_dependencies(name=concrete.name)[0].update_virtuals(
- virtuals=virtuals
- )
-
- def _expand_virtual_packages(self, concretizer):
- """Find virtual packages in this spec, replace them with providers,
- and normalize again to include the provider's (potentially virtual)
- dependencies. Repeat until there are no virtual deps.
-
- Precondition: spec is normalized.
-
- .. todo::
-
- If a provider depends on something that conflicts with
- other dependencies in the spec being expanded, this can
- produce a conflicting spec. For example, if mpich depends
- on hwloc@:1.3 but something in the spec needs hwloc1.4:,
- then we should choose an MPI other than mpich. Cases like
- this are infrequent, but should implement this before it is
- a problem.
- """
- # Make an index of stuff this spec already provides
- self_index = spack.provider_index.ProviderIndex(
- repository=spack.repo.PATH, specs=self.traverse(), restrict=True
- )
- changed = False
- done = False
-
- while not done:
- done = True
- for spec in list(self.traverse()):
- replacement = None
- if spec.external:
- continue
- if spec.virtual:
- replacement = self._find_provider(spec, self_index)
- if replacement:
- # TODO: may break if in-place on self but
- # shouldn't happen if root is traversed first.
- spec._replace_with(replacement)
- done = False
- break
-
- if not replacement:
- # Get a list of possible replacements in order of
- # preference.
- candidates = concretizer.choose_virtual_or_external(spec)
-
- # Try the replacements in order, skipping any that cause
- # satisfiability problems.
- for replacement in candidates:
- if replacement is spec:
- break
-
- # Replace spec with the candidate and normalize
- copy = self.copy()
- copy[spec.name]._dup(replacement, deps=False)
-
- try:
- # If there are duplicate providers or duplicate
- # provider deps, consolidate them and merge
- # constraints.
- copy.normalize(force=True)
- break
- except spack.error.SpecError:
- # On error, we'll try the next replacement.
- continue
-
- # If replacement is external then trim the dependencies
- if replacement.external:
- if spec._dependencies:
- for dep in spec.dependencies():
- del dep._dependents.edges[spec.name]
- changed = True
- spec.clear_dependencies()
- replacement.clear_dependencies()
- replacement.architecture = self.architecture
-
- # TODO: could this and the stuff in _dup be cleaned up?
- def feq(cfield, sfield):
- return (not cfield) or (cfield == sfield)
-
- if replacement is spec or (
- feq(replacement.name, spec.name)
- and feq(replacement.versions, spec.versions)
- and feq(replacement.compiler, spec.compiler)
- and feq(replacement.architecture, spec.architecture)
- and feq(replacement._dependencies, spec._dependencies)
- and feq(replacement.variants, spec.variants)
- and feq(replacement.external_path, spec.external_path)
- and feq(replacement.external_modules, spec.external_modules)
- ):
- continue
- # Refine this spec to the candidate. This uses
- # replace_with AND dup so that it can work in
- # place. TODO: make this more efficient.
- if spec.virtual:
- spec._replace_with(replacement)
- changed = True
- if spec._dup(replacement, deps=False, cleardeps=False):
- changed = True
-
- self_index.update(spec)
- done = False
- break
-
- return changed
-
- def _old_concretize(self, tests=False, deprecation_warning=True):
- """A spec is concrete if it describes one build of a package uniquely.
- This will ensure that this spec is concrete.
-
- Args:
- tests (list or bool): list of packages that will need test
- dependencies, or True/False for test all/none
- deprecation_warning (bool): enable or disable the deprecation
- warning for the old concretizer
-
- If this spec could describe more than one version, variant, or build
- of a package, this will add constraints to make it concrete.
-
- Some rigorous validation and checks are also performed on the spec.
- Concretizing ensures that it is self-consistent and that it's
- consistent with requirements of its packages. See flatten() and
- normalize() for more details on this.
- """
- import spack.concretize
-
- # Add a warning message to inform users that the original concretizer
- # will be removed
- if deprecation_warning:
- msg = (
- "the original concretizer is currently being used.\n\tUpgrade to "
- '"clingo" at your earliest convenience. The original concretizer '
- "will be removed from Spack in a future version."
- )
- warnings.warn(msg)
-
- self.replace_hash()
-
- if not self.name:
- raise spack.error.SpecError("Attempting to concretize anonymous spec")
-
- if self._concrete:
- return
-
- # take the spec apart once before starting the main concretization loop and resolving
- # deps, but don't break dependencies during concretization as the spec is built.
- user_spec_deps = self.flat_dependencies(disconnect=True)
-
- changed = True
- force = False
- concretizer = spack.concretize.Concretizer(self.copy())
- while changed:
- changes = (
- self.normalize(force, tests, user_spec_deps, disconnect=False),
- self._expand_virtual_packages(concretizer),
- self._concretize_helper(concretizer),
- )
- changed = any(changes)
- force = True
-
- visited_user_specs = set()
- for dep in self.traverse():
- visited_user_specs.add(dep.name)
- pkg_cls = spack.repo.PATH.get_pkg_class(dep.name)
- visited_user_specs.update(pkg_cls(dep).provided_virtual_names())
-
- extra = set(user_spec_deps.keys()).difference(visited_user_specs)
- if extra:
- raise InvalidDependencyError(self.name, extra)
-
- Spec.inject_patches_variant(self)
-
- for s in self.traverse():
- # TODO: Refactor this into a common method to build external specs
- # TODO: or turn external_path into a lazy property
- Spec.ensure_external_path_if_external(s)
-
- # assign hashes and mark concrete
- self._finalize_concretization()
-
- # If any spec in the DAG is deprecated, throw an error
- Spec.ensure_no_deprecated(self)
-
- # Update externals as needed
- for dep in self.traverse():
- if dep.external:
- dep.package.update_external_dependencies()
-
- # Now that the spec is concrete we should check if
- # there are declared conflicts
- #
- # TODO: this needs rethinking, as currently we can only express
- # TODO: internal configuration conflicts within one package.
- matches = []
- for x in self.traverse():
- if x.external:
- # external specs are already built, don't worry about whether
- # it's possible to build that configuration with Spack
- continue
-
- for when_spec, conflict_list in x.package_class.conflicts.items():
- if x.satisfies(when_spec):
- for conflict_spec, msg in conflict_list:
- if x.satisfies(conflict_spec):
- when = when_spec.copy()
- when.name = x.name
- matches.append((x, conflict_spec, when, msg))
- if matches:
- raise ConflictsInSpecError(self, matches)
-
- # Check if we can produce an optimized binary (will throw if
- # there are declared inconsistencies)
- self.architecture.target.optimization_flags(self.compiler)
-
def _patches_assigned(self):
"""Whether patches have been assigned to this spec by the concretizer."""
# FIXME: _patches_in_order_of_appearance is attached after concretization
@@ -3032,7 +2743,13 @@ class Spec:
msg += " For each package listed, choose another spec\n"
raise SpecDeprecatedError(msg)
- def _new_concretize(self, tests=False):
+ def concretize(self, tests: Union[bool, List[str]] = False) -> None:
+ """Concretize the current spec.
+
+ Args:
+ tests: if False disregard 'test' dependencies, if a list of names activate them for
+ the packages in the list, if True activate 'test' dependencies for all packages.
+ """
import spack.solver.asp
self.replace_hash()
@@ -3066,19 +2783,6 @@ class Spec:
concretized = answer[node]
self._dup(concretized)
- def concretize(self, tests=False):
- """Concretize the current spec.
-
- Args:
- tests (bool or list): if False disregard 'test' dependencies,
- if a list of names activate them for the packages in the list,
- if True activate 'test' dependencies for all packages.
- """
- if spack.config.get("config:concretizer", "clingo") == "clingo":
- self._new_concretize(tests)
- else:
- self._old_concretize(tests)
-
def _mark_root_concrete(self, value=True):
"""Mark just this spec (not dependencies) concrete."""
if (not value) and self.concrete and self.installed:
@@ -3182,34 +2886,6 @@ class Spec:
clone.concretize(tests=tests)
return clone
- def flat_dependencies(self, disconnect: bool = False):
- """Build DependencyMap of all of this spec's dependencies with their constraints merged.
-
- Arguments:
- disconnect: if True, disconnect all dependents and dependencies among nodes in this
- spec's DAG.
- """
- flat_deps = {}
- deptree = self.traverse(root=False)
-
- for spec in deptree:
- if spec.name not in flat_deps:
- flat_deps[spec.name] = spec
- else:
- try:
- flat_deps[spec.name].constrain(spec)
- except spack.error.UnsatisfiableSpecError as e:
- # DAG contains two instances of the same package with inconsistent constraints.
- raise InconsistentSpecError("Invalid Spec DAG: %s" % e.message) from e
-
- if disconnect:
- for spec in flat_deps.values():
- if not spec.concrete:
- spec.clear_edges()
- self.clear_dependencies()
-
- return flat_deps
-
def index(self, deptype="all"):
"""Return a dictionary that points to all the dependencies in this
spec.
@@ -3219,312 +2895,6 @@ class Spec:
dm[spec.name].append(spec)
return dm
- def _evaluate_dependency_conditions(self, name):
- """Evaluate all the conditions on a dependency with this name.
-
- Args:
- name (str): name of dependency to evaluate conditions on.
-
- Returns:
- (Dependency): new Dependency object combining all constraints.
-
- If the package depends on <name> in the current spec
- configuration, return the constrained dependency and
- corresponding dependency types.
-
- If no conditions are True (and we don't depend on it), return
- ``(None, None)``.
- """
- vt.substitute_abstract_variants(self)
- # evaluate when specs to figure out constraints on the dependency.
- dep = None
- for when_spec, deps_by_name in self.package_class.dependencies.items():
- if not self.satisfies(when_spec):
- continue
-
- for dep_name, dependency in deps_by_name.items():
- if dep_name != name:
- continue
-
- if dep is None:
- dep = dp.Dependency(Spec(self.name), Spec(name), depflag=0)
- try:
- dep.merge(dependency)
- except spack.error.UnsatisfiableSpecError as e:
- e.message = (
- "Conflicting conditional dependencies for spec"
- "\n\n\t{0}\n\n"
- "Cannot merge constraint"
- "\n\n\t{1}\n\n"
- "into"
- "\n\n\t{2}".format(self, dependency.spec, dep.spec)
- )
- raise e
-
- return dep
-
- def _find_provider(self, vdep, provider_index):
- """Find provider for a virtual spec in the provider index.
- Raise an exception if there is a conflicting virtual
- dependency already in this spec.
- """
- assert spack.repo.PATH.is_virtual_safe(vdep.name), vdep
-
- # note that this defensively copies.
- providers = provider_index.providers_for(vdep)
-
- # If there is a provider for the vpkg, then use that instead of
- # the virtual package.
- if providers:
- # Remove duplicate providers that can concretize to the same
- # result.
- for provider in providers:
- for spec in providers:
- if spec is not provider and provider.intersects(spec):
- providers.remove(spec)
- # Can't have multiple providers for the same thing in one spec.
- if len(providers) > 1:
- raise MultipleProviderError(vdep, providers)
- return providers[0]
- else:
- # The user might have required something insufficient for
- # pkg_dep -- so we'll get a conflict. e.g., user asked for
- # mpi@:1.1 but some package required mpi@2.1:.
- required = provider_index.providers_for(vdep.name)
- if len(required) > 1:
- raise MultipleProviderError(vdep, required)
- elif required:
- raise UnsatisfiableProviderSpecError(required[0], vdep)
-
- def _merge_dependency(self, dependency, visited, spec_deps, provider_index, tests):
- """Merge dependency information from a Package into this Spec.
-
- Args:
- dependency (Dependency): dependency metadata from a package;
- this is typically the result of merging *all* matching
- dependency constraints from the package.
- visited (set): set of dependency nodes already visited by
- ``normalize()``.
- spec_deps (dict): ``dict`` of all dependencies from the spec
- being normalized.
- provider_index (dict): ``provider_index`` of virtual dep
- providers in the ``Spec`` as normalized so far.
-
- NOTE: Caller should assume that this routine owns the
- ``dependency`` parameter, i.e., it needs to be a copy of any
- internal structures.
-
- This is the core of ``normalize()``. There are some basic steps:
-
- * If dep is virtual, evaluate whether it corresponds to an
- existing concrete dependency, and merge if so.
-
- * If it's real and it provides some virtual dep, see if it provides
- what some virtual dependency wants and merge if so.
-
- * Finally, if none of the above, merge dependency and its
- constraints into this spec.
-
- This method returns True if the spec was changed, False otherwise.
-
- """
- changed = False
- dep = dependency.spec
-
- # If it's a virtual dependency, try to find an existing
- # provider in the spec, and merge that.
- virtuals = ()
- if spack.repo.PATH.is_virtual_safe(dep.name):
- virtuals = (dep.name,)
- visited.add(dep.name)
- provider = self._find_provider(dep, provider_index)
- if provider:
- dep = provider
- else:
- index = spack.provider_index.ProviderIndex(
- repository=spack.repo.PATH, specs=[dep], restrict=True
- )
- items = list(spec_deps.items())
- for name, vspec in items:
- if not spack.repo.PATH.is_virtual_safe(vspec.name):
- continue
-
- if index.providers_for(vspec):
- vspec._replace_with(dep)
- del spec_deps[vspec.name]
- changed = True
- else:
- required = index.providers_for(vspec.name)
- if required:
- raise UnsatisfiableProviderSpecError(required[0], dep)
- provider_index.update(dep)
-
- # If the spec isn't already in the set of dependencies, add it.
- # Note: dep is always owned by this method. If it's from the
- # caller, it's a copy from _evaluate_dependency_conditions. If it
- # comes from a vdep, it's a defensive copy from _find_provider.
- if dep.name not in spec_deps:
- if self.concrete:
- return False
-
- spec_deps[dep.name] = dep
- changed = True
- else:
- # merge package/vdep information into spec
- try:
- tty.debug("{0} applying constraint {1}".format(self.name, str(dep)))
- changed |= spec_deps[dep.name].constrain(dep)
- except spack.error.UnsatisfiableSpecError as e:
- fmt = "An unsatisfiable {0}".format(e.constraint_type)
- fmt += " constraint has been detected for spec:"
- fmt += "\n\n{0}\n\n".format(spec_deps[dep.name].tree(indent=4))
- fmt += "while trying to concretize the partial spec:"
- fmt += "\n\n{0}\n\n".format(self.tree(indent=4))
- fmt += "{0} requires {1} {2} {3}, but spec asked for {4}"
-
- e.message = fmt.format(
- self.name, dep.name, e.constraint_type, e.required, e.provided
- )
-
- raise
-
- # Add merged spec to my deps and recurse
- spec_dependency = spec_deps[dep.name]
- if dep.name not in self._dependencies:
- self._add_dependency(spec_dependency, depflag=dependency.depflag, virtuals=virtuals)
-
- changed |= spec_dependency._normalize_helper(visited, spec_deps, provider_index, tests)
- return changed
-
- def _normalize_helper(self, visited, spec_deps, provider_index, tests):
- """Recursive helper function for _normalize."""
- if self.name in visited:
- return False
- visited.add(self.name)
-
- # If we descend into a virtual spec, there's nothing more
- # to normalize. Concretize will finish resolving it later.
- if self.virtual or self.external:
- return False
-
- # Avoid recursively adding constraints for already-installed packages:
- # these may include build dependencies which are not needed for this
- # install (since this package is already installed).
- if self.concrete and self.installed:
- return False
-
- # Combine constraints from package deps with constraints from
- # the spec, until nothing changes.
- any_change = False
- changed = True
-
- while changed:
- changed = False
- for dep_name in self.package_class.dependency_names():
- # Do we depend on dep_name? If so pkg_dep is not None.
- dep = self._evaluate_dependency_conditions(dep_name)
-
- # If dep is a needed dependency, merge it.
- if dep:
- merge = (
- # caller requested test dependencies
- tests is True
- or (tests and self.name in tests)
- or
- # this is not a test-only dependency
- (dep.depflag & ~dt.TEST)
- )
-
- if merge:
- changed |= self._merge_dependency(
- dep, visited, spec_deps, provider_index, tests
- )
- any_change |= changed
-
- return any_change
-
- def normalize(self, force=False, tests=False, user_spec_deps=None, disconnect=True):
- """When specs are parsed, any dependencies specified are hanging off
- the root, and ONLY the ones that were explicitly provided are there.
- Normalization turns a partial flat spec into a DAG, where:
-
- 1. Known dependencies of the root package are in the DAG.
- 2. Each node's dependencies dict only contains its known direct
- deps.
- 3. There is only ONE unique spec for each package in the DAG.
-
- * This includes virtual packages. If there a non-virtual
- package that provides a virtual package that is in the spec,
- then we replace the virtual package with the non-virtual one.
-
- TODO: normalize should probably implement some form of cycle
- detection, to ensure that the spec is actually a DAG.
- """
- if not self.name:
- raise spack.error.SpecError("Attempting to normalize anonymous spec")
-
- # Set _normal and _concrete to False when forced
- if force and not self._concrete:
- self._normal = False
-
- if self._normal:
- return False
-
- # Ensure first that all packages & compilers in the DAG exist.
- self.validate_or_raise()
- # Clear the DAG and collect all dependencies in the DAG, which will be
- # reapplied as constraints. All dependencies collected this way will
- # have been created by a previous execution of 'normalize'.
- # A dependency extracted here will only be reintegrated if it is
- # discovered to apply according to _normalize_helper, so
- # user-specified dependencies are recorded separately in case they
- # refer to specs which take several normalization passes to
- # materialize.
- all_spec_deps = self.flat_dependencies(disconnect=disconnect)
-
- if user_spec_deps:
- for name, spec in user_spec_deps.items():
- if not name:
- msg = "Attempted to normalize anonymous dependency spec"
- msg += " %s" % spec
- raise InvalidSpecDetected(msg)
- if name not in all_spec_deps:
- all_spec_deps[name] = spec
- else:
- all_spec_deps[name].constrain(spec)
-
- # Initialize index of virtual dependency providers if
- # concretize didn't pass us one already
- provider_index = spack.provider_index.ProviderIndex(
- repository=spack.repo.PATH, specs=[s for s in all_spec_deps.values()], restrict=True
- )
-
- # traverse the package DAG and fill out dependencies according
- # to package files & their 'when' specs
- visited = set()
-
- any_change = self._normalize_helper(visited, all_spec_deps, provider_index, tests)
-
- # remove any leftover dependents outside the spec from, e.g., pruning externals
- valid = {id(spec) for spec in all_spec_deps.values()} | {id(self)}
- for spec in all_spec_deps.values():
- remove = [dep for dep in spec.dependents() if id(dep) not in valid]
- for dep in remove:
- del spec._dependents.edges[dep.name]
- del dep._dependencies.edges[spec.name]
-
- # Mark the spec as normal once done.
- self._normal = True
- return any_change
-
- def normalized(self):
- """
- Return a normalized copy of this spec without modifying this spec.
- """
- clone = self.copy()
- clone.normalize()
- return clone
-
def validate_or_raise(self):
"""Checks that names and values in this spec are real. If they're not,
it will raise an appropriate exception.