summaryrefslogtreecommitdiff
path: root/lib
diff options
context:
space:
mode:
Diffstat (limited to 'lib')
-rw-r--r--lib/spack/spack/cmd/config.py93
-rw-r--r--lib/spack/spack/config.py3
-rw-r--r--lib/spack/spack/test/cmd/env.py97
3 files changed, 192 insertions, 1 deletions
diff --git a/lib/spack/spack/cmd/config.py b/lib/spack/spack/cmd/config.py
index 50987ea069..adfe286c37 100644
--- a/lib/spack/spack/cmd/config.py
+++ b/lib/spack/spack/cmd/config.py
@@ -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)
diff --git a/lib/spack/spack/config.py b/lib/spack/spack/config.py
index d13cc643a2..be6aeea7f0 100644
--- a/lib/spack/spack/config.py
+++ b/lib/spack/spack/config.py
@@ -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(
diff --git a/lib/spack/spack/test/cmd/env.py b/lib/spack/spack/test/cmd/env.py
index aaf2b83e23..0908ae3be3 100644
--- a/lib/spack/spack/test/cmd/env.py
+++ b/lib/spack/spack/test/cmd/env.py
@@ -48,6 +48,7 @@ env = SpackCommand("env")
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 @@ spack:
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."""