From 76b1d97ca5a57ec504097df233f352462ebebc06 Mon Sep 17 00:00:00 2001 From: Peter Scheibel Date: Tue, 23 Feb 2021 11:45:50 -0800 Subject: "spack build-env" searches env for relevant spec (#21642) If you install packages using spack install in an environment with complex spec constraints, and the install fails, you may want to test out the build using spack build-env; one issue (particularly if you use concretize: together) is that it may be hard to pass the appropriate spec that matches what the environment is attempting to install. This updates the build-env command to default to pulling a matching spec from the environment rather than concretizing what the user provides on the command line independently. This makes a similar change to spack cd. If the user-provided spec matches multiple specs in the environment, then these commands will now report an error and display all matching specs (to help the user specify). Co-authored-by: Gregory Becker --- lib/spack/spack/cmd/__init__.py | 13 ++++++ lib/spack/spack/cmd/common/env_utility.py | 4 +- lib/spack/spack/cmd/location.py | 5 +-- lib/spack/spack/environment.py | 61 ++++++++++++++++++++++++++++ lib/spack/spack/test/cmd/common/arguments.py | 38 +++++++++++++++++ 5 files changed, 117 insertions(+), 4 deletions(-) (limited to 'lib') diff --git a/lib/spack/spack/cmd/__init__.py b/lib/spack/spack/cmd/__init__.py index 4295ffe6a2..e9b4b60299 100644 --- a/lib/spack/spack/cmd/__init__.py +++ b/lib/spack/spack/cmd/__init__.py @@ -181,6 +181,19 @@ def parse_specs(args, **kwargs): raise spack.error.SpackError(msg) +def matching_spec_from_env(spec): + """ + Returns a concrete spec, matching what is available in the environment. + If no matching spec is found in the environment (or if no environment is + active), this will return the given spec but concretized. + """ + env = spack.environment.get_env({}, cmd_name) + if env: + return env.matching_spec(spec) or spec.concretized() + else: + return spec.concretized() + + def elide_list(line_list, max_num=10): """Takes a long list and limits it to a smaller number of elements, replacing intervening elements with '...'. For example:: diff --git a/lib/spack/spack/cmd/common/env_utility.py b/lib/spack/spack/cmd/common/env_utility.py index 6a706b1b87..76226aa12e 100644 --- a/lib/spack/spack/cmd/common/env_utility.py +++ b/lib/spack/spack/cmd/common/env_utility.py @@ -53,11 +53,13 @@ def emulate_env_utility(cmd_name, context, args): spec = args.spec[0] cmd = args.spec[1:] - specs = spack.cmd.parse_specs(spec, concretize=True) + specs = spack.cmd.parse_specs(spec, concretize=False) if len(specs) > 1: tty.die("spack %s only takes one spec." % cmd_name) spec = specs[0] + spec = spack.cmd.matching_spec_from_env(spec) + build_environment.setup_package(spec.package, args.dirty, context) if args.dump: diff --git a/lib/spack/spack/cmd/location.py b/lib/spack/spack/cmd/location.py index a1f35423ff..9050e3111c 100644 --- a/lib/spack/spack/cmd/location.py +++ b/lib/spack/spack/cmd/location.py @@ -98,9 +98,8 @@ def location(parser, args): print(spack.repo.path.dirname_for_package_name(spec.name)) else: - # These versions need concretized specs. - spec.concretize() - pkg = spack.repo.get(spec) + spec = spack.cmd.matching_spec_from_env(spec) + pkg = spec.package if args.stage_dir: print(pkg.stage.path) diff --git a/lib/spack/spack/environment.py b/lib/spack/spack/environment.py index 4518cb25a9..5b8e944250 100644 --- a/lib/spack/spack/environment.py +++ b/lib/spack/spack/environment.py @@ -1509,6 +1509,67 @@ class Environment(object): for s, h in zip(self.concretized_user_specs, self.concretized_order): yield (s, self.specs_by_hash[h]) + def matching_spec(self, spec): + """ + Given a spec (likely not concretized), find a matching concretized + spec in the environment. + + The matching spec does not have to be installed in the environment, + but must be concrete (specs added with `spack add` without an + intervening `spack concretize` will not be matched). + + If there is a single root spec that matches the provided spec or a + single dependency spec that matches the provided spec, then the + concretized instance of that spec will be returned. + + If multiple root specs match the provided spec, or no root specs match + and multiple dependency specs match, then this raises an error + and reports all matching specs. + """ + # Root specs will be keyed by concrete spec, value abstract + # Dependency-only specs will have value None + matches = {} + + for user_spec, concretized_user_spec in self.concretized_specs(): + if concretized_user_spec.satisfies(spec): + matches[concretized_user_spec] = user_spec + for dep_spec in concretized_user_spec.traverse(root=False): + if dep_spec.satisfies(spec): + # Don't overwrite the abstract spec if present + # If not present already, set to None + matches[dep_spec] = matches.get(dep_spec, None) + + if not matches: + return None + elif len(matches) == 1: + return list(matches.keys())[0] + + root_matches = dict((concrete, abstract) + for concrete, abstract in matches.items() + if abstract) + + if len(root_matches) == 1: + return root_matches[0][1] + + # More than one spec matched, and either multiple roots matched or + # none of the matches were roots + # If multiple root specs match, it is assumed that the abstract + # spec will most-succinctly summarize the difference between them + # (and the user can enter one of these to disambiguate) + match_strings = [] + fmt_str = '{hash:7} ' + spack.spec.default_format + for concrete, abstract in matches.items(): + if abstract: + s = 'Root spec %s\n %s' % (abstract, concrete.format(fmt_str)) + else: + s = 'Dependency spec\n %s' % concrete.format(fmt_str) + match_strings.append(s) + matches_str = '\n'.join(match_strings) + + msg = ("{0} matches multiple specs in the environment {1}: \n" + "{2}".format(str(spec), self.name, matches_str)) + raise SpackEnvironmentError(msg) + def removed_specs(self): """Tuples of (user spec, concrete spec) for all specs that will be removed on nexg concretize.""" diff --git a/lib/spack/spack/test/cmd/common/arguments.py b/lib/spack/spack/test/cmd/common/arguments.py index dd92ec92ea..d3c3f81279 100644 --- a/lib/spack/spack/test/cmd/common/arguments.py +++ b/lib/spack/spack/test/cmd/common/arguments.py @@ -12,6 +12,7 @@ import pytest import spack.cmd import spack.cmd.common.arguments as arguments import spack.config +import spack.environment as ev @pytest.fixture() @@ -81,3 +82,40 @@ def test_parse_spec_flags_with_spaces( assert all(x not in s.variants for x in unexpected_variants) assert all(x in s.variants for x in expected_variants) + + +@pytest.mark.usefixtures('config') +def test_match_spec_env(mock_packages, mutable_mock_env_path): + """ + Concretize a spec with non-default options in an environment. Make + sure that when we ask for a matching spec when the environment is + active that we get the instance concretized in the environment. + """ + # Initial sanity check: we are planning on choosing a non-default + # value, so make sure that is in fact not the default. + check_defaults = spack.cmd.parse_specs(['a'], concretize=True)[0] + assert not check_defaults.satisfies('foobar=baz') + + e = ev.create('test') + e.add('a foobar=baz') + e.concretize() + with e: + env_spec = spack.cmd.matching_spec_from_env( + spack.cmd.parse_specs(['a'])[0]) + assert env_spec.satisfies('foobar=baz') + assert env_spec.concrete + + +@pytest.mark.usefixtures('config') +def test_multiple_env_match_raises_error(mock_packages, mutable_mock_env_path): + e = ev.create('test') + e.add('a foobar=baz') + e.add('a foobar=fee') + e.concretize() + with e: + with pytest.raises( + spack.environment.SpackEnvironmentError) as exc_info: + + spack.cmd.matching_spec_from_env(spack.cmd.parse_specs(['a'])[0]) + + assert 'matches multiple specs' in exc_info.value.message -- cgit v1.2.3-60-g2f50