diff options
-rw-r--r-- | etc/spack/defaults/config.yaml | 9 | ||||
-rw-r--r-- | lib/spack/spack/directory_layout.py | 79 | ||||
-rw-r--r-- | lib/spack/spack/schema/config.py | 61 | ||||
-rw-r--r-- | lib/spack/spack/schema/projections.py | 1 | ||||
-rw-r--r-- | lib/spack/spack/store.py | 38 | ||||
-rw-r--r-- | lib/spack/spack/test/cmd/config.py | 67 | ||||
-rw-r--r-- | lib/spack/spack/test/config.py | 33 | ||||
-rw-r--r-- | lib/spack/spack/test/config_values.py | 12 | ||||
-rw-r--r-- | lib/spack/spack/test/data/config/config.yaml | 3 | ||||
-rw-r--r-- | lib/spack/spack/test/directory_layout.py | 38 | ||||
-rw-r--r-- | 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: <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 diff --git a/lib/spack/spack/schema/projections.py b/lib/spack/spack/schema/projections.py index d6f506f090..bd0693f30e 100644 --- a/lib/spack/spack/schema/projections.py +++ b/lib/spack/spack/schema/projections.py @@ -14,7 +14,6 @@ properties = { 'projections': { 'type': 'object', - 'default': {}, 'patternProperties': { r'all|\w[\w-]*': { 'type': 'string' diff --git a/lib/spack/spack/store.py b/lib/spack/spack/store.py index 2b80e994c7..756a0a2e03 100644 --- a/lib/spack/spack/store.py +++ b/lib/spack/spack/store.py @@ -24,14 +24,16 @@ configuration. """ 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 @@ class Store(object): 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')) diff --git a/lib/spack/spack/test/cmd/config.py b/lib/spack/spack/test/cmd/config.py index 32e55f90b8..d330754aa1 100644 --- a/lib/spack/spack/test/cmd/config.py +++ b/lib/spack/spack/test/cmd/config.py @@ -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 @@ config = spack.main.SpackCommand('config') 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 @@ packages: 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}'} diff --git a/lib/spack/spack/test/config.py b/lib/spack/spack/test/config.py index 41757683aa..55bdcb674c 100644 --- a/lib/spack/spack/test/config.py +++ b/lib/spack/spack/test/config.py @@ -29,16 +29,16 @@ import spack.util.path as spack_path # 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') diff --git a/lib/spack/spack/test/config_values.py b/lib/spack/spack/test/config_values.py index ff97f26db4..bea1283b74 100644 --- a/lib/spack/spack/test/config_values.py +++ b/lib/spack/spack/test/config_values.py @@ -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() diff --git a/lib/spack/spack/test/data/config/config.yaml b/lib/spack/spack/test/data/config/config.yaml index 0cf0d32cb0..efbe625151 100644 --- a/lib/spack/spack/test/data/config/config.yaml +++ b/lib/spack/spack/test/data/config/config.yaml @@ -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 diff --git a/lib/spack/spack/test/directory_layout.py b/lib/spack/spack/test/directory_layout.py index c9ef68b3cf..3a144621eb 100644 --- a/lib/spack/spack/test/directory_layout.py +++ b/lib/spack/spack/test/directory_layout.py @@ -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): diff --git a/lib/spack/spack/test/util/spack_yaml.py b/lib/spack/spack/test/util/spack_yaml.py index ed7ba789c0..23e02d7be6 100644 --- a/lib/spack/spack/test/util/spack_yaml.py +++ b/lib/spack/spack/test/util/spack_yaml.py @@ -13,7 +13,7 @@ config_cmd = SpackCommand('config') 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) |