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
This commit is contained in:
Greg Becker 2020-09-25 11:15:49 -05:00 committed by GitHub
parent 51b90edd78
commit f616422fd7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 243 additions and 102 deletions

View file

@ -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

View file

@ -7,7 +7,6 @@
import shutil
import glob
import tempfile
import re
from contextlib import contextmanager
import ruamel.yaml as yaml
@ -19,6 +18,11 @@
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 @@ def hidden_file_paths(self):
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 @@ def all_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.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 = {}

View file

@ -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 @@
'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 @@
'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: <string>
# install_path_scheme: <string>
# updated: install_tree: {root: <string>,
# projections: <projections_dict}
# root replaces install_tree, projections replace install_path_scheme
changed = False
install_tree = data.get('install_tree', None)
if isinstance(install_tree, six.string_types):
# deprecated short-form install tree
# add value as `root` in updated install_tree
data['install_tree'] = {'root': install_tree}
changed = True
install_path_scheme = data.pop('install_path_scheme', None)
if install_path_scheme:
projections_data = {
'projections': {
'all': install_path_scheme
}
}
# update projections with install_scheme
# whether install_tree was updated or not
# we merge the yaml to ensure we don't invalidate other projections
update_data = data.get('install_tree', {})
update_data = spack.config.merge_yaml(update_data, projections_data)
data['install_tree'] = update_data
changed = True
return changed

View file

@ -14,7 +14,6 @@
properties = {
'projections': {
'type': 'object',
'default': {},
'patternProperties': {
r'all|\w[\w-]*': {
'type': 'string'

View file

@ -24,14 +24,16 @@
"""
import os
import six
import llnl.util.lang
import llnl.util.tty as tty
import spack.paths
import spack.config
import spack.util.path
import spack.database
import spack.directory_layout
import spack.directory_layout as dir_layout
#: default installation root, relative to the Spack install path
default_root = os.path.join(spack.paths.opt_path, 'spack')
@ -56,12 +58,12 @@ class Store(object):
hash_length (int): length of the hashes used in the directory
layout; spec hash suffixes will be truncated to this length
"""
def __init__(self, root, path_scheme=None, hash_length=None):
def __init__(self, root, projections=None, hash_length=None):
self.root = root
self.db = spack.database.Database(
root, upstream_dbs=retrieve_upstream_dbs())
self.layout = spack.directory_layout.YamlDirectoryLayout(
root, hash_len=hash_length, path_scheme=path_scheme)
self.layout = dir_layout.YamlDirectoryLayout(
root, projections=projections, hash_length=hash_length)
def reindex(self):
"""Convenience function to reindex the store DB with its own layout."""
@ -70,11 +72,31 @@ def reindex(self):
def _store():
"""Get the singleton store instance."""
root = spack.config.get('config:install_tree', default_root)
root = spack.util.path.canonicalize_path(root)
install_tree = spack.config.get('config:install_tree', {})
return Store(root,
spack.config.get('config:install_path_scheme'),
if isinstance(install_tree, six.string_types):
tty.warn("Using deprecated format for configuring install_tree")
root = install_tree
# construct projection from previous values for backwards compatibility
all_projection = spack.config.get(
'config:install_path_scheme',
dir_layout.default_projections['all'])
projections = {'all': all_projection}
else:
root = install_tree.get('root', default_root)
root = spack.util.path.canonicalize_path(root)
projections = install_tree.get(
'projections', dir_layout.default_projections)
path_scheme = spack.config.get('config:install_path_scheme', None)
if path_scheme:
tty.warn("Deprecated config value 'install_path_scheme' ignored"
" when using new install_tree syntax")
return Store(root, projections,
spack.config.get('config:install_hash_length'))

View file

@ -2,8 +2,8 @@
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
import functools
import os
import pytest
import llnl.util.filesystem as fs
@ -16,26 +16,40 @@
env = spack.main.SpackCommand('env')
def _create_config(scope=None, data={}, section='packages'):
scope = scope or spack.config.default_modify_scope()
cfg_file = spack.config.config.get_config_filename(scope, section)
with open(cfg_file, 'w') as f:
syaml.dump(data, stream=f)
return cfg_file
@pytest.fixture()
def packages_yaml_v015(mutable_config):
"""Create a packages.yaml in the old format"""
def _create(scope=None):
old_data = {
'packages': {
'cmake': {
'paths': {'cmake@3.14.0': '/usr'}
},
'gcc': {
'modules': {'gcc@8.3.0': 'gcc-8'}
}
old_data = {
'packages': {
'cmake': {
'paths': {'cmake@3.14.0': '/usr'}
},
'gcc': {
'modules': {'gcc@8.3.0': 'gcc-8'}
}
}
scope = scope or spack.config.default_modify_scope()
cfg_file = spack.config.config.get_config_filename(scope, 'packages')
with open(cfg_file, 'w') as f:
syaml.dump(old_data, stream=f)
return cfg_file
return _create
}
return functools.partial(_create_config, data=old_data, section='packages')
@pytest.fixture()
def config_yaml_v015(mutable_config):
"""Create a packages.yaml in the old format"""
old_data = {
'config': {
'install_tree': '/fake/path',
'install_path_scheme': '{name}-{version}',
}
}
return functools.partial(_create_config, data=old_data, section='config')
def test_get_config_scope(mock_low_high_config):
@ -469,7 +483,16 @@ def test_config_update_packages(packages_yaml_v015):
# Check the entries have been transformed
data = spack.config.get('packages')
check_update(data)
check_packages_updated(data)
def test_config_update_config(config_yaml_v015):
config_yaml_v015()
config('update', '-y', 'config')
# Check the entires have been transformed
data = spack.config.get('config')
check_config_updated(data)
def test_config_update_not_needed(mutable_config):
@ -543,7 +566,7 @@ def test_updating_multiple_scopes_at_once(packages_yaml_v015):
for scope in ('user', 'site'):
data = spack.config.get('packages', scope=scope)
check_update(data)
check_packages_updated(data)
@pytest.mark.regression('18031')
@ -603,7 +626,7 @@ def test_config_update_works_for_empty_paths(mutable_config):
assert '[backup=' in output
def check_update(data):
def check_packages_updated(data):
"""Check that the data from the packages_yaml_v015
has been updated.
"""
@ -615,3 +638,9 @@ def check_update(data):
externals = data['gcc']['externals']
assert {'spec': 'gcc@8.3.0', 'modules': ['gcc-8']} in externals
assert 'modules' not in data['gcc']
def check_config_updated(data):
assert isinstance(data['install_tree'], dict)
assert data['install_tree']['root'] == '/fake/path'
assert data['install_tree']['projections'] == {'all': '{name}-{version}'}

View file

@ -29,16 +29,16 @@
# sample config data
config_low = {
'config': {
'install_tree': 'install_tree_path',
'install_tree': {'root': 'install_tree_path'},
'build_stage': ['path1', 'path2', 'path3']}}
config_override_all = {
'config:': {
'install_tree:': 'override_all'}}
'install_tree:': {'root': 'override_all'}}}
config_override_key = {
'config': {
'install_tree:': 'override_key'}}
'install_tree:': {'root': 'override_key'}}}
config_merge_list = {
'config': {
@ -391,7 +391,9 @@ def test_read_config_override_all(mock_low_high_config, write_config_file):
write_config_file('config', config_low, 'low')
write_config_file('config', config_override_all, 'high')
assert spack.config.get('config') == {
'install_tree': 'override_all'
'install_tree': {
'root': 'override_all'
}
}
@ -399,7 +401,9 @@ def test_read_config_override_key(mock_low_high_config, write_config_file):
write_config_file('config', config_low, 'low')
write_config_file('config', config_override_key, 'high')
assert spack.config.get('config') == {
'install_tree': 'override_key',
'install_tree': {
'root': 'override_key'
},
'build_stage': ['path1', 'path2', 'path3']
}
@ -408,7 +412,9 @@ def test_read_config_merge_list(mock_low_high_config, write_config_file):
write_config_file('config', config_low, 'low')
write_config_file('config', config_merge_list, 'high')
assert spack.config.get('config') == {
'install_tree': 'install_tree_path',
'install_tree': {
'root': 'install_tree_path'
},
'build_stage': ['patha', 'pathb', 'path1', 'path2', 'path3']
}
@ -417,7 +423,9 @@ def test_read_config_override_list(mock_low_high_config, write_config_file):
write_config_file('config', config_low, 'low')
write_config_file('config', config_override_list, 'high')
assert spack.config.get('config') == {
'install_tree': 'install_tree_path',
'install_tree': {
'root': 'install_tree_path'
},
'build_stage': config_override_list['config']['build_stage:']
}
@ -426,7 +434,7 @@ def test_internal_config_update(mock_low_high_config, write_config_file):
write_config_file('config', config_low, 'low')
before = mock_low_high_config.get('config')
assert before['install_tree'] == 'install_tree_path'
assert before['install_tree']['root'] == 'install_tree_path'
# add an internal configuration scope
scope = spack.config.InternalConfigScope('command_line')
@ -435,12 +443,12 @@ def test_internal_config_update(mock_low_high_config, write_config_file):
mock_low_high_config.push_scope(scope)
command_config = mock_low_high_config.get('config', scope='command_line')
command_config['install_tree'] = 'foo/bar'
command_config['install_tree'] = {'root': 'foo/bar'}
mock_low_high_config.set('config', command_config, scope='command_line')
after = mock_low_high_config.get('config')
assert after['install_tree'] == 'foo/bar'
assert after['install_tree']['root'] == 'foo/bar'
def test_internal_config_filename(mock_low_high_config, write_config_file):
@ -714,12 +722,13 @@ def test_immutable_scope(tmpdir):
with open(config_yaml, 'w') as f:
f.write("""\
config:
install_tree: dummy_tree_value
install_tree:
root: dummy_tree_value
""")
scope = spack.config.ImmutableConfigScope('test', str(tmpdir))
data = scope.get_section('config')
assert data['config']['install_tree'] == 'dummy_tree_value'
assert data['config']['install_tree'] == {'root': 'dummy_tree_value'}
with pytest.raises(spack.config.ConfigError):
scope.write_section('config')

View file

@ -6,9 +6,11 @@
import spack.spec
def test_set_install_hash_length(mock_packages, mutable_config, monkeypatch):
def test_set_install_hash_length(mock_packages, mutable_config, monkeypatch,
tmpdir):
# spack.store.layout caches initial config values, so we monkeypatch
mutable_config.set('config:install_hash_length', 5)
mutable_config.set('config:install_tree', {'root': str(tmpdir)})
monkeypatch.setattr(spack.store, 'store', spack.store._store())
spec = spack.spec.Spec('libelf').concretized()
@ -28,10 +30,14 @@ def test_set_install_hash_length(mock_packages, mutable_config, monkeypatch):
def test_set_install_hash_length_upper_case(mock_packages, mutable_config,
monkeypatch):
monkeypatch, tmpdir):
# spack.store.layout caches initial config values, so we monkeypatch
mutable_config.set('config:install_path_scheme', '{name}-{HASH}')
mutable_config.set('config:install_hash_length', 5)
mutable_config.set(
'config:install_tree',
{'root': str(tmpdir),
'projections': {'all': '{name}-{HASH}'}}
)
monkeypatch.setattr(spack.store, 'store', spack.store._store())
spec = spack.spec.Spec('libelf').concretized()

View file

@ -1,5 +1,6 @@
config:
install_tree: $spack/opt/spack
install_tree:
root: $spack/opt/spack
template_dirs:
- $spack/share/spack/templates
- $spack/lib/spack/spack/test/data/templates

View file

@ -44,9 +44,9 @@ def test_yaml_directory_layout_parameters(tmpdir, config):
"{name}-{version}-{hash}"))
# Test hash_length parameter works correctly
layout_10 = YamlDirectoryLayout(str(tmpdir), hash_len=10)
layout_10 = YamlDirectoryLayout(str(tmpdir), hash_length=10)
path_10 = layout_10.relative_path_for_spec(spec)
layout_7 = YamlDirectoryLayout(str(tmpdir), hash_len=7)
layout_7 = YamlDirectoryLayout(str(tmpdir), hash_length=7)
path_7 = layout_7.relative_path_for_spec(spec)
assert(len(path_default) - len(path_10) == 22)
@ -54,32 +54,34 @@ def test_yaml_directory_layout_parameters(tmpdir, config):
# Test path_scheme
arch, compiler, package7 = path_7.split('/')
scheme_package7 = "{name}-{version}-{hash:7}"
projections_package7 = {'all': "{name}-{version}-{hash:7}"}
layout_package7 = YamlDirectoryLayout(str(tmpdir),
path_scheme=scheme_package7)
projections=projections_package7)
path_package7 = layout_package7.relative_path_for_spec(spec)
assert(package7 == path_package7)
# Test separation of architecture
arch_scheme_package = "{architecture.platform}/{architecture.target}/{architecture.os}/{name}/{version}/{hash:7}" # NOQA: ignore=E501
layout_arch_package = YamlDirectoryLayout(str(tmpdir),
path_scheme=arch_scheme_package)
arch_path_package = layout_arch_package.relative_path_for_spec(spec)
assert(arch_path_package == spec.format(arch_scheme_package))
# Test separation of architecture or namespace
spec2 = Spec('libelf').concretized()
# Test separation of namespace
ns_scheme_package = "${ARCHITECTURE}/${NAMESPACE}/${PACKAGE}-${VERSION}-${HASH:7}" # NOQA: ignore=E501
layout_ns_package = YamlDirectoryLayout(str(tmpdir),
path_scheme=ns_scheme_package)
ns_path_package = layout_ns_package.relative_path_for_spec(spec)
assert(ns_path_package == spec.format(ns_scheme_package))
arch_scheme = "{architecture.platform}/{architecture.target}/{architecture.os}/{name}/{version}/{hash:7}" # NOQA: ignore=E501
ns_scheme = "${ARCHITECTURE}/${NAMESPACE}/${PACKAGE}-${VERSION}-${HASH:7}" # NOQA: ignore=E501
arch_ns_scheme_projections = {'all': arch_scheme,
'python': ns_scheme}
layout_arch_ns = YamlDirectoryLayout(
str(tmpdir), projections=arch_ns_scheme_projections)
arch_path_spec2 = layout_arch_ns.relative_path_for_spec(spec2)
assert(arch_path_spec2 == spec2.format(arch_scheme))
ns_path_spec = layout_arch_ns.relative_path_for_spec(spec)
assert(ns_path_spec == spec.format(ns_scheme))
# Ensure conflicting parameters caught
with pytest.raises(InvalidDirectoryLayoutParametersError):
YamlDirectoryLayout(str(tmpdir),
hash_len=20,
path_scheme=scheme_package7)
hash_length=20,
projections=projections_package7)
def test_read_and_write_spec(layout_and_dir, config, mock_packages):

View file

@ -13,7 +13,7 @@
def get_config_line(pattern, lines):
"""Get a configuration line that matches a particular pattern."""
line = next((l for l in lines if re.search(pattern, l)), None)
line = next((x for x in lines if re.search(pattern, x)), None)
assert line is not None, 'no such line!'
return line
@ -57,7 +57,7 @@ def test_config_blame_with_override(config):
"""check blame for an element from an override scope"""
config_file = config.get_config_filename('site', 'config')
with spack.config.override('config:install_tree', 'foobar'):
with spack.config.override('config:install_tree', {'root': 'foobar'}):
check_blame('install_tree', 'overrides')
check_blame('source_cache', config_file, 11)