filesystem_view: add a class to handle a view via a Yaml description
This commit is contained in:
parent
21dc31a4a1
commit
538d855e1b
524
lib/spack/spack/filesystem_view.py
Normal file
524
lib/spack/spack/filesystem_view.py
Normal file
@ -0,0 +1,524 @@
|
|||||||
|
##############################################################################
|
||||||
|
# Copyright (c) 2013-2016, Lawrence Livermore National Security, LLC.
|
||||||
|
# Produced at the Lawrence Livermore National Laboratory.
|
||||||
|
#
|
||||||
|
# This file is part of Spack.
|
||||||
|
# Created by Todd Gamblin, tgamblin@llnl.gov, All rights reserved.
|
||||||
|
# LLNL-CODE-647188
|
||||||
|
#
|
||||||
|
# For details, see https://github.com/llnl/spack
|
||||||
|
# Please also see the LICENSE file for our notice and the LGPL.
|
||||||
|
#
|
||||||
|
# This program is free software; you can redistribute it and/or modify
|
||||||
|
# it under the terms of the GNU Lesser General Public License (as
|
||||||
|
# published by the Free Software Foundation) version 2.1, February 1999.
|
||||||
|
#
|
||||||
|
# This program is distributed in the hope that it will be useful, but
|
||||||
|
# WITHOUT ANY WARRANTY; without even the IMPLIED WARRANTY OF
|
||||||
|
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the terms and
|
||||||
|
# conditions of the GNU Lesser General Public License for more details.
|
||||||
|
#
|
||||||
|
# You should have received a copy of the GNU Lesser General Public
|
||||||
|
# License along with this program; if not, write to the Free Software
|
||||||
|
# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
||||||
|
##############################################################################
|
||||||
|
import functools as ft
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
|
||||||
|
from llnl.util.filesystem import join_path
|
||||||
|
from llnl.util.link_tree import LinkTree
|
||||||
|
from llnl.util import tty
|
||||||
|
|
||||||
|
import spack
|
||||||
|
import spack.spec
|
||||||
|
import spack.store
|
||||||
|
from spack.directory_layout import ExtensionAlreadyInstalledError
|
||||||
|
from spack.directory_layout import YamlViewExtensionsLayout
|
||||||
|
|
||||||
|
# compatability
|
||||||
|
if sys.version_info < (3, 0):
|
||||||
|
from itertools import imap as map
|
||||||
|
from itertools import ifilter as filter
|
||||||
|
from itertools import izip as zip
|
||||||
|
|
||||||
|
__all__ = ["FilesystemView", "YamlFilesystemView"]
|
||||||
|
|
||||||
|
|
||||||
|
class FilesystemView(object):
|
||||||
|
"""
|
||||||
|
Governs a filesystem view that is located at certain root-directory.
|
||||||
|
|
||||||
|
Packages are linked from their install directories into a common file
|
||||||
|
hierachy.
|
||||||
|
|
||||||
|
In distributed filesystems, loading each installed package seperately
|
||||||
|
can lead to slow-downs due to too many directories being traversed.
|
||||||
|
This can be circumvented by loading all needed modules into a common
|
||||||
|
directory structure.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, root, layout, **kwargs):
|
||||||
|
"""
|
||||||
|
Initialize a filesystem view under the given `root` directory with
|
||||||
|
corresponding directory `layout`.
|
||||||
|
|
||||||
|
Files are linked by method `link` (os.symlink by default).
|
||||||
|
"""
|
||||||
|
self.root = root
|
||||||
|
self.layout = layout
|
||||||
|
|
||||||
|
self.ignore_conflicts = kwargs.get("ignore_conflicts", False)
|
||||||
|
self.link = kwargs.get("link", os.symlink)
|
||||||
|
self.verbose = kwargs.get("verbose", False)
|
||||||
|
|
||||||
|
def add_specs(self, *specs, **kwargs):
|
||||||
|
"""
|
||||||
|
Add given specs to view.
|
||||||
|
|
||||||
|
The supplied specs might be standalone packages or extensions of
|
||||||
|
other packages.
|
||||||
|
|
||||||
|
Should accept `with_dependencies` as keyword argument (default
|
||||||
|
True) to indicate wether or not dependencies should be activated as
|
||||||
|
well.
|
||||||
|
|
||||||
|
Should except an `exclude` keyword argument containing a list of
|
||||||
|
regexps that filter out matching spec names.
|
||||||
|
|
||||||
|
This method should make use of `activate_{extension,standalone}`.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def add_extension(self, spec):
|
||||||
|
"""
|
||||||
|
Add (link) an extension in this view.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def add_standalone(self, spec):
|
||||||
|
"""
|
||||||
|
Add (link) a standalone package into this view.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def check_added(self, spec):
|
||||||
|
"""
|
||||||
|
Check if the given concrete spec is active in this view.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def remove_specs(self, *specs, **kwargs):
|
||||||
|
"""
|
||||||
|
Removes given specs from view.
|
||||||
|
|
||||||
|
The supplied spec might be a standalone package or an extension of
|
||||||
|
another package.
|
||||||
|
|
||||||
|
Should accept `with_dependencies` as keyword argument (default
|
||||||
|
True) to indicate wether or not dependencies should be deactivated
|
||||||
|
as well.
|
||||||
|
|
||||||
|
Should accept `with_dependents` as keyword argument (default True)
|
||||||
|
to indicate wether or not dependents on the deactivated specs
|
||||||
|
should be removed as well.
|
||||||
|
|
||||||
|
Should except an `exclude` keyword argument containing a list of
|
||||||
|
regexps that filter out matching spec names.
|
||||||
|
|
||||||
|
This method should make use of `deactivate_{extension,standalone}`.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def remove_extension(self, spec):
|
||||||
|
"""
|
||||||
|
Remove (unlink) an extension from this view.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def remove_standalone(self, spec):
|
||||||
|
"""
|
||||||
|
Remove (unlink) a standalone package from this view.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def get_all_specs(self):
|
||||||
|
"""
|
||||||
|
Get all specs currently active in this view.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def get_spec(self, spec):
|
||||||
|
"""
|
||||||
|
Return the actual spec linked in this view (i.e. do not look it up
|
||||||
|
in the database by name).
|
||||||
|
|
||||||
|
`spec` can be a name or a spec from which the name is extracted.
|
||||||
|
|
||||||
|
As there can only be a single version active for any spec the name
|
||||||
|
is enough to identify the spec in the view.
|
||||||
|
|
||||||
|
If no spec is present, returns None.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
def print_status(self, *specs, **kwargs):
|
||||||
|
"""
|
||||||
|
Print a short summary about the given specs, detailing whether..
|
||||||
|
* ..they are active in the view.
|
||||||
|
* ..they are active but the activated version differs.
|
||||||
|
* ..they are not activte in the view.
|
||||||
|
|
||||||
|
Takes `with_dependencies` keyword argument so that the status of
|
||||||
|
dependencies is printed as well.
|
||||||
|
"""
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
|
||||||
|
class YamlFilesystemView(FilesystemView):
|
||||||
|
"""
|
||||||
|
Filesystem view to work with a yaml based directory layout.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, root, layout, **kwargs):
|
||||||
|
super(YamlFilesystemView, self).__init__(root, layout, **kwargs)
|
||||||
|
|
||||||
|
self.extensions_layout = YamlViewExtensionsLayout(root, layout)
|
||||||
|
|
||||||
|
self._croot = colorize_root(self.root) + " "
|
||||||
|
|
||||||
|
def add_specs(self, *specs, **kwargs):
|
||||||
|
assert all((s.concrete for s in specs))
|
||||||
|
specs = set(specs)
|
||||||
|
|
||||||
|
if kwargs.get("with_dependencies", True):
|
||||||
|
specs.update(get_dependencies(specs))
|
||||||
|
|
||||||
|
if kwargs.get("exclude", None):
|
||||||
|
specs = set(filter_exclude(specs, kwargs["exclude"]))
|
||||||
|
|
||||||
|
conflicts = self.get_conflicts(*specs)
|
||||||
|
|
||||||
|
if conflicts:
|
||||||
|
for s, v in conflicts:
|
||||||
|
self.print_conflict(v, s)
|
||||||
|
return
|
||||||
|
|
||||||
|
extensions = set(filter(lambda s: s.package.is_extension, specs))
|
||||||
|
standalones = specs - extensions
|
||||||
|
|
||||||
|
set(map(self._check_no_ext_conflicts, extensions))
|
||||||
|
# fail on first error, otherwise link extensions as well
|
||||||
|
if all(map(self.add_standalone, standalones)):
|
||||||
|
all(map(self.add_extension, extensions))
|
||||||
|
|
||||||
|
def add_extension(self, spec):
|
||||||
|
if not spec.package.is_extension:
|
||||||
|
tty.error(self._croot + 'Package %s is not an extension.'
|
||||||
|
% spec.name)
|
||||||
|
return False
|
||||||
|
|
||||||
|
try:
|
||||||
|
if not spec.package.is_activated(self.extensions_layout):
|
||||||
|
spec.package.do_activate(
|
||||||
|
verbose=self.verbose,
|
||||||
|
extensions_layout=self.extensions_layout)
|
||||||
|
|
||||||
|
except ExtensionAlreadyInstalledError:
|
||||||
|
# As we use sets in add_specs(), the order in which packages get
|
||||||
|
# activated is essentially random. So this spec might have already
|
||||||
|
# been activated as dependency of another package -> fail silently
|
||||||
|
pass
|
||||||
|
|
||||||
|
# make sure the meta folder is linked as well (this is not done by the
|
||||||
|
# extension-activation mechnism)
|
||||||
|
if not self.check_added(spec):
|
||||||
|
self.link_meta_folder(spec)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
def add_standalone(self, spec):
|
||||||
|
if spec.package.is_extension:
|
||||||
|
tty.error(self._croot + 'Package %s is an extension.'
|
||||||
|
% spec.name)
|
||||||
|
return False
|
||||||
|
|
||||||
|
if self.check_added(spec):
|
||||||
|
tty.warn(self._croot + 'Skipping already linked package: %s'
|
||||||
|
% colorize_spec(spec))
|
||||||
|
return True
|
||||||
|
|
||||||
|
tree = LinkTree(spec.prefix)
|
||||||
|
|
||||||
|
if not self.ignore_conflicts:
|
||||||
|
conflict = tree.find_conflict(self.root)
|
||||||
|
if conflict is not None:
|
||||||
|
tty.error(self._croot +
|
||||||
|
"Cannot link package %s, file already exists: %s"
|
||||||
|
% (spec.name, conflict))
|
||||||
|
return False
|
||||||
|
|
||||||
|
conflicts = tree.merge(self.root, link=self.link,
|
||||||
|
ignore=ignore_metadata_dir,
|
||||||
|
ignore_conflicts=self.ignore_conflicts)
|
||||||
|
self.link_meta_folder(spec)
|
||||||
|
|
||||||
|
if self.ignore_conflicts:
|
||||||
|
for c in conflicts:
|
||||||
|
tty.warn(self._croot + "Could not link: %s" % c)
|
||||||
|
|
||||||
|
if self.verbose:
|
||||||
|
tty.info(self._croot + 'Linked package: %s' % colorize_spec(spec))
|
||||||
|
return True
|
||||||
|
|
||||||
|
def check_added(self, spec):
|
||||||
|
assert spec.concrete
|
||||||
|
return spec == self.get_spec(spec)
|
||||||
|
|
||||||
|
def remove_specs(self, *specs, **kwargs):
|
||||||
|
assert all((s.concrete for s in specs))
|
||||||
|
with_dependents = kwargs.get("with_dependents", True)
|
||||||
|
with_dependencies = kwargs.get("with_dependencies", False)
|
||||||
|
|
||||||
|
specs = set(specs)
|
||||||
|
|
||||||
|
if with_dependencies:
|
||||||
|
specs = get_dependencies(specs)
|
||||||
|
|
||||||
|
if kwargs.get("exclude", None):
|
||||||
|
specs = set(filter_exclude(specs, kwargs["exclude"]))
|
||||||
|
|
||||||
|
all_specs = set(self.get_all_specs())
|
||||||
|
|
||||||
|
to_deactivate = specs
|
||||||
|
to_keep = all_specs - to_deactivate
|
||||||
|
|
||||||
|
dependents = find_dependents(to_keep, to_deactivate)
|
||||||
|
|
||||||
|
if with_dependents:
|
||||||
|
# remove all packages depending on the ones to remove
|
||||||
|
if len(dependents) > 0:
|
||||||
|
tty.warn(self._croot +
|
||||||
|
"The following dependents will be removed: %s"
|
||||||
|
% ", ".join((s.name for s in dependents)))
|
||||||
|
to_deactivate.update(dependents)
|
||||||
|
elif len(dependents) > 0:
|
||||||
|
tty.warn(self._croot +
|
||||||
|
"The following packages will be unusable: %s"
|
||||||
|
% ", ".join((s.name for s in dependents)))
|
||||||
|
|
||||||
|
extensions = set(filter(lambda s: s.package.is_extension,
|
||||||
|
to_deactivate))
|
||||||
|
standalones = to_deactivate - extensions
|
||||||
|
|
||||||
|
# Please note that a traversal of the DAG in post-order and then
|
||||||
|
# forcibly removing each package should remove the need to specify
|
||||||
|
# with_dependents for deactivating extensions/allow removal without
|
||||||
|
# additional checks (force=True). If removal performance becomes
|
||||||
|
# unbearable for whatever reason, this should be the first point of
|
||||||
|
# attack.
|
||||||
|
#
|
||||||
|
# see: https://github.com/LLNL/spack/pull/3227#discussion_r117147475
|
||||||
|
remove_extension = ft.partial(self.remove_extension,
|
||||||
|
with_dependents=with_dependents)
|
||||||
|
|
||||||
|
set(map(remove_extension, extensions))
|
||||||
|
set(map(self.remove_standalone, standalones))
|
||||||
|
|
||||||
|
self.purge_empty_directories()
|
||||||
|
|
||||||
|
def remove_extension(self, spec, with_dependents=True):
|
||||||
|
"""
|
||||||
|
Remove (unlink) an extension from this view.
|
||||||
|
"""
|
||||||
|
if not self.check_added(spec):
|
||||||
|
tty.warn(self._croot +
|
||||||
|
'Skipping package not linked in view: %s' % spec.name)
|
||||||
|
return
|
||||||
|
|
||||||
|
# The spec might have been deactivated as depdency of another package
|
||||||
|
# already
|
||||||
|
if spec.package.is_activated(self.extensions_layout):
|
||||||
|
spec.package.do_deactivate(
|
||||||
|
verbose=self.verbose,
|
||||||
|
remove_dependents=with_dependents,
|
||||||
|
extensions_layout=self.extensions_layout)
|
||||||
|
self.unlink_meta_folder(spec)
|
||||||
|
|
||||||
|
def remove_standalone(self, spec):
|
||||||
|
"""
|
||||||
|
Remove (unlink) a standalone package from this view.
|
||||||
|
"""
|
||||||
|
if not self.check_added(spec):
|
||||||
|
tty.warn(self._croot +
|
||||||
|
'Skipping package not linked in view: %s' % spec.name)
|
||||||
|
return
|
||||||
|
|
||||||
|
tree = LinkTree(spec.prefix)
|
||||||
|
tree.unmerge(self.root, ignore=ignore_metadata_dir)
|
||||||
|
self.unlink_meta_folder(spec)
|
||||||
|
|
||||||
|
if self.verbose:
|
||||||
|
tty.info(self._croot + 'Removed package: %s' % colorize_spec(spec))
|
||||||
|
|
||||||
|
def get_all_specs(self):
|
||||||
|
dotspack = join_path(self.root, spack.store.layout.metadata_dir)
|
||||||
|
if os.path.exists(dotspack):
|
||||||
|
return list(filter(None, map(self.get_spec, os.listdir(dotspack))))
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
|
||||||
|
def get_conflicts(self, *specs):
|
||||||
|
"""
|
||||||
|
Return list of tuples (<spec>, <spec in view>) where the spec
|
||||||
|
active in the view differs from the one to be activated.
|
||||||
|
"""
|
||||||
|
in_view = map(self.get_spec, specs)
|
||||||
|
return [(s, v) for s, v in zip(specs, in_view)
|
||||||
|
if v is not None and s != v]
|
||||||
|
|
||||||
|
def get_path_meta_folder(self, spec):
|
||||||
|
"Get path to meta folder for either spec or spec name."
|
||||||
|
return join_path(self.root, spack.store.layout.metadata_dir,
|
||||||
|
getattr(spec, "name", spec))
|
||||||
|
|
||||||
|
def get_spec(self, spec):
|
||||||
|
dotspack = self.get_path_meta_folder(spec)
|
||||||
|
filename = join_path(dotspack, spack.store.layout.spec_file_name)
|
||||||
|
|
||||||
|
try:
|
||||||
|
with open(filename, "r") as f:
|
||||||
|
return spack.spec.Spec.from_yaml(f)
|
||||||
|
except IOError:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def link_meta_folder(self, spec):
|
||||||
|
src = spack.store.layout.metadata_path(spec)
|
||||||
|
tgt = self.get_path_meta_folder(spec)
|
||||||
|
|
||||||
|
tree = LinkTree(src)
|
||||||
|
# there should be no conflicts when linking the meta folder
|
||||||
|
tree.merge(tgt, link=self.link)
|
||||||
|
|
||||||
|
def print_conflict(self, spec_active, spec_specified, level="error"):
|
||||||
|
"Singular print function for spec conflicts."
|
||||||
|
cprint = getattr(tty, level)
|
||||||
|
color = sys.stdout.isatty()
|
||||||
|
linked = tty.color.colorize(" (@gLinked@.)", color=color)
|
||||||
|
specified = tty.color.colorize("(@rSpecified@.)", color=color)
|
||||||
|
cprint(self._croot + "Package conflict detected:\n"
|
||||||
|
"%s %s\n" % (linked, colorize_spec(spec_active)) +
|
||||||
|
"%s %s" % (specified, colorize_spec(spec_specified)))
|
||||||
|
|
||||||
|
def print_status(self, *specs, **kwargs):
|
||||||
|
if kwargs.get("with_dependencies", False):
|
||||||
|
specs = set(get_dependencies(specs))
|
||||||
|
|
||||||
|
specs = sorted(specs, key=lambda s: s.name)
|
||||||
|
in_view = list(map(self.get_spec, specs))
|
||||||
|
|
||||||
|
for s, v in zip(specs, in_view):
|
||||||
|
if not v:
|
||||||
|
tty.error(self._croot +
|
||||||
|
'Package not linked: %s' % s.name)
|
||||||
|
elif s != v:
|
||||||
|
self.print_conflict(v, s, level="warn")
|
||||||
|
|
||||||
|
in_view = list(filter(None, in_view))
|
||||||
|
|
||||||
|
if len(specs) > 0:
|
||||||
|
tty.msg("Packages linked in %s:" % self._croot[:-1])
|
||||||
|
|
||||||
|
# avoid circular dependency
|
||||||
|
import spack.cmd
|
||||||
|
spack.cmd.display_specs(in_view, flags=True, variants=True,
|
||||||
|
long=self.verbose)
|
||||||
|
else:
|
||||||
|
tty.warn(self._croot + "No packages found.")
|
||||||
|
|
||||||
|
def purge_empty_directories(self):
|
||||||
|
"""
|
||||||
|
Ascend up from the leaves accessible from `path`
|
||||||
|
and remove empty directories.
|
||||||
|
"""
|
||||||
|
for dirpath, subdirs, files in os.walk(self.root, topdown=False):
|
||||||
|
for sd in subdirs:
|
||||||
|
sdp = os.path.join(dirpath, sd)
|
||||||
|
try:
|
||||||
|
os.rmdir(sdp)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
|
|
||||||
|
def unlink_meta_folder(self, spec):
|
||||||
|
path = self.get_path_meta_folder(spec)
|
||||||
|
assert os.path.exists(path)
|
||||||
|
shutil.rmtree(path)
|
||||||
|
|
||||||
|
def _check_no_ext_conflicts(self, spec):
|
||||||
|
"""
|
||||||
|
Check that there is no extension conflict for specs.
|
||||||
|
"""
|
||||||
|
extendee = spec.package.extendee_spec
|
||||||
|
try:
|
||||||
|
self.extensions_layout.check_extension_conflict(extendee, spec)
|
||||||
|
except ExtensionAlreadyInstalledError:
|
||||||
|
# we print the warning here because later on the order in which
|
||||||
|
# packages get activated is not clear (set-sorting)
|
||||||
|
tty.warn(self._croot +
|
||||||
|
'Skipping already activated package: %s' % spec.name)
|
||||||
|
|
||||||
|
|
||||||
|
#####################
|
||||||
|
# utility functions #
|
||||||
|
#####################
|
||||||
|
|
||||||
|
def colorize_root(root):
|
||||||
|
colorize = ft.partial(tty.color.colorize, color=sys.stdout.isatty())
|
||||||
|
pre, post = map(colorize, "@M[@. @M]@.".split())
|
||||||
|
return "".join([pre, root, post])
|
||||||
|
|
||||||
|
|
||||||
|
def colorize_spec(spec):
|
||||||
|
"Colorize spec output if in TTY."
|
||||||
|
if sys.stdout.isatty():
|
||||||
|
return spec.cshort_spec
|
||||||
|
else:
|
||||||
|
return spec.short_spec
|
||||||
|
|
||||||
|
|
||||||
|
def find_dependents(all_specs, providers, deptype='run'):
|
||||||
|
"""
|
||||||
|
Return a set containing all those specs from all_specs that depend on
|
||||||
|
providers at the given dependency type.
|
||||||
|
"""
|
||||||
|
dependents = set()
|
||||||
|
for s in all_specs:
|
||||||
|
for dep in s.traverse(deptype=deptype):
|
||||||
|
if dep in providers:
|
||||||
|
dependents.add(s)
|
||||||
|
return dependents
|
||||||
|
|
||||||
|
|
||||||
|
def filter_exclude(specs, exclude):
|
||||||
|
"Filter specs given sequence of exclude regex"
|
||||||
|
to_exclude = [re.compile(e) for e in exclude]
|
||||||
|
|
||||||
|
def keep(spec):
|
||||||
|
for e in to_exclude:
|
||||||
|
if e.match(spec.name):
|
||||||
|
return False
|
||||||
|
return True
|
||||||
|
return filter(keep, specs)
|
||||||
|
|
||||||
|
|
||||||
|
def get_dependencies(specs):
|
||||||
|
"Get set of dependencies (includes specs)"
|
||||||
|
retval = set()
|
||||||
|
set(map(retval.update, (set(s.traverse()) for s in specs)))
|
||||||
|
return retval
|
||||||
|
|
||||||
|
|
||||||
|
def ignore_metadata_dir(f):
|
||||||
|
return f in spack.store.layout.hidden_file_paths
|
Loading…
Reference in New Issue
Block a user