New command, spack change
, to change existing env specs (#31995)
If you have an environment like ``` $ cat spack.yaml spack: specs: [openmpi@4.1.0+cuda] ``` this PR provides a new command `spack change` that you can use to adjust environment specs from the command line: ``` $ spack change openmpi~cuda $ cat spack.yaml spack: specs: [openmpi@4.1.0~cuda] ``` in other words, this allows you to tweak the details of environment specs from the command line. Notes: * This is only allowed for environments that do not define matrices * This is possible but not anticipated to be needed immediately * If this were done, it should probably only be done for "named"/not-anonymous specs (i.e. we can change `openmpi+cuda` but not spec like `+cuda` or `@4.0.1~cuda`)
This commit is contained in:
parent
92b72f186e
commit
2968ae667f
51
lib/spack/spack/cmd/change.py
Normal file
51
lib/spack/spack/cmd/change.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# Copyright 2013-2022 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 spack.cmd
|
||||||
|
import spack.cmd.common.arguments as arguments
|
||||||
|
|
||||||
|
description = "change an existing spec in an environment"
|
||||||
|
section = "environments"
|
||||||
|
level = "long"
|
||||||
|
|
||||||
|
|
||||||
|
def setup_parser(subparser):
|
||||||
|
subparser.add_argument(
|
||||||
|
"-l",
|
||||||
|
"--list-name",
|
||||||
|
dest="list_name",
|
||||||
|
default="specs",
|
||||||
|
help="name of the list to remove specs from",
|
||||||
|
)
|
||||||
|
subparser.add_argument(
|
||||||
|
"--match-spec",
|
||||||
|
dest="match_spec",
|
||||||
|
help="if name is ambiguous, supply a spec to match",
|
||||||
|
)
|
||||||
|
subparser.add_argument(
|
||||||
|
"-a",
|
||||||
|
"--all",
|
||||||
|
action="store_true",
|
||||||
|
help="change all matching specs (allow changing more than one spec)",
|
||||||
|
)
|
||||||
|
arguments.add_common_arguments(subparser, ["specs"])
|
||||||
|
|
||||||
|
|
||||||
|
def change(parser, args):
|
||||||
|
env = spack.cmd.require_active_env(cmd_name="change")
|
||||||
|
|
||||||
|
with env.write_transaction():
|
||||||
|
if args.match_spec:
|
||||||
|
match_spec = spack.spec.Spec(args.match_spec)
|
||||||
|
else:
|
||||||
|
match_spec = None
|
||||||
|
for spec in spack.cmd.parse_specs(args.specs):
|
||||||
|
env.change_existing_spec(
|
||||||
|
spec,
|
||||||
|
list_name=args.list_name,
|
||||||
|
match_spec=match_spec,
|
||||||
|
allow_changing_multiple_specs=args.all,
|
||||||
|
)
|
||||||
|
env.write()
|
@ -1073,6 +1073,58 @@ def add(self, user_spec, list_name=user_speclist_name):
|
|||||||
|
|
||||||
return bool(not existing)
|
return bool(not existing)
|
||||||
|
|
||||||
|
def change_existing_spec(
|
||||||
|
self,
|
||||||
|
change_spec,
|
||||||
|
list_name=user_speclist_name,
|
||||||
|
match_spec=None,
|
||||||
|
allow_changing_multiple_specs=False,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Find the spec identified by `match_spec` and change it to `change_spec`.
|
||||||
|
|
||||||
|
Arguments:
|
||||||
|
change_spec (spack.spec.Spec): defines the spec properties that
|
||||||
|
need to be changed. This will not change attributes of the
|
||||||
|
matched spec unless they conflict with `change_spec`.
|
||||||
|
list_name (str): identifies the spec list in the environment that
|
||||||
|
should be modified
|
||||||
|
match_spec (spack.spec.Spec): if set, this identifies the spec
|
||||||
|
that should be changed. If not set, it is assumed we are
|
||||||
|
looking for a spec with the same name as `change_spec`.
|
||||||
|
"""
|
||||||
|
if not (change_spec.name or (match_spec and match_spec.name)):
|
||||||
|
raise ValueError(
|
||||||
|
"Must specify a spec name to identify a single spec"
|
||||||
|
" in the environment that will be changed"
|
||||||
|
)
|
||||||
|
match_spec = match_spec or Spec(change_spec.name)
|
||||||
|
|
||||||
|
list_to_change = self.spec_lists[list_name]
|
||||||
|
if list_to_change.is_matrix:
|
||||||
|
raise SpackEnvironmentError(
|
||||||
|
"Cannot directly change specs in matrices:"
|
||||||
|
" specify a named list that is not a matrix"
|
||||||
|
)
|
||||||
|
|
||||||
|
matches = list(x for x in list_to_change if x.satisfies(match_spec))
|
||||||
|
if len(matches) == 0:
|
||||||
|
raise ValueError(
|
||||||
|
"There are no specs named {0} in {1}".format(match_spec.name, list_name)
|
||||||
|
)
|
||||||
|
elif len(matches) > 1 and not allow_changing_multiple_specs:
|
||||||
|
raise ValueError("{0} matches multiple specs".format(str(match_spec)))
|
||||||
|
|
||||||
|
new_speclist = SpecList(list_name)
|
||||||
|
for i, spec in enumerate(list_to_change):
|
||||||
|
if spec.satisfies(match_spec):
|
||||||
|
new_speclist.add(Spec.override(spec, change_spec))
|
||||||
|
else:
|
||||||
|
new_speclist.add(spec)
|
||||||
|
|
||||||
|
self.spec_lists[list_name] = new_speclist
|
||||||
|
self.update_stale_references()
|
||||||
|
|
||||||
def remove(self, query_spec, list_name=user_speclist_name, force=False):
|
def remove(self, query_spec, list_name=user_speclist_name, force=False):
|
||||||
"""Remove specs from an environment that match a query_spec"""
|
"""Remove specs from an environment that match a query_spec"""
|
||||||
query_spec = Spec(query_spec)
|
query_spec = Spec(query_spec)
|
||||||
|
@ -284,6 +284,22 @@ def _string_or_none(s):
|
|||||||
|
|
||||||
self.platform, self.os, self.target = platform_tuple
|
self.platform, self.os, self.target = platform_tuple
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def override(init_spec, change_spec):
|
||||||
|
if init_spec:
|
||||||
|
new_spec = init_spec.copy()
|
||||||
|
else:
|
||||||
|
new_spec = ArchSpec()
|
||||||
|
if change_spec.platform:
|
||||||
|
new_spec.platform = change_spec.platform
|
||||||
|
# TODO: if the platform is changed to something that is incompatible
|
||||||
|
# with the current os, we should implicitly remove it
|
||||||
|
if change_spec.os:
|
||||||
|
new_spec.os = change_spec.os
|
||||||
|
if change_spec.target:
|
||||||
|
new_spec.target = change_spec.target
|
||||||
|
return new_spec
|
||||||
|
|
||||||
def _autospec(self, spec_like):
|
def _autospec(self, spec_like):
|
||||||
if isinstance(spec_like, ArchSpec):
|
if isinstance(spec_like, ArchSpec):
|
||||||
return spec_like
|
return spec_like
|
||||||
@ -2242,6 +2258,33 @@ def read_yaml_dep_specs(deps, hash_type=ht.dag_hash.name):
|
|||||||
raise spack.error.SpecError("Couldn't parse dependency types in spec.")
|
raise spack.error.SpecError("Couldn't parse dependency types in spec.")
|
||||||
yield dep_name, dep_hash, list(deptypes), hash_type
|
yield dep_name, dep_hash, list(deptypes), hash_type
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def override(init_spec, change_spec):
|
||||||
|
# TODO: this doesn't account for the case where the changed spec
|
||||||
|
# (and the user spec) have dependencies
|
||||||
|
new_spec = init_spec.copy()
|
||||||
|
package_cls = spack.repo.path.get_pkg_class(new_spec.name)
|
||||||
|
if change_spec.versions and not change_spec.versions == spack.version.ver(":"):
|
||||||
|
new_spec.versions = change_spec.versions
|
||||||
|
for variant, value in change_spec.variants.items():
|
||||||
|
if variant in package_cls.variants:
|
||||||
|
if variant in new_spec.variants:
|
||||||
|
new_spec.variants.substitute(value)
|
||||||
|
else:
|
||||||
|
new_spec.variants[variant] = value
|
||||||
|
else:
|
||||||
|
raise ValueError("{0} is not a variant of {1}".format(variant, new_spec.name))
|
||||||
|
if change_spec.compiler:
|
||||||
|
new_spec.compiler = change_spec.compiler
|
||||||
|
if change_spec.compiler_flags:
|
||||||
|
for flagname, flagvals in change_spec.compiler_flags.items():
|
||||||
|
new_spec.compiler_flags[flagname] = flagvals
|
||||||
|
if change_spec.architecture:
|
||||||
|
new_spec.architecture = ArchSpec.override(
|
||||||
|
new_spec.architecture, change_spec.architecture
|
||||||
|
)
|
||||||
|
return new_spec
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def from_literal(spec_dict, normal=True):
|
def from_literal(spec_dict, normal=True):
|
||||||
"""Builds a Spec from a dictionary containing the spec literal.
|
"""Builds a Spec from a dictionary containing the spec literal.
|
||||||
|
@ -34,6 +34,13 @@ def __init__(self, name="specs", yaml_list=None, reference=None):
|
|||||||
self._constraints = None
|
self._constraints = None
|
||||||
self._specs = None
|
self._specs = None
|
||||||
|
|
||||||
|
@property
|
||||||
|
def is_matrix(self):
|
||||||
|
for item in self.specs_as_yaml_list:
|
||||||
|
if isinstance(item, dict):
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def specs_as_yaml_list(self):
|
def specs_as_yaml_list(self):
|
||||||
if self._expanded_list is None:
|
if self._expanded_list is None:
|
||||||
|
@ -43,6 +43,7 @@
|
|||||||
env = SpackCommand("env")
|
env = SpackCommand("env")
|
||||||
install = SpackCommand("install")
|
install = SpackCommand("install")
|
||||||
add = SpackCommand("add")
|
add = SpackCommand("add")
|
||||||
|
change = SpackCommand("change")
|
||||||
remove = SpackCommand("remove")
|
remove = SpackCommand("remove")
|
||||||
concretize = SpackCommand("concretize")
|
concretize = SpackCommand("concretize")
|
||||||
stage = SpackCommand("stage")
|
stage = SpackCommand("stage")
|
||||||
@ -71,6 +72,35 @@ def test_add():
|
|||||||
assert Spec("mpileaks") in e.user_specs
|
assert Spec("mpileaks") in e.user_specs
|
||||||
|
|
||||||
|
|
||||||
|
def test_change_match_spec():
|
||||||
|
env("create", "test")
|
||||||
|
|
||||||
|
e = ev.read("test")
|
||||||
|
with e:
|
||||||
|
add("mpileaks@2.1")
|
||||||
|
add("mpileaks@2.2")
|
||||||
|
|
||||||
|
change("--match-spec", "mpileaks@2.2", "mpileaks@2.3")
|
||||||
|
|
||||||
|
assert not any(x.satisfies("mpileaks@2.2") for x in e.user_specs)
|
||||||
|
assert any(x.satisfies("mpileaks@2.3") for x in e.user_specs)
|
||||||
|
|
||||||
|
|
||||||
|
def test_change_multiple_matches():
|
||||||
|
env("create", "test")
|
||||||
|
|
||||||
|
e = ev.read("test")
|
||||||
|
with e:
|
||||||
|
add("mpileaks@2.1")
|
||||||
|
add("mpileaks@2.2")
|
||||||
|
add("libelf@0.8.12%clang")
|
||||||
|
|
||||||
|
change("--match-spec", "mpileaks", "-a", "mpileaks%gcc")
|
||||||
|
|
||||||
|
assert all(x.satisfies("%gcc") for x in e.user_specs if x.name == "mpileaks")
|
||||||
|
assert any(x.satisfies("%clang") for x in e.user_specs if x.name == "libelf")
|
||||||
|
|
||||||
|
|
||||||
def test_env_add_virtual():
|
def test_env_add_virtual():
|
||||||
env("create", "test")
|
env("create", "test")
|
||||||
|
|
||||||
|
@ -4,16 +4,19 @@
|
|||||||
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
|
# SPDX-License-Identifier: (Apache-2.0 OR MIT)
|
||||||
|
|
||||||
"""Test environment internals without CLI"""
|
"""Test environment internals without CLI"""
|
||||||
|
import sys
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from six import StringIO
|
||||||
|
|
||||||
import spack.environment as ev
|
import spack.environment as ev
|
||||||
import spack.spec
|
import spack.spec
|
||||||
|
|
||||||
|
pytestmark = pytest.mark.skipif(
|
||||||
@pytest.mark.skipif(
|
sys.platform == "win32", reason="Envs are not supported on windows"
|
||||||
str(spack.platforms.host()) == "windows", reason="Not supported on Windows (yet)"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def test_hash_change_no_rehash_concrete(tmpdir, mock_packages, config):
|
def test_hash_change_no_rehash_concrete(tmpdir, mock_packages, config):
|
||||||
# create an environment
|
# create an environment
|
||||||
env_path = tmpdir.mkdir("env_dir").strpath
|
env_path = tmpdir.mkdir("env_dir").strpath
|
||||||
@ -43,6 +46,66 @@ def test_hash_change_no_rehash_concrete(tmpdir, mock_packages, config):
|
|||||||
assert read_in.specs_by_hash[read_in.concretized_order[0]]._hash == new_hash
|
assert read_in.specs_by_hash[read_in.concretized_order[0]]._hash == new_hash
|
||||||
|
|
||||||
|
|
||||||
|
def test_env_change_spec(tmpdir, mock_packages, config):
|
||||||
|
env_path = tmpdir.mkdir("env_dir").strpath
|
||||||
|
env = ev.Environment(env_path)
|
||||||
|
env.write()
|
||||||
|
|
||||||
|
spec = spack.spec.Spec("mpileaks@2.1~shared+debug")
|
||||||
|
env.add(spec)
|
||||||
|
env.write()
|
||||||
|
|
||||||
|
change_spec = spack.spec.Spec("mpileaks@2.2")
|
||||||
|
env.change_existing_spec(change_spec)
|
||||||
|
(spec,) = env.added_specs()
|
||||||
|
assert spec == spack.spec.Spec("mpileaks@2.2~shared+debug")
|
||||||
|
|
||||||
|
change_spec = spack.spec.Spec("mpileaks~debug")
|
||||||
|
env.change_existing_spec(change_spec)
|
||||||
|
(spec,) = env.added_specs()
|
||||||
|
assert spec == spack.spec.Spec("mpileaks@2.2~shared~debug")
|
||||||
|
|
||||||
|
|
||||||
|
_test_matrix_yaml = """\
|
||||||
|
env:
|
||||||
|
definitions:
|
||||||
|
- compilers: ["%gcc", "%clang"]
|
||||||
|
- desired_specs: ["mpileaks@2.1"]
|
||||||
|
specs:
|
||||||
|
- matrix:
|
||||||
|
- [$compilers]
|
||||||
|
- [$desired_specs]
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def test_env_change_spec_in_definition(tmpdir, mock_packages, config, mutable_mock_env_path):
|
||||||
|
initial_yaml = StringIO(_test_matrix_yaml)
|
||||||
|
e = ev.create("test", initial_yaml)
|
||||||
|
e.concretize()
|
||||||
|
e.write()
|
||||||
|
|
||||||
|
assert any(x.satisfies("mpileaks@2.1%gcc") for x in e.user_specs)
|
||||||
|
|
||||||
|
e.change_existing_spec(spack.spec.Spec("mpileaks@2.2"), list_name="desired_specs")
|
||||||
|
e.write()
|
||||||
|
|
||||||
|
assert any(x.satisfies("mpileaks@2.2%gcc") for x in e.user_specs)
|
||||||
|
assert not any(x.satisfies("mpileaks@2.1%gcc") for x in e.user_specs)
|
||||||
|
|
||||||
|
|
||||||
|
def test_env_change_spec_in_matrix_raises_error(
|
||||||
|
tmpdir, mock_packages, config, mutable_mock_env_path
|
||||||
|
):
|
||||||
|
initial_yaml = StringIO(_test_matrix_yaml)
|
||||||
|
e = ev.create("test", initial_yaml)
|
||||||
|
e.concretize()
|
||||||
|
e.write()
|
||||||
|
|
||||||
|
with pytest.raises(spack.environment.SpackEnvironmentError) as error:
|
||||||
|
e.change_existing_spec(spack.spec.Spec("mpileaks@2.2"))
|
||||||
|
assert "Cannot directly change specs in matrices" in str(error)
|
||||||
|
|
||||||
|
|
||||||
def test_activate_should_require_an_env():
|
def test_activate_should_require_an_env():
|
||||||
with pytest.raises(TypeError):
|
with pytest.raises(TypeError):
|
||||||
ev.activate(env="name")
|
ev.activate(env="name")
|
||||||
|
@ -1111,6 +1111,20 @@ def test_splice_swap_names_mismatch_virtuals(self, transitive):
|
|||||||
with pytest.raises(spack.spec.SpliceError, match="will not provide the same virtuals."):
|
with pytest.raises(spack.spec.SpliceError, match="will not provide the same virtuals."):
|
||||||
spec.splice(dep, transitive)
|
spec.splice(dep, transitive)
|
||||||
|
|
||||||
|
def test_spec_override(self):
|
||||||
|
init_spec = Spec("a foo=baz foobar=baz cflags=-O3 cxxflags=-O1")
|
||||||
|
change_spec = Spec("a foo=fee cflags=-O2")
|
||||||
|
new_spec = Spec.override(init_spec, change_spec)
|
||||||
|
new_spec.concretize()
|
||||||
|
assert "foo=fee" in new_spec
|
||||||
|
# This check fails without concretizing: apparently if both specs are
|
||||||
|
# abstract, then the spec will always be considered to satisfy
|
||||||
|
# 'variant=value' (regardless of whether it in fact does).
|
||||||
|
assert "foo=baz" not in new_spec
|
||||||
|
assert "foobar=baz" in new_spec
|
||||||
|
assert new_spec.compiler_flags["cflags"] == ["-O2"]
|
||||||
|
assert new_spec.compiler_flags["cxxflags"] == ["-O1"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.regression("3887")
|
@pytest.mark.regression("3887")
|
||||||
@pytest.mark.parametrize("spec_str", ["git", "hdf5", "py-flake8"])
|
@pytest.mark.parametrize("spec_str", ["git", "hdf5", "py-flake8"])
|
||||||
|
@ -337,7 +337,7 @@ _spack() {
|
|||||||
then
|
then
|
||||||
SPACK_COMPREPLY="-h --help -H --all-help --color -c --config -C --config-scope -d --debug --timestamp --pdb -e --env -D --env-dir -E --no-env --use-env-repo -k --insecure -l --enable-locks -L --disable-locks -m --mock -b --bootstrap -p --profile --sorted-profile --lines -v --verbose --stacktrace -V --version --print-shell-vars"
|
SPACK_COMPREPLY="-h --help -H --all-help --color -c --config -C --config-scope -d --debug --timestamp --pdb -e --env -D --env-dir -E --no-env --use-env-repo -k --insecure -l --enable-locks -L --disable-locks -m --mock -b --bootstrap -p --profile --sorted-profile --lines -v --verbose --stacktrace -V --version --print-shell-vars"
|
||||||
else
|
else
|
||||||
SPACK_COMPREPLY="activate add arch audit blame bootstrap build-env buildcache cd checksum ci clean clone commands compiler compilers concretize config containerize create deactivate debug dependencies dependents deprecate dev-build develop diff docs edit env extensions external fetch find gc gpg graph help info install license list load location log-parse maintainers make-installer mark mirror module patch pkg providers pydoc python reindex remove rm repo resource restage solve spec stage style tags test test-env tutorial undevelop uninstall unit-test unload url verify versions view"
|
SPACK_COMPREPLY="activate add arch audit blame bootstrap build-env buildcache cd change checksum ci clean clone commands compiler compilers concretize config containerize create deactivate debug dependencies dependents deprecate dev-build develop diff docs edit env extensions external fetch find gc gpg graph help info install license list load location log-parse maintainers make-installer mark mirror module patch pkg providers pydoc python reindex remove rm repo resource restage solve spec stage style tags test test-env tutorial undevelop uninstall unit-test unload url verify versions view"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -585,6 +585,15 @@ _spack_cd() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_spack_change() {
|
||||||
|
if $list_options
|
||||||
|
then
|
||||||
|
SPACK_COMPREPLY="-h --help -l --list-name --match-spec -a --all"
|
||||||
|
else
|
||||||
|
_all_packages
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
_spack_checksum() {
|
_spack_checksum() {
|
||||||
if $list_options
|
if $list_options
|
||||||
then
|
then
|
||||||
|
Loading…
Reference in New Issue
Block a user