"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:
parent
2496c7b514
commit
5546b22c70
@ -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::
|
||||
|
@ -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:
|
||||
|
@ -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)
|
||||
|
@ -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."""
|
||||
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user