directory_layout: factor out an ExtensionsLayout class
This commit is contained in:
parent
650ca7db9e
commit
b9e8402104
@ -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):
|
||||||
|
Loading…
Reference in New Issue
Block a user