From f616422fd7a7201144934ffaa64483115f60bdd0 Mon Sep 17 00:00:00 2001 From: Greg Becker Date: Fri, 25 Sep 2020 11:15:49 -0500 Subject: refactor install_tree to use projections format (#18341) * refactor install_tree to use projections format * Add update method for config.yaml * add test for config update config --- etc/spack/defaults/config.yaml | 9 ++-- lib/spack/spack/directory_layout.py | 79 +++++++++++++++++----------- lib/spack/spack/schema/config.py | 61 +++++++++++++++++++-- lib/spack/spack/schema/projections.py | 1 - lib/spack/spack/store.py | 38 ++++++++++--- lib/spack/spack/test/cmd/config.py | 67 ++++++++++++++++------- lib/spack/spack/test/config.py | 33 +++++++----- lib/spack/spack/test/config_values.py | 12 +++-- lib/spack/spack/test/data/config/config.yaml | 3 +- lib/spack/spack/test/directory_layout.py | 38 ++++++------- lib/spack/spack/test/util/spack_yaml.py | 4 +- 11 files changed, 243 insertions(+), 102 deletions(-) diff --git a/etc/spack/defaults/config.yaml b/etc/spack/defaults/config.yaml index a5727874ea..aa4ee2bbc6 100644 --- a/etc/spack/defaults/config.yaml +++ b/etc/spack/defaults/config.yaml @@ -16,7 +16,10 @@ config: # This is the path to the root of the Spack install tree. # You can use $spack here to refer to the root of the spack instance. - install_tree: $spack/opt/spack + install_tree: + root: $spack/opt/spack + projections: + all: "${ARCHITECTURE}/${COMPILERNAME}-${COMPILERVER}/${PACKAGE}-${VERSION}-${HASH}" # Locations where templates should be found @@ -24,10 +27,6 @@ config: - $spack/share/spack/templates - # Default directory layout - install_path_scheme: "${ARCHITECTURE}/${COMPILERNAME}-${COMPILERVER}/${PACKAGE}-${VERSION}-${HASH}" - - # Locations where different types of modules should be installed. module_roots: tcl: $spack/share/spack/modules diff --git a/lib/spack/spack/directory_layout.py b/lib/spack/spack/directory_layout.py index 0f4c9663ba..06a652f450 100644 --- a/lib/spack/spack/directory_layout.py +++ b/lib/spack/spack/directory_layout.py @@ -7,7 +7,6 @@ import os import shutil import glob import tempfile -import re from contextlib import contextmanager import ruamel.yaml as yaml @@ -19,6 +18,11 @@ import spack.spec from spack.error import SpackError +default_projections = {'all': ('{architecture}/' + '{compiler.name}-{compiler.version}/' + '{name}-{version}-{hash}')} + + def _check_concrete(spec): """If the spec is not concrete, raise a ValueError""" if not spec.concrete: @@ -179,24 +183,31 @@ class YamlDirectoryLayout(DirectoryLayout): The hash here is a SHA-1 hash for the full DAG plus the build spec. TODO: implement the build spec. - The installation directory scheme can be modified with the - arguments hash_len and path_scheme. + The installation directory projections can be modified with the + projections argument. """ def __init__(self, root, **kwargs): super(YamlDirectoryLayout, self).__init__(root) - self.hash_len = kwargs.get('hash_len') - self.path_scheme = kwargs.get('path_scheme') or ( - "{architecture}/" - "{compiler.name}-{compiler.version}/" - "{name}-{version}-{hash}") - self.path_scheme = self.path_scheme.lower() - if self.hash_len is not None: - if re.search(r'{hash:\d+}', self.path_scheme): - raise InvalidDirectoryLayoutParametersError( - "Conflicting options for installation layout hash length") - self.path_scheme = self.path_scheme.replace( - "{hash}", "{hash:%d}" % self.hash_len) + projections = kwargs.get('projections') or default_projections + self.projections = dict((key, projection.lower()) + for key, projection in projections.items()) + + # apply hash length as appropriate + self.hash_length = kwargs.get('hash_length', None) + if self.hash_length is not None: + for when_spec, projection in self.projections.items(): + if '{hash}' not in projection: + if '{hash' in projection: + raise InvalidDirectoryLayoutParametersError( + "Conflicting options for installation layout hash" + " length") + else: + raise InvalidDirectoryLayoutParametersError( + "Cannot specify hash length when the hash is not" + " part of all install_tree projections") + self.projections[when_spec] = projection.replace( + "{hash}", "{hash:%d}" % self.hash_length) # If any of these paths change, downstream databases may not be able to # locate files in older upstream databases @@ -214,7 +225,8 @@ class YamlDirectoryLayout(DirectoryLayout): def relative_path_for_spec(self, spec): _check_concrete(spec) - path = spec.format(self.path_scheme) + projection = spack.projections.get_projection(self.projections, spec) + path = spec.format(projection) return path def write_spec(self, spec, path): @@ -336,25 +348,32 @@ class YamlDirectoryLayout(DirectoryLayout): if not os.path.isdir(self.root): return [] - path_elems = ["*"] * len(self.path_scheme.split(os.sep)) - path_elems += [self.metadata_dir, self.spec_file_name] - pattern = os.path.join(self.root, *path_elems) - spec_files = glob.glob(pattern) - return [self.read_spec(s) for s in spec_files] + specs = [] + for _, path_scheme in self.projections.items(): + path_elems = ["*"] * len(path_scheme.split(os.sep)) + path_elems += [self.metadata_dir, self.spec_file_name] + pattern = os.path.join(self.root, *path_elems) + spec_files = glob.glob(pattern) + specs.extend([self.read_spec(s) for s in spec_files]) + return specs def all_deprecated_specs(self): if not os.path.isdir(self.root): return [] - path_elems = ["*"] * len(self.path_scheme.split(os.sep)) - path_elems += [self.metadata_dir, self.deprecated_dir, - '*_' + self.spec_file_name] - pattern = os.path.join(self.root, *path_elems) - spec_files = glob.glob(pattern) - get_depr_spec_file = lambda x: os.path.join( - os.path.dirname(os.path.dirname(x)), self.spec_file_name) - return set((self.read_spec(s), self.read_spec(get_depr_spec_file(s))) - for s in spec_files) + deprecated_specs = set() + for _, path_scheme in self.projections.items(): + path_elems = ["*"] * len(path_scheme.split(os.sep)) + path_elems += [self.metadata_dir, self.deprecated_dir, + '*_' + self.spec_file_name] + pattern = os.path.join(self.root, *path_elems) + spec_files = glob.glob(pattern) + get_depr_spec_file = lambda x: os.path.join( + os.path.dirname(os.path.dirname(x)), self.spec_file_name) + deprecated_specs |= set((self.read_spec(s), + self.read_spec(get_depr_spec_file(s))) + for s in spec_files) + return deprecated_specs def specs_by_hash(self): by_hash = {} diff --git a/lib/spack/spack/schema/config.py b/lib/spack/spack/schema/config.py index d56321d09c..7315300aa6 100644 --- a/lib/spack/spack/schema/config.py +++ b/lib/spack/spack/schema/config.py @@ -8,7 +8,9 @@ .. literalinclude:: _spack_root/lib/spack/spack/schema/config.py :lines: 13- """ - +import six +from llnl.util.lang import union_dicts +import spack.schema.projections #: Properties for inclusion in other schemas properties = { @@ -20,9 +22,20 @@ properties = { 'type': 'string', 'enum': ['rpath', 'runpath'] }, - 'install_tree': {'type': 'string'}, + 'install_tree': { + 'anyOf': [ + { + 'type': 'object', + 'properties': union_dicts( + {'root': {'type': 'string'}}, + spack.schema.projections.properties, + ), + }, + {'type': 'string'} # deprecated + ], + }, 'install_hash_length': {'type': 'integer', 'minimum': 1}, - 'install_path_scheme': {'type': 'string'}, + 'install_path_scheme': {'type': 'string'}, # deprecated 'build_stage': { 'oneOf': [ {'type': 'string'}, @@ -87,3 +100,45 @@ schema = { 'additionalProperties': False, 'properties': properties, } + + +def update(data): + """Update the data in place to remove deprecated properties. + + Args: + data (dict): dictionary to be updated + + Returns: + True if data was changed, False otherwise + """ + # currently deprecated properties are + # install_tree: + # install_path_scheme: + # updated: install_tree: {root: , + # projections: