summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPeter Scheibel <scheibel1@llnl.gov>2021-02-23 11:45:50 -0800
committerTodd Gamblin <tgamblin@llnl.gov>2021-05-22 11:51:20 -0700
commit5546b22c70585c956ad9fd88f68d8139ff966ded (patch)
tree1428cb835a7d7f73b1b62fabbbf51ea92e29dbc6
parent2496c7b514a500da032218960cf8b8e50cc7d29d (diff)
downloadspack-5546b22c70585c956ad9fd88f68d8139ff966ded.tar.gz
spack-5546b22c70585c956ad9fd88f68d8139ff966ded.tar.bz2
spack-5546b22c70585c956ad9fd88f68d8139ff966ded.tar.xz
spack-5546b22c70585c956ad9fd88f68d8139ff966ded.zip
"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 <becker33@llnl.gov>
-rw-r--r--lib/spack/spack/cmd/__init__.py13
-rw-r--r--lib/spack/spack/cmd/common/env_utility.py4
-rw-r--r--lib/spack/spack/cmd/location.py5
-rw-r--r--lib/spack/spack/environment.py61
-rw-r--r--lib/spack/spack/test/cmd/common/arguments.py38
5 files changed, 117 insertions, 4 deletions
diff --git a/lib/spack/spack/cmd/__init__.py b/lib/spack/spack/cmd/__init__.py
index 130abea4cc..d728325699 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 e3f32737b4..a16cc3ba0d 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 cced5d29a0..4981056265 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 868ebb6ac5..7f7625f2e6 100644
--- a/lib/spack/spack/environment.py
+++ b/lib/spack/spack/environment.py
@@ -1505,6 +1505,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 6646144a81..4f6cd1a527 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()
@@ -65,3 +66,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