installer.py: don't dereference stage before installing from binaries (#41986)
This fixes an issue where pkg.stage throws because a patch cannot be found, but the patch is redundant because the spec is reused from a build cache and will be installed from existing binaries.
This commit is contained in:
		@@ -1326,7 +1326,6 @@ def _prepare_for_install(self, task: BuildTask) -> None:
 | 
				
			|||||||
        """
 | 
					        """
 | 
				
			||||||
        install_args = task.request.install_args
 | 
					        install_args = task.request.install_args
 | 
				
			||||||
        keep_prefix = install_args.get("keep_prefix")
 | 
					        keep_prefix = install_args.get("keep_prefix")
 | 
				
			||||||
        restage = install_args.get("restage")
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # Make sure the package is ready to be locally installed.
 | 
					        # Make sure the package is ready to be locally installed.
 | 
				
			||||||
        self._ensure_install_ready(task.pkg)
 | 
					        self._ensure_install_ready(task.pkg)
 | 
				
			||||||
@@ -1358,10 +1357,6 @@ def _prepare_for_install(self, task: BuildTask) -> None:
 | 
				
			|||||||
                else:
 | 
					                else:
 | 
				
			||||||
                    tty.debug(f"{task.pkg_id} is partially installed")
 | 
					                    tty.debug(f"{task.pkg_id} is partially installed")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # Destroy the stage for a locally installed, non-DIYStage, package
 | 
					 | 
				
			||||||
            if restage and task.pkg.stage.managed_by_spack:
 | 
					 | 
				
			||||||
                task.pkg.stage.destroy()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        if (
 | 
					        if (
 | 
				
			||||||
            rec
 | 
					            rec
 | 
				
			||||||
            and installed_in_db
 | 
					            and installed_in_db
 | 
				
			||||||
@@ -1687,6 +1682,10 @@ def _install_task(self, task: BuildTask, install_status: InstallStatus) -> None:
 | 
				
			|||||||
        try:
 | 
					        try:
 | 
				
			||||||
            self._setup_install_dir(pkg)
 | 
					            self._setup_install_dir(pkg)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            # Create stage object now and let it be serialized for the child process. That
 | 
				
			||||||
 | 
					            # way monkeypatch in tests works correctly.
 | 
				
			||||||
 | 
					            pkg.stage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            # Create a child process to do the actual installation.
 | 
					            # Create a child process to do the actual installation.
 | 
				
			||||||
            # Preserve verbosity settings across installs.
 | 
					            # Preserve verbosity settings across installs.
 | 
				
			||||||
            spack.package_base.PackageBase._verbose = spack.build_environment.start_build_process(
 | 
					            spack.package_base.PackageBase._verbose = spack.build_environment.start_build_process(
 | 
				
			||||||
@@ -2221,11 +2220,6 @@ def install(self) -> None:
 | 
				
			|||||||
                if not keep_prefix and not action == InstallAction.OVERWRITE:
 | 
					                if not keep_prefix and not action == InstallAction.OVERWRITE:
 | 
				
			||||||
                    pkg.remove_prefix()
 | 
					                    pkg.remove_prefix()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
                # The subprocess *may* have removed the build stage. Mark it
 | 
					 | 
				
			||||||
                # not created so that the next time pkg.stage is invoked, we
 | 
					 | 
				
			||||||
                # check the filesystem for it.
 | 
					 | 
				
			||||||
                pkg.stage.created = False
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            # Perform basic task cleanup for the installed spec to
 | 
					            # Perform basic task cleanup for the installed spec to
 | 
				
			||||||
            # include downgrading the write to a read lock
 | 
					            # include downgrading the write to a read lock
 | 
				
			||||||
            self._cleanup_task(pkg)
 | 
					            self._cleanup_task(pkg)
 | 
				
			||||||
@@ -2295,6 +2289,9 @@ def __init__(self, pkg: "spack.package_base.PackageBase", install_args: dict):
 | 
				
			|||||||
        # whether to keep the build stage after installation
 | 
					        # whether to keep the build stage after installation
 | 
				
			||||||
        self.keep_stage = install_args.get("keep_stage", False)
 | 
					        self.keep_stage = install_args.get("keep_stage", False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        # whether to restage
 | 
				
			||||||
 | 
					        self.restage = install_args.get("restage", False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        # whether to skip the patch phase
 | 
					        # whether to skip the patch phase
 | 
				
			||||||
        self.skip_patch = install_args.get("skip_patch", False)
 | 
					        self.skip_patch = install_args.get("skip_patch", False)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -2325,9 +2322,13 @@ def __init__(self, pkg: "spack.package_base.PackageBase", install_args: dict):
 | 
				
			|||||||
    def run(self) -> bool:
 | 
					    def run(self) -> bool:
 | 
				
			||||||
        """Main entry point from ``build_process`` to kick off install in child."""
 | 
					        """Main entry point from ``build_process`` to kick off install in child."""
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.pkg.stage.keep = self.keep_stage
 | 
					        stage = self.pkg.stage
 | 
				
			||||||
 | 
					        stage.keep = self.keep_stage
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        with self.pkg.stage:
 | 
					        if self.restage:
 | 
				
			||||||
 | 
					            stage.destroy()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        with stage:
 | 
				
			||||||
            self.timer.start("stage")
 | 
					            self.timer.start("stage")
 | 
				
			||||||
 | 
					
 | 
				
			||||||
            if not self.fake:
 | 
					            if not self.fake:
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -12,10 +12,12 @@
 | 
				
			|||||||
import llnl.util.filesystem as fs
 | 
					import llnl.util.filesystem as fs
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import spack.error
 | 
					import spack.error
 | 
				
			||||||
 | 
					import spack.mirror
 | 
				
			||||||
import spack.patch
 | 
					import spack.patch
 | 
				
			||||||
import spack.repo
 | 
					import spack.repo
 | 
				
			||||||
import spack.store
 | 
					import spack.store
 | 
				
			||||||
import spack.util.spack_json as sjson
 | 
					import spack.util.spack_json as sjson
 | 
				
			||||||
 | 
					from spack import binary_distribution
 | 
				
			||||||
from spack.package_base import (
 | 
					from spack.package_base import (
 | 
				
			||||||
    InstallError,
 | 
					    InstallError,
 | 
				
			||||||
    PackageBase,
 | 
					    PackageBase,
 | 
				
			||||||
@@ -118,39 +120,11 @@ def remove_prefix(self):
 | 
				
			|||||||
        self.wrapped_rm_prefix()
 | 
					        self.wrapped_rm_prefix()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
class MockStage:
 | 
					 | 
				
			||||||
    def __init__(self, wrapped_stage):
 | 
					 | 
				
			||||||
        self.wrapped_stage = wrapped_stage
 | 
					 | 
				
			||||||
        self.test_destroyed = False
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def __enter__(self):
 | 
					 | 
				
			||||||
        self.create()
 | 
					 | 
				
			||||||
        return self
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def __exit__(self, exc_type, exc_val, exc_tb):
 | 
					 | 
				
			||||||
        if exc_type is None:
 | 
					 | 
				
			||||||
            self.destroy()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def destroy(self):
 | 
					 | 
				
			||||||
        self.test_destroyed = True
 | 
					 | 
				
			||||||
        self.wrapped_stage.destroy()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def create(self):
 | 
					 | 
				
			||||||
        self.wrapped_stage.create()
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    def __getattr__(self, attr):
 | 
					 | 
				
			||||||
        if attr == "wrapped_stage":
 | 
					 | 
				
			||||||
            # This attribute may not be defined at some point during unpickling
 | 
					 | 
				
			||||||
            raise AttributeError()
 | 
					 | 
				
			||||||
        return getattr(self.wrapped_stage, attr)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
def test_partial_install_delete_prefix_and_stage(install_mockery, mock_fetch, working_env):
 | 
					def test_partial_install_delete_prefix_and_stage(install_mockery, mock_fetch, working_env):
 | 
				
			||||||
    s = Spec("canfail").concretized()
 | 
					    s = Spec("canfail").concretized()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    instance_rm_prefix = s.package.remove_prefix
 | 
					    instance_rm_prefix = s.package.remove_prefix
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    try:
 | 
					 | 
				
			||||||
    s.package.remove_prefix = mock_remove_prefix
 | 
					    s.package.remove_prefix = mock_remove_prefix
 | 
				
			||||||
    with pytest.raises(MockInstallError):
 | 
					    with pytest.raises(MockInstallError):
 | 
				
			||||||
        s.package.do_install()
 | 
					        s.package.do_install()
 | 
				
			||||||
@@ -162,16 +136,10 @@ def test_partial_install_delete_prefix_and_stage(install_mockery, mock_fetch, wo
 | 
				
			|||||||
    spack.store.STORE.failure_tracker.clear(s, True)
 | 
					    spack.store.STORE.failure_tracker.clear(s, True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    s.package.set_install_succeed()
 | 
					    s.package.set_install_succeed()
 | 
				
			||||||
        s.package.stage = MockStage(s.package.stage)
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    s.package.do_install(restage=True)
 | 
					    s.package.do_install(restage=True)
 | 
				
			||||||
    assert rm_prefix_checker.removed
 | 
					    assert rm_prefix_checker.removed
 | 
				
			||||||
        assert s.package.stage.test_destroyed
 | 
					 | 
				
			||||||
    assert s.package.spec.installed
 | 
					    assert s.package.spec.installed
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    finally:
 | 
					 | 
				
			||||||
        s.package.remove_prefix = instance_rm_prefix
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
@pytest.mark.disable_clean_stage_check
 | 
					@pytest.mark.disable_clean_stage_check
 | 
				
			||||||
def test_failing_overwrite_install_should_keep_previous_installation(
 | 
					def test_failing_overwrite_install_should_keep_previous_installation(
 | 
				
			||||||
@@ -357,10 +325,8 @@ def test_partial_install_keep_prefix(install_mockery, mock_fetch, monkeypatch, w
 | 
				
			|||||||
    spack.store.STORE.failure_tracker.clear(s, True)
 | 
					    spack.store.STORE.failure_tracker.clear(s, True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    s.package.set_install_succeed()
 | 
					    s.package.set_install_succeed()
 | 
				
			||||||
    s.package.stage = MockStage(s.package.stage)
 | 
					 | 
				
			||||||
    s.package.do_install(keep_prefix=True)
 | 
					    s.package.do_install(keep_prefix=True)
 | 
				
			||||||
    assert s.package.spec.installed
 | 
					    assert s.package.spec.installed
 | 
				
			||||||
    assert not s.package.stage.test_destroyed
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
def test_second_install_no_overwrite_first(install_mockery, mock_fetch, monkeypatch):
 | 
					def test_second_install_no_overwrite_first(install_mockery, mock_fetch, monkeypatch):
 | 
				
			||||||
@@ -644,3 +610,48 @@ def test_empty_install_sanity_check_prefix(
 | 
				
			|||||||
    spec = Spec("failing-empty-install").concretized()
 | 
					    spec = Spec("failing-empty-install").concretized()
 | 
				
			||||||
    with pytest.raises(spack.build_environment.ChildError, match="Nothing was installed"):
 | 
					    with pytest.raises(spack.build_environment.ChildError, match="Nothing was installed"):
 | 
				
			||||||
        spec.package.do_install()
 | 
					        spec.package.do_install()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					def test_install_from_binary_with_missing_patch_succeeds(
 | 
				
			||||||
 | 
					    temporary_store: spack.store.Store, mutable_config, tmp_path, mock_packages
 | 
				
			||||||
 | 
					):
 | 
				
			||||||
 | 
					    """If a patch is missing in the local package repository, but was present when building and
 | 
				
			||||||
 | 
					    pushing the package to a binary cache, installation from that binary cache shouldn't error out
 | 
				
			||||||
 | 
					    because of the missing patch."""
 | 
				
			||||||
 | 
					    # Create a spec s with non-existing patches
 | 
				
			||||||
 | 
					    s = Spec("trivial-install-test-package").concretized()
 | 
				
			||||||
 | 
					    patches = ["a" * 64]
 | 
				
			||||||
 | 
					    s_dict = s.to_dict()
 | 
				
			||||||
 | 
					    s_dict["spec"]["nodes"][0]["patches"] = patches
 | 
				
			||||||
 | 
					    s_dict["spec"]["nodes"][0]["parameters"]["patches"] = patches
 | 
				
			||||||
 | 
					    s = Spec.from_dict(s_dict)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Create an install dir for it
 | 
				
			||||||
 | 
					    os.makedirs(os.path.join(s.prefix, ".spack"))
 | 
				
			||||||
 | 
					    with open(os.path.join(s.prefix, ".spack", "spec.json"), "w") as f:
 | 
				
			||||||
 | 
					        s.to_json(f)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # And register it in the database
 | 
				
			||||||
 | 
					    temporary_store.db.add(s, directory_layout=temporary_store.layout, explicit=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Push it to a binary cache
 | 
				
			||||||
 | 
					    build_cache = tmp_path / "my_build_cache"
 | 
				
			||||||
 | 
					    binary_distribution.push_or_raise(
 | 
				
			||||||
 | 
					        s,
 | 
				
			||||||
 | 
					        build_cache.as_uri(),
 | 
				
			||||||
 | 
					        binary_distribution.PushOptions(unsigned=True, regenerate_index=True),
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Now re-install it.
 | 
				
			||||||
 | 
					    s.package.do_uninstall()
 | 
				
			||||||
 | 
					    assert not temporary_store.db.query_local_by_spec_hash(s.dag_hash())
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Source install: fails, we don't have the patch.
 | 
				
			||||||
 | 
					    with pytest.raises(spack.error.SpecError, match="Couldn't find patch for package"):
 | 
				
			||||||
 | 
					        s.package.do_install()
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    # Binary install: succeeds, we don't need the patch.
 | 
				
			||||||
 | 
					    spack.mirror.add(spack.mirror.Mirror.from_local_path(str(build_cache)))
 | 
				
			||||||
 | 
					    s.package.do_install(package_cache_only=True, dependencies_cache_only=True, unsigned=True)
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    assert temporary_store.db.query_local_by_spec_hash(s.dag_hash())
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user