From db529f5b61331da83c6ae1a325ac620b527f917c Mon Sep 17 00:00:00 2001 From: Oliver Breitwieser Date: Mon, 18 Sep 2017 09:22:13 -0400 Subject: view: use the FilesystemView abstraction for creating views --- lib/spack/spack/cmd/view.py | 325 +++++++++++++++++--------------------------- 1 file changed, 127 insertions(+), 198 deletions(-) (limited to 'lib') 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) -- cgit v1.2.3-60-g2f50