Repository: spack/spack

Issue:
`spack install --overwrite ...` install prefix backup doesn't work, leaving spec installed in db without install prefix dir
### Steps to reproduce

```
$ spack install zlib
$ spack find -p zlib
/spack/opt/spack/linux-ubuntu20.04-zen2/gcc-10.3.0/zlib-1.2.11-6g6ahoqlyqjbpwbxlcej3hwhrbngkt5i
$ spack edit zlib # change make('install') into make('agjaeoigjaerogiaeorgaejoaejoaejigaeorgi')
$ spack install --overwrite zlib
$ ls /spack/opt/spack/linux-ubuntu20.04-zen2/gcc-10.3.0/zlib-1.2.11-6g6ahoqlyqjbpwbxlcej3hwhrbngkt5i
ls: cannot access '/spack/opt/spack/linux-ubuntu20.04-zen2/gcc-10.3.0/zlib-1.2.11-6g6ahoqlyqjbpwbxlcej3hwhrbngkt5i': No such file or directory
```

### Error message

```
==> Error: Failed to install zlib due to RuntimeError: the transactional move of "/spack/opt/spack/linux-ubuntu20.04-zen2/gcc-10.3.0/zlib-1.2.11-6g6ahoqlyqjbpwbxlcej3hwhrbngkt5i" failed.
```

### Information on your system

* **Spack:** 0.16.2-4012-c3b1bae67b
* **Python:** 3.8.11
* **Platform:** linux-ubuntu20.04-zen2
* **Concretizer:** clingo


### General 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
- [X] I have run the failing commands in debug mode and reported the output


Discussion:
Great, so `replace_directory_transaction` actually works and moves the original install prefix back in place, but then it gets removed immediately by the PackageInstaller :laughing: 

```
            finally:
                # Remove the install prefix if anything went wrong during
                # install.
                if not keep_prefix:
                    pkg.remove_prefix()
```

Resolving patch:
diff --git a/lib/spack/llnl/util/filesystem.py b/lib/spack/llnl/util/filesystem.py
--- a/lib/spack/llnl/util/filesystem.py
+++ b/lib/spack/llnl/util/filesystem.py
@@ -692,7 +692,7 @@ def replace_directory_transaction(directory_name, tmp_root=None):
 
     try:
         yield tmp_dir
-    except (Exception, KeyboardInterrupt, SystemExit) as e:
+    except (Exception, KeyboardInterrupt, SystemExit):
         # Delete what was there, before copying back the original content
         if os.path.exists(directory_name):
             shutil.rmtree(directory_name)
@@ -701,10 +701,7 @@ def replace_directory_transaction(directory_name, tmp_root=None):
             dst=os.path.dirname(directory_name)
         )
         tty.debug('DIRECTORY RECOVERED [{0}]'.format(directory_name))
-
-        msg = 'the transactional move of "{0}" failed.'
-        msg += '\n    ' + str(e)
-        raise RuntimeError(msg.format(directory_name))
+        raise
     else:
         # Otherwise delete the temporary directory
         shutil.rmtree(tmp_dir)
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
@@ -1564,6 +1564,9 @@ def install(self):
                             if os.path.exists(rec.path):
                                 with fs.replace_directory_transaction(
                                         rec.path):
+                                    # fs transaction will put the old prefix
+                                    # back on failure, so make sure to keep it.
+                                    keep_prefix = True
                                     self._install_task(task)
                             else:
                                 tty.debug("Missing installation to overwrite")


Test patch:
diff --git a/lib/spack/spack/test/install.py b/lib/spack/spack/test/install.py
--- a/lib/spack/spack/test/install.py
+++ b/lib/spack/spack/test/install.py
@@ -126,6 +126,31 @@ def test_partial_install_delete_prefix_and_stage(install_mockery, mock_fetch):
         pkg.remove_prefix = instance_rm_prefix
 
 
+@pytest.mark.disable_clean_stage_check
+def test_failing_overwrite_install_should_keep_previous_installation(
+    mock_fetch, install_mockery
+):
+    """
+    Make sure that whenever `spack install --overwrite` fails, spack restores
+    the original install prefix instead of cleaning it.
+    """
+    # Do a successful install
+    spec = Spec('canfail').concretized()
+    pkg = spack.repo.get(spec)
+    pkg.succeed = True
+
+    # Do a failing overwrite install
+    pkg.do_install()
+    pkg.succeed = False
+    kwargs = {'overwrite': [spec.dag_hash()]}
+
+    with pytest.raises(Exception):
+        pkg.do_install(**kwargs)
+
+    assert pkg.installed
+    assert os.path.exists(spec.prefix)
+
+
 def test_dont_add_patches_to_installed_package(install_mockery, mock_fetch):
     dependency = Spec('dependency-install')
     dependency.concretize()