From 96932d65a819a08fe40dd4120b0ed05ed8011e01 Mon Sep 17 00:00:00 2001
From: Tamara Dahlgren <dahlgren1@llnl.gov>
Date: Mon, 9 Mar 2020 15:52:13 -0700
Subject: [PATCH] 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 @@ def setup_parser(subparser):
     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 @@ def package_id(pkg):
             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 @@ def install(self, **kwargs):
 
         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 @@ def install(self, **kwargs):
             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 @@ def install(self, **kwargs):
 
                 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 _interrupt(installer, task, **kwargs):
     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
-- 
GitLab