# Copyright 2013-2022 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
from llnl.util.tty.color import colorize
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]
if len(matching_in_view) > 1:
spec_format = '{name}{@version}{%compiler}{arch=architecture}'
args = ["Spec matches multiple packages.",
"Matching packages:"]
args += [colorize(" @K{%s} " % s.dag_hash(7)) +
s.cformat(spec_format) for s in matching_in_view]
args += ["Use a more specific spec."]
tty.die(*args)
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.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.
help_msg = "Initialize view using projections from file."
act.add_argument('--projection-file', dest='projection_file',
type=spack.cmd.extant_file, help=help_msg)
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.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)