bugfix: ensure proper dependency handling for package-only installs (#15197)
The distributed build PR (#13100) -- did not check the install status of dependencies when using the `--only package` option so would refuse to install a package with the claim that it had uninstalled dependencies whether that was the case or not. - [x] add install status checks for the `--only package` case. - [x] add initial set of tests
This commit is contained in:
parent
1db1ae6887
commit
73f7301ec4
3 changed files with 149 additions and 17 deletions
|
@ -651,6 +651,66 @@ def _add_bootstrap_compilers(self, pkg):
|
||||||
if package_id(comp_pkg) not in self.build_tasks:
|
if package_id(comp_pkg) not in self.build_tasks:
|
||||||
self._push_task(comp_pkg, is_compiler, 0, 0, STATUS_ADDED)
|
self._push_task(comp_pkg, is_compiler, 0, 0, STATUS_ADDED)
|
||||||
|
|
||||||
|
def _check_db(self, spec):
|
||||||
|
"""Determine if the spec is flagged as installed in the database
|
||||||
|
|
||||||
|
Args:
|
||||||
|
spec (Spec): spec whose database install status is being checked
|
||||||
|
|
||||||
|
Return:
|
||||||
|
(rec, installed_in_db) tuple where rec is the database record, or
|
||||||
|
None, if there is no matching spec, and installed_in_db is
|
||||||
|
``True`` if the spec is considered installed and ``False``
|
||||||
|
otherwise
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
rec = spack.store.db.get_record(spec)
|
||||||
|
installed_in_db = rec.installed if rec else False
|
||||||
|
except KeyError:
|
||||||
|
# KeyError is raised if there is no matching spec in the database
|
||||||
|
# (versus no matching specs that are installed).
|
||||||
|
rec = None
|
||||||
|
installed_in_db = False
|
||||||
|
return rec, installed_in_db
|
||||||
|
|
||||||
|
def _check_deps_status(self):
|
||||||
|
"""Check the install status of the explicit spec's dependencies"""
|
||||||
|
|
||||||
|
err = 'Cannot proceed with {0}: {1}'
|
||||||
|
for dep in self.spec.traverse(order='post', root=False):
|
||||||
|
dep_pkg = dep.package
|
||||||
|
dep_id = package_id(dep_pkg)
|
||||||
|
|
||||||
|
# Check for failure since a prefix lock is not required
|
||||||
|
if spack.store.db.prefix_failed(dep):
|
||||||
|
action = "'spack install' the dependency"
|
||||||
|
msg = '{0} is marked as an install failure: {1}' \
|
||||||
|
.format(dep_id, action)
|
||||||
|
raise InstallError(err.format(self.pkg_id, msg))
|
||||||
|
|
||||||
|
# Attempt to get a write lock to ensure another process does not
|
||||||
|
# uninstall the dependency while the requested spec is being
|
||||||
|
# installed
|
||||||
|
ltype, lock = self._ensure_locked('write', dep_pkg)
|
||||||
|
if lock is None:
|
||||||
|
msg = '{0} is write locked by another process'.format(dep_id)
|
||||||
|
raise InstallError(err.format(self.pkg_id, msg))
|
||||||
|
|
||||||
|
# Flag external and upstream packages as being installed
|
||||||
|
if dep_pkg.spec.external or dep_pkg.installed_upstream:
|
||||||
|
form = 'external' if dep_pkg.spec.external else 'upstream'
|
||||||
|
tty.debug('Flagging {0} {1} as installed'.format(form, dep_id))
|
||||||
|
self.installed.add(dep_id)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Check the database to see if the dependency has been installed
|
||||||
|
# and flag as such if appropriate
|
||||||
|
rec, installed_in_db = self._check_db(dep)
|
||||||
|
if installed_in_db:
|
||||||
|
tty.debug('Flagging {0} as installed per the database'
|
||||||
|
.format(dep_id))
|
||||||
|
self.installed.add(dep_id)
|
||||||
|
|
||||||
def _prepare_for_install(self, task, keep_prefix, keep_stage,
|
def _prepare_for_install(self, task, keep_prefix, keep_stage,
|
||||||
restage=False):
|
restage=False):
|
||||||
"""
|
"""
|
||||||
|
@ -680,14 +740,7 @@ def _prepare_for_install(self, task, keep_prefix, keep_stage,
|
||||||
return
|
return
|
||||||
|
|
||||||
# Determine if the spec is flagged as installed in the database
|
# Determine if the spec is flagged as installed in the database
|
||||||
try:
|
rec, installed_in_db = self._check_db(task.pkg.spec)
|
||||||
rec = spack.store.db.get_record(task.pkg.spec)
|
|
||||||
installed_in_db = rec.installed if rec else False
|
|
||||||
except KeyError:
|
|
||||||
# KeyError is raised if there is no matching spec in the database
|
|
||||||
# (versus no matching specs that are installed).
|
|
||||||
rec = None
|
|
||||||
installed_in_db = False
|
|
||||||
|
|
||||||
# Make sure the installation directory is in the desired state
|
# Make sure the installation directory is in the desired state
|
||||||
# for uninstalled specs.
|
# for uninstalled specs.
|
||||||
|
@ -929,6 +982,11 @@ def _init_queue(self, install_deps, install_package):
|
||||||
# Be sure to clear any previous failure
|
# Be sure to clear any previous failure
|
||||||
spack.store.db.clear_failure(self.pkg.spec, force=True)
|
spack.store.db.clear_failure(self.pkg.spec, force=True)
|
||||||
|
|
||||||
|
# If not installing dependencies, then determine their
|
||||||
|
# installation status before proceeding
|
||||||
|
if not install_deps:
|
||||||
|
self._check_deps_status()
|
||||||
|
|
||||||
# Now add the package itself, if appropriate
|
# Now add the package itself, if appropriate
|
||||||
self._push_task(self.pkg, False, 0, 0, STATUS_ADDED)
|
self._push_task(self.pkg, False, 0, 0, STATUS_ADDED)
|
||||||
|
|
||||||
|
@ -1022,7 +1080,8 @@ def build_process():
|
||||||
configure_args = getattr(pkg, attr)()
|
configure_args = getattr(pkg, attr)()
|
||||||
configure_args = ' '.join(configure_args)
|
configure_args = ' '.join(configure_args)
|
||||||
|
|
||||||
with open(pkg.configure_args_path, 'w') as args_file:
|
with open(pkg.configure_args_path, 'w') as \
|
||||||
|
args_file:
|
||||||
args_file.write(configure_args)
|
args_file.write(configure_args)
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
|
@ -632,6 +632,30 @@ def test_install_only_dependencies(tmpdir, mock_fetch, install_mockery):
|
||||||
assert not os.path.exists(root.prefix)
|
assert not os.path.exists(root.prefix)
|
||||||
|
|
||||||
|
|
||||||
|
def test_install_only_package(tmpdir, mock_fetch, install_mockery, capfd):
|
||||||
|
msg = ''
|
||||||
|
with capfd.disabled():
|
||||||
|
try:
|
||||||
|
install('--only', 'package', 'dependent-install')
|
||||||
|
except spack.installer.InstallError as e:
|
||||||
|
msg = str(e)
|
||||||
|
|
||||||
|
assert 'Cannot proceed with dependent-install' in msg
|
||||||
|
assert '1 uninstalled dependency' in msg
|
||||||
|
|
||||||
|
|
||||||
|
def test_install_deps_then_package(tmpdir, mock_fetch, install_mockery):
|
||||||
|
dep = Spec('dependency-install').concretized()
|
||||||
|
root = Spec('dependent-install').concretized()
|
||||||
|
|
||||||
|
install('--only', 'dependencies', 'dependent-install')
|
||||||
|
assert os.path.exists(dep.prefix)
|
||||||
|
assert not os.path.exists(root.prefix)
|
||||||
|
|
||||||
|
install('--only', 'package', 'dependent-install')
|
||||||
|
assert os.path.exists(root.prefix)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.regression('12002')
|
@pytest.mark.regression('12002')
|
||||||
def test_install_only_dependencies_in_env(tmpdir, mock_fetch, install_mockery,
|
def test_install_only_dependencies_in_env(tmpdir, mock_fetch, install_mockery,
|
||||||
mutable_mock_env_path):
|
mutable_mock_env_path):
|
||||||
|
|
|
@ -27,6 +27,12 @@ def _none(*args, **kwargs):
|
||||||
return None
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _not_locked(installer, lock_type, pkg):
|
||||||
|
"""Generic monkeypatch function for _ensure_locked to return no lock"""
|
||||||
|
tty.msg('{0} locked {1}' .format(lock_type, pkg.spec.name))
|
||||||
|
return lock_type, None
|
||||||
|
|
||||||
|
|
||||||
def _true(*args, **kwargs):
|
def _true(*args, **kwargs):
|
||||||
"""Generic monkeypatch function that always returns True."""
|
"""Generic monkeypatch function that always returns True."""
|
||||||
return True
|
return True
|
||||||
|
@ -285,6 +291,57 @@ def test_dump_packages_deps(install_mockery, tmpdir):
|
||||||
inst.dump_packages(spec, '.')
|
inst.dump_packages(spec, '.')
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.tld
|
||||||
|
def test_check_deps_status_errs(install_mockery, monkeypatch):
|
||||||
|
"""Test to cover _check_deps_status failures."""
|
||||||
|
spec, installer = create_installer('a')
|
||||||
|
|
||||||
|
# Make sure the package is identified as failed
|
||||||
|
orig_fn = spack.database.Database.prefix_failed
|
||||||
|
monkeypatch.setattr(spack.database.Database, 'prefix_failed', _true)
|
||||||
|
|
||||||
|
with pytest.raises(inst.InstallError, matches='install failure'):
|
||||||
|
installer._check_deps_status()
|
||||||
|
|
||||||
|
monkeypatch.setattr(spack.database.Database, 'prefix_failed', orig_fn)
|
||||||
|
|
||||||
|
# Ensure do not acquire the lock
|
||||||
|
monkeypatch.setattr(inst.PackageInstaller, '_ensure_locked', _not_locked)
|
||||||
|
|
||||||
|
with pytest.raises(inst.InstallError, matches='write locked by another'):
|
||||||
|
installer._check_deps_status()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.tld
|
||||||
|
def test_check_deps_status_external(install_mockery, monkeypatch):
|
||||||
|
"""Test to cover _check_deps_status for external."""
|
||||||
|
spec, installer = create_installer('a')
|
||||||
|
|
||||||
|
deps = spec.dependencies()
|
||||||
|
assert len(deps) > 0
|
||||||
|
dep_id = 'b'
|
||||||
|
|
||||||
|
# Ensure the known dependent is installed if flagged as external
|
||||||
|
monkeypatch.setattr(spack.spec.Spec, 'external', True)
|
||||||
|
installer._check_deps_status()
|
||||||
|
assert dep_id in installer.installed
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.tld
|
||||||
|
def test_check_deps_status_upstream(install_mockery, monkeypatch):
|
||||||
|
"""Test to cover _check_deps_status for upstream."""
|
||||||
|
spec, installer = create_installer('a')
|
||||||
|
|
||||||
|
deps = spec.dependencies()
|
||||||
|
assert len(deps) > 0
|
||||||
|
dep_id = 'b'
|
||||||
|
|
||||||
|
# Ensure the known dependent, b, is installed if flagged as upstream
|
||||||
|
monkeypatch.setattr(spack.package.PackageBase, 'installed_upstream', True)
|
||||||
|
installer._check_deps_status()
|
||||||
|
assert dep_id in installer.installed
|
||||||
|
|
||||||
|
|
||||||
def test_add_bootstrap_compilers(install_mockery, monkeypatch):
|
def test_add_bootstrap_compilers(install_mockery, monkeypatch):
|
||||||
"""Test to cover _add_bootstrap_compilers."""
|
"""Test to cover _add_bootstrap_compilers."""
|
||||||
def _pkgs(pkg):
|
def _pkgs(pkg):
|
||||||
|
@ -458,10 +515,6 @@ def test_install_lock_failures(install_mockery, monkeypatch, capfd):
|
||||||
def _requeued(installer, task):
|
def _requeued(installer, task):
|
||||||
tty.msg('requeued {0}' .format(task.pkg.spec.name))
|
tty.msg('requeued {0}' .format(task.pkg.spec.name))
|
||||||
|
|
||||||
def _not_locked(installer, lock_type, pkg):
|
|
||||||
tty.msg('{0} locked {1}' .format(lock_type, pkg.spec.name))
|
|
||||||
return lock_type, None
|
|
||||||
|
|
||||||
spec, installer = create_installer('b')
|
spec, installer = create_installer('b')
|
||||||
|
|
||||||
# Ensure never acquire a lock
|
# Ensure never acquire a lock
|
||||||
|
@ -485,10 +538,6 @@ def test_install_lock_installed_requeue(install_mockery, monkeypatch, capfd):
|
||||||
def _install(installer, task, **kwargs):
|
def _install(installer, task, **kwargs):
|
||||||
tty.msg('{0} installing'.format(task.pkg.spec.name))
|
tty.msg('{0} installing'.format(task.pkg.spec.name))
|
||||||
|
|
||||||
def _not_locked(installer, lock_type, pkg):
|
|
||||||
tty.msg('{0} locked {1}' .format(lock_type, pkg.spec.name))
|
|
||||||
return lock_type, None
|
|
||||||
|
|
||||||
def _prep(installer, task, keep_prefix, keep_stage, restage):
|
def _prep(installer, task, keep_prefix, keep_stage, restage):
|
||||||
installer.installed.add('b')
|
installer.installed.add('b')
|
||||||
tty.msg('{0} is installed' .format(task.pkg.spec.name))
|
tty.msg('{0} is installed' .format(task.pkg.spec.name))
|
||||||
|
|
Loading…
Reference in a new issue