Enable/disable bootstrapping and customize store location (#23677)

* Permit to enable/disable bootstrapping and customize store location

This PR adds configuration handles to allow enabling
and disabling bootstrapping, and to customize the store
location.

* Move bootstrap related configuration into its own YAML file

* Add a bootstrap command to manage configuration
This commit is contained in:
Massimiliano Culpo 2021-07-13 01:00:37 +02:00 committed by GitHub
parent 9fb1c3e143
commit 3228c35df6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 337 additions and 11 deletions

View File

@ -0,0 +1,7 @@
bootstrap:
# If set to false Spack will not bootstrap missing software,
# but will instead raise an error.
enable: true
# Root directory for bootstrapping work. The software bootstrapped
# by Spack is installed in a "store" subfolder of this root directory
root: ~/.spack/bootstrap

View File

@ -25,6 +25,7 @@
import spack.store import spack.store
import spack.user_environment as uenv import spack.user_environment as uenv
import spack.util.executable import spack.util.executable
import spack.util.path
from spack.util.environment import EnvironmentModifications from spack.util.environment import EnvironmentModifications
@ -216,9 +217,10 @@ def _bootstrap_config_scopes():
@contextlib.contextmanager @contextlib.contextmanager
def ensure_bootstrap_configuration(): def ensure_bootstrap_configuration():
bootstrap_store_path = store_path()
with spack.architecture.use_platform(spack.architecture.real_platform()): with spack.architecture.use_platform(spack.architecture.real_platform()):
with spack.repo.use_repositories(spack.paths.packages_path): with spack.repo.use_repositories(spack.paths.packages_path):
with spack.store.use_store(spack.paths.user_bootstrap_store): with spack.store.use_store(bootstrap_store_path):
# Default configuration scopes excluding command line # Default configuration scopes excluding command line
# and builtin but accounting for platform specific scopes # and builtin but accounting for platform specific scopes
config_scopes = _bootstrap_config_scopes() config_scopes = _bootstrap_config_scopes()
@ -227,6 +229,23 @@ def ensure_bootstrap_configuration():
yield yield
def store_path():
"""Path to the store used for bootstrapped software"""
enabled = spack.config.get('bootstrap:enable', True)
if not enabled:
msg = ('bootstrapping is currently disabled. '
'Use "spack bootstrap enable" to enable it')
raise RuntimeError(msg)
bootstrap_root_path = spack.config.get(
'bootstrap:root', spack.paths.user_bootstrap_path
)
bootstrap_store_path = spack.util.path.canonicalize_path(
os.path.join(bootstrap_root_path, 'store')
)
return bootstrap_store_path
def clingo_root_spec(): def clingo_root_spec():
# Construct the root spec that will be used to bootstrap clingo # Construct the root spec that will be used to bootstrap clingo
spec_str = 'clingo-bootstrap@spack+python' spec_str = 'clingo-bootstrap@spack+python'

View File

@ -0,0 +1,110 @@
# Copyright 2013-2021 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
import os.path
import shutil
import llnl.util.tty
import spack.cmd.common.arguments
import spack.config
import spack.main
import spack.util.path
description = "manage bootstrap configuration"
section = "system"
level = "long"
def _add_scope_option(parser):
scopes = spack.config.scopes()
scopes_metavar = spack.config.scopes_metavar
parser.add_argument(
'--scope', choices=scopes, metavar=scopes_metavar,
help="configuration scope to read/modify"
)
def setup_parser(subparser):
sp = subparser.add_subparsers(dest='subcommand')
enable = sp.add_parser('enable', help='enable bootstrapping')
_add_scope_option(enable)
disable = sp.add_parser('disable', help='disable bootstrapping')
_add_scope_option(disable)
reset = sp.add_parser(
'reset', help='reset bootstrapping configuration to Spack defaults'
)
spack.cmd.common.arguments.add_common_arguments(
reset, ['yes_to_all']
)
root = sp.add_parser(
'root', help='get/set the root bootstrap directory'
)
_add_scope_option(root)
root.add_argument(
'path', nargs='?', default=None,
help='set the bootstrap directory to this value'
)
def _enable_or_disable(args):
# Set to True if we called "enable", otherwise set to false
value = args.subcommand == 'enable'
spack.config.set('bootstrap:enable', value, scope=args.scope)
def _reset(args):
if not args.yes_to_all:
msg = [
"Bootstrapping configuration is being reset to Spack's defaults. "
"Current configuration will be lost.\n",
"Do you want to continue?"
]
ok_to_continue = llnl.util.tty.get_yes_or_no(
''.join(msg), default=True
)
if not ok_to_continue:
raise RuntimeError('Aborting')
for scope in spack.config.config.file_scopes:
# The default scope should stay untouched
if scope.name == 'defaults':
continue
# If we are in an env scope we can't delete a file, but the best we
# can do is nullify the corresponding configuration
if (scope.name.startswith('env') and
spack.config.get('bootstrap', scope=scope.name)):
spack.config.set('bootstrap', {}, scope=scope.name)
continue
# If we are outside of an env scope delete the bootstrap.yaml file
bootstrap_yaml = os.path.join(scope.path, 'bootstrap.yaml')
backup_file = bootstrap_yaml + '.bkp'
if os.path.exists(bootstrap_yaml):
shutil.move(bootstrap_yaml, backup_file)
def _root(args):
if args.path:
spack.config.set('bootstrap:root', args.path, scope=args.scope)
root = spack.config.get('bootstrap:root', default=None, scope=args.scope)
if root:
root = spack.util.path.canonicalize_path(root)
print(root)
def bootstrap(parser, args):
callbacks = {
'enable': _enable_or_disable,
'disable': _enable_or_disable,
'reset': _reset,
'root': _root
}
callbacks[args.subcommand](args)

View File

@ -9,6 +9,7 @@
import llnl.util.tty as tty import llnl.util.tty as tty
import spack.bootstrap
import spack.caches import spack.caches
import spack.cmd.common.arguments as arguments import spack.cmd.common.arguments as arguments
import spack.cmd.test import spack.cmd.test
@ -102,7 +103,7 @@ def clean(parser, args):
if args.bootstrap: if args.bootstrap:
msg = 'Removing software in "{0}"' msg = 'Removing software in "{0}"'
tty.msg(msg.format(spack.paths.user_bootstrap_store)) tty.msg(msg.format(spack.bootstrap.store_path()))
with spack.store.use_store(spack.paths.user_bootstrap_store): with spack.store.use_store(spack.bootstrap.store_path()):
uninstall = spack.main.SpackCommand('uninstall') uninstall = spack.main.SpackCommand('uninstall')
uninstall('-a', '-y') uninstall('-a', '-y')

View File

@ -13,6 +13,7 @@
import llnl.util.tty as tty import llnl.util.tty as tty
import llnl.util.tty.color as color import llnl.util.tty.color as color
import spack.bootstrap
import spack.cmd as cmd import spack.cmd as cmd
import spack.cmd.common.arguments as arguments import spack.cmd.common.arguments as arguments
import spack.environment as ev import spack.environment as ev
@ -207,9 +208,10 @@ def find(parser, args):
q_args = query_arguments(args) q_args = query_arguments(args)
# Query the current store or the internal bootstrap store if required # Query the current store or the internal bootstrap store if required
if args.bootstrap: if args.bootstrap:
bootstrap_store_path = spack.bootstrap.store_path()
msg = 'Showing internal bootstrap store at "{0}"' msg = 'Showing internal bootstrap store at "{0}"'
tty.msg(msg.format(spack.paths.user_bootstrap_store)) tty.msg(msg.format(bootstrap_store_path))
with spack.store.use_store(spack.paths.user_bootstrap_store): with spack.store.use_store(bootstrap_store_path):
results = args.specs(**q_args) results = args.specs(**q_args)
else: else:
results = args.specs(**q_args) results = args.specs(**q_args)

View File

@ -51,6 +51,7 @@
import spack.compilers import spack.compilers
import spack.paths import spack.paths
import spack.schema import spack.schema
import spack.schema.bootstrap
import spack.schema.compilers import spack.schema.compilers
import spack.schema.config import spack.schema.config
import spack.schema.env import spack.schema.env
@ -74,6 +75,7 @@
'modules': spack.schema.modules.schema, 'modules': spack.schema.modules.schema,
'config': spack.schema.config.schema, 'config': spack.schema.config.schema,
'upstreams': spack.schema.upstreams.schema, 'upstreams': spack.schema.upstreams.schema,
'bootstrap': spack.schema.bootstrap.schema
} }
# Same as above, but including keys for environments # Same as above, but including keys for environments

View File

@ -11,10 +11,10 @@
""" """
import os import os
from llnl.util.filesystem import ancestor import llnl.util.filesystem
#: This file lives in $prefix/lib/spack/spack/__file__ #: This file lives in $prefix/lib/spack/spack/__file__
prefix = ancestor(__file__, 4) prefix = llnl.util.filesystem.ancestor(__file__, 4)
#: synonym for prefix #: synonym for prefix
spack_root = prefix spack_root = prefix
@ -53,7 +53,6 @@
#: User configuration location #: User configuration location
user_config_path = os.path.expanduser('~/.spack') user_config_path = os.path.expanduser('~/.spack')
user_bootstrap_path = os.path.join(user_config_path, 'bootstrap') user_bootstrap_path = os.path.join(user_config_path, 'bootstrap')
user_bootstrap_store = os.path.join(user_bootstrap_path, 'store')
reports_path = os.path.join(user_config_path, "reports") reports_path = os.path.join(user_config_path, "reports")
monitor_path = os.path.join(reports_path, "monitor") monitor_path = os.path.join(reports_path, "monitor")

View File

@ -0,0 +1,26 @@
# Copyright 2013-2021 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
"""Schema for bootstrap.yaml configuration file."""
properties = {
'bootstrap': {
'type': 'object',
'properties': {
'enable': {'type': 'boolean'},
'root': {
'type': 'string'
},
}
}
}
#: Full schema with metadata
schema = {
'$schema': 'http://json-schema.org/schema#',
'title': 'Spack bootstrap configuration file schema',
'type': 'object',
'additionalProperties': False,
'properties': properties,
}

View File

@ -10,6 +10,7 @@
""" """
from llnl.util.lang import union_dicts from llnl.util.lang import union_dicts
import spack.schema.bootstrap
import spack.schema.cdash import spack.schema.cdash
import spack.schema.compilers import spack.schema.compilers
import spack.schema.config import spack.schema.config
@ -23,6 +24,7 @@
#: Properties for inclusion in other schemas #: Properties for inclusion in other schemas
properties = union_dicts( properties = union_dicts(
spack.schema.bootstrap.properties,
spack.schema.cdash.properties, spack.schema.cdash.properties,
spack.schema.compilers.properties, spack.schema.compilers.properties,
spack.schema.config.properties, spack.schema.config.properties,

View File

@ -193,6 +193,7 @@ def deserialize(token):
def _store(): def _store():
"""Get the singleton store instance.""" """Get the singleton store instance."""
import spack.bootstrap
config_dict = spack.config.get('config') config_dict = spack.config.get('config')
root, unpadded_root, projections = parse_install_tree(config_dict) root, unpadded_root, projections = parse_install_tree(config_dict)
hash_length = spack.config.get('config:install_hash_length') hash_length = spack.config.get('config:install_hash_length')
@ -201,7 +202,8 @@ def _store():
# reserved by Spack to bootstrap its own dependencies, since this would # reserved by Spack to bootstrap its own dependencies, since this would
# lead to bizarre behaviors (e.g. cleaning the bootstrap area would wipe # lead to bizarre behaviors (e.g. cleaning the bootstrap area would wipe
# user installed software) # user installed software)
if spack.paths.user_bootstrap_store == root: enable_bootstrap = spack.config.get('bootstrap:enable', True)
if enable_bootstrap and spack.bootstrap.store_path() == root:
msg = ('please change the install tree root "{0}" in your ' msg = ('please change the install tree root "{0}" in your '
'configuration [path reserved for Spack internal use]') 'configuration [path reserved for Spack internal use]')
raise ValueError(msg.format(root)) raise ValueError(msg.format(root))

View File

@ -6,6 +6,7 @@
import spack.bootstrap import spack.bootstrap
import spack.store import spack.store
import spack.util.path
@pytest.mark.regression('22294') @pytest.mark.regression('22294')
@ -22,5 +23,29 @@ def test_store_is_restored_correctly_after_bootstrap(mutable_config, tmpdir):
# Test that within the context manager we use the bootstrap store # Test that within the context manager we use the bootstrap store
# and that outside we restore the correct location # and that outside we restore the correct location
with spack.bootstrap.ensure_bootstrap_configuration(): with spack.bootstrap.ensure_bootstrap_configuration():
assert spack.store.root == spack.paths.user_bootstrap_store assert spack.store.root == spack.bootstrap.store_path()
assert spack.store.root == user_path assert spack.store.root == user_path
@pytest.mark.parametrize('config_value,expected', [
# Absolute path without expansion
('/opt/spack/bootstrap', '/opt/spack/bootstrap/store'),
# Path with placeholder
('$spack/opt/bootstrap', '$spack/opt/bootstrap/store'),
])
def test_store_path_customization(config_value, expected, mutable_config):
# Set the current configuration to a specific value
spack.config.set('bootstrap:root', config_value)
# Check the store path
current = spack.bootstrap.store_path()
assert current == spack.util.path.canonicalize_path(expected)
def test_raising_exception_if_bootstrap_disabled(mutable_config):
# Disable bootstrapping in config.yaml
spack.config.set('bootstrap:enable', False)
# Check the correct exception is raised
with pytest.raises(RuntimeError, match='bootstrapping is currently disabled'):
spack.bootstrap.store_path()

View File

@ -0,0 +1,101 @@
# Copyright 2013-2021 Lawrence Livermore National Security, LLC and other
# Spack Project Developers. See the top-level COPYRIGHT file for details.
#
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
import os.path
import pytest
import spack.config
import spack.environment
import spack.main
_bootstrap = spack.main.SpackCommand('bootstrap')
@pytest.mark.parametrize('scope', [
None, 'site', 'system', 'user'
])
def test_enable_and_disable(mutable_config, scope):
scope_args = []
if scope:
scope_args = ['--scope={0}'.format(scope)]
_bootstrap('enable', *scope_args)
assert spack.config.get('bootstrap:enable', scope=scope) is True
_bootstrap('disable', *scope_args)
assert spack.config.get('bootstrap:enable', scope=scope) is False
@pytest.mark.parametrize('scope', [
None, 'site', 'system', 'user'
])
def test_root_get_and_set(mutable_config, scope):
scope_args, path = [], '/scratch/spack/bootstrap'
if scope:
scope_args = ['--scope={0}'.format(scope)]
_bootstrap('root', path, *scope_args)
out = _bootstrap('root', *scope_args, output=str)
assert out.strip() == path
@pytest.mark.parametrize('scopes', [
('site',),
('system', 'user')
])
def test_reset_in_file_scopes(mutable_config, scopes):
# Assert files are created in the right scopes
bootstrap_yaml_files = []
for s in scopes:
_bootstrap('disable', '--scope={0}'.format(s))
scope_path = spack.config.config.scopes[s].path
bootstrap_yaml = os.path.join(
scope_path, 'bootstrap.yaml'
)
assert os.path.exists(bootstrap_yaml)
bootstrap_yaml_files.append(bootstrap_yaml)
_bootstrap('reset', '-y')
for bootstrap_yaml in bootstrap_yaml_files:
assert not os.path.exists(bootstrap_yaml)
def test_reset_in_environment(mutable_mock_env_path, mutable_config):
env = spack.main.SpackCommand('env')
env('create', 'bootstrap-test')
current_environment = spack.environment.read('bootstrap-test')
with current_environment:
_bootstrap('disable')
assert spack.config.get('bootstrap:enable') is False
_bootstrap('reset', '-y')
# We have no default settings in tests
assert spack.config.get('bootstrap:enable') is None
# Check that reset didn't delete the entire file
spack_yaml = os.path.join(current_environment.path, 'spack.yaml')
assert os.path.exists(spack_yaml)
def test_reset_in_file_scopes_overwrites_backup_files(mutable_config):
# Create a bootstrap.yaml with some config
_bootstrap('disable', '--scope=site')
scope_path = spack.config.config.scopes['site'].path
bootstrap_yaml = os.path.join(scope_path, 'bootstrap.yaml')
assert os.path.exists(bootstrap_yaml)
# Reset the bootstrap configuration
_bootstrap('reset', '-y')
backup_file = bootstrap_yaml + '.bkp'
assert not os.path.exists(bootstrap_yaml)
assert os.path.exists(backup_file)
# Iterate another time
_bootstrap('disable', '--scope=site')
assert os.path.exists(bootstrap_yaml)
assert os.path.exists(backup_file)
_bootstrap('reset', '-y')
assert not os.path.exists(bootstrap_yaml)
assert os.path.exists(backup_file)

View File

@ -333,7 +333,7 @@ _spack() {
then then
SPACK_COMPREPLY="-h --help -H --all-help --color -c --config -C --config-scope -d --debug --timestamp --pdb -e --env -D --env-dir -E --no-env --use-env-repo -k --insecure -l --enable-locks -L --disable-locks -m --mock -p --profile --sorted-profile --lines -v --verbose --stacktrace -V --version --print-shell-vars" SPACK_COMPREPLY="-h --help -H --all-help --color -c --config -C --config-scope -d --debug --timestamp --pdb -e --env -D --env-dir -E --no-env --use-env-repo -k --insecure -l --enable-locks -L --disable-locks -m --mock -p --profile --sorted-profile --lines -v --verbose --stacktrace -V --version --print-shell-vars"
else else
SPACK_COMPREPLY="activate add analyze arch audit blame build-env buildcache cd checksum ci clean clone commands compiler compilers concretize config containerize create deactivate debug dependencies dependents deprecate dev-build develop docs edit env extensions external fetch find flake8 gc gpg graph help info install license list load location log-parse maintainers mark mirror module monitor patch pkg providers pydoc python reindex remove rm repo resource restage solve spec stage style test test-env tutorial undevelop uninstall unit-test unload url verify versions view" SPACK_COMPREPLY="activate add analyze arch audit blame bootstrap build-env buildcache cd checksum ci clean clone commands compiler compilers concretize config containerize create deactivate debug dependencies dependents deprecate dev-build develop docs edit env extensions external fetch find flake8 gc gpg graph help info install license list load location log-parse maintainers mark mirror module monitor patch pkg providers pydoc python reindex remove rm repo resource restage solve spec stage style test test-env tutorial undevelop uninstall unit-test unload url verify versions view"
fi fi
} }
@ -416,6 +416,36 @@ _spack_blame() {
fi fi
} }
_spack_bootstrap() {
if $list_options
then
SPACK_COMPREPLY="-h --help"
else
SPACK_COMPREPLY="enable disable reset root"
fi
}
_spack_bootstrap_enable() {
SPACK_COMPREPLY="-h --help --scope"
}
_spack_bootstrap_disable() {
SPACK_COMPREPLY="-h --help --scope"
}
_spack_bootstrap_reset() {
SPACK_COMPREPLY="-h --help -y --yes-to-all"
}
_spack_bootstrap_root() {
if $list_options
then
SPACK_COMPREPLY="-h --help --scope"
else
SPACK_COMPREPLY=""
fi
}
_spack_build_env() { _spack_build_env() {
if $list_options if $list_options
then then