summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorHarmen Stoppels <harmenstoppels@gmail.com>2022-02-04 20:31:39 +0100
committerGitHub <noreply@github.com>2022-02-04 19:31:39 +0000
commit73077f3a67011818ef503b660af8cd251839f92f (patch)
treef8431b635fa8c96372addb22c96e6b82d1861d76
parent5881a03408df63e07d1f02ac0840a751a4936b10 (diff)
downloadspack-73077f3a67011818ef503b660af8cd251839f92f.tar.gz
spack-73077f3a67011818ef503b660af8cd251839f92f.tar.bz2
spack-73077f3a67011818ef503b660af8cd251839f92f.tar.xz
spack-73077f3a67011818ef503b660af8cd251839f92f.zip
database: fix reindex with uninstalled deps (#28764)
* Fix reindex with uninstalled deps When a prefix of a dep is removed, and the db is reindexed, it is added through the dependent, but until now it incorrectly listed the spec as 'installed'. There was also some questionable behavior in the db when the same spec was added multiple times, it would always be marked installed. * Always reserve path * Only add installed spec's prefixes to install prefixes set * Improve warning, and ensure ensure only ensures * test: reindex with every file system remnant removed except for the old index; it should give a database with nothing installed, including records with installed==False,external==False,ref_count==0,explicit=True, and these should be removable from the database
-rw-r--r--lib/spack/spack/database.py68
-rw-r--r--lib/spack/spack/directory_layout.py15
-rw-r--r--lib/spack/spack/test/conftest.py4
-rw-r--r--lib/spack/spack/test/database.py49
4 files changed, 94 insertions, 42 deletions
diff --git a/lib/spack/spack/database.py b/lib/spack/spack/database.py
index 6c2ac7001c..152b2e2c0e 100644
--- a/lib/spack/spack/database.py
+++ b/lib/spack/spack/database.py
@@ -940,22 +940,15 @@ class Database(object):
tty.debug(
'RECONSTRUCTING FROM OLD DB: {0}'.format(entry.spec))
try:
- layout = spack.store.layout
- if entry.spec.external:
- layout = None
- install_check = True
- else:
- install_check = layout.check_installed(entry.spec)
-
- if install_check:
- kwargs = {
- 'spec': entry.spec,
- 'directory_layout': layout,
- 'explicit': entry.explicit,
- 'installation_time': entry.installation_time # noqa: E501
- }
- self._add(**kwargs)
- processed_specs.add(entry.spec)
+ layout = None if entry.spec.external else spack.store.layout
+ kwargs = {
+ 'spec': entry.spec,
+ 'directory_layout': layout,
+ 'explicit': entry.explicit,
+ 'installation_time': entry.installation_time # noqa: E501
+ }
+ self._add(**kwargs)
+ processed_specs.add(entry.spec)
except Exception as e:
# Something went wrong, so the spec was not restored
# from old data
@@ -1099,24 +1092,28 @@ class Database(object):
}
self._add(dep, directory_layout, **extra_args)
- if key not in self._data:
- installed = bool(spec.external)
- path = None
- if not spec.external and directory_layout:
- path = directory_layout.path_for_spec(spec)
- if path in self._installed_prefixes:
- raise Exception("Install prefix collision.")
- try:
- directory_layout.check_installed(spec)
- installed = True
- except DirectoryLayoutError as e:
- tty.warn(
- 'Dependency missing: may be deprecated or corrupted:',
- path, str(e))
+ # Make sure the directory layout agrees whether the spec is installed
+ if not spec.external and directory_layout:
+ path = directory_layout.path_for_spec(spec)
+ installed = False
+ try:
+ directory_layout.ensure_installed(spec)
+ installed = True
self._installed_prefixes.add(path)
- elif spec.external_path:
- path = spec.external_path
+ except DirectoryLayoutError as e:
+ msg = ("{0} is being {1} in the database with prefix {2}, "
+ "but this directory does not contain an installation of "
+ "the spec, due to: {3}")
+ action = "updated" if key in self._data else "registered"
+ tty.warn(msg.format(spec.short_spec, action, path, str(e)))
+ elif spec.external_path:
+ path = spec.external_path
+ installed = True
+ else:
+ path = None
+ installed = True
+ if key not in self._data:
# Create a new install record with no deps initially.
new_spec = spec.copy(deps=False)
extra_args = {
@@ -1144,9 +1141,8 @@ class Database(object):
new_spec._full_hash = spec._full_hash
else:
- # If it is already there, mark it as installed and update
- # installation time
- self._data[key].installed = True
+ # It is already in the database
+ self._data[key].installed = installed
self._data[key].installation_time = _now()
self._data[key].explicit = explicit
@@ -1213,7 +1209,7 @@ class Database(object):
# This install prefix is now free for other specs to use, even if the
# spec is only marked uninstalled.
- if not rec.spec.external:
+ if not rec.spec.external and rec.installed:
self._installed_prefixes.remove(rec.path)
if rec.ref_count > 0:
diff --git a/lib/spack/spack/directory_layout.py b/lib/spack/spack/directory_layout.py
index be9e7b997b..1351863f68 100644
--- a/lib/spack/spack/directory_layout.py
+++ b/lib/spack/spack/directory_layout.py
@@ -234,13 +234,20 @@ class DirectoryLayout(object):
self.write_spec(spec, self.spec_file_path(spec))
- def check_installed(self, spec):
+ def ensure_installed(self, spec):
+ """
+ Throws DirectoryLayoutError if:
+ 1. spec prefix does not exist
+ 2. spec prefix does not contain a spec file
+ 3. the spec file does not correspond to the spec
+ """
_check_concrete(spec)
path = self.path_for_spec(spec)
spec_file_path = self.spec_file_path(spec)
if not os.path.isdir(path):
- return None
+ raise InconsistentInstallDirectoryError(
+ "Install prefix {0} does not exist.".format(path))
if not os.path.isfile(spec_file_path):
raise InconsistentInstallDirectoryError(
@@ -249,7 +256,7 @@ class DirectoryLayout(object):
installed_spec = self.read_spec(spec_file_path)
if installed_spec == spec:
- return path
+ return
# DAG hashes currently do not include build dependencies.
#
@@ -262,7 +269,7 @@ class DirectoryLayout(object):
# may be installed. This means for example that for two instances
# that differ only in CMake version used to build, only one will
# be installed.
- return path
+ return
if spec.dag_hash() == installed_spec.dag_hash():
raise SpecHashCollisionError(spec, installed_spec)
diff --git a/lib/spack/spack/test/conftest.py b/lib/spack/spack/test/conftest.py
index d805112e5f..cedc9d4f22 100644
--- a/lib/spack/spack/test/conftest.py
+++ b/lib/spack/spack/test/conftest.py
@@ -883,8 +883,8 @@ class MockLayout(object):
def path_for_spec(self, spec):
return '/'.join([self.root, spec.name + '-' + spec.dag_hash()])
- def check_installed(self, spec):
- return True
+ def ensure_installed(self, spec):
+ pass
@pytest.fixture()
diff --git a/lib/spack/spack/test/database.py b/lib/spack/spack/test/database.py
index 50cb467b36..1509dbb335 100644
--- a/lib/spack/spack/test/database.py
+++ b/lib/spack/spack/test/database.py
@@ -11,6 +11,7 @@ import datetime
import functools
import json
import os
+import shutil
import pytest
@@ -924,3 +925,51 @@ def test_store_find_failures(database, query_arg, exc_type, msg_str):
def test_store_find_accept_string(database):
result = spack.store.find('callpath', multiple=True)
assert len(result) == 3
+
+
+def test_reindex_removed_prefix_is_not_installed(mutable_database, mock_store, capfd):
+ """When a prefix of a dependency is removed and the database is reindexed,
+ the spec should still be added through the dependent, but should be listed as
+ not installed."""
+
+ # Remove libelf from the filesystem
+ prefix = mutable_database.query_one('libelf').prefix
+ assert prefix.startswith(str(mock_store))
+ shutil.rmtree(prefix)
+
+ # Reindex should pick up libelf as a dependency of libdwarf
+ spack.store.store.reindex()
+
+ # Reindexing should warn about libelf not being found on the filesystem
+ err = capfd.readouterr()[1]
+ assert 'this directory does not contain an installation of the spec' in err
+
+ # And we should still have libelf in the database, but not installed.
+ assert not mutable_database.query_one('libelf', installed=True)
+ assert mutable_database.query_one('libelf', installed=False)
+
+
+def test_reindex_when_all_prefixes_are_removed(mutable_database, mock_store):
+ # Remove all non-external installations from the filesystem
+ for spec in spack.store.db.query_local():
+ if not spec.external:
+ assert spec.prefix.startswith(str(mock_store))
+ shutil.rmtree(spec.prefix)
+
+ # Make sure we have some explicitly installed specs
+ num = len(mutable_database.query_local(installed=True, explicit=True))
+ assert num > 0
+
+ # Reindex uses the current index to repopulate itself
+ spack.store.store.reindex()
+
+ # Make sure all explicit specs are still there, but are now uninstalled.
+ specs = mutable_database.query_local(installed=False, explicit=True)
+ assert len(specs) == num
+
+ # And make sure they can be removed from the database (covers the case where
+ # `ref_count == 0 and not installed`, which hits some obscure branches.
+ for s in specs:
+ mutable_database.remove(s)
+
+ assert len(mutable_database.query_local(installed=False, explicit=True)) == 0