"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>
This commit is contained in:
Peter Scheibel 2021-02-23 11:45:50 -08:00 committed by Todd Gamblin
parent 2496c7b514
commit 5546b22c70
5 changed files with 117 additions and 4 deletions

View File

@ -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::

View File

@ -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:

View File

@ -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)

View File

@ -1505,6 +1505,67 @@ def concretized_specs(self):
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."""

View File

@ -12,6 +12,7 @@
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