directory_layout: factor out an ExtensionsLayout class

This commit is contained in:
Oliver Breitwieser 2017-09-14 14:37:36 -04:00 committed by scheibelp
parent 650ca7db9e
commit b9e8402104

View File

@ -88,37 +88,6 @@ def check_installed(self, spec):
""" """
raise NotImplementedError() 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): def path_for_spec(self, spec):
"""Return absolute path from the root to a directory for the spec.""" """Return absolute path from the root to a directory for the spec."""
_check_concrete(spec) _check_concrete(spec)
@ -149,6 +118,50 @@ def remove_install_directory(self, spec):
path = os.path.dirname(path) 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): class YamlDirectoryLayout(DirectoryLayout):
"""By default lays out installation directories like this:: """By default lays out installation directories like this::
<install root>/ <install root>/
@ -184,9 +197,6 @@ def __init__(self, root, **kwargs):
self.build_env_name = 'build.env' # build environment self.build_env_name = 'build.env' # build environment
self.packages_dir = 'repos' # archive of package.py files self.packages_dir = 'repos' # archive of package.py files
# Cache of already written/read extension maps.
self._extension_maps = {}
@property @property
def hidden_file_paths(self): def hidden_file_paths(self):
return (self.metadata_dir,) return (self.metadata_dir,)
@ -297,31 +307,69 @@ def specs_by_hash(self):
by_hash[spec.dag_hash()] = spec by_hash[spec.dag_hash()] = spec
return by_hash 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): def extension_file_path(self, spec):
"""Gets full path to an installed package's extension file""" """Gets full path to an installed package's extension file"""
_check_concrete(spec) _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): def extension_map(self, spec):
path = self.extension_file_path(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. def remove_extension(self, spec, ext_spec):
dirname, basename = os.path.split(path) _check_concrete(spec)
tmp = tempfile.NamedTemporaryFile( _check_concrete(ext_spec)
prefix=basename, dir=dirname, delete=False)
# write tmp file # Make sure it's installed before removing.
with tmp: exts = self._extension_map(spec)
yaml.dump({ self.check_activated(spec, ext_spec)
'extensions': [
{ext.name: {
'hash': ext.dag_hash(),
'path': str(ext.prefix)
}} for ext in sorted(extensions.values())]
}, tmp, default_flow_style=False)
# Atomic update by moving tmpfile on top of old one. # do the actual removing.
os.rename(tmp.name, path) del exts[ext_spec.name]
self._write_extensions(spec, exts)
def _extension_map(self, spec): def _extension_map(self, spec):
"""Get a dict<name -> spec> for all extensions currently """Get a dict<name -> spec> for all extensions currently
@ -334,7 +382,7 @@ def _extension_map(self, spec):
self._extension_maps[spec] = {} self._extension_maps[spec] = {}
else: else:
by_hash = self.specs_by_hash() by_hash = self.layout.specs_by_hash()
exts = {} exts = {}
with open(path) as ext_file: with open(path) as ext_file:
yaml_file = yaml.load(ext_file) yaml_file = yaml.load(ext_file)
@ -358,48 +406,26 @@ def _extension_map(self, spec):
return self._extension_maps[spec] return self._extension_maps[spec]
def extension_map(self, spec): def _write_extensions(self, spec, extensions):
"""Defensive copying version of _extension_map() for external API.""" path = self.extension_file_path(spec)
_check_concrete(spec)
return self._extension_map(spec).copy()
def check_extension_conflict(self, spec, ext_spec): # Create a temp file in the same directory as the actual file.
exts = self._extension_map(spec) dirname, basename = os.path.split(path)
if ext_spec.name in exts: tmp = tempfile.NamedTemporaryFile(
installed_spec = exts[ext_spec.name] prefix=basename, dir=dirname, delete=False)
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): # write tmp file
exts = self._extension_map(spec) with tmp:
if (ext_spec.name not in exts) or (ext_spec != exts[ext_spec.name]): yaml.dump({
raise NoSuchExtensionError(spec, ext_spec) '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')
def add_extension(self, spec, ext_spec): # Atomic update by moving tmpfile on top of old one.
_check_concrete(spec) os.rename(tmp.name, path)
_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 remove_extension(self, spec, ext_spec):
_check_concrete(spec)
_check_concrete(ext_spec)
# Make sure it's installed before removing.
exts = self._extension_map(spec)
self.check_activated(spec, ext_spec)
# do the actual removing.
del exts[ext_spec.name]
self._write_extensions(spec, exts)
class DirectoryLayoutError(SpackError): class DirectoryLayoutError(SpackError):