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:
Peter Scheibel 2024-01-18 00:21:17 -08:00 committed by GitHub
parent 9539037096
commit 7b27591321
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 211 additions and 2 deletions

View File

@ -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)

View File

@ -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(

View File

@ -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."""

View File

@ -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"
}

View File

@ -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