diff options
-rw-r--r-- | lib/spack/spack/cmd/__init__.py | 43 | ||||
-rw-r--r-- | lib/spack/spack/cmd/clean.py | 3 | ||||
-rw-r--r-- | lib/spack/spack/cmd/patch.py | 3 | ||||
-rw-r--r-- | lib/spack/spack/cmd/stage.py | 2 | ||||
-rw-r--r-- | lib/spack/spack/concretize.py | 140 | ||||
-rw-r--r-- | lib/spack/spack/environment/environment.py | 149 | ||||
-rw-r--r-- | lib/spack/spack/solver/asp.py | 2 | ||||
-rw-r--r-- | lib/spack/spack/test/cmd/install.py | 2 | ||||
-rw-r--r-- | lib/spack/spack/test/concretize.py | 18 |
9 files changed, 237 insertions, 125 deletions
diff --git a/lib/spack/spack/cmd/__init__.py b/lib/spack/spack/cmd/__init__.py index c481e93131..031b29f952 100644 --- a/lib/spack/spack/cmd/__init__.py +++ b/lib/spack/spack/cmd/__init__.py @@ -173,10 +173,29 @@ def parse_specs( arg_string = " ".join([quote_kvp(arg) for arg in args]) specs = spack.parser.parse(arg_string) - for spec in specs: - if concretize: - spec.concretize(tests=tests) - return specs + if not concretize: + return specs + + to_concretize = [(s, None) for s in specs] + return _concretize_spec_pairs(to_concretize, tests=tests) + + +def _concretize_spec_pairs(to_concretize, tests=False): + """Helper method that concretizes abstract specs from a list of abstract,concrete pairs. + + Any spec with a concrete spec associated with it will concretize to that spec. Any spec + with ``None`` for its concrete spec will be newly concretized. This method respects unification + rules from config.""" + unify = spack.config.get("concretizer:unify", False) + + concretize_method = spack.concretize.concretize_separately # unify: false + if unify is True: + concretize_method = spack.concretize.concretize_together + elif unify == "when_possible": + concretize_method = spack.concretize.concretize_together_when_possible + + concretized = concretize_method(*to_concretize, tests=tests) + return [concrete for _, concrete in concretized] def matching_spec_from_env(spec): @@ -192,6 +211,22 @@ def matching_spec_from_env(spec): return spec.concretized() +def matching_specs_from_env(specs): + """ + Same as ``matching_spec_from_env`` but respects spec unification rules. + + For each spec, if there is a matching spec in the environment it is used. If no + matching spec is found, this will return the given spec but concretized in the + context of the active environment and other given specs, with unification rules applied. + """ + env = ev.active_environment() + spec_pairs = [(spec, env.matching_spec(spec) if env else None) for spec in specs] + additional_concrete_specs = ( + [(concrete, concrete) for _, concrete in env.concretized_specs()] if env else [] + ) + return _concretize_spec_pairs(spec_pairs + additional_concrete_specs)[: len(spec_pairs)] + + def disambiguate_spec(spec, env, local=False, installed=True, first=False): """Given a spec, figure out which installed package it refers to. diff --git a/lib/spack/spack/cmd/clean.py b/lib/spack/spack/cmd/clean.py index 0b8fb6d6bb..59d650fd12 100644 --- a/lib/spack/spack/cmd/clean.py +++ b/lib/spack/spack/cmd/clean.py @@ -105,7 +105,8 @@ def clean(parser, args): # Then do the cleaning falling through the cases if args.specs: specs = spack.cmd.parse_specs(args.specs, concretize=False) - specs = list(spack.cmd.matching_spec_from_env(x) for x in specs) + specs = spack.cmd.matching_specs_from_env(specs) + for spec in specs: msg = "Cleaning build stage [{0}]" tty.msg(msg.format(spec.short_spec)) diff --git a/lib/spack/spack/cmd/patch.py b/lib/spack/spack/cmd/patch.py index 885ff2f746..dbd18f7948 100644 --- a/lib/spack/spack/cmd/patch.py +++ b/lib/spack/spack/cmd/patch.py @@ -33,8 +33,9 @@ def patch(parser, args): spack.config.set("config:checksum", False, scope="command_line") specs = spack.cmd.parse_specs(args.specs, concretize=False) + specs = spack.cmd.matching_specs_from_env(specs) for spec in specs: - _patch(spack.cmd.matching_spec_from_env(spec).package) + _patch(spec.package) def _patch_env(env: ev.Environment): diff --git a/lib/spack/spack/cmd/stage.py b/lib/spack/spack/cmd/stage.py index af5fa412ea..20da92926f 100644 --- a/lib/spack/spack/cmd/stage.py +++ b/lib/spack/spack/cmd/stage.py @@ -47,8 +47,8 @@ def stage(parser, args): if len(specs) > 1 and custom_path: tty.die("`--path` requires a single spec, but multiple were provided") + specs = spack.cmd.matching_specs_from_env(specs) for spec in specs: - spec = spack.cmd.matching_spec_from_env(spec) pkg = spec.package if custom_path: diff --git a/lib/spack/spack/concretize.py b/lib/spack/spack/concretize.py index 387c7f2de2..fabfdbb523 100644 --- a/lib/spack/spack/concretize.py +++ b/lib/spack/spack/concretize.py @@ -5,11 +5,17 @@ """ (DEPRECATED) Used to contain the code for the original concretizer """ +import sys +import time from contextlib import contextmanager from itertools import chain +from typing import Tuple + +import llnl.util.tty as tty import spack.config import spack.error +from spack.spec import Spec CHECK_COMPILER_EXISTENCE = True @@ -83,6 +89,140 @@ def concretize_specs_together(*abstract_specs, **kwargs): return [s.copy() for s in result.specs] +def concretize_together(*spec_list, **kwargs): + """Given a number of specs as input, tries to concretize them together. + + Args: + tests (bool or list or set): False to run no tests, True to test + all packages, or a list of package names to run tests for some + *spec_list: list of tuples to concretize. First entry is abstract spec, second entry is + already concrete spec or None if not yet concretized + + Returns: + List of tuples of abstract and concretized specs + """ + to_concretize = [concrete if concrete else abstract for abstract, concrete in spec_list] + abstract_specs = [abstract for abstract, _ in spec_list] + concrete_specs = concretize_specs_together(*to_concretize, **kwargs) + return list(zip(abstract_specs, concrete_specs)) + + +def concretize_together_when_possible(*spec_list, **kwargs): + """Given a number of specs as input, tries to concretize them together to the extent possible. + + See documentation for ``unify: when_possible`` concretization for the precise definition of + "to the extent possible". + + Args: + tests (bool or list or set): False to run no tests, True to test + all packages, or a list of package names to run tests for some + *spec_list: list of tuples to concretize. First entry is abstract spec, second entry is + already concrete spec or None if not yet concretized + + Returns: + List of tuples of abstract and concretized specs + """ + to_concretize = [concrete if concrete else abstract for abstract, concrete in spec_list] + old_concrete_to_abstract = { + concrete: abstract for (abstract, concrete) in spec_list if concrete + } + + result_by_user_spec = {} + solver = spack.solver.asp.Solver() + allow_deprecated = spack.config.get("config:deprecated", False) + for result in solver.solve_in_rounds( + to_concretize, tests=kwargs.get("tests", False), allow_deprecated=allow_deprecated + ): + result_by_user_spec.update(result.specs_by_input) + + # If the "abstract" spec is a concrete spec from the previous concretization + # translate it back to an abstract spec. Otherwise, keep the abstract spec + return [ + (old_concrete_to_abstract.get(abstract, abstract), concrete) + for abstract, concrete in sorted(result_by_user_spec.items()) + ] + + +def concretize_separately(*spec_list, **kwargs): + """Given a number of specs as input, tries to concretize them together. + + Args: + tests (bool or list or set): False to run no tests, True to test + all packages, or a list of package names to run tests for some + *spec_list: list of tuples to concretize. First entry is abstract spec, second entry is + already concrete spec or None if not yet concretized + + Returns: + List of tuples of abstract and concretized specs + """ + tests = kwargs.get("tests", False) + to_concretize = [abstract for abstract, concrete in spec_list if not concrete] + args = [ + (i, str(abstract), tests) + for i, abstract in enumerate(to_concretize) + if not abstract.concrete + ] + ret = [(i, abstract) for i, abstract in enumerate(to_concretize) if abstract.concrete] + # Ensure we don't try to bootstrap clingo in parallel + with spack.bootstrap.ensure_bootstrap_configuration(): + spack.bootstrap.ensure_clingo_importable_or_raise() + + # Ensure all the indexes have been built or updated, since + # otherwise the processes in the pool may timeout on waiting + # for a write lock. We do this indirectly by retrieving the + # provider index, which should in turn trigger the update of + # all the indexes if there's any need for that. + _ = spack.repo.PATH.provider_index + + # Ensure we have compilers in compilers.yaml to avoid that + # processes try to write the config file in parallel + _ = spack.compilers.all_compilers_config(spack.config.CONFIG) + + # Early return if there is nothing to do + if len(args) == 0: + # Still have to combine the things that were passed in as abstract with the things + # that were passed in as pairs + return [(abstract, concrete) for abstract, (_, concrete) in zip(to_concretize, ret)] + [ + (abstract, concrete) for abstract, concrete in spec_list if concrete + ] + + # Solve the environment in parallel on Linux + # TODO: support parallel concretization on macOS and Windows + num_procs = min(len(args), spack.config.determine_number_of_jobs(parallel=True)) + + for j, (i, concrete, duration) in enumerate( + spack.util.parallel.imap_unordered( + spack.concretize._concretize_task, + args, + processes=num_procs, + debug=tty.is_debug(), + maxtaskperchild=1, + ) + ): + ret.append((i, concrete)) + percentage = (j + 1) / len(args) * 100 + tty.verbose( + f"{duration:6.1f}s [{percentage:3.0f}%] {concrete.cformat('{hash:7}')} " + f"{to_concretize[i].colored_str}" + ) + sys.stdout.flush() + + # Add specs in original order + ret.sort(key=lambda x: x[0]) + + return [(abstract, concrete) for abstract, (_, concrete) in zip(to_concretize, ret)] + [ + (abstract, concrete) for abstract, concrete in spec_list if concrete + ] + + +def _concretize_task(packed_arguments) -> Tuple[int, Spec, float]: + index, spec_str, tests = packed_arguments + with tty.SuppressOutput(msg_enabled=False): + start = time.time() + spec = Spec(spec_str).concretized(tests=tests) + return index, spec, time.time() - start + + class UnavailableCompilerVersionError(spack.error.SpackError): """Raised when there is no available compiler that satisfies a compiler spec.""" diff --git a/lib/spack/spack/environment/environment.py b/lib/spack/spack/environment/environment.py index 81a2223c49..b61332a0ab 100644 --- a/lib/spack/spack/environment/environment.py +++ b/lib/spack/spack/environment/environment.py @@ -11,12 +11,10 @@ import pathlib import re import shutil import stat -import sys -import time import urllib.parse import urllib.request import warnings -from typing import Any, Dict, Iterable, List, Optional, Set, Tuple, Union +from typing import Any, Dict, Iterable, List, Optional, Tuple, Union import llnl.util.filesystem as fs import llnl.util.tty as tty @@ -57,6 +55,8 @@ from spack.spec import Spec from spack.spec_list import SpecList from spack.util.path import substitute_path_variables +SpecPair = Tuple[spack.spec.Spec, spack.spec.Spec] + #: environment variable used to indicate the active environment spack_env_var = "SPACK_ENV" @@ -1510,7 +1510,7 @@ class Environment: def _get_specs_to_concretize( self, - ) -> Tuple[Set[spack.spec.Spec], Set[spack.spec.Spec], List[spack.spec.Spec]]: + ) -> Tuple[List[spack.spec.Spec], List[spack.spec.Spec], List[SpecPair]]: """Compute specs to concretize for unify:true and unify:when_possible. This includes new user specs and any already concretized specs. @@ -1520,19 +1520,17 @@ class Environment: """ # Exit early if the set of concretized specs is the set of user specs - new_user_specs = set(self.user_specs) - set(self.concretized_user_specs) - kept_user_specs = set(self.user_specs) & set(self.concretized_user_specs) - kept_user_specs |= set(self.included_user_specs) + new_user_specs = list(set(self.user_specs) - set(self.concretized_user_specs)) + kept_user_specs = list(set(self.user_specs) & set(self.concretized_user_specs)) + kept_user_specs += self.included_user_specs if not new_user_specs: return new_user_specs, kept_user_specs, [] - concrete_specs_to_keep = [ - concrete + specs_to_concretize = [(s, None) for s in new_user_specs] + [ + (abstract, concrete) for abstract, concrete in self.concretized_specs() if abstract in kept_user_specs ] - - specs_to_concretize = list(new_user_specs) + concrete_specs_to_keep return new_user_specs, kept_user_specs, specs_to_concretize def _concretize_together_where_possible( @@ -1546,39 +1544,26 @@ class Environment: if not new_user_specs: return [] - old_concrete_to_abstract = { - concrete: abstract for (abstract, concrete) in self.concretized_specs() - } - self.concretized_user_specs = [] self.concretized_order = [] self.specs_by_hash = {} - result_by_user_spec = {} - solver = spack.solver.asp.Solver() - allow_deprecated = spack.config.get("config:deprecated", False) - for result in solver.solve_in_rounds( - specs_to_concretize, tests=tests, allow_deprecated=allow_deprecated - ): - result_by_user_spec.update(result.specs_by_input) - - result = [] - for abstract, concrete in sorted(result_by_user_spec.items()): - # If the "abstract" spec is a concrete spec from the previous concretization - # translate it back to an abstract spec. Otherwise, keep the abstract spec - abstract = old_concrete_to_abstract.get(abstract, abstract) - if abstract in new_user_specs: - result.append((abstract, concrete)) - - # Only add to the environment if it's from this environment (not just included) + ret = [] + result = spack.concretize.concretize_together_when_possible( + *specs_to_concretize, tests=tests + ) + for abstract, concrete in result: + # Only add to the environment if it's from this environment (not included in) if abstract in self.user_specs: self._add_concrete_spec(abstract, concrete) - return result + # Return only the new specs + if abstract in new_user_specs: + ret.append((abstract, concrete)) - def _concretize_together( - self, tests: bool = False - ) -> List[Tuple[spack.spec.Spec, spack.spec.Spec]]: + return ret + + def _concretize_together(self, tests: bool = False) -> List[SpecPair]: """Concretization strategy that concretizes all the specs in the same DAG. """ @@ -1592,7 +1577,7 @@ class Environment: self.specs_by_hash = {} try: - concrete_specs: List[spack.spec.Spec] = spack.concretize.concretize_specs_together( + concretized_specs: List[SpecPair] = spack.concretize.concretize_together( *specs_to_concretize, tests=tests ) except spack.error.UnsatisfiableSpecError as e: @@ -1611,16 +1596,13 @@ class Environment: ) raise - # set() | set() does not preserve ordering, even though sets are ordered - ordered_user_specs = list(new_user_specs) + list(kept_user_specs) - concretized_specs = [x for x in zip(ordered_user_specs, concrete_specs)] for abstract, concrete in concretized_specs: # Don't add if it's just included if abstract in self.user_specs: self._add_concrete_spec(abstract, concrete) - # zip truncates the longer list, which is exactly what we want here - return list(zip(new_user_specs, concrete_specs)) + # Return the portion of the return value that is new + return concretized_specs[: len(new_user_specs)] def _concretize_separately(self, tests=False): """Concretization strategy that concretizes separately one @@ -1642,71 +1624,16 @@ class Environment: concrete = old_specs_by_hash[h] self._add_concrete_spec(s, concrete, new=False) - # Concretize any new user specs that we haven't concretized yet - args, root_specs, i = [], [], 0 - for uspec in self.user_specs: - if uspec not in old_concretized_user_specs: - root_specs.append(uspec) - args.append((i, str(uspec), tests)) - i += 1 - - # Ensure we don't try to bootstrap clingo in parallel - with spack.bootstrap.ensure_bootstrap_configuration(): - spack.bootstrap.ensure_clingo_importable_or_raise() - - # Ensure all the indexes have been built or updated, since - # otherwise the processes in the pool may timeout on waiting - # for a write lock. We do this indirectly by retrieving the - # provider index, which should in turn trigger the update of - # all the indexes if there's any need for that. - _ = spack.repo.PATH.provider_index - - # Ensure we have compilers in compilers.yaml to avoid that - # processes try to write the config file in parallel - _ = spack.compilers.all_compilers_config(spack.config.CONFIG) - - # Early return if there is nothing to do - if len(args) == 0: - return [] - - # Solve the environment in parallel on Linux - start = time.time() - num_procs = min(len(args), spack.config.determine_number_of_jobs(parallel=True)) - - # TODO: support parallel concretization on macOS and Windows - msg = "Starting concretization" - if sys.platform not in ("darwin", "win32") and num_procs > 1: - msg += f" pool with {num_procs} processes" - tty.msg(msg) - - batch = [] - for j, (i, concrete, duration) in enumerate( - spack.util.parallel.imap_unordered( - _concretize_task, - args, - processes=num_procs, - debug=tty.is_debug(), - maxtaskperchild=1, - ) - ): - batch.append((i, concrete)) - percentage = (j + 1) / len(args) * 100 - tty.verbose( - f"{duration:6.1f}s [{percentage:3.0f}%] {concrete.cformat('{hash:7}')} " - f"{root_specs[i].colored_str}" - ) - sys.stdout.flush() + to_concretize = [ + (root, None) for root in self.user_specs if root not in old_concretized_user_specs + ] + concretized_specs = spack.concretize.concretize_separately(*to_concretize, tests=tests) - # Add specs in original order - batch.sort(key=lambda x: x[0]) - by_hash = {} # for attaching information on test dependencies - for root, (_, concrete) in zip(root_specs, batch): - self._add_concrete_spec(root, concrete) + by_hash = {} + for abstract, concrete in concretized_specs: + self._add_concrete_spec(abstract, concrete) by_hash[concrete.dag_hash()] = concrete - finish = time.time() - tty.msg(f"Environment concretized in {finish - start:.2f} seconds") - # Unify the specs objects, so we get correct references to all parents self._read_lockfile_dict(self._to_lockfile_dict()) @@ -1726,11 +1653,7 @@ class Environment: test_dependency.copy(), depflag=dt.TEST, virtuals=current_edge.virtuals ) - results = [ - (abstract, self.specs_by_hash[h]) - for abstract, h in zip(self.concretized_user_specs, self.concretized_order) - ] - return results + return concretized_specs @property def default_view(self): @@ -2537,14 +2460,6 @@ def display_specs(specs): print(tree_string) -def _concretize_task(packed_arguments) -> Tuple[int, Spec, float]: - index, spec_str, tests = packed_arguments - with tty.SuppressOutput(msg_enabled=False): - start = time.time() - spec = Spec(spec_str).concretized(tests=tests) - return index, spec, time.time() - start - - def make_repo_path(root): """Make a RepoPath from the repo subdirectories in an environment.""" path = spack.repo.RepoPath(cache=spack.caches.MISC_CACHE) diff --git a/lib/spack/spack/solver/asp.py b/lib/spack/spack/solver/asp.py index 97fbd03e8f..af2b8a70c3 100644 --- a/lib/spack/spack/solver/asp.py +++ b/lib/spack/spack/solver/asp.py @@ -515,6 +515,8 @@ class Result: best = min(self.answers) opt, _, answer = best for input_spec in self.abstract_specs: + # The specs must be unified to get here, so it is safe to associate any satisfying spec + # with the input. Multiple inputs may be matched to the same concrete spec node = SpecBuilder.make_node(pkg=input_spec.name) if input_spec.virtual: providers = [ diff --git a/lib/spack/spack/test/cmd/install.py b/lib/spack/spack/test/cmd/install.py index 13721b2a0d..445f376b1b 100644 --- a/lib/spack/spack/test/cmd/install.py +++ b/lib/spack/spack/test/cmd/install.py @@ -906,7 +906,7 @@ def test_cdash_configure_warning(tmpdir, mock_fetch, install_mockery, capfd): specfile = "./spec.json" with open(specfile, "w") as f: f.write(spec.to_json()) - + print(spec.to_json()) install("--log-file=cdash_reports", "--log-format=cdash", specfile) # Verify Configure.xml exists with expected contents. report_dir = tmpdir.join("cdash_reports") diff --git a/lib/spack/spack/test/concretize.py b/lib/spack/spack/test/concretize.py index 553d8fd642..4d9940ea9b 100644 --- a/lib/spack/spack/test/concretize.py +++ b/lib/spack/spack/test/concretize.py @@ -14,6 +14,7 @@ import archspec.cpu import llnl.util.lang import spack.binary_distribution +import spack.cmd import spack.compiler import spack.compilers import spack.concretize @@ -3106,3 +3107,20 @@ def test_reuse_prefers_standard_over_git_versions( test_spec = spack.spec.Spec("git-ref-package@2").concretized() assert git_spec.dag_hash() != test_spec.dag_hash() assert standard_spec.dag_hash() == test_spec.dag_hash() + + +@pytest.mark.parametrize("unify", [True, "when_possible", False]) +def test_spec_unification(unify, mutable_config, mock_packages): + spack.config.set("concretizer:unify", unify) + a = "pkg-a" + a_restricted = "pkg-a^pkg-b foo=baz" + b = "pkg-b foo=none" + + unrestricted = spack.cmd.parse_specs([a, b], concretize=True) + a_concrete_unrestricted = [s for s in unrestricted if s.name == "pkg-a"][0] + b_concrete_unrestricted = [s for s in unrestricted if s.name == "pkg-b"][0] + assert (a_concrete_unrestricted["pkg-b"] == b_concrete_unrestricted) == (unify is not False) + + maybe_fails = pytest.raises if unify is True else llnl.util.lang.nullcontext + with maybe_fails(spack.solver.asp.UnsatisfiableSpecError): + _ = spack.cmd.parse_specs([a_restricted, b], concretize=True) |