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