summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
authorOliver Breitwieser <oliver.breitwieser@kip.uni-heidelberg.de>2017-09-18 09:09:13 -0400
committerscheibelp <scheibel1@llnl.gov>2017-11-02 18:45:40 -0700
commit538d855e1b8a3e4e892d40211002dc8df6f6a7fb (patch)
tree2ecdd28284d00e4797e01b04c8a145213c2eac04 /lib
parent21dc31a4a19670d04cdd8f6aa952b641651015ea (diff)
downloadspack-538d855e1b8a3e4e892d40211002dc8df6f6a7fb.tar.gz
spack-538d855e1b8a3e4e892d40211002dc8df6f6a7fb.tar.bz2
spack-538d855e1b8a3e4e892d40211002dc8df6f6a7fb.tar.xz
spack-538d855e1b8a3e4e892d40211002dc8df6f6a7fb.zip
filesystem_view: add a class to handle a view via a Yaml description
Diffstat (limited to 'lib')
-rw-r--r--lib/spack/spack/filesystem_view.py524
1 files changed, 524 insertions, 0 deletions
diff --git a/lib/spack/spack/filesystem_view.py b/lib/spack/spack/filesystem_view.py
new file mode 100644
index 0000000000..7eda38b627
--- /dev/null
+++ b/lib/spack/spack/filesystem_view.py
@@ -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