diff options
author | Todd Gamblin <gamblin2@llnl.gov> | 2022-02-22 11:35:34 -0800 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-02-22 12:35:34 -0700 |
commit | 36b0730fac31463b58f5317dbfa60ee28c6e8bb0 (patch) | |
tree | 7b6dde125e44b0b8b373a879bbfb6325a0e5a4d3 /lib | |
parent | 800933bbdfc4caff6285806249edd20f025fa99b (diff) | |
download | spack-36b0730fac31463b58f5317dbfa60ee28c6e8bb0.tar.gz spack-36b0730fac31463b58f5317dbfa60ee28c6e8bb0.tar.bz2 spack-36b0730fac31463b58f5317dbfa60ee28c6e8bb0.tar.xz spack-36b0730fac31463b58f5317dbfa60ee28c6e8bb0.zip |
Add `spack --bootstrap` option for accessing bootstrap store (#25601)
We can see what is in the bootstrap store with `spack find -b`, and you can clean it with `spack
clean -b`, but we can't do much else with it, and if there are bootstrap issues they can be hard to
debug.
We already have `spack --mock`, which allows you to swap in the mock packages from the command
line. This PR introduces `spack -b` / `spack --bootstrap`, which runs all of spack with
`ensure_bootstrap_configuration()` set. This means that you can run `spack -b find`, `spack -b
install`, `spack -b spec`, etc. to see what *would* happen with bootstrap configuration, to remove
specific bootstrap packages, etc. This will hopefully make developers' lives easier as they deal
with bootstrap packages.
This PR also uses a `nullcontext` context manager. `nullcontext` has been implemented in several
other places in Spack, and this PR consolidates them to `llnl.util.lang`, with a note that we can
delete the function if we ever reqyire a new enough Python.
- [x] introduce `spack --bootstrap` option
- [x] consolidated all `nullcontext` usages to `llnl.util.lang`
Diffstat (limited to 'lib')
-rw-r--r-- | lib/spack/llnl/util/lang.py | 9 | ||||
-rw-r--r-- | lib/spack/spack/cmd/spec.py | 12 | ||||
-rw-r--r-- | lib/spack/spack/database.py | 11 | ||||
-rw-r--r-- | lib/spack/spack/main.py | 20 | ||||
-rw-r--r-- | lib/spack/spack/test/llnl/util/tty/log.py | 70 |
5 files changed, 64 insertions, 58 deletions
diff --git a/lib/spack/llnl/util/lang.py b/lib/spack/llnl/util/lang.py index c806cab1d7..c0b8c863d4 100644 --- a/lib/spack/llnl/util/lang.py +++ b/lib/spack/llnl/util/lang.py @@ -5,6 +5,7 @@ from __future__ import division +import contextlib import functools import inspect import os @@ -921,3 +922,11 @@ def elide_list(line_list, max_num=10): return line_list[:max_num - 1] + ['...'] + line_list[-1:] else: return line_list + + +@contextlib.contextmanager +def nullcontext(*args, **kwargs): + """Empty context manager. + TODO: replace with contextlib.nullcontext() if we ever require python 3.7. + """ + yield diff --git a/lib/spack/spack/cmd/spec.py b/lib/spack/spack/cmd/spec.py index d15d63bb82..d908944976 100644 --- a/lib/spack/spack/cmd/spec.py +++ b/lib/spack/spack/cmd/spec.py @@ -5,9 +5,9 @@ from __future__ import print_function -import contextlib import sys +import llnl.util.lang as lang import llnl.util.tty as tty import spack @@ -59,14 +59,6 @@ for further documentation regarding the spec syntax, see: spack.cmd.common.arguments.add_concretizer_args(subparser) -@contextlib.contextmanager -def nullcontext(): - """Empty context manager. - TODO: replace with contextlib.nullcontext() if we ever require python 3.7. - """ - yield - - def spec(parser, args): name_fmt = '{namespace}.{name}' if args.namespaces else '{name}' fmt = '{@version}{%compiler}{compiler_flags}{variants}{arch=architecture}' @@ -81,7 +73,7 @@ def spec(parser, args): # use a read transaction if we are getting install status for every # spec in the DAG. This avoids repeatedly querying the DB. - tree_context = nullcontext + tree_context = lang.nullcontext if args.install_status: tree_context = spack.store.db.read_transaction diff --git a/lib/spack/spack/database.py b/lib/spack/spack/database.py index 152b2e2c0e..53b5ec2ab2 100644 --- a/lib/spack/spack/database.py +++ b/lib/spack/spack/database.py @@ -38,6 +38,7 @@ except ImportError: pass import llnl.util.filesystem as fs +import llnl.util.lang as lang import llnl.util.tty as tty import spack.hash_types as ht @@ -52,12 +53,6 @@ from spack.filesystem_view import YamlFilesystemView from spack.util.crypto import bit_length from spack.version import Version - -@contextlib.contextmanager -def nullcontext(*args, **kwargs): - yield - - # TODO: Provide an API automatically retyring a build after detecting and # TODO: clearing a failure. @@ -404,8 +399,8 @@ class Database(object): self._write_transaction_impl = lk.WriteTransaction self._read_transaction_impl = lk.ReadTransaction else: - self._write_transaction_impl = nullcontext - self._read_transaction_impl = nullcontext + self._write_transaction_impl = lang.nullcontext + self._read_transaction_impl = lang.nullcontext self._record_fields = record_fields diff --git a/lib/spack/spack/main.py b/lib/spack/spack/main.py index 34ba3c3349..b3e0b8a971 100644 --- a/lib/spack/spack/main.py +++ b/lib/spack/spack/main.py @@ -448,6 +448,9 @@ def make_argument_parser(**kwargs): '-m', '--mock', action='store_true', help="use mock packages instead of real ones") parser.add_argument( + '-b', '--bootstrap', action='store_true', + help="use bootstrap configuration (bootstrap store, config, externals)") + parser.add_argument( '-p', '--profile', action='store_true', dest='spack_profile', help="profile execution using cProfile") parser.add_argument( @@ -856,9 +859,22 @@ def _main(argv=None): cmd_name = args.command[0] cmd_name = aliases.get(cmd_name, cmd_name) - command = parser.add_command(cmd_name) + # set up a bootstrap context, if asked. + # bootstrap context needs to include parsing the command, b/c things + # like `ConstraintAction` and `ConfigSetAction` happen at parse time. + bootstrap_context = llnl.util.lang.nullcontext() + if args.bootstrap: + import spack.bootstrap as bootstrap # avoid circular imports + bootstrap_context = bootstrap.ensure_bootstrap_configuration() + + with bootstrap_context: + finish_parse_and_run(parser, cmd_name, env_format_error) - # Re-parse with the proper sub-parser added. + +def finish_parse_and_run(parser, cmd_name, env_format_error): + """Finish parsing after we know the command to run.""" + # add the found command to the parser and re-run then re-parse + command = parser.add_command(cmd_name) args, unknown = parser.parse_known_args() # Now that we know what command this is and what its args are, determine diff --git a/lib/spack/spack/test/llnl/util/tty/log.py b/lib/spack/spack/test/llnl/util/tty/log.py index 7e5e728eb0..8d44c1f577 100644 --- a/lib/spack/spack/test/llnl/util/tty/log.py +++ b/lib/spack/spack/test/llnl/util/tty/log.py @@ -18,10 +18,9 @@ if TYPE_CHECKING: import pytest -import llnl.util.tty.log -from llnl.util.lang import uniq -from llnl.util.tty.log import log_output -from llnl.util.tty.pty import PseudoShell +import llnl.util.tty.log as log +import llnl.util.lang as lang +import llnl.util.tty.pty as pty from spack.util.executable import which @@ -33,14 +32,9 @@ except ImportError: pass -@contextlib.contextmanager -def nullcontext(): - yield - - def test_log_python_output_with_echo(capfd, tmpdir): with tmpdir.as_cwd(): - with log_output('foo.txt', echo=True): + with log.log_output('foo.txt', echo=True): print('logged') # foo.txt has output @@ -53,7 +47,7 @@ def test_log_python_output_with_echo(capfd, tmpdir): def test_log_python_output_without_echo(capfd, tmpdir): with tmpdir.as_cwd(): - with log_output('foo.txt'): + with log.log_output('foo.txt'): print('logged') # foo.txt has output @@ -66,7 +60,7 @@ def test_log_python_output_without_echo(capfd, tmpdir): def test_log_python_output_with_invalid_utf8(capfd, tmpdir): with tmpdir.as_cwd(): - with log_output('foo.txt'): + with log.log_output('foo.txt'): sys.stdout.buffer.write(b'\xc3\x28\n') # python2 and 3 treat invalid UTF-8 differently @@ -85,7 +79,7 @@ def test_log_python_output_with_invalid_utf8(capfd, tmpdir): def test_log_python_output_and_echo_output(capfd, tmpdir): with tmpdir.as_cwd(): # echo two lines - with log_output('foo.txt') as logger: + with log.log_output('foo.txt') as logger: with logger.force_echo(): print('force echo') print('logged') @@ -104,7 +98,7 @@ def _log_filter_fn(string): def test_log_output_with_filter(capfd, tmpdir): with tmpdir.as_cwd(): - with log_output('foo.txt', filter_fn=_log_filter_fn): + with log.log_output('foo.txt', filter_fn=_log_filter_fn): print('foo blah') print('blah foo') print('foo foo') @@ -118,7 +112,7 @@ def test_log_output_with_filter(capfd, tmpdir): # now try with echo with tmpdir.as_cwd(): - with log_output('foo.txt', echo=True, filter_fn=_log_filter_fn): + with log.log_output('foo.txt', echo=True, filter_fn=_log_filter_fn): print('foo blah') print('blah foo') print('foo foo') @@ -140,7 +134,7 @@ def test_log_subproc_and_echo_output_no_capfd(capfd, tmpdir): # here, and echoing in test_log_subproc_and_echo_output_capfd below. with capfd.disabled(): with tmpdir.as_cwd(): - with log_output('foo.txt') as logger: + with log.log_output('foo.txt') as logger: with logger.force_echo(): echo('echo') print('logged') @@ -157,7 +151,7 @@ def test_log_subproc_and_echo_output_capfd(capfd, tmpdir): # interferes with the logged data. See # test_log_subproc_and_echo_output_no_capfd for tests on the logfile. with tmpdir.as_cwd(): - with log_output('foo.txt') as logger: + with log.log_output('foo.txt') as logger: with logger.force_echo(): echo('echo') print('logged') @@ -177,7 +171,7 @@ def simple_logger(**kwargs): signal.signal(signal.SIGUSR1, handler) log_path = kwargs["log_path"] - with log_output(log_path): + with log.log_output(log_path): while running[0]: print("line") time.sleep(1e-3) @@ -306,25 +300,25 @@ def mock_shell_fg_bg_no_termios(proc, ctl, **kwargs): @contextlib.contextmanager def no_termios(): - saved = llnl.util.tty.log.termios - llnl.util.tty.log.termios = None + saved = log.termios + log.termios = None try: yield finally: - llnl.util.tty.log.termios = saved + log.termios = saved @pytest.mark.skipif(not which("ps"), reason="requires ps utility") @pytest.mark.skipif(not termios, reason="requires termios support") @pytest.mark.parametrize('test_fn,termios_on_or_off', [ # tests with termios - (mock_shell_fg, nullcontext), - (mock_shell_bg, nullcontext), - (mock_shell_bg_fg, nullcontext), - (mock_shell_fg_bg, nullcontext), - (mock_shell_tstp_cont, nullcontext), - (mock_shell_tstp_tstp_cont, nullcontext), - (mock_shell_tstp_tstp_cont_cont, nullcontext), + (mock_shell_fg, lang.nullcontext), + (mock_shell_bg, lang.nullcontext), + (mock_shell_bg_fg, lang.nullcontext), + (mock_shell_fg_bg, lang.nullcontext), + (mock_shell_tstp_cont, lang.nullcontext), + (mock_shell_tstp_tstp_cont, lang.nullcontext), + (mock_shell_tstp_tstp_cont_cont, lang.nullcontext), # tests without termios (mock_shell_fg_no_termios, no_termios), (mock_shell_bg, no_termios), @@ -342,7 +336,7 @@ def test_foreground_background(test_fn, termios_on_or_off, tmpdir): process stop and start. """ - shell = PseudoShell(test_fn, simple_logger) + shell = pty.PseudoShell(test_fn, simple_logger) log_path = str(tmpdir.join("log.txt")) # run the shell test @@ -375,7 +369,7 @@ def synchronized_logger(**kwargs): v_lock = kwargs["v_lock"] sys.stderr.write(os.getcwd() + "\n") - with log_output(log_path) as logger: + with log.log_output(log_path) as logger: with logger.force_echo(): print("forced output") @@ -446,7 +440,7 @@ def mock_shell_v_v_no_termios(proc, ctl, **kwargs): @pytest.mark.skipif(not which("ps"), reason="requires ps utility") @pytest.mark.skipif(not termios, reason="requires termios support") @pytest.mark.parametrize('test_fn,termios_on_or_off', [ - (mock_shell_v_v, nullcontext), + (mock_shell_v_v, lang.nullcontext), (mock_shell_v_v_no_termios, no_termios), ]) def test_foreground_background_output( @@ -457,7 +451,7 @@ def test_foreground_background_output( return - shell = PseudoShell(test_fn, synchronized_logger) + shell = pty.PseudoShell(test_fn, synchronized_logger) log_path = str(tmpdir.join("log.txt")) # Locks for synchronizing with minion @@ -485,8 +479,8 @@ def test_foreground_background_output( # also get lines of log file assert os.path.exists(log_path) - with open(log_path) as log: - log = log.read().strip().split("\n") + with open(log_path) as logfile: + log_data = logfile.read().strip().split("\n") # Controller and minion process coordinate with locks such that the # minion writes "off" when echo is off, and "on" when echo is on. The @@ -494,12 +488,12 @@ def test_foreground_background_output( # lines if the controller is slow. The important thing to observe # here is that we started seeing 'on' in the end. assert ( - ['forced output', 'on'] == uniq(output) or - ['forced output', 'off', 'on'] == uniq(output) + ['forced output', 'on'] == lang.uniq(output) or + ['forced output', 'off', 'on'] == lang.uniq(output) ) # log should be off for a while, then on, then off assert ( - ['forced output', 'off', 'on', 'off'] == uniq(log) and - log.count("off") > 2 # ensure some "off" lines were omitted + ['forced output', 'off', 'on', 'off'] == lang.uniq(log_data) and + log_data.count("off") > 2 # ensure some "off" lines were omitted ) |