"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)
|
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):
|
def elide_list(line_list, max_num=10):
|
||||||
"""Takes a long list and limits it to a smaller number of elements,
|
"""Takes a long list and limits it to a smaller number of elements,
|
||||||
replacing intervening elements with '...'. For example::
|
replacing intervening elements with '...'. For example::
|
||||||
|
@ -53,11 +53,13 @@ def emulate_env_utility(cmd_name, context, args):
|
|||||||
spec = args.spec[0]
|
spec = args.spec[0]
|
||||||
cmd = args.spec[1:]
|
cmd = args.spec[1:]
|
||||||
|
|
||||||
specs = spack.cmd.parse_specs(spec, concretize=True)
|
specs = spack.cmd.parse_specs(spec, concretize=False)
|
||||||
if len(specs) > 1:
|
if len(specs) > 1:
|
||||||
tty.die("spack %s only takes one spec." % cmd_name)
|
tty.die("spack %s only takes one spec." % cmd_name)
|
||||||
spec = specs[0]
|
spec = specs[0]
|
||||||
|
|
||||||
|
spec = spack.cmd.matching_spec_from_env(spec)
|
||||||
|
|
||||||
build_environment.setup_package(spec.package, args.dirty, context)
|
build_environment.setup_package(spec.package, args.dirty, context)
|
||||||
|
|
||||||
if args.dump:
|
if args.dump:
|
||||||
|
@ -98,9 +98,8 @@ def location(parser, args):
|
|||||||
print(spack.repo.path.dirname_for_package_name(spec.name))
|
print(spack.repo.path.dirname_for_package_name(spec.name))
|
||||||
|
|
||||||
else:
|
else:
|
||||||
# These versions need concretized specs.
|
spec = spack.cmd.matching_spec_from_env(spec)
|
||||||
spec.concretize()
|
pkg = spec.package
|
||||||
pkg = spack.repo.get(spec)
|
|
||||||
|
|
||||||
if args.stage_dir:
|
if args.stage_dir:
|
||||||
print(pkg.stage.path)
|
print(pkg.stage.path)
|
||||||
|
@ -1505,6 +1505,67 @@ def concretized_specs(self):
|
|||||||
for s, h in zip(self.concretized_user_specs, self.concretized_order):
|
for s, h in zip(self.concretized_user_specs, self.concretized_order):
|
||||||
yield (s, self.specs_by_hash[h])
|
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):
|
def removed_specs(self):
|
||||||
"""Tuples of (user spec, concrete spec) for all specs that will be
|
"""Tuples of (user spec, concrete spec) for all specs that will be
|
||||||
removed on nexg concretize."""
|
removed on nexg concretize."""
|
||||||
|
@ -12,6 +12,7 @@
|
|||||||
import spack.cmd
|
import spack.cmd
|
||||||
import spack.cmd.common.arguments as arguments
|
import spack.cmd.common.arguments as arguments
|
||||||
import spack.config
|
import spack.config
|
||||||
|
import spack.environment as ev
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture()
|
@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 not in s.variants for x in unexpected_variants)
|
||||||
assert all(x in s.variants for x in expected_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