diff options
author | Harmen Stoppels <harmenstoppels@gmail.com> | 2021-06-29 23:44:56 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2021-06-29 14:44:56 -0700 |
commit | 304249604ab465dbfc3ee560884bf4a1460f1e34 (patch) | |
tree | d46ff02da323ee6a7dbe96100a8fa41c77cb1c76 /lib | |
parent | 1eb2798c430e221ab945334014a60e0254d80f67 (diff) | |
download | spack-304249604ab465dbfc3ee560884bf4a1460f1e34.tar.gz spack-304249604ab465dbfc3ee560884bf4a1460f1e34.tar.bz2 spack-304249604ab465dbfc3ee560884bf4a1460f1e34.tar.xz spack-304249604ab465dbfc3ee560884bf4a1460f1e34.zip |
Fix prefix-collision detection for projections (#24049)
If two Specs have the same hash (and prefix) but are not equal, Spack
originally had logic to detect this and raise an error (since both
cannot be installed in the same place). Recently this has eroded and
the check no-longer works; moreover, when defining projections (which
may truncate the hash or other distinguishing properties from the
prefix) Spack was also failing to detect collisions (in both of these
cases, Spack would overwrite the old prefix with the new Spec).
This PR maintains a list of all "taken" prefixes: if a hash is not
registered (i.e. recorded as installed in the database) but the prefix
is occupied, that is a collision. This can detect collisions created
by defining projections (specifically when they omit the hash).
The PR does not detect collisions where specs have the same hash
(and prefix) but are not equal.
Diffstat (limited to 'lib')
-rw-r--r-- | lib/spack/spack/database.py | 32 | ||||
-rw-r--r-- | lib/spack/spack/installer.py | 37 | ||||
-rw-r--r-- | lib/spack/spack/test/conftest.py | 18 | ||||
-rw-r--r-- | lib/spack/spack/test/install.py | 25 |
4 files changed, 80 insertions, 32 deletions
diff --git a/lib/spack/spack/database.py b/lib/spack/spack/database.py index dee9a90d10..d998895fed 100644 --- a/lib/spack/spack/database.py +++ b/lib/spack/spack/database.py @@ -23,12 +23,13 @@ filesystem. import contextlib import datetime import os -import six import socket import sys import time from typing import Dict # novm +import six + try: import uuid _use_uuid = True @@ -38,7 +39,6 @@ except ImportError: import llnl.util.filesystem as fs import llnl.util.tty as tty - import spack.repo import spack.spec import spack.store @@ -382,6 +382,11 @@ class Database(object): desc='database') self._data = {} + # For every installed spec we keep track of its install prefix, so that + # we can answer the simple query whether a given path is already taken + # before installing a different spec. + self._installed_prefixes = set() + self.upstream_dbs = list(upstream_dbs) if upstream_dbs else [] # whether there was an error at the start of a read transaction @@ -774,6 +779,7 @@ class Database(object): # Pass 1: Iterate through database and build specs w/o dependencies data = {} + installed_prefixes = set() for hash_key, rec in installs.items(): try: # This constructs a spec DAG from the list of all installs @@ -784,6 +790,9 @@ class Database(object): # TODO: would a more immmutable spec implementation simplify # this? data[hash_key] = InstallRecord.from_dict(spec, rec) + + if not spec.external and 'installed' in rec and rec['installed']: + installed_prefixes.add(rec['path']) except Exception as e: invalid_record(hash_key, e) @@ -805,6 +814,7 @@ class Database(object): rec.spec._mark_root_concrete() self._data = data + self._installed_prefixes = installed_prefixes def reindex(self, directory_layout): """Build database index from scratch based on a directory layout. @@ -824,6 +834,7 @@ class Database(object): except CorruptDatabaseError as e: self._error = e self._data = {} + self._installed_prefixes = set() transaction = lk.WriteTransaction( self.lock, acquire=_read_suppress_error, release=self._write @@ -838,12 +849,14 @@ class Database(object): self._error = None old_data = self._data + old_installed_prefixes = self._installed_prefixes try: self._construct_from_directory_layout( directory_layout, old_data) except BaseException: # If anything explodes, restore old data, skip write. self._data = old_data + self._installed_prefixes = old_installed_prefixes raise def _construct_entry_from_directory_layout(self, directory_layout, @@ -880,6 +893,7 @@ class Database(object): with directory_layout.disable_upstream_check(): # Initialize data in the reconstructed DB self._data = {} + self._installed_prefixes = set() # Start inspecting the installed prefixes processed_specs = set() @@ -1087,6 +1101,8 @@ class Database(object): 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 @@ -1094,6 +1110,7 @@ class Database(object): tty.warn( 'Dependency missing: may be deprecated or corrupted:', path, str(e)) + self._installed_prefixes.add(path) elif spec.external_path: path = spec.external_path @@ -1173,6 +1190,7 @@ class Database(object): if rec.ref_count == 0 and not rec.installed: del self._data[key] + for dep in spec.dependencies(_tracked_deps): self._decrement_ref_count(dep) @@ -1190,11 +1208,17 @@ class Database(object): key = self._get_matching_spec_key(spec) rec = self._data[key] + # This install prefix is now free for other specs to use, even if the + # spec is only marked uninstalled. + if not rec.spec.external: + self._installed_prefixes.remove(rec.path) + if rec.ref_count > 0: rec.installed = False return rec.spec del self._data[key] + for dep in rec.spec.dependencies(_tracked_deps): # FIXME: the two lines below needs to be updated once #11983 is # FIXME: fixed. The "if" statement should be deleted and specs are @@ -1538,6 +1562,10 @@ class Database(object): upstream, record = self.query_by_spec_hash(key) return record and not record.installed + def is_occupied_install_prefix(self, path): + with self.read_transaction(): + return path in self._installed_prefixes + @property def unused_specs(self): """Return all the specs that are currently installed but not needed diff --git a/lib/spack/spack/installer.py b/lib/spack/spack/installer.py index fd6bc31d4b..50218be5b9 100644 --- a/lib/spack/spack/installer.py +++ b/lib/spack/spack/installer.py @@ -33,12 +33,13 @@ import heapq import itertools import os import shutil -import six import sys import time - from collections import defaultdict +import six + + import llnl.util.filesystem as fs import llnl.util.lock as lk import llnl.util.tty as tty @@ -51,7 +52,6 @@ import spack.package import spack.package_prefs as prefs import spack.repo import spack.store - from llnl.util.tty.color import colorize from llnl.util.tty.log import log_output from spack.util.environment import dump_environment @@ -814,24 +814,29 @@ class PackageInstaller(object): # Determine if the spec is flagged as installed in the database rec, installed_in_db = self._check_db(task.pkg.spec) - # Make sure the installation directory is in the desired state - # for uninstalled specs. - partial = False - if not installed_in_db and os.path.isdir(task.pkg.spec.prefix): - if not keep_prefix: - task.pkg.remove_prefix() - else: - tty.debug('{0} is partially installed' - .format(task.pkg_id)) - partial = True + if not installed_in_db: + # Ensure there is no other installed spec with the same prefix dir + if spack.store.db.is_occupied_install_prefix(task.pkg.spec.prefix): + raise InstallError( + "Install prefix collision for {0}".format(task.pkg_id), + long_msg="Prefix directory {0} already used by another " + "installed spec.".format(task.pkg.spec.prefix)) + + # Make sure the installation directory is in the desired state + # for uninstalled specs. + if os.path.isdir(task.pkg.spec.prefix): + if not keep_prefix: + task.pkg.remove_prefix() + else: + tty.debug('{0} is partially installed'.format(task.pkg_id)) # Destroy the stage for a locally installed, non-DIYStage, package if restage and task.pkg.stage.managed_by_spack: task.pkg.stage.destroy() - if not partial and self.layout.check_installed(task.pkg.spec) and ( - rec.spec.dag_hash() not in task.request.overwrite or - rec.installation_time > task.request.overwrite_time + if installed_in_db and ( + rec.spec.dag_hash() not in task.request.overwrite or + rec.installation_time > task.request.overwrite_time ): self._update_installed(task) diff --git a/lib/spack/spack/test/conftest.py b/lib/spack/spack/test/conftest.py index 2443d0edf0..3d9f9429dc 100644 --- a/lib/spack/spack/test/conftest.py +++ b/lib/spack/spack/test/conftest.py @@ -26,17 +26,15 @@ except ImportError: def parse_date(string): # type: ignore pytest.skip("dateutil package not available") -import py -import pytest - import archspec.cpu.microarchitecture import archspec.cpu.schema -from llnl.util.filesystem import mkdirp, remove_linked_tree, working_dir +import py +import pytest import spack.architecture +import spack.caches import spack.compilers import spack.config -import spack.caches import spack.database import spack.directory_layout import spack.environment as ev @@ -47,14 +45,14 @@ import spack.platforms.test import spack.repo import spack.stage import spack.store +import spack.subprocess_context import spack.util.executable import spack.util.gpg -import spack.subprocess_context import spack.util.spack_yaml as syaml - -from spack.util.pattern import Bunch -from spack.fetch_strategy import FetchStrategyComposite, URLFetchStrategy +from llnl.util.filesystem import mkdirp, remove_linked_tree, working_dir from spack.fetch_strategy import FetchError +from spack.fetch_strategy import FetchStrategyComposite, URLFetchStrategy +from spack.util.pattern import Bunch # @@ -767,7 +765,7 @@ class MockLayout(object): self.root = root def path_for_spec(self, spec): - return '/'.join([self.root, spec.name]) + return '/'.join([self.root, spec.name + '-' + spec.dag_hash()]) def check_installed(self, spec): return True diff --git a/lib/spack/spack/test/install.py b/lib/spack/spack/test/install.py index 6a533bb8b1..90bab99c20 100644 --- a/lib/spack/spack/test/install.py +++ b/lib/spack/spack/test/install.py @@ -4,20 +4,20 @@ # SPDX-License-Identifier: (Apache-2.0 OR MIT) import os -import pytest import shutil -import llnl.util.filesystem as fs +import pytest -from spack.package import InstallError, PackageBase, PackageStillNeededError +import llnl.util.filesystem as fs import spack.error import spack.patch import spack.repo import spack.store -from spack.spec import Spec import spack.util.spack_json as sjson +from spack.package import InstallError, PackageBase, PackageStillNeededError from spack.package import (_spack_build_envfile, _spack_build_logfile, _spack_configure_argsfile) +from spack.spec import Spec def find_nothing(*args): @@ -325,6 +325,23 @@ def test_second_install_no_overwrite_first(install_mockery, mock_fetch): spack.package.Package.remove_prefix = remove_prefix +def test_install_prefix_collision_fails(config, mock_fetch, mock_packages, tmpdir): + """ + Test that different specs with coinciding install prefixes will fail + to install. + """ + projections = {'all': 'all-specs-project-to-this-prefix'} + store = spack.store.Store(str(tmpdir), projections=projections) + with spack.store.use_store(store): + with spack.config.override('config:checksum', False): + pkg_a = Spec('libelf@0.8.13').concretized().package + pkg_b = Spec('libelf@0.8.12').concretized().package + pkg_a.do_install() + + with pytest.raises(InstallError, match="Install prefix collision"): + pkg_b.do_install() + + def test_store(install_mockery, mock_fetch): spec = Spec('cmake-client').concretized() pkg = spec.package |