New command: spack config change
(#41147)
Like `spack change` for specs in environments, this can e.g. replace `examplespec+debug` with `examplespec~debug` in a `require:` section. Example behavior for a config like: ``` packages: foo: require: - spec: +debug ``` * `spack config change packages:foo:require:~debug` replaces `+debug` with `~debug` * `spack config change packages:foo:require:@1.1` adds a requirement to the list * `spack config change packages:bar:require:~debug` adds a requirement
This commit is contained in:
parent
9539037096
commit
7b27591321
@ -76,6 +76,10 @@ def setup_parser(subparser):
|
||||
)
|
||||
add_parser.add_argument("-f", "--file", help="file from which to set all config values")
|
||||
|
||||
change_parser = sp.add_parser("change", help="swap variants etc. on specs in config")
|
||||
change_parser.add_argument("path", help="colon-separated path to config section with specs")
|
||||
change_parser.add_argument("--match-spec", help="only change constraints that match this")
|
||||
|
||||
prefer_upstream_parser = sp.add_parser(
|
||||
"prefer-upstream", help="set package preferences from upstream"
|
||||
)
|
||||
@ -263,6 +267,94 @@ def _can_update_config_file(scope: spack.config.ConfigScope, cfg_file):
|
||||
return fs.can_write_to_dir(scope.path) and fs.can_access(cfg_file)
|
||||
|
||||
|
||||
def _config_change_requires_scope(path, spec, scope, match_spec=None):
|
||||
"""Return whether or not anything changed."""
|
||||
require = spack.config.get(path, scope=scope)
|
||||
if not require:
|
||||
return False
|
||||
|
||||
changed = False
|
||||
|
||||
def override_cfg_spec(spec_str):
|
||||
nonlocal changed
|
||||
|
||||
init_spec = spack.spec.Spec(spec_str)
|
||||
# Overridden spec cannot be anonymous
|
||||
init_spec.name = spec.name
|
||||
if match_spec and not init_spec.satisfies(match_spec):
|
||||
# If there is a match_spec, don't change constraints that
|
||||
# don't match it
|
||||
return spec_str
|
||||
elif not init_spec.intersects(spec):
|
||||
changed = True
|
||||
return str(spack.spec.Spec.override(init_spec, spec))
|
||||
else:
|
||||
# Don't override things if they intersect, otherwise we'd
|
||||
# be e.g. attaching +debug to every single version spec
|
||||
return spec_str
|
||||
|
||||
if isinstance(require, str):
|
||||
new_require = override_cfg_spec(require)
|
||||
else:
|
||||
new_require = []
|
||||
for item in require:
|
||||
if "one_of" in item:
|
||||
item["one_of"] = [override_cfg_spec(x) for x in item["one_of"]]
|
||||
elif "any_of" in item:
|
||||
item["any_of"] = [override_cfg_spec(x) for x in item["any_of"]]
|
||||
elif "spec" in item:
|
||||
item["spec"] = override_cfg_spec(item["spec"])
|
||||
new_require.append(item)
|
||||
|
||||
spack.config.set(path, new_require, scope=scope)
|
||||
return changed
|
||||
|
||||
|
||||
def _config_change(config_path, match_spec_str=None):
|
||||
all_components = spack.config.process_config_path(config_path)
|
||||
key_components = all_components[:-1]
|
||||
key_path = ":".join(key_components)
|
||||
|
||||
spec = spack.spec.Spec(syaml.syaml_str(all_components[-1]))
|
||||
|
||||
match_spec = None
|
||||
if match_spec_str:
|
||||
match_spec = spack.spec.Spec(match_spec_str)
|
||||
|
||||
if key_components[-1] == "require":
|
||||
# Extract the package name from the config path, which allows
|
||||
# args.spec to be anonymous if desired
|
||||
pkg_name = key_components[1]
|
||||
spec.name = pkg_name
|
||||
|
||||
changed = False
|
||||
for scope in spack.config.writable_scope_names():
|
||||
changed |= _config_change_requires_scope(key_path, spec, scope, match_spec=match_spec)
|
||||
|
||||
if not changed:
|
||||
existing_requirements = spack.config.get(key_path)
|
||||
if isinstance(existing_requirements, str):
|
||||
raise spack.config.ConfigError(
|
||||
"'config change' needs to append a requirement,"
|
||||
" but existing require: config is not a list"
|
||||
)
|
||||
|
||||
ideal_scope_to_modify = None
|
||||
for scope in spack.config.writable_scope_names():
|
||||
if spack.config.get(key_path, scope=scope):
|
||||
ideal_scope_to_modify = scope
|
||||
break
|
||||
|
||||
update_path = f"{key_path}:[{str(spec)}]"
|
||||
spack.config.add(update_path, scope=ideal_scope_to_modify)
|
||||
else:
|
||||
raise ValueError("'config change' can currently only change 'require' sections")
|
||||
|
||||
|
||||
def config_change(args):
|
||||
_config_change(args.path, args.match_spec)
|
||||
|
||||
|
||||
def config_update(args):
|
||||
# Read the configuration files
|
||||
spack.config.CONFIG.get_config(args.section, scope=args.scope)
|
||||
@ -490,5 +582,6 @@ def config(parser, args):
|
||||
"update": config_update,
|
||||
"revert": config_revert,
|
||||
"prefer-upstream": config_prefer_upstream,
|
||||
"change": config_change,
|
||||
}
|
||||
action[args.config_command](args)
|
||||
|
@ -950,7 +950,8 @@ def scopes() -> Dict[str, ConfigScope]:
|
||||
|
||||
def writable_scopes() -> List[ConfigScope]:
|
||||
"""
|
||||
Return list of writable scopes
|
||||
Return list of writable scopes. Higher-priority scopes come first in the
|
||||
list.
|
||||
"""
|
||||
return list(
|
||||
reversed(
|
||||
|
@ -48,6 +48,7 @@
|
||||
install = SpackCommand("install")
|
||||
add = SpackCommand("add")
|
||||
change = SpackCommand("change")
|
||||
config = SpackCommand("config")
|
||||
remove = SpackCommand("remove")
|
||||
concretize = SpackCommand("concretize")
|
||||
stage = SpackCommand("stage")
|
||||
@ -869,6 +870,102 @@ def test_env_with_included_config_file(mutable_mock_env_path, packages_file):
|
||||
assert any(x.satisfies("mpileaks@2.2") for x in e._get_environment_specs())
|
||||
|
||||
|
||||
def test_config_change_existing(mutable_mock_env_path, tmp_path, mock_packages, mutable_config):
|
||||
"""Test ``config change`` with config in the ``spack.yaml`` as well as an
|
||||
included file scope.
|
||||
"""
|
||||
|
||||
included_file = "included-packages.yaml"
|
||||
included_path = tmp_path / included_file
|
||||
with open(included_path, "w") as f:
|
||||
f.write(
|
||||
"""\
|
||||
packages:
|
||||
mpich:
|
||||
require:
|
||||
- spec: "@3.0.2"
|
||||
libelf:
|
||||
require: "@0.8.10"
|
||||
bowtie:
|
||||
require:
|
||||
- one_of: ["@1.3.0", "@1.2.0"]
|
||||
"""
|
||||
)
|
||||
|
||||
spack_yaml = tmp_path / ev.manifest_name
|
||||
spack_yaml.write_text(
|
||||
f"""\
|
||||
spack:
|
||||
packages:
|
||||
mpich:
|
||||
require:
|
||||
- spec: "+debug"
|
||||
include:
|
||||
- {os.path.join(".", included_file)}
|
||||
specs: []
|
||||
"""
|
||||
)
|
||||
|
||||
e = ev.Environment(tmp_path)
|
||||
with e:
|
||||
# List of requirements, flip a variant
|
||||
config("change", "packages:mpich:require:~debug")
|
||||
test_spec = spack.spec.Spec("mpich").concretized()
|
||||
assert test_spec.satisfies("@3.0.2~debug")
|
||||
|
||||
# List of requirements, change the version (in a different scope)
|
||||
config("change", "packages:mpich:require:@3.0.3")
|
||||
test_spec = spack.spec.Spec("mpich").concretized()
|
||||
assert test_spec.satisfies("@3.0.3")
|
||||
|
||||
# "require:" as a single string, also try specifying
|
||||
# a spec string that requires enclosing in quotes as
|
||||
# part of the config path
|
||||
config("change", 'packages:libelf:require:"@0.8.12:"')
|
||||
test_spec = spack.spec.Spec("libelf@0.8.12").concretized()
|
||||
# No need for assert, if there wasn't a failure, we
|
||||
# changed the requirement successfully.
|
||||
|
||||
# Use "--match-spec" to change one spec in a "one_of"
|
||||
# list
|
||||
config("change", "packages:bowtie:require:@1.2.2", "--match-spec", "@1.2.0")
|
||||
spack.spec.Spec("bowtie@1.3.0").concretize()
|
||||
spack.spec.Spec("bowtie@1.2.2").concretized()
|
||||
|
||||
|
||||
def test_config_change_new(mutable_mock_env_path, tmp_path, mock_packages, mutable_config):
|
||||
spack_yaml = tmp_path / ev.manifest_name
|
||||
spack_yaml.write_text(
|
||||
"""\
|
||||
spack:
|
||||
specs: []
|
||||
"""
|
||||
)
|
||||
|
||||
e = ev.Environment(tmp_path)
|
||||
with e:
|
||||
config("change", "packages:mpich:require:~debug")
|
||||
with pytest.raises(spack.solver.asp.UnsatisfiableSpecError):
|
||||
spack.spec.Spec("mpich+debug").concretized()
|
||||
spack.spec.Spec("mpich~debug").concretized()
|
||||
|
||||
# Now check that we raise an error if we need to add a require: constraint
|
||||
# when preexisting config manually specified it as a singular spec
|
||||
spack_yaml.write_text(
|
||||
"""\
|
||||
spack:
|
||||
specs: []
|
||||
packages:
|
||||
mpich:
|
||||
require: "@3.0.3"
|
||||
"""
|
||||
)
|
||||
with e:
|
||||
assert spack.spec.Spec("mpich").concretized().satisfies("@3.0.3")
|
||||
with pytest.raises(spack.config.ConfigError, match="not a list"):
|
||||
config("change", "packages:mpich:require:~debug")
|
||||
|
||||
|
||||
def test_env_with_included_config_file_url(tmpdir, mutable_empty_config, packages_file):
|
||||
"""Test configuration inclusion of a file whose path is a URL before
|
||||
the environment is concretized."""
|
||||
|
@ -824,7 +824,7 @@ _spack_config() {
|
||||
then
|
||||
SPACK_COMPREPLY="-h --help --scope"
|
||||
else
|
||||
SPACK_COMPREPLY="get blame edit list add prefer-upstream remove rm update revert"
|
||||
SPACK_COMPREPLY="get blame edit list add change prefer-upstream remove rm update revert"
|
||||
fi
|
||||
}
|
||||
|
||||
@ -868,6 +868,15 @@ _spack_config_add() {
|
||||
fi
|
||||
}
|
||||
|
||||
_spack_config_change() {
|
||||
if $list_options
|
||||
then
|
||||
SPACK_COMPREPLY="-h --help --match-spec"
|
||||
else
|
||||
SPACK_COMPREPLY=""
|
||||
fi
|
||||
}
|
||||
|
||||
_spack_config_prefer_upstream() {
|
||||
SPACK_COMPREPLY="-h --help --local"
|
||||
}
|
||||
|
@ -1165,6 +1165,7 @@ complete -c spack -n '__fish_spack_using_command_pos 0 config' -f -a blame -d 'p
|
||||
complete -c spack -n '__fish_spack_using_command_pos 0 config' -f -a edit -d 'edit configuration file'
|
||||
complete -c spack -n '__fish_spack_using_command_pos 0 config' -f -a list -d 'list configuration sections'
|
||||
complete -c spack -n '__fish_spack_using_command_pos 0 config' -f -a add -d 'add configuration parameters'
|
||||
complete -c spack -n '__fish_spack_using_command_pos 0 config' -f -a change -d 'swap variants etc. on specs in config'
|
||||
complete -c spack -n '__fish_spack_using_command_pos 0 config' -f -a prefer-upstream -d 'set package preferences from upstream'
|
||||
complete -c spack -n '__fish_spack_using_command_pos 0 config' -f -a remove -d 'remove configuration parameters'
|
||||
complete -c spack -n '__fish_spack_using_command_pos 0 config' -f -a rm -d 'remove configuration parameters'
|
||||
@ -1208,6 +1209,14 @@ complete -c spack -n '__fish_spack_using_command config add' -s h -l help -d 'sh
|
||||
complete -c spack -n '__fish_spack_using_command config add' -s f -l file -r -f -a file
|
||||
complete -c spack -n '__fish_spack_using_command config add' -s f -l file -r -d 'file from which to set all config values'
|
||||
|
||||
# spack config change
|
||||
set -g __fish_spack_optspecs_spack_config_change h/help match-spec=
|
||||
complete -c spack -n '__fish_spack_using_command_pos 0 config change' -f -a '(__fish_spack_colon_path)'
|
||||
complete -c spack -n '__fish_spack_using_command config change' -s h -l help -f -a help
|
||||
complete -c spack -n '__fish_spack_using_command config change' -s h -l help -d 'show this help message and exit'
|
||||
complete -c spack -n '__fish_spack_using_command config change' -l match-spec -r -f -a match_spec
|
||||
complete -c spack -n '__fish_spack_using_command config change' -l match-spec -r -d 'only change constraints that match this'
|
||||
|
||||
# spack config prefer-upstream
|
||||
set -g __fish_spack_optspecs_spack_config_prefer_upstream h/help local
|
||||
complete -c spack -n '__fish_spack_using_command config prefer-upstream' -s h -l help -f -a help
|
||||
|
Loading…
Reference in New Issue
Block a user