Repository: spack/spack

Issue:
Spack dev-build -u <phase> marks package as installed whatever the <phase>
Spack dev-build -u <phase> marks package as installed whatever the <phase>

### Steps to reproduce the issue

```console
$ git clone https://github.com/spack/spack.git
$ cd spack
$ . share/spack/setup-env.sh
$ cd ..
$ curl -o pkgconf-1.6.3.tar.xz http://distfiles.dereferenced.org/pkgconf/pkgconf-1.6.3.tar.xz
$ tar -xf pkgconf-1.6.3.tar.xz
$ cd pkgconf-1.6.3
$ spack dev-build -u configure pkgconf@1.6.3
$ spack spec -I pkgconf@1.6.3
$ spack find -p pkgconf@1.6.3
```

### Error Message

Everything runs fine, but `spack spec -I` will show the package as installed.
Then `spack find -p` will return a path that does not exist.  

### Information on your system

Latest Spack@develop, on MacOs and Toss3.

```
* **Spack:** 0.13.4-2143-1c45153e0
* **Python:** 3.7.7
* **Platform:** darwin-mojave-skylake
```

```
* **Spack:** 0.14.2-848-1c45153
* **Python:** 3.7.2
* **Platform:** linux-rhel7-broadwell
```

Note: Although pointing to the exact same commit, `spack debug report` gives different info on both spack instances. I haven’t done a fresh clone on darwin, meaning spack version doesn’t seem to update correctly.

### Additional information

- [x] I have run `spack debug report` and reported the version of Spack/Python/Platform
- [x] I have searched the issues of this repo and believe this is not a duplicate
- [ ] I have run the failing commands in debug mode and reported the output




Discussion:

Resolving patch:
diff --git a/lib/spack/spack/build_environment.py b/lib/spack/spack/build_environment.py
--- a/lib/spack/spack/build_environment.py
+++ b/lib/spack/spack/build_environment.py
@@ -812,12 +812,11 @@ def child_process(child_pipe, input_stream):
                 setup_package(pkg, dirty=dirty)
             return_value = function()
             child_pipe.send(return_value)
-        except StopIteration as e:
-            # StopIteration is used to stop installations
-            # before the final stage, mainly for debug purposes
-            tty.msg(e)
-            child_pipe.send(None)
 
+        except StopPhase as e:
+            # Do not create a full ChildError from this, it's not an error
+            # it's a control statement.
+            child_pipe.send(e)
         except BaseException:
             # catch ANYTHING that goes wrong in the child process
             exc_type, exc, tb = sys.exc_info()
@@ -869,15 +868,20 @@ def child_process(child_pipe, input_stream):
     child_result = parent_pipe.recv()
     p.join()
 
+    # If returns a StopPhase, raise it
+    if isinstance(child_result, StopPhase):
+        # do not print
+        raise child_result
+
     # let the caller know which package went wrong.
     if isinstance(child_result, InstallError):
         child_result.pkg = pkg
 
-    # If the child process raised an error, print its output here rather
-    # than waiting until the call to SpackError.die() in main(). This
-    # allows exception handling output to be logged from within Spack.
-    # see spack.main.SpackCommand.
     if isinstance(child_result, ChildError):
+        # If the child process raised an error, print its output here rather
+        # than waiting until the call to SpackError.die() in main(). This
+        # allows exception handling output to be logged from within Spack.
+        # see spack.main.SpackCommand.
         child_result.print_context()
         raise child_result
 
@@ -1066,3 +1070,13 @@ def __reduce__(self):
 def _make_child_error(msg, module, name, traceback, build_log, context):
     """Used by __reduce__ in ChildError to reconstruct pickled errors."""
     return ChildError(msg, module, name, traceback, build_log, context)
+
+
+class StopPhase(spack.error.SpackError):
+    """Pickle-able exception to control stopped builds."""
+    def __reduce__(self):
+        return _make_stop_phase, (self.message, self.long_message)
+
+
+def _make_stop_phase(msg, long_msg):
+    return StopPhase(msg, long_msg)
diff --git a/lib/spack/spack/installer.py b/lib/spack/spack/installer.py
--- a/lib/spack/spack/installer.py
+++ b/lib/spack/spack/installer.py
@@ -784,6 +784,9 @@ def _check_last_phase(self, **kwargs):
         The ``stop_before`` or ``stop_at`` arguments are removed from the
         installation arguments.
 
+        The last phase is also set to None if it is the last phase of the
+        package already
+
         Args:
             kwargs:
               ``stop_before``': stop before execution of this phase (or None)
@@ -800,6 +803,10 @@ def _check_last_phase(self, **kwargs):
                 self.pkg.last_phase not in self.pkg.phases:
             tty.die('\'{0}\' is not an allowed phase for package {1}'
                     .format(self.pkg.last_phase, self.pkg.name))
+        # If we got a last_phase, make sure it's not already last
+        if self.pkg.last_phase and \
+                self.pkg.last_phase == self.pkg.phases[-1]:
+            self.pkg.last_phase = None
 
     def _cleanup_all_tasks(self):
         """Cleanup all build tasks to include releasing their locks."""
@@ -1164,13 +1171,12 @@ def build_process():
             if task.compiler:
                 spack.compilers.add_compilers_to_config(
                     spack.compilers.find_compilers([pkg.spec.prefix]))
-
-        except StopIteration as e:
-            # A StopIteration exception means that do_install was asked to
-            # stop early from clients.
-            tty.msg('{0} {1}'.format(self.pid, str(e)))
-            tty.msg('Package stage directory : {0}'
-                    .format(pkg.stage.source_path))
+        except spack.build_environment.StopPhase as e:
+            # A StopPhase exception means that do_install was asked to
+            # stop early from clients, and is not an error at this point
+            tty.debug('{0} {1}'.format(self.pid, str(e)))
+            tty.debug('Package stage directory : {0}'
+                      .format(pkg.stage.source_path))
 
     _install_task.__doc__ += install_args_docstring
 
diff --git a/lib/spack/spack/package.py b/lib/spack/spack/package.py
--- a/lib/spack/spack/package.py
+++ b/lib/spack/spack/package.py
@@ -116,16 +116,17 @@ def phase_wrapper(spec, prefix):
 
     def _on_phase_start(self, instance):
         # If a phase has a matching stop_before_phase attribute,
-        # stop the installation process raising a StopIteration
+        # stop the installation process raising a StopPhase
         if getattr(instance, 'stop_before_phase', None) == self.name:
-            raise StopIteration('Stopping before \'{0}\' phase'
-                                .format(self.name))
+            from spack.build_environment import StopPhase
+            raise StopPhase('Stopping before \'{0}\' phase'.format(self.name))
 
     def _on_phase_exit(self, instance):
         # If a phase has a matching last_phase attribute,
-        # stop the installation process raising a StopIteration
+        # stop the installation process raising a StopPhase
         if getattr(instance, 'last_phase', None) == self.name:
-            raise StopIteration('Stopping at \'{0}\' phase'.format(self.name))
+            from spack.build_environment import StopPhase
+            raise StopPhase('Stopping at \'{0}\' phase'.format(self.name))
 
     def copy(self):
         try:


Test patch:
diff --git a/lib/spack/spack/test/cmd/dev_build.py b/lib/spack/spack/test/cmd/dev_build.py
--- a/lib/spack/spack/test/cmd/dev_build.py
+++ b/lib/spack/spack/test/cmd/dev_build.py
@@ -54,6 +54,25 @@ def test_dev_build_until(tmpdir, mock_packages, install_mockery):
             assert f.read() == spec.package.replacement_string
 
     assert not os.path.exists(spec.prefix)
+    assert not spack.store.db.query(spec, installed=True)
+
+
+def test_dev_build_until_last_phase(tmpdir, mock_packages, install_mockery):
+    # Test that we ignore the last_phase argument if it is already last
+    spec = spack.spec.Spec('dev-build-test-install@0.0.0').concretized()
+
+    with tmpdir.as_cwd():
+        with open(spec.package.filename, 'w') as f:
+            f.write(spec.package.original_string)
+
+        dev_build('-u', 'install', 'dev-build-test-install@0.0.0')
+
+        assert spec.package.filename in os.listdir(os.getcwd())
+        with open(spec.package.filename, 'r') as f:
+            assert f.read() == spec.package.replacement_string
+
+    assert os.path.exists(spec.prefix)
+    assert spack.store.db.query(spec, installed=True)
 
 
 def test_dev_build_before_until(tmpdir, mock_packages, install_mockery):