summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--lib/spack/spack/cmd/view.py325
1 files changed, 127 insertions, 198 deletions
diff --git a/lib/spack/spack/cmd/view.py b/lib/spack/spack/cmd/view.py
index 7ddcb36019..03670329e6 100644
--- a/lib/spack/spack/cmd/view.py
+++ b/lib/spack/spack/cmd/view.py
@@ -48,31 +48,53 @@ The `view` can be built and tore down via a number of methods (the "actions"):
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.
+
'''
-# Implementation notes:
-#
-# This is implemented as a visitor pattern on the set of package specs.
-#
-# The command line ACTION maps to a visitor_*() function which takes
-# the set of package specs and any args which may be specific to the
-# ACTION.
-#
-# To add a new view:
-# 1. add a new cmd line args sub parser ACTION
-# 2. add any action-specific options/arguments, most likely a list of specs.
-# 3. add a visitor_MYACTION() function
-# 4. add any visitor_MYALIAS assignments to match any command line aliases
import os
-import re
import spack
import spack.cmd
+import spack.store
+from spack.filesystem_view import YamlFilesystemView
import llnl.util.tty as tty
description = "produce a single-rooted directory view of packages"
section = "environment"
level = "short"
+actions_link = ["symlink", "add", "soft", "hardlink", "hard"]
+actions_remove = ["remove", "rm"]
+actions_status = ["statlink", "status", "check"]
+
+
+def relaxed_disambiguate(specs, view):
+ """
+ When dealing with querying actions (remove/status) the name of the spec
+ is sufficient even though more versions of that name might be in the
+ database.
+ """
+ name_to_spec = dict((s.name, s) for s in view.get_all_specs())
+
+ def squash(matching_specs):
+ if not matching_specs:
+ tty.die("Spec matches no installed packages.")
+
+ elif len(matching_specs) == 1:
+ return matching_specs[0]
+
+ elif matching_specs[0].name in name_to_spec:
+ return name_to_spec[matching_specs[0].name]
+
+ else:
+ # we just return the first matching spec, the error about the
+ # missing spec will be printed later on
+ return 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)))
+
def setup_parser(sp):
setup_parser.parser = sp
@@ -90,216 +112,123 @@ def setup_parser(sp):
ssp = sp.add_subparsers(metavar='ACTION', dest='action')
- specs_opts = dict(metavar='spec', nargs='+',
+ 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 = [
- ssp.add_parser(
+ file_system_view_actions = {
+ "symlink": ssp.add_parser(
'symlink', aliases=['add', 'soft'],
help='add package files to a filesystem view via symbolic links'),
- ssp.add_parser(
+ "hardlink": ssp.add_parser(
'hardlink', aliases=['hard'],
help='add packages files to a filesystem via via hard links'),
- ssp.add_parser(
+ "remove": ssp.add_parser(
'remove', aliases=['rm'],
help='remove packages from a filesystem view'),
- ssp.add_parser(
+ "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 act in file_system_view_actions:
+ for cmd, act in file_system_view_actions.items():
act.add_argument('path', nargs=1,
help="path to file system view directory")
- act.add_argument('specs', **specs_opts)
-
- return
+ 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"]:
+ act = file_system_view_actions[cmd]
+ act.add_argument("-i", "--ignore-conflicts", action='store_true')
-def assuredir(path):
- 'Assure path exists as a directory'
- if not os.path.exists(path):
- os.makedirs(path)
-
-
-def relative_to(prefix, path):
- 'Return end of `path` relative to `prefix`'
- assert 0 == path.find(prefix)
- reldir = path[len(prefix):]
- if reldir.startswith('/'):
- reldir = reldir[1:]
- return reldir
-
-
-def transform_path(spec, path, prefix=None):
- 'Return the a relative path corresponding to given path spec.prefix'
- if os.path.isabs(path):
- path = relative_to(spec.prefix, path)
- subdirs = path.split(os.path.sep)
- if subdirs[0] == '.spack':
- lst = ['.spack', spec.name] + subdirs[1:]
- path = os.path.join(*lst)
- if prefix:
- path = os.path.join(prefix, path)
- return path
-
-
-def purge_empty_directories(path):
- '''Ascend up from the leaves accessible from `path`
- and remove empty directories.'''
- for dirpath, subdirs, files in os.walk(path, topdown=False):
- for sd in subdirs:
- sdp = os.path.join(dirpath, sd)
- try:
- os.rmdir(sdp)
- except OSError:
- pass
-
-
-def filter_exclude(specs, exclude):
- 'Filter specs given sequence of exclude regex'
- to_exclude = [re.compile(e) for e in exclude]
-
- def exclude(spec):
- for e in to_exclude:
- if e.match(spec.name):
- return True
- return False
- return [s for s in specs if not exclude(s)]
-
-
-def flatten(seeds, descend=True):
- 'Normalize and flattend seed specs and descend hiearchy'
- flat = set()
- for spec in seeds:
- if not descend:
- flat.add(spec)
- continue
- flat.update(spec.normalized().traverse())
- return flat
-
-
-def check_one(spec, path, verbose=False):
- 'Check status of view in path against spec'
- dotspack = os.path.join(path, '.spack', spec.name)
- if os.path.exists(os.path.join(dotspack)):
- tty.info('Package in view: "%s"' % spec.name)
- return
- tty.info('Package not in view: "%s"' % spec.name)
return
-def remove_one(spec, path, verbose=False):
- 'Remove any files found in `spec` from `path` and purge empty directories.'
-
- if not os.path.exists(path):
- return # done, short circuit
-
- dotspack = transform_path(spec, '.spack', path)
- if not os.path.exists(dotspack):
- if verbose:
- tty.info('Skipping nonexistent package: "%s"' % spec.name)
- return
-
- if verbose:
- tty.info('Removing package: "%s"' % spec.name)
- for dirpath, dirnames, filenames in os.walk(spec.prefix):
- if not filenames:
- continue
- targdir = transform_path(spec, dirpath, path)
- for fname in filenames:
- dst = os.path.join(targdir, fname)
- if not os.path.exists(dst):
- continue
- os.unlink(dst)
-
-
-def link_one(spec, path, link=os.symlink, verbose=False):
- 'Link all files in `spec` into directory `path`.'
-
- dotspack = transform_path(spec, '.spack', path)
- if os.path.exists(dotspack):
- tty.warn('Skipping existing package: "%s"' % spec.name)
- return
-
- if verbose:
- tty.info('Linking package: "%s"' % spec.name)
- for dirpath, dirnames, filenames in os.walk(spec.prefix):
- if not filenames:
- continue # avoid explicitly making empty dirs
-
- targdir = transform_path(spec, dirpath, path)
- assuredir(targdir)
-
- for fname in filenames:
- src = os.path.join(dirpath, fname)
- dst = os.path.join(targdir, fname)
- if os.path.exists(dst):
- if '.spack' in dst.split(os.path.sep):
- continue # silence these
- tty.warn("Skipping existing file: %s" % dst)
- continue
- link(src, dst)
-
-
-def visitor_symlink(specs, args):
- 'Symlink all files found in specs'
- path = args.path[0]
- assuredir(path)
- for spec in specs:
- link_one(spec, path, verbose=args.verbose)
-
-
-visitor_add = visitor_symlink
-visitor_soft = visitor_symlink
-
-
-def visitor_hardlink(specs, args):
- 'Hardlink all files found in specs'
- path = args.path[0]
- assuredir(path)
- for spec in specs:
- link_one(spec, path, os.link, verbose=args.verbose)
-
-
-visitor_hard = visitor_hardlink
-
-
-def visitor_remove(specs, args):
- 'Remove all files and directories found in specs from args.path'
- path = args.path[0]
- for spec in specs:
- remove_one(spec, path, verbose=args.verbose)
- purge_empty_directories(path)
-
-
-visitor_rm = visitor_remove
-
+def view(parser, args):
+ 'Produce a view of a set of packages.'
-def visitor_statlink(specs, args):
- 'Give status of view in args.path relative to specs'
path = args.path[0]
- for spec in specs:
- check_one(spec, path, verbose=args.verbose)
+ view = YamlFilesystemView(
+ path, spack.store.layout,
+ ignore_conflicts=getattr(args, "ignore_conflicts", False),
+ link=os.hardlink if args.action in ["hardlink", "hard"]
+ else os.symlink,
+ 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
+ specs = [spack.cmd.disambiguate_spec(s) for s in args.specs]
+
+ elif args.action in actions_status:
+ # no specs implies all
+ if len(args.specs) == 0:
+ specs = view.get_all_specs()
+ else:
+ specs = relaxed_disambiguate(args.specs, view)
+
+ else:
+ # status and remove can map the name to packages in view
+ specs = relaxed_disambiguate(args.specs, view)
+
+ activated = list(filter(lambda s: s.package.is_extension and
+ s.package.is_activated(), specs))
+
+ if len(activated) > 0:
+ tty.error("Globally activated extensions cannot be used in "
+ "conjunction with filesystem views. "
+ "Please deactivate the following specs: ")
+ spack.cmd.display_specs(activated, flags=True, variants=True,
+ long=args.verbose)
+ return
-visitor_status = visitor_statlink
-visitor_check = visitor_statlink
+ with_dependencies = args.dependencies.lower() in ['true', 'yes']
+ # Map action to corresponding functionality
+ if args.action in actions_link:
+ view.add_specs(*specs,
+ with_dependencies=with_dependencies,
+ exclude=args.exclude)
-def view(parser, args):
- 'Produce a view of a set of packages.'
+ elif args.action in actions_remove:
+ view.remove_specs(*specs,
+ with_dependencies=with_dependencies,
+ exclude=args.exclude,
+ with_dependents=not args.no_remove_dependents)
- # Process common args
- seeds = [spack.cmd.disambiguate_spec(s) for s in args.specs]
- specs = flatten(seeds, args.dependencies.lower() in ['yes', 'true'])
- specs = filter_exclude(specs, args.exclude)
+ elif args.action in actions_status:
+ view.print_status(*specs, with_dependencies=with_dependencies)
- # Execute the visitation.
- try:
- visitor = globals()['visitor_' + args.action]
- except KeyError:
+ else:
tty.error('Unknown action: "%s"' % args.action)
- visitor(specs, args)