"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:
		| @@ -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) | ||||
|   | ||||
| @@ -1509,6 +1509,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() | ||||
| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Peter Scheibel
					Peter Scheibel