spack deconcretize command (#38803)
				
					
				
			We have two ways to concretize now: * `spack concretize` concretizes only the root specs that are not concrete in the environment. * `spack concretize -f` eliminates all cached concretization data and reconcretizes the *entire* environment. This PR adds `spack deconcretize`, which eliminates cached concretization data for a spec. This allows users greater control over what is preserved from their `spack.lock` file and what is reused when not using `spack concretize -f`. If you want to update a spec installed in your environment, you can call `spack deconcretize` on it, and that spec and any relevant dependents will be removed from the lock file. `spack concretize` has two options: * `--root`: limits deconcretized specs to *specific* roots in the environment. You can use this to deconcretize exactly one root in a `unify: false` environment. i.e., if `foo` root is a dependent of `bar`, both roots, `spack deconcretize bar` will *not* deconcretize `foo`. * `--all`: deconcretize *all* specs that match the input spec. By default `spack deconcretize` will complain about multiple matches, like `spack uninstall`.
This commit is contained in:
		
							
								
								
									
										30
									
								
								lib/spack/spack/cmd/common/confirmation.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								lib/spack/spack/cmd/common/confirmation.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
# Copyright 2013-2023 Lawrence Livermore National Security, LLC and other
 | 
			
		||||
# Spack Project Developers. See the top-level COPYRIGHT file for details.
 | 
			
		||||
#
 | 
			
		||||
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
 | 
			
		||||
 | 
			
		||||
import sys
 | 
			
		||||
from typing import List
 | 
			
		||||
 | 
			
		||||
import llnl.util.tty as tty
 | 
			
		||||
 | 
			
		||||
import spack.cmd
 | 
			
		||||
 | 
			
		||||
display_args = {"long": True, "show_flags": False, "variants": False, "indent": 4}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def confirm_action(specs: List[spack.spec.Spec], participle: str, noun: str):
 | 
			
		||||
    """Display the list of specs to be acted on and ask for confirmation.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        specs: specs to be removed
 | 
			
		||||
        participle: action expressed as a participle, e.g. "uninstalled"
 | 
			
		||||
        noun: action expressed as a noun, e.g. "uninstallation"
 | 
			
		||||
    """
 | 
			
		||||
    tty.msg(f"The following {len(specs)} packages will be {participle}:\n")
 | 
			
		||||
    spack.cmd.display_specs(specs, **display_args)
 | 
			
		||||
    print("")
 | 
			
		||||
    answer = tty.get_yes_or_no("Do you want to proceed?", default=False)
 | 
			
		||||
    if not answer:
 | 
			
		||||
        tty.msg(f"Aborting {noun}")
 | 
			
		||||
        sys.exit(0)
 | 
			
		||||
							
								
								
									
										103
									
								
								lib/spack/spack/cmd/deconcretize.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								lib/spack/spack/cmd/deconcretize.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,103 @@
 | 
			
		||||
# Copyright 2013-2023 Lawrence Livermore National Security, LLC and other
 | 
			
		||||
# Spack Project Developers. See the top-level COPYRIGHT file for details.
 | 
			
		||||
#
 | 
			
		||||
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
 | 
			
		||||
 | 
			
		||||
import argparse
 | 
			
		||||
import sys
 | 
			
		||||
from typing import List
 | 
			
		||||
 | 
			
		||||
import llnl.util.tty as tty
 | 
			
		||||
 | 
			
		||||
import spack.cmd
 | 
			
		||||
import spack.cmd.common.arguments as arguments
 | 
			
		||||
import spack.cmd.common.confirmation as confirmation
 | 
			
		||||
import spack.environment as ev
 | 
			
		||||
import spack.spec
 | 
			
		||||
 | 
			
		||||
description = "remove specs from the concretized lockfile of an environment"
 | 
			
		||||
section = "environments"
 | 
			
		||||
level = "long"
 | 
			
		||||
 | 
			
		||||
# Arguments for display_specs when we find ambiguity
 | 
			
		||||
display_args = {"long": True, "show_flags": False, "variants": False, "indent": 4}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def setup_parser(subparser):
 | 
			
		||||
    subparser.add_argument(
 | 
			
		||||
        "--root", action="store_true", help="deconcretize only specific environment roots"
 | 
			
		||||
    )
 | 
			
		||||
    arguments.add_common_arguments(subparser, ["yes_to_all", "specs"])
 | 
			
		||||
    subparser.add_argument(
 | 
			
		||||
        "-a",
 | 
			
		||||
        "--all",
 | 
			
		||||
        action="store_true",
 | 
			
		||||
        dest="all",
 | 
			
		||||
        help="deconcretize ALL specs that match each supplied spec",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_deconcretize_list(
 | 
			
		||||
    args: argparse.Namespace, specs: List[spack.spec.Spec], env: ev.Environment
 | 
			
		||||
) -> List[spack.spec.Spec]:
 | 
			
		||||
    """
 | 
			
		||||
    Get list of environment roots to deconcretize
 | 
			
		||||
    """
 | 
			
		||||
    env_specs = [s for _, s in env.concretized_specs()]
 | 
			
		||||
    to_deconcretize = []
 | 
			
		||||
    errors = []
 | 
			
		||||
 | 
			
		||||
    for s in specs:
 | 
			
		||||
        if args.root:
 | 
			
		||||
            # find all roots matching given spec
 | 
			
		||||
            to_deconc = [e for e in env_specs if e.satisfies(s)]
 | 
			
		||||
        else:
 | 
			
		||||
            # find all roots matching or depending on a matching spec
 | 
			
		||||
            to_deconc = [e for e in env_specs if any(d.satisfies(s) for d in e.traverse())]
 | 
			
		||||
 | 
			
		||||
        if len(to_deconc) < 1:
 | 
			
		||||
            tty.warn(f"No matching specs to deconcretize for {s}")
 | 
			
		||||
 | 
			
		||||
        elif len(to_deconc) > 1 and not args.all:
 | 
			
		||||
            errors.append((s, to_deconc))
 | 
			
		||||
 | 
			
		||||
        to_deconcretize.extend(to_deconc)
 | 
			
		||||
 | 
			
		||||
    if errors:
 | 
			
		||||
        for spec, matching in errors:
 | 
			
		||||
            tty.error(f"{spec} matches multiple concrete specs:")
 | 
			
		||||
            sys.stderr.write("\n")
 | 
			
		||||
            spack.cmd.display_specs(matching, output=sys.stderr, **display_args)
 | 
			
		||||
            sys.stderr.write("\n")
 | 
			
		||||
            sys.stderr.flush()
 | 
			
		||||
        tty.die("Use '--all' to deconcretize all matching specs, or be more specific")
 | 
			
		||||
 | 
			
		||||
    return to_deconcretize
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def deconcretize_specs(args, specs):
 | 
			
		||||
    env = spack.cmd.require_active_env(cmd_name="deconcretize")
 | 
			
		||||
 | 
			
		||||
    if args.specs:
 | 
			
		||||
        deconcretize_list = get_deconcretize_list(args, specs, env)
 | 
			
		||||
    else:
 | 
			
		||||
        deconcretize_list = [s for _, s in env.concretized_specs()]
 | 
			
		||||
 | 
			
		||||
    if not args.yes_to_all:
 | 
			
		||||
        confirmation.confirm_action(deconcretize_list, "deconcretized", "deconcretization")
 | 
			
		||||
 | 
			
		||||
    with env.write_transaction():
 | 
			
		||||
        for spec in deconcretize_list:
 | 
			
		||||
            env.deconcretize(spec)
 | 
			
		||||
        env.write()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def deconcretize(parser, args):
 | 
			
		||||
    if not args.specs and not args.all:
 | 
			
		||||
        tty.die(
 | 
			
		||||
            "deconcretize requires at least one spec argument.",
 | 
			
		||||
            " Use `spack deconcretize --all` to deconcretize ALL specs.",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    specs = spack.cmd.parse_specs(args.specs) if args.specs else [any]
 | 
			
		||||
    deconcretize_specs(args, specs)
 | 
			
		||||
@@ -6,6 +6,7 @@
 | 
			
		||||
import llnl.util.tty as tty
 | 
			
		||||
 | 
			
		||||
import spack.cmd.common.arguments
 | 
			
		||||
import spack.cmd.common.confirmation
 | 
			
		||||
import spack.cmd.uninstall
 | 
			
		||||
import spack.environment as ev
 | 
			
		||||
import spack.store
 | 
			
		||||
@@ -41,6 +42,6 @@ def gc(parser, args):
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    if not args.yes_to_all:
 | 
			
		||||
        spack.cmd.uninstall.confirm_removal(specs)
 | 
			
		||||
        spack.cmd.common.confirmation.confirm_action(specs, "uninstalled", "uninstallation")
 | 
			
		||||
 | 
			
		||||
    spack.cmd.uninstall.do_uninstall(specs, force=False)
 | 
			
		||||
 
 | 
			
		||||
@@ -11,10 +11,9 @@
 | 
			
		||||
 | 
			
		||||
import spack.cmd
 | 
			
		||||
import spack.cmd.common.arguments as arguments
 | 
			
		||||
import spack.cmd.common.confirmation as confirmation
 | 
			
		||||
import spack.environment as ev
 | 
			
		||||
import spack.error
 | 
			
		||||
import spack.package_base
 | 
			
		||||
import spack.repo
 | 
			
		||||
import spack.spec
 | 
			
		||||
import spack.store
 | 
			
		||||
import spack.traverse as traverse
 | 
			
		||||
@@ -278,7 +277,7 @@ def uninstall_specs(args, specs):
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    if not args.yes_to_all:
 | 
			
		||||
        confirm_removal(uninstall_list)
 | 
			
		||||
        confirmation.confirm_action(uninstall_list, "uninstalled", "uninstallation")
 | 
			
		||||
 | 
			
		||||
    # Uninstall everything on the list
 | 
			
		||||
    do_uninstall(uninstall_list, args.force)
 | 
			
		||||
@@ -292,21 +291,6 @@ def uninstall_specs(args, specs):
 | 
			
		||||
        env.regenerate_views()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def confirm_removal(specs: List[spack.spec.Spec]):
 | 
			
		||||
    """Display the list of specs to be removed and ask for confirmation.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        specs: specs to be removed
 | 
			
		||||
    """
 | 
			
		||||
    tty.msg("The following {} packages will be uninstalled:\n".format(len(specs)))
 | 
			
		||||
    spack.cmd.display_specs(specs, **display_args)
 | 
			
		||||
    print("")
 | 
			
		||||
    answer = tty.get_yes_or_no("Do you want to proceed?", default=False)
 | 
			
		||||
    if not answer:
 | 
			
		||||
        tty.msg("Aborting uninstallation")
 | 
			
		||||
        sys.exit(0)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def uninstall(parser, args):
 | 
			
		||||
    if not args.specs and not args.all:
 | 
			
		||||
        tty.die(
 | 
			
		||||
 
 | 
			
		||||
@@ -1358,7 +1358,7 @@ def concretize(self, force=False, tests=False):
 | 
			
		||||
 | 
			
		||||
        # Remove concrete specs that no longer correlate to a user spec
 | 
			
		||||
        for spec in set(self.concretized_user_specs) - set(self.user_specs):
 | 
			
		||||
            self.deconcretize(spec)
 | 
			
		||||
            self.deconcretize(spec, concrete=False)
 | 
			
		||||
 | 
			
		||||
        # Pick the right concretization strategy
 | 
			
		||||
        if self.unify == "when_possible":
 | 
			
		||||
@@ -1373,15 +1373,36 @@ def concretize(self, force=False, tests=False):
 | 
			
		||||
        msg = "concretization strategy not implemented [{0}]"
 | 
			
		||||
        raise SpackEnvironmentError(msg.format(self.unify))
 | 
			
		||||
 | 
			
		||||
    def deconcretize(self, spec):
 | 
			
		||||
    def deconcretize(self, spec: spack.spec.Spec, concrete: bool = True):
 | 
			
		||||
        """
 | 
			
		||||
        Remove specified spec from environment concretization
 | 
			
		||||
 | 
			
		||||
        Arguments:
 | 
			
		||||
            spec: Spec to deconcretize. This must be a root of the environment
 | 
			
		||||
            concrete: If True, find all instances of spec as concrete in the environemnt.
 | 
			
		||||
                If False, find a single instance of the abstract spec as root of the environment.
 | 
			
		||||
        """
 | 
			
		||||
        # spec has to be a root of the environment
 | 
			
		||||
        index = self.concretized_user_specs.index(spec)
 | 
			
		||||
        dag_hash = self.concretized_order.pop(index)
 | 
			
		||||
        del self.concretized_user_specs[index]
 | 
			
		||||
        if concrete:
 | 
			
		||||
            dag_hash = spec.dag_hash()
 | 
			
		||||
 | 
			
		||||
            pairs = zip(self.concretized_user_specs, self.concretized_order)
 | 
			
		||||
            filtered = [(spec, h) for spec, h in pairs if h != dag_hash]
 | 
			
		||||
            # Cannot use zip and unpack two values; it fails if filtered is empty
 | 
			
		||||
            self.concretized_user_specs = [s for s, _ in filtered]
 | 
			
		||||
            self.concretized_order = [h for _, h in filtered]
 | 
			
		||||
        else:
 | 
			
		||||
            index = self.concretized_user_specs.index(spec)
 | 
			
		||||
            dag_hash = self.concretized_order.pop(index)
 | 
			
		||||
 | 
			
		||||
            del self.concretized_user_specs[index]
 | 
			
		||||
 | 
			
		||||
        # If this was the only user spec that concretized to this concrete spec, remove it
 | 
			
		||||
        if dag_hash not in self.concretized_order:
 | 
			
		||||
            del self.specs_by_hash[dag_hash]
 | 
			
		||||
            # if we deconcretized a dependency that doesn't correspond to a root, it
 | 
			
		||||
            # won't be here.
 | 
			
		||||
            if dag_hash in self.specs_by_hash:
 | 
			
		||||
                del self.specs_by_hash[dag_hash]
 | 
			
		||||
 | 
			
		||||
    def _get_specs_to_concretize(
 | 
			
		||||
        self,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										78
									
								
								lib/spack/spack/test/cmd/deconcretize.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										78
									
								
								lib/spack/spack/test/cmd/deconcretize.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,78 @@
 | 
			
		||||
# Copyright 2013-2023 Lawrence Livermore National Security, LLC and other
 | 
			
		||||
# Spack Project Developers. See the top-level COPYRIGHT file for details.
 | 
			
		||||
#
 | 
			
		||||
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
 | 
			
		||||
 | 
			
		||||
import pytest
 | 
			
		||||
 | 
			
		||||
import spack.environment as ev
 | 
			
		||||
from spack.main import SpackCommand, SpackCommandError
 | 
			
		||||
 | 
			
		||||
deconcretize = SpackCommand("deconcretize")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@pytest.fixture(scope="function")
 | 
			
		||||
def test_env(mutable_mock_env_path, config, mock_packages):
 | 
			
		||||
    ev.create("test")
 | 
			
		||||
    with ev.read("test") as e:
 | 
			
		||||
        e.add("a@2.0 foobar=bar ^b@1.0")
 | 
			
		||||
        e.add("a@1.0 foobar=bar ^b@0.9")
 | 
			
		||||
        e.concretize()
 | 
			
		||||
        e.write()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_deconcretize_dep(test_env):
 | 
			
		||||
    with ev.read("test") as e:
 | 
			
		||||
        deconcretize("-y", "b@1.0")
 | 
			
		||||
        specs = [s for s, _ in e.concretized_specs()]
 | 
			
		||||
 | 
			
		||||
    assert len(specs) == 1
 | 
			
		||||
    assert specs[0].satisfies("a@1.0")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_deconcretize_all_dep(test_env):
 | 
			
		||||
    with ev.read("test") as e:
 | 
			
		||||
        with pytest.raises(SpackCommandError):
 | 
			
		||||
            deconcretize("-y", "b")
 | 
			
		||||
        deconcretize("-y", "--all", "b")
 | 
			
		||||
        specs = [s for s, _ in e.concretized_specs()]
 | 
			
		||||
 | 
			
		||||
    assert len(specs) == 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_deconcretize_root(test_env):
 | 
			
		||||
    with ev.read("test") as e:
 | 
			
		||||
        output = deconcretize("-y", "--root", "b@1.0")
 | 
			
		||||
        assert "No matching specs to deconcretize" in output
 | 
			
		||||
        assert len(e.concretized_order) == 2
 | 
			
		||||
 | 
			
		||||
        deconcretize("-y", "--root", "a@2.0")
 | 
			
		||||
        specs = [s for s, _ in e.concretized_specs()]
 | 
			
		||||
 | 
			
		||||
    assert len(specs) == 1
 | 
			
		||||
    assert specs[0].satisfies("a@1.0")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_deconcretize_all_root(test_env):
 | 
			
		||||
    with ev.read("test") as e:
 | 
			
		||||
        with pytest.raises(SpackCommandError):
 | 
			
		||||
            deconcretize("-y", "--root", "a")
 | 
			
		||||
 | 
			
		||||
        output = deconcretize("-y", "--root", "--all", "b")
 | 
			
		||||
        assert "No matching specs to deconcretize" in output
 | 
			
		||||
        assert len(e.concretized_order) == 2
 | 
			
		||||
 | 
			
		||||
        deconcretize("-y", "--root", "--all", "a")
 | 
			
		||||
        specs = [s for s, _ in e.concretized_specs()]
 | 
			
		||||
 | 
			
		||||
    assert len(specs) == 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def test_deconcretize_all(test_env):
 | 
			
		||||
    with ev.read("test") as e:
 | 
			
		||||
        with pytest.raises(SpackCommandError):
 | 
			
		||||
            deconcretize()
 | 
			
		||||
        deconcretize("-y", "--all")
 | 
			
		||||
        specs = [s for s, _ in e.concretized_specs()]
 | 
			
		||||
 | 
			
		||||
    assert len(specs) == 0
 | 
			
		||||
		Reference in New Issue
	
	Block a user