From b9e840210402c07bb9a803586985cc1a6818f3fc Mon Sep 17 00:00:00 2001 From: Oliver Breitwieser Date: Thu, 14 Sep 2017 14:37:36 -0400 Subject: directory_layout: factor out an ExtensionsLayout class --- lib/spack/spack/directory_layout.py | 210 ++++++++++++++++++++---------------- 1 file changed, 118 insertions(+), 92 deletions(-) diff --git a/lib/spack/spack/directory_layout.py b/lib/spack/spack/directory_layout.py index 54fffeabfb..4bc7a26926 100644 --- a/lib/spack/spack/directory_layout.py +++ b/lib/spack/spack/directory_layout.py @@ -88,37 +88,6 @@ class DirectoryLayout(object): """ raise NotImplementedError() - def extension_map(self, spec): - """Get a dict of currently installed extension packages for a spec. - - Dict maps { name : extension_spec } - Modifying dict does not affect internals of this layout. - """ - raise NotImplementedError() - - def check_extension_conflict(self, spec, ext_spec): - """Ensure that ext_spec can be activated in spec. - - If not, raise ExtensionAlreadyInstalledError or - ExtensionConflictError. - """ - raise NotImplementedError() - - def check_activated(self, spec, ext_spec): - """Ensure that ext_spec can be removed from spec. - - If not, raise NoSuchExtensionError. - """ - raise NotImplementedError() - - def add_extension(self, spec, ext_spec): - """Add to the list of currently installed extensions.""" - raise NotImplementedError() - - def remove_extension(self, spec, ext_spec): - """Remove from the list of currently installed extensions.""" - raise NotImplementedError() - def path_for_spec(self, spec): """Return absolute path from the root to a directory for the spec.""" _check_concrete(spec) @@ -149,6 +118,50 @@ class DirectoryLayout(object): path = os.path.dirname(path) +class ExtensionsLayout(object): + """A directory layout is used to associate unique paths with specs for + package extensions. + Keeps track of which extensions are activated for what package. + Depending on the use case, this can mean globally activated extensions + directly in the installation folder - or extensions activated in + filesystem views. + """ + def __init__(self, root, **kwargs): + self.root = root + self.link = kwargs.get("link", os.symlink) + + def add_extension(self, spec, ext_spec): + """Add to the list of currently installed extensions.""" + raise NotImplementedError() + + def check_activated(self, spec, ext_spec): + """Ensure that ext_spec can be removed from spec. + + If not, raise NoSuchExtensionError. + """ + raise NotImplementedError() + + def check_extension_conflict(self, spec, ext_spec): + """Ensure that ext_spec can be activated in spec. + + If not, raise ExtensionAlreadyInstalledError or + ExtensionConflictError. + """ + raise NotImplementedError() + + def extension_map(self, spec): + """Get a dict of currently installed extension packages for a spec. + + Dict maps { name : extension_spec } + Modifying dict does not affect internals of this layout. + """ + raise NotImplementedError() + + def remove_extension(self, spec, ext_spec): + """Remove from the list of currently installed extensions.""" + raise NotImplementedError() + + class YamlDirectoryLayout(DirectoryLayout): """By default lays out installation directories like this:: / @@ -184,9 +197,6 @@ class YamlDirectoryLayout(DirectoryLayout): self.build_env_name = 'build.env' # build environment self.packages_dir = 'repos' # archive of package.py files - # Cache of already written/read extension maps. - self._extension_maps = {} - @property def hidden_file_paths(self): return (self.metadata_dir,) @@ -297,31 +307,69 @@ class YamlDirectoryLayout(DirectoryLayout): by_hash[spec.dag_hash()] = spec return by_hash + +class YamlExtensionsLayout(ExtensionsLayout): + """Implements globally activated extensions within a YamlDirectoryLayout. + """ + def __init__(self, root, layout): + """layout is the corresponding YamlDirectoryLayout object for which + we implement extensions. + """ + super(YamlExtensionsLayout, self).__init__(root) + self.layout = layout + self.extension_file_name = 'extensions.yaml' + + # Cache of already written/read extension maps. + self._extension_maps = {} + + def add_extension(self, spec, ext_spec): + _check_concrete(spec) + _check_concrete(ext_spec) + + # Check whether it's already installed or if it's a conflict. + exts = self._extension_map(spec) + self.check_extension_conflict(spec, ext_spec) + + # do the actual adding. + exts[ext_spec.name] = ext_spec + self._write_extensions(spec, exts) + + def check_extension_conflict(self, spec, ext_spec): + exts = self._extension_map(spec) + if ext_spec.name in exts: + installed_spec = exts[ext_spec.name] + if ext_spec == installed_spec: + raise ExtensionAlreadyInstalledError(spec, ext_spec) + else: + raise ExtensionConflictError(spec, ext_spec, installed_spec) + + def check_activated(self, spec, ext_spec): + exts = self._extension_map(spec) + if (ext_spec.name not in exts) or (ext_spec != exts[ext_spec.name]): + raise NoSuchExtensionError(spec, ext_spec) + def extension_file_path(self, spec): """Gets full path to an installed package's extension file""" _check_concrete(spec) - return join_path(self.metadata_path(spec), self.extension_file_name) + return join_path(self.layout.metadata_path(spec), + self.extension_file_name) - def _write_extensions(self, spec, extensions): - path = self.extension_file_path(spec) + def extension_map(self, spec): + """Defensive copying version of _extension_map() for external API.""" + _check_concrete(spec) + return self._extension_map(spec).copy() - # Create a temp file in the same directory as the actual file. - dirname, basename = os.path.split(path) - tmp = tempfile.NamedTemporaryFile( - prefix=basename, dir=dirname, delete=False) + def remove_extension(self, spec, ext_spec): + _check_concrete(spec) + _check_concrete(ext_spec) - # write tmp file - with tmp: - yaml.dump({ - 'extensions': [ - {ext.name: { - 'hash': ext.dag_hash(), - 'path': str(ext.prefix) - }} for ext in sorted(extensions.values())] - }, tmp, default_flow_style=False) + # Make sure it's installed before removing. + exts = self._extension_map(spec) + self.check_activated(spec, ext_spec) - # Atomic update by moving tmpfile on top of old one. - os.rename(tmp.name, path) + # do the actual removing. + del exts[ext_spec.name] + self._write_extensions(spec, exts) def _extension_map(self, spec): """Get a dict spec> for all extensions currently @@ -334,7 +382,7 @@ class YamlDirectoryLayout(DirectoryLayout): self._extension_maps[spec] = {} else: - by_hash = self.specs_by_hash() + by_hash = self.layout.specs_by_hash() exts = {} with open(path) as ext_file: yaml_file = yaml.load(ext_file) @@ -358,48 +406,26 @@ class YamlDirectoryLayout(DirectoryLayout): return self._extension_maps[spec] - def extension_map(self, spec): - """Defensive copying version of _extension_map() for external API.""" - _check_concrete(spec) - return self._extension_map(spec).copy() - - def check_extension_conflict(self, spec, ext_spec): - exts = self._extension_map(spec) - if ext_spec.name in exts: - installed_spec = exts[ext_spec.name] - if ext_spec == installed_spec: - raise ExtensionAlreadyInstalledError(spec, ext_spec) - else: - raise ExtensionConflictError(spec, ext_spec, installed_spec) - - def check_activated(self, spec, ext_spec): - exts = self._extension_map(spec) - if (ext_spec.name not in exts) or (ext_spec != exts[ext_spec.name]): - raise NoSuchExtensionError(spec, ext_spec) - - def add_extension(self, spec, ext_spec): - _check_concrete(spec) - _check_concrete(ext_spec) - - # Check whether it's already installed or if it's a conflict. - exts = self._extension_map(spec) - self.check_extension_conflict(spec, ext_spec) - - # do the actual adding. - exts[ext_spec.name] = ext_spec - self._write_extensions(spec, exts) + def _write_extensions(self, spec, extensions): + path = self.extension_file_path(spec) - def remove_extension(self, spec, ext_spec): - _check_concrete(spec) - _check_concrete(ext_spec) + # Create a temp file in the same directory as the actual file. + dirname, basename = os.path.split(path) + tmp = tempfile.NamedTemporaryFile( + prefix=basename, dir=dirname, delete=False) - # Make sure it's installed before removing. - exts = self._extension_map(spec) - self.check_activated(spec, ext_spec) + # write tmp file + with tmp: + yaml.dump({ + 'extensions': [ + {ext.name: { + 'hash': ext.dag_hash(), + 'path': str(ext.prefix) + }} for ext in sorted(extensions.values())] + }, tmp, default_flow_style=False, encoding='utf-8') - # do the actual removing. - del exts[ext_spec.name] - self._write_extensions(spec, exts) + # Atomic update by moving tmpfile on top of old one. + os.rename(tmp.name, path) class DirectoryLayoutError(SpackError): -- cgit v1.2.3-60-g2f50