view: use the FilesystemView abstraction for creating views
This commit is contained in:
parent
af42c24ef5
commit
db529f5b61
@ -48,31 +48,53 @@
|
|||||||
The file system view concept is imspired by Nix, implemented by
|
The file system view concept is imspired by Nix, implemented by
|
||||||
brett.viren@gmail.com ca 2016.
|
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 os
|
||||||
import re
|
|
||||||
import spack
|
import spack
|
||||||
import spack.cmd
|
import spack.cmd
|
||||||
|
import spack.store
|
||||||
|
from spack.filesystem_view import YamlFilesystemView
|
||||||
import llnl.util.tty as tty
|
import llnl.util.tty as tty
|
||||||
|
|
||||||
description = "produce a single-rooted directory view of packages"
|
description = "produce a single-rooted directory view of packages"
|
||||||
section = "environment"
|
section = "environment"
|
||||||
level = "short"
|
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):
|
def setup_parser(sp):
|
||||||
setup_parser.parser = sp
|
setup_parser.parser = sp
|
||||||
@ -90,216 +112,123 @@ def setup_parser(sp):
|
|||||||
|
|
||||||
ssp = sp.add_subparsers(metavar='ACTION', dest='action')
|
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")
|
help="seed specs of the packages to view")
|
||||||
|
|
||||||
# The action parameterizes the command but in keeping with Spack
|
# The action parameterizes the command but in keeping with Spack
|
||||||
# patterns we make it a subcommand.
|
# patterns we make it a subcommand.
|
||||||
file_system_view_actions = [
|
file_system_view_actions = {
|
||||||
ssp.add_parser(
|
"symlink": ssp.add_parser(
|
||||||
'symlink', aliases=['add', 'soft'],
|
'symlink', aliases=['add', 'soft'],
|
||||||
help='add package files to a filesystem view via symbolic links'),
|
help='add package files to a filesystem view via symbolic links'),
|
||||||
ssp.add_parser(
|
"hardlink": ssp.add_parser(
|
||||||
'hardlink', aliases=['hard'],
|
'hardlink', aliases=['hard'],
|
||||||
help='add packages files to a filesystem via via hard links'),
|
help='add packages files to a filesystem via via hard links'),
|
||||||
ssp.add_parser(
|
"remove": ssp.add_parser(
|
||||||
'remove', aliases=['rm'],
|
'remove', aliases=['rm'],
|
||||||
help='remove packages from a filesystem view'),
|
help='remove packages from a filesystem view'),
|
||||||
ssp.add_parser(
|
"statlink": ssp.add_parser(
|
||||||
'statlink', aliases=['status', 'check'],
|
'statlink', aliases=['status', 'check'],
|
||||||
help='check status of packages in a filesystem view')
|
help='check status of packages in a filesystem view')
|
||||||
]
|
}
|
||||||
|
|
||||||
# All these options and arguments are common to every action.
|
# 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,
|
act.add_argument('path', nargs=1,
|
||||||
help="path to file system view directory")
|
help="path to file system view directory")
|
||||||
act.add_argument('specs', **specs_opts)
|
|
||||||
|
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')
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|
||||||
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 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)
|
|
||||||
|
|
||||||
|
|
||||||
visitor_status = visitor_statlink
|
|
||||||
visitor_check = visitor_statlink
|
|
||||||
|
|
||||||
|
|
||||||
def view(parser, args):
|
def view(parser, args):
|
||||||
'Produce a view of a set of packages.'
|
'Produce a view of a set of packages.'
|
||||||
|
|
||||||
# Process common args
|
path = args.path[0]
|
||||||
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)
|
|
||||||
|
|
||||||
# Execute the visitation.
|
view = YamlFilesystemView(
|
||||||
try:
|
path, spack.store.layout,
|
||||||
visitor = globals()['visitor_' + args.action]
|
ignore_conflicts=getattr(args, "ignore_conflicts", False),
|
||||||
except KeyError:
|
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
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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)
|
tty.error('Unknown action: "%s"' % args.action)
|
||||||
visitor(specs, args)
|
|
||||||
|
Loading…
Reference in New Issue
Block a user