Consolidate concretization output for environments (#44489)

When Spack concretizes environments, it prints every (newly concretized) root spec
individually with all of its dependencies. For most reasonably sized environments, this
is too much output. This is true for three commands:

* `spack concretize` when concretizing an environment with newly added specs
* `spack install` when installing an environment with newly added specs
* `spack spec` with no arguments in an environment

The output dates back to before we had unified environments or nicer spec traversal
routines, and we can improve it.

This PR makes environment concretization output analogous to what we do for regular
specs. Just like `spack spec` for a single spec, we show all root specs with no
indentation, so you can easily see the specs you explicitly requested. Dependencies are
shown:

1. With indentation according to their depth in a breadth-first traversal starting at
   the roots;
2. Only once if they appear on paths from multiple roots

So, the default is now consistent with `spack spec` for one spec--it's `--cover=nodes`.
i.e., if there are 100 specs in your environment, you'll get 100 lines of output.

If you want to see more details, you can do that with `spack spec` using the arguments
you're already familiar with. For example, if you wanted to see dependency types and
*all* dependencies, you could use `spack spec -l --cover=edges`. Or you could add
deptypes and namespaces with, e.g. `spack spec -ltN`.

With no arguments in an environment, `spack spec` concretizes (if necessary) and shows
the concretized environment. If you run `spack concretize` *first*, inspecting the
environment repeatedly with `spack spec` will be fast, as everything is already in the
`spack.lock` file.

- [x] factor most logic of `Spec.tree()` out of `Spec` class into `spack.spec.tree()`,
      which can take multiple specs as roots.
- [x] make `Spec.tree()` call `spack.spec.tree()`
- [x] `spack.environment.display_specs()` now uses `spack.spec.tree()`
- [x] Update `spack concretize`
- [x] Update `spack install`
- [x] Update `spack spec` to call `spack.spec.tree()` for environments.
- [x] Continue to output specs individually for `spack spec` when using
      `--yaml` or `--json`
This commit is contained in:
Todd Gamblin 2024-06-03 22:29:14 +02:00 committed by GitHub
parent f478a65635
commit 85f13442d2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 161 additions and 87 deletions

View File

@ -3,6 +3,9 @@
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
import llnl.util.tty as tty
from llnl.string import plural
import spack.cmd
import spack.cmd.common.arguments
import spack.environment as ev
@ -43,5 +46,9 @@ def concretize(parser, args):
with env.write_transaction():
concretized_specs = env.concretize(force=args.force, tests=tests)
if not args.quiet:
ev.display_specs(concretized_specs)
if concretized_specs:
tty.msg(f"Concretized {plural(len(concretized_specs), 'spec')}:")
ev.display_specs([concrete for _, concrete in concretized_specs])
else:
tty.msg("No new specs to concretize.")
env.write()

View File

@ -10,6 +10,7 @@
from typing import List
import llnl.util.filesystem as fs
from llnl.string import plural
from llnl.util import lang, tty
import spack.build_environment
@ -375,7 +376,9 @@ def _maybe_add_and_concretize(args, env, specs):
# `spack concretize`
tests = compute_tests_install_kwargs(env.user_specs, args.test)
concretized_specs = env.concretize(tests=tests)
ev.display_specs(concretized_specs)
if concretized_specs:
tty.msg(f"Concretized {plural(len(concretized_specs), 'spec')}")
ev.display_specs([concrete for _, concrete in concretized_specs])
# save view regeneration for later, so that we only do it
# once, as it can be slow.

View File

@ -114,15 +114,16 @@ def _process_result(result, show, required_format, kwargs):
# dump the solutions as concretized specs
if "solutions" in show:
for spec in result.specs:
# With -y, just print YAML to output.
if required_format == "yaml":
# use write because to_yaml already has a newline.
sys.stdout.write(spec.to_yaml(hash=ht.dag_hash))
elif required_format == "json":
sys.stdout.write(spec.to_json(hash=ht.dag_hash))
else:
sys.stdout.write(spec.tree(color=sys.stdout.isatty(), **kwargs))
if required_format:
for spec in result.specs:
# With -y, just print YAML to output.
if required_format == "yaml":
# use write because to_yaml already has a newline.
sys.stdout.write(spec.to_yaml(hash=ht.dag_hash))
elif required_format == "json":
sys.stdout.write(spec.to_json(hash=ht.dag_hash))
else:
sys.stdout.write(spack.spec.tree(result.specs, color=sys.stdout.isatty(), **kwargs))
print()
if result.unsolved_specs and "solutions" in show:

View File

@ -105,11 +105,19 @@ def spec(parser, args):
if env:
env.concretize()
specs = env.concretized_specs()
# environments are printed together in a combined tree() invocation,
# except when using --yaml or --json, which we print spec by spec below.
if not args.format:
tree_kwargs["key"] = spack.traverse.by_dag_hash
tree_kwargs["hashes"] = args.long or args.very_long
print(spack.spec.tree([concrete for _, concrete in specs], **tree_kwargs))
return
else:
tty.die("spack spec requires at least one spec or an active environment")
for input, output in specs:
# With -y, just print YAML to output.
# With --yaml or --json, just print the raw specs to output
if args.format:
if args.format == "yaml":
# use write because to_yaml already has a newline.

View File

@ -24,6 +24,7 @@
from llnl.util.link_tree import ConflictingSpecsError
from llnl.util.symlink import readlink, symlink
import spack.cmd
import spack.compilers
import spack.concretize
import spack.config
@ -2473,27 +2474,21 @@ def _equiv_dict(first, second):
return same_values and same_keys_with_same_overrides
def display_specs(concretized_specs):
"""Displays the list of specs returned by `Environment.concretize()`.
def display_specs(specs):
"""Displays a list of specs traversed breadth-first, covering nodes, with install status.
Args:
concretized_specs (list): list of specs returned by
`Environment.concretize()`
specs (list): list of specs
"""
def _tree_to_display(spec):
return spec.tree(
recurse_dependencies=True,
format=spack.spec.DISPLAY_FORMAT,
status_fn=spack.spec.Spec.install_status,
hashlen=7,
hashes=True,
)
for user_spec, concrete_spec in concretized_specs:
tty.msg("Concretized {0}".format(user_spec))
sys.stdout.write(_tree_to_display(concrete_spec))
print("")
tree_string = spack.spec.tree(
specs,
format=spack.spec.DISPLAY_FORMAT,
hashes=True,
hashlen=7,
status_fn=spack.spec.Spec.install_status,
key=traverse.by_dag_hash,
)
print(tree_string)
def _concretize_from_constraints(spec_constraints, tests=False):

View File

@ -1287,6 +1287,102 @@ def copy(self, *args, **kwargs):
return self.wrapped_obj.copy(*args, **kwargs)
def tree(
specs: List["spack.spec.Spec"],
*,
color: Optional[bool] = None,
depth: bool = False,
hashes: bool = False,
hashlen: Optional[int] = None,
cover: str = "nodes",
indent: int = 0,
format: str = DEFAULT_FORMAT,
deptypes: Union[Tuple[str, ...], str] = "all",
show_types: bool = False,
depth_first: bool = False,
recurse_dependencies: bool = True,
status_fn: Optional[Callable[["Spec"], InstallStatus]] = None,
prefix: Optional[Callable[["Spec"], str]] = None,
key=id,
) -> str:
"""Prints out specs and their dependencies, tree-formatted with indentation.
Status function may either output a boolean or an InstallStatus
Args:
color: if True, always colorize the tree. If False, don't colorize the tree. If None,
use the default from llnl.tty.color
depth: print the depth from the root
hashes: if True, print the hash of each node
hashlen: length of the hash to be printed
cover: either "nodes" or "edges"
indent: extra indentation for the tree being printed
format: format to be used to print each node
deptypes: dependency types to be represented in the tree
show_types: if True, show the (merged) dependency type of a node
depth_first: if True, traverse the DAG depth first when representing it as a tree
recurse_dependencies: if True, recurse on dependencies
status_fn: optional callable that takes a node as an argument and return its
installation status
prefix: optional callable that takes a node as an argument and return its
installation prefix
"""
out = ""
if color is None:
color = clr.get_color_when()
for d, dep_spec in traverse.traverse_tree(
sorted(specs), cover=cover, deptype=deptypes, depth_first=depth_first, key=key
):
node = dep_spec.spec
if prefix is not None:
out += prefix(node)
out += " " * indent
if depth:
out += "%-4d" % d
if status_fn:
status = status_fn(node)
if status in list(InstallStatus):
out += clr.colorize(status.value, color=color)
elif status:
out += clr.colorize("@g{[+]} ", color=color)
else:
out += clr.colorize("@r{[-]} ", color=color)
if hashes:
out += clr.colorize("@K{%s} ", color=color) % node.dag_hash(hashlen)
if show_types:
if cover == "nodes":
# when only covering nodes, we merge dependency types
# from all dependents before showing them.
depflag = 0
for ds in node.edges_from_dependents():
depflag |= ds.depflag
else:
# when covering edges or paths, we show dependency
# types only for the edge through which we visited
depflag = dep_spec.depflag
type_chars = dt.flag_to_chars(depflag)
out += "[%s] " % type_chars
out += " " * d
if d > 0:
out += "^"
out += node.format(format, color=color) + "\n"
# Check if we wanted just the first line
if not recurse_dependencies:
break
return out
@lang.lazy_lexicographic_ordering(set_hash=False)
class Spec:
#: Cache for spec's prefix, computed lazily in the corresponding property
@ -4604,13 +4700,14 @@ def tree(
recurse_dependencies: bool = True,
status_fn: Optional[Callable[["Spec"], InstallStatus]] = None,
prefix: Optional[Callable[["Spec"], str]] = None,
key=id,
) -> str:
"""Prints out this spec and its dependencies, tree-formatted
with indentation.
"""Prints out this spec and its dependencies, tree-formatted with indentation.
Status function may either output a boolean or an InstallStatus
See multi-spec ``spack.spec.tree()`` function for details.
Args:
specs: List of specs to format.
color: if True, always colorize the tree. If False, don't colorize the tree. If None,
use the default from llnl.tty.color
depth: print the depth from the root
@ -4628,60 +4725,23 @@ def tree(
prefix: optional callable that takes a node as an argument and return its
installation prefix
"""
out = ""
if color is None:
color = clr.get_color_when()
for d, dep_spec in traverse.traverse_tree(
[self], cover=cover, deptype=deptypes, depth_first=depth_first
):
node = dep_spec.spec
if prefix is not None:
out += prefix(node)
out += " " * indent
if depth:
out += "%-4d" % d
if status_fn:
status = status_fn(node)
if status in list(InstallStatus):
out += clr.colorize(status.value, color=color)
elif status:
out += clr.colorize("@g{[+]} ", color=color)
else:
out += clr.colorize("@r{[-]} ", color=color)
if hashes:
out += clr.colorize("@K{%s} ", color=color) % node.dag_hash(hashlen)
if show_types:
if cover == "nodes":
# when only covering nodes, we merge dependency types
# from all dependents before showing them.
depflag = 0
for ds in node.edges_from_dependents():
depflag |= ds.depflag
else:
# when covering edges or paths, we show dependency
# types only for the edge through which we visited
depflag = dep_spec.depflag
type_chars = dt.flag_to_chars(depflag)
out += "[%s] " % type_chars
out += " " * d
if d > 0:
out += "^"
out += node.format(format, color=color) + "\n"
# Check if we wanted just the first line
if not recurse_dependencies:
break
return out
return tree(
[self],
color=color,
depth=depth,
hashes=hashes,
hashlen=hashlen,
cover=cover,
indent=indent,
format=format,
deptypes=deptypes,
show_types=show_types,
depth_first=depth_first,
recurse_dependencies=recurse_dependencies,
status_fn=status_fn,
prefix=prefix,
key=key,
)
def __repr__(self):
return str(self)