# Copyright 2013-2024 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
"""Produce a "view" of a Spack DAG.
A "view" is file hierarchy representing the union of a number of
Spack-installed package file hierarchies. The union is formed from:
- specs resolved from the package names given by the user (the seeds)
- all dependencies of the seeds unless user specifies `--no-dependencies`
- less any specs with names matching the regular expressions given by
`--exclude`
The `view` can be built and tore down via a number of methods (the "actions"):
- symlink :: a file system view which is a directory hierarchy that is
the union of the hierarchies of the installed packages in the DAG
where installed files are referenced via symlinks.
- hardlink :: like the symlink view but hardlinks are used.
- statlink :: a view producing a status report of a symlink or
hardlink view.
The file system view concept is imspired by Nix, implemented by
brett.viren@gmail.com ca 2016.
All operations on views are performed via proxy objects such as
YamlFilesystemView.
"""
import llnl.util.tty as tty
from llnl.util.link_tree import MergeConflictError
import spack.cmd
import spack.environment as ev
import spack.schema.projections
import spack.store
from spack.config import validate
from spack.filesystem_view import YamlFilesystemView, view_func_parser
from spack.util import spack_yaml as s_yaml
description = "project packages to a compact naming scheme on the filesystem"
section = "environments"
level = "short"
actions_link = ["symlink", "add", "soft", "hardlink", "hard", "copy", "relocate"]
actions_remove = ["remove", "rm"]
actions_status = ["statlink", "status", "check"]
[docs]
def disambiguate_in_view(specs, view):
"""
When dealing with querying actions (remove/status) we only need to
disambiguate among specs in the view
"""
view_specs = set(view.get_all_specs())
def squash(matching_specs):
if not matching_specs:
tty.die("Spec matches no installed packages.")
matching_in_view = [ms for ms in matching_specs if ms in view_specs]
spack.cmd.ensure_single_spec_or_die("Spec", matching_in_view)
return matching_in_view[0] if matching_in_view else matching_specs[0]
# make function always return a list to keep consistency between py2/3
return list(map(squash, map(spack.store.STORE.db.query, specs)))
[docs]
def setup_parser(sp):
setup_parser.parser = sp
sp.add_argument(
"-v",
"--verbose",
action="store_true",
default=False,
help="if not verbose only warnings/errors will be printed",
)
sp.add_argument(
"-e",
"--exclude",
action="append",
default=[],
help="exclude packages with names matching the given regex pattern",
)
sp.add_argument(
"-d",
"--dependencies",
choices=["true", "false", "yes", "no"],
default="true",
help="link/remove/list dependencies",
)
ssp = sp.add_subparsers(metavar="ACTION", dest="action")
specs_opts = dict(metavar="spec", action="store", help="seed specs of the packages to view")
# The action parameterizes the command but in keeping with Spack
# patterns we make it a subcommand.
file_system_view_actions = {
"symlink": ssp.add_parser(
"symlink",
aliases=["add", "soft"],
help="add package files to a filesystem view via symbolic links",
),
"hardlink": ssp.add_parser(
"hardlink",
aliases=["hard"],
help="add packages files to a filesystem view via hard links",
),
"copy": ssp.add_parser(
"copy",
aliases=["relocate"],
help="add package files to a filesystem view via copy/relocate",
),
"remove": ssp.add_parser(
"remove", aliases=["rm"], help="remove packages from a filesystem view"
),
"statlink": ssp.add_parser(
"statlink",
aliases=["status", "check"],
help="check status of packages in a filesystem view",
),
}
# All these options and arguments are common to every action.
for cmd, act in file_system_view_actions.items():
act.add_argument("path", nargs=1, help="path to file system view directory")
if cmd in ("symlink", "hardlink", "copy"):
# invalid for remove/statlink, for those commands the view needs to
# already know its own projections.
act.add_argument(
"--projection-file",
dest="projection_file",
type=spack.cmd.extant_file,
help="initialize view using projections from file",
)
if cmd == "remove":
grp = act.add_mutually_exclusive_group(required=True)
act.add_argument(
"--no-remove-dependents",
action="store_true",
help="do not remove dependents of specified specs",
)
# with all option, spec is an optional argument
so = specs_opts.copy()
so["nargs"] = "*"
so["default"] = []
grp.add_argument("specs", **so)
grp.add_argument("-a", "--all", action="store_true", help="act on all specs in view")
elif cmd == "statlink":
so = specs_opts.copy()
so["nargs"] = "*"
act.add_argument("specs", **so)
else:
# without all option, spec is required
so = specs_opts.copy()
so["nargs"] = "+"
act.add_argument("specs", **so)
for cmd in ["symlink", "hardlink", "copy"]:
act = file_system_view_actions[cmd]
act.add_argument("-i", "--ignore-conflicts", action="store_true")
return
[docs]
def view(parser, args):
"Produce a view of a set of packages."
specs = spack.cmd.parse_specs(args.specs)
path = args.path[0]
if args.action in actions_link and args.projection_file:
# argparse confirms file exists
with open(args.projection_file, "r") as f:
projections_data = s_yaml.load(f)
validate(projections_data, spack.schema.projections.schema)
ordered_projections = projections_data["projections"]
else:
ordered_projections = {}
# What method are we using for this view
if args.action in actions_link:
link_fn = view_func_parser(args.action)
else:
link_fn = view_func_parser("symlink")
view = YamlFilesystemView(
path,
spack.store.STORE.layout,
projections=ordered_projections,
ignore_conflicts=getattr(args, "ignore_conflicts", False),
link=link_fn,
verbose=args.verbose,
)
# Process common args and specs
if getattr(args, "all", False):
specs = view.get_all_specs()
if len(specs) == 0:
tty.warn("Found no specs in %s" % path)
elif args.action in actions_link:
# only link commands need to disambiguate specs
env = ev.active_environment()
specs = [spack.cmd.disambiguate_spec(s, env) for s in specs]
elif args.action in actions_status:
# no specs implies all
if len(specs) == 0:
specs = view.get_all_specs()
else:
specs = disambiguate_in_view(specs, view)
else:
# status and remove can map a partial spec to packages in view
specs = disambiguate_in_view(specs, view)
with_dependencies = args.dependencies.lower() in ["true", "yes"]
# Map action to corresponding functionality
if args.action in actions_link:
try:
view.add_specs(*specs, with_dependencies=with_dependencies, exclude=args.exclude)
except MergeConflictError:
tty.info(
"Some file blocked the merge, adding the '-i' flag will "
"ignore this conflict. For more information see e.g. "
"https://github.com/spack/spack/issues/9029"
)
raise
elif args.action in actions_remove:
view.remove_specs(
*specs,
with_dependencies=with_dependencies,
exclude=args.exclude,
with_dependents=not args.no_remove_dependents,
)
elif args.action in actions_status:
view.print_status(*specs, with_dependencies=with_dependencies)
else:
tty.error('Unknown action: "%s"' % args.action)