From 96932d65a819a08fe40dd4120b0ed05ed8011e01 Mon Sep 17 00:00:00 2001 From: Tamara Dahlgren Date: Mon, 9 Mar 2020 15:52:13 -0700 Subject: Added support for --fail-fast install option to terminate on first failure --- lib/spack/docs/packaging_guide.rst | 11 ++++++++-- lib/spack/spack/cmd/install.py | 4 ++++ lib/spack/spack/installer.py | 16 +++++++++++++++ lib/spack/spack/test/installer.py | 41 +++++++++++++++++++++++++++++++++++--- share/spack/spack-completion.bash | 2 +- 5 files changed, 68 insertions(+), 6 deletions(-) diff --git a/lib/spack/docs/packaging_guide.rst b/lib/spack/docs/packaging_guide.rst index 840c29454b..1f246c0faa 100644 --- a/lib/spack/docs/packaging_guide.rst +++ b/lib/spack/docs/packaging_guide.rst @@ -4167,16 +4167,23 @@ want to clean up the temporary directory, or if the package isn't downloading properly, you might want to run *only* the ``fetch`` stage of the build. +Spack performs best-effort installation of package dependencies by default, +which means it will continue to install as many dependencies as possible +after detecting failures. If you are trying to install a package with a +lot of dependencies where one or more may fail to build, you might want to +try the ``--fail-fast`` option to stop the installation process on the first +failure. + A typical package workflow might look like this: .. code-block:: console $ spack edit mypackage - $ spack install mypackage + $ spack install --fail-fast mypackage ... build breaks! ... $ spack clean mypackage $ spack edit mypackage - $ spack install mypackage + $ spack install --fail-fast mypackage ... repeat clean/install until install works ... Below are some commands that will allow you some finer-grained diff --git a/lib/spack/spack/cmd/install.py b/lib/spack/spack/cmd/install.py index e2a6327f5f..10eb3c327f 100644 --- a/lib/spack/spack/cmd/install.py +++ b/lib/spack/spack/cmd/install.py @@ -32,6 +32,7 @@ def update_kwargs_from_args(args, kwargs): that will be passed to Package.do_install API""" kwargs.update({ + 'fail_fast': args.fail_fast, 'keep_prefix': args.keep_prefix, 'keep_stage': args.keep_stage, 'restage': not args.dont_restage, @@ -78,6 +79,9 @@ the dependencies""" subparser.add_argument( '--overwrite', action='store_true', help="reinstall an existing spec, even if it has dependents") + subparser.add_argument( + '--fail-fast', action='store_true', + help="stop all builds if any build fails (default is best effort)") subparser.add_argument( '--keep-prefix', action='store_true', help="don't remove the install prefix if installation fails") diff --git a/lib/spack/spack/installer.py b/lib/spack/spack/installer.py index 786eec5383..cd2aa00bd0 100644 --- a/lib/spack/spack/installer.py +++ b/lib/spack/spack/installer.py @@ -549,6 +549,9 @@ install_args_docstring = """ dirty (bool): Don't clean the build environment before installing. explicit (bool): True if package was explicitly installed, False if package was implicitly installed (as a dependency). + fail_fast (bool): Fail if any dependency fails to install; + otherwise, the default is to install as many dependencies as + possible (i.e., best effort installation). fake (bool): Don't really build; install fake stub files instead. force (bool): Install again, even if already installed. install_deps (bool): Install dependencies before installing this @@ -1385,11 +1388,14 @@ class PackageInstaller(object): Args:""" + fail_fast = kwargs.get('fail_fast', False) install_deps = kwargs.get('install_deps', True) keep_prefix = kwargs.get('keep_prefix', False) keep_stage = kwargs.get('keep_stage', False) restage = kwargs.get('restage', False) + fail_fast_err = 'Terminating after first install failure' + # install_package defaults True and is popped so that dependencies are # always installed regardless of whether the root was installed install_package = kwargs.pop('install_package', True) @@ -1449,6 +1455,10 @@ class PackageInstaller(object): if pkg_id in self.failed or spack.store.db.prefix_failed(spec): tty.warn('{0} failed to install'.format(pkg_id)) self._update_failed(task) + + if fail_fast: + raise InstallError(fail_fast_err) + continue # Attempt to get a write lock. If we can't get the lock then @@ -1546,6 +1556,12 @@ class PackageInstaller(object): self._update_failed(task, True, exc) + if fail_fast: + # The user requested the installation to terminate on + # failure. + raise InstallError('{0}: {1}' + .format(fail_fast_err, str(exc))) + if pkg_id == self.pkg_id: raise diff --git a/lib/spack/spack/test/installer.py b/lib/spack/spack/test/installer.py index 96612c8f5f..89efbe4fbe 100644 --- a/lib/spack/spack/test/installer.py +++ b/lib/spack/spack/test/installer.py @@ -718,7 +718,7 @@ def test_install_failed(install_mockery, monkeypatch, capsys): assert 'Warning: b failed to install' in out -def test_install_fail_on_interrupt(install_mockery, monkeypatch, capsys): +def test_install_fail_on_interrupt(install_mockery, monkeypatch): """Test ctrl-c interrupted install.""" err_msg = 'mock keyboard interrupt' @@ -733,9 +733,44 @@ def test_install_fail_on_interrupt(install_mockery, monkeypatch, capsys): with pytest.raises(KeyboardInterrupt, match=err_msg): installer.install() + +def test_install_fail_fast_on_detect(install_mockery, monkeypatch, capsys): + """Test fail_fast install when an install failure is detected.""" + spec, installer = create_installer('a') + + # Make sure the package is identified as failed + # + # This will prevent b from installing, which will cause the build of a + # to be skipped. + monkeypatch.setattr(spack.database.Database, 'prefix_failed', _true) + + with pytest.raises(spack.installer.InstallError): + installer.install(fail_fast=True) + + out = str(capsys.readouterr()) + assert 'Skipping build of a' in out + + +def test_install_fail_fast_on_except(install_mockery, monkeypatch, capsys): + """Test fail_fast install when an install failure results from an error.""" + err_msg = 'mock patch failure' + + def _patch(installer, task, **kwargs): + raise RuntimeError(err_msg) + + spec, installer = create_installer('a') + + # Raise a non-KeyboardInterrupt exception to trigger fast failure. + # + # This will prevent b from installing, which will cause the build of a + # to be skipped. + monkeypatch.setattr(spack.package.PackageBase, 'do_patch', _patch) + + with pytest.raises(spack.installer.InstallError, matches=err_msg): + installer.install(fail_fast=True) + out = str(capsys.readouterr()) - assert 'Failed to install' in out - assert err_msg in out + assert 'Skipping build of a' in out def test_install_lock_failures(install_mockery, monkeypatch, capfd): diff --git a/share/spack/spack-completion.bash b/share/spack/spack-completion.bash index ed42ad4edd..620dd9bef2 100755 --- a/share/spack/spack-completion.bash +++ b/share/spack/spack-completion.bash @@ -962,7 +962,7 @@ _spack_info() { _spack_install() { if $list_options then - SPACK_COMPREPLY="-h --help --only -u --until -j --jobs --overwrite --keep-prefix --keep-stage --dont-restage --use-cache --no-cache --cache-only --no-check-signature --show-log-on-error --source -n --no-checksum -v --verbose --fake --only-concrete -f --file --clean --dirty --test --run-tests --log-format --log-file --help-cdash --cdash-upload-url --cdash-build --cdash-site --cdash-track --cdash-buildstamp -y --yes-to-all" + SPACK_COMPREPLY="-h --help --only -u --until -j --jobs --overwrite --fail-fast --keep-prefix --keep-stage --dont-restage --use-cache --no-cache --cache-only --no-check-signature --show-log-on-error --source -n --no-checksum -v --verbose --fake --only-concrete -f --file --clean --dirty --test --run-tests --log-format --log-file --help-cdash --cdash-upload-url --cdash-build --cdash-site --cdash-track --cdash-buildstamp -y --yes-to-all" else _all_packages fi -- cgit v1.2.3-60-g2f50