Skip to content

Commit c702f5b

Browse files
authored
concretizer: make --force config and a common argument (spack#48838)
`--force` was previously available only on `spack concretize` but not on `spack spec`, `spack solve`, and other commands that do concretization. This means you can now preview a force-concretize on an environment or spec with `spack spec -f` or `spack solve -f`. You can also set concretization to *always* force in config with: ```yaml spack: concretizer: force: true ``` Making room for this required two breaking changes: 1. `spack install --file` no longer works. The `-f` / `--file` option conflicted with `--force`. If you wrote: ``` spack install -f ./spec.json` ``` you should simply remove the flag: ``` spack install ./spec.json` ``` Files as spec arguments have been supported since at least Spack `v0.10.0`. We did not preserve `--file` because it took a separate code path that was already buggy. Using one code path for spec files will allow us to handle them more reliably. 2. `spack mirror create --file` no longer has a short `-f` option If you wrote: ``` spack mirror create -f FILE ``` You should now use the long argument: ``` spack mirror create --file FILE ``` We removed the `-f` option to make room for `-f / --force`. - [x] make `concretizer:force` a configuration option - [x] add `--force` to common concretizer arguments - [x] remove the `-f` short option from `spack mirror create --file` - [x] remove the `-f` / `--file` option from `spack install` --------- Signed-off-by: Todd Gamblin <[email protected]>
1 parent b0f8445 commit c702f5b

File tree

10 files changed

+120
-132
lines changed

10 files changed

+120
-132
lines changed

lib/spack/spack/cmd/common/arguments.py

Lines changed: 32 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -175,20 +175,17 @@ def _cdash_reporter(namespace):
175175

176176
def _factory():
177177
def installed_specs(args):
178+
packages = []
179+
178180
if getattr(args, "spec", ""):
179181
packages = args.spec
180182
elif getattr(args, "specs", ""):
181183
packages = args.specs
182184
elif getattr(args, "package", ""):
183185
# Ensure CI 'spack test run' can output CDash results
184186
packages = args.package
185-
else:
186-
packages = []
187-
for file in args.specfiles:
188-
with open(file, "r", encoding="utf-8") as f:
189-
s = spack.spec.Spec.from_yaml(f)
190-
packages.append(s.format())
191-
return packages
187+
188+
return [str(spack.spec.Spec(s)) for s in packages]
192189

193190
configuration = spack.reporters.CDashConfiguration(
194191
upload_url=namespace.cdash_upload_url,
@@ -198,6 +195,7 @@ def installed_specs(args):
198195
buildstamp=namespace.cdash_buildstamp,
199196
track=namespace.cdash_track,
200197
)
198+
201199
return spack.reporters.CDash(configuration=configuration)
202200

203201
return _factory
@@ -533,11 +531,22 @@ class ConfigSetAction(argparse.Action):
533531
"""
534532

535533
def __init__(
536-
self, option_strings, dest, const, default=None, required=False, help=None, metavar=None
534+
self,
535+
option_strings,
536+
dest,
537+
const,
538+
default=None,
539+
required=False,
540+
help=None,
541+
metavar=None,
542+
require_environment=False,
537543
):
538544
# save the config option we're supposed to set
539545
self.config_path = dest
540546

547+
# save whether the option requires an active env
548+
self.require_environment = require_environment
549+
541550
# destination is translated to a legal python identifier by
542551
# substituting '_' for ':'.
543552
dest = dest.replace(":", "_")
@@ -553,6 +562,11 @@ def __init__(
553562
)
554563

555564
def __call__(self, parser, namespace, values, option_string):
565+
if self.require_environment and not ev.active_environment():
566+
raise argparse.ArgumentTypeError(
567+
f"argument '{self.option_strings[-1]}' requires an environment"
568+
)
569+
556570
# Retrieve the name of the config option and set it to
557571
# the const from the constructor or a value from the CLI.
558572
# Note that this is only called if the argument is actually
@@ -573,6 +587,16 @@ def add_concretizer_args(subparser):
573587
Just substitute ``_`` for ``:``.
574588
"""
575589
subgroup = subparser.add_argument_group("concretizer arguments")
590+
subgroup.add_argument(
591+
"-f",
592+
"--force",
593+
action=ConfigSetAction,
594+
require_environment=True,
595+
dest="concretizer:force",
596+
const=True,
597+
default=False,
598+
help="allow changes to concretized specs in spack.lock (in an env)",
599+
)
576600
subgroup.add_argument(
577601
"-U",
578602
"--fresh",

lib/spack/spack/cmd/concretize.py

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17,9 +17,6 @@
1717

1818

1919
def setup_parser(subparser: argparse.ArgumentParser) -> None:
20-
subparser.add_argument(
21-
"-f", "--force", action="store_true", help="re-concretize even if already concretized"
22-
)
2320
subparser.add_argument(
2421
"--test",
2522
default=None,
@@ -45,7 +42,7 @@ def concretize(parser, args):
4542
tests = False
4643

4744
with env.write_transaction():
48-
concretized_specs = env.concretize(force=args.force, tests=tests)
45+
concretized_specs = env.concretize(tests=tests)
4946
if not args.quiet:
5047
if concretized_specs:
5148
tty.msg(f"Concretized {plural(len(concretized_specs), 'spec')}:")

lib/spack/spack/cmd/install.py

Lines changed: 2 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
from llnl.util import tty
1414

1515
import spack.cmd
16-
import spack.concretize
1716
import spack.config
1817
import spack.environment as ev
1918
import spack.paths
@@ -204,16 +203,6 @@ def setup_parser(subparser: argparse.ArgumentParser) -> None:
204203
help="(with environment) do not add spec to the environment as a root",
205204
)
206205

207-
subparser.add_argument(
208-
"-f",
209-
"--file",
210-
action="append",
211-
default=[],
212-
dest="specfiles",
213-
metavar="SPEC_YAML_FILE",
214-
help="read specs to install from .yaml files",
215-
)
216-
217206
cd_group = subparser.add_mutually_exclusive_group()
218207
arguments.add_common_arguments(cd_group, ["clean", "dirty"])
219208

@@ -334,7 +323,7 @@ def install(parser, args):
334323
install_kwargs = install_kwargs_from_args(args)
335324
env = ev.active_environment()
336325

337-
if not env and not args.spec and not args.specfiles:
326+
if not env and not args.spec:
338327
_die_require_env()
339328

340329
try:
@@ -436,28 +425,8 @@ def concrete_specs_from_cli(args, install_kwargs):
436425
return concrete_specs
437426

438427

439-
def concrete_specs_from_file(args):
440-
"""Return the list of concrete specs read from files."""
441-
result = []
442-
for file in args.specfiles:
443-
with open(file, "r", encoding="utf-8") as f:
444-
if file.endswith("yaml") or file.endswith("yml"):
445-
s = spack.spec.Spec.from_yaml(f)
446-
else:
447-
s = spack.spec.Spec.from_json(f)
448-
449-
concretized = spack.concretize.concretize_one(s)
450-
if concretized.dag_hash() != s.dag_hash():
451-
msg = 'skipped invalid file "{0}". '
452-
msg += "The file does not contain a concrete spec."
453-
tty.warn(msg.format(file))
454-
continue
455-
result.append(concretized)
456-
return result
457-
458-
459428
def install_without_active_env(args, install_kwargs, reporter):
460-
concrete_specs = concrete_specs_from_cli(args, install_kwargs) + concrete_specs_from_file(args)
429+
concrete_specs = concrete_specs_from_cli(args, install_kwargs)
461430

462431
if len(concrete_specs) == 0:
463432
tty.die("The `spack install` command requires a spec to install.")

lib/spack/spack/cmd/mirror.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def setup_parser(subparser: argparse.ArgumentParser) -> None:
4646
" in the current environment if there is an active environment"
4747
" (this requires significant time and space)",
4848
)
49-
create_parser.add_argument("-f", "--file", help="file with specs of packages to put in mirror")
49+
create_parser.add_argument("--file", help="file with specs of packages to put in mirror")
5050
create_parser.add_argument(
5151
"--exclude-file",
5252
help="specs which Spack should not try to add to a mirror"

lib/spack/spack/environment/environment.py

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1422,7 +1422,9 @@ def is_develop(self, spec):
14221422
"""Returns true when the spec is built from local sources"""
14231423
return spec.name in self.dev_specs
14241424

1425-
def concretize(self, force=False, tests=False):
1425+
def concretize(
1426+
self, force: Optional[bool] = None, tests: Union[bool, Sequence] = False
1427+
) -> Sequence[SpecPair]:
14261428
"""Concretize user_specs in this environment.
14271429
14281430
Only concretizes specs that haven't been concretized yet unless
@@ -1432,15 +1434,18 @@ def concretize(self, force=False, tests=False):
14321434
write out a lockfile containing concretized specs.
14331435
14341436
Arguments:
1435-
force (bool): re-concretize ALL specs, even those that were
1436-
already concretized
1437-
tests (bool or list or set): False to run no tests, True to test
1438-
all packages, or a list of package names to run tests for some
1437+
force: re-concretize ALL specs, even those that were already concretized;
1438+
defaults to ``spack.config.get("concretizer:force")``
1439+
tests: False to run no tests, True to test all packages, or a list of
1440+
package names to run tests for some
14391441
14401442
Returns:
14411443
List of specs that have been concretized. Each entry is a tuple of
14421444
the user spec and the corresponding concretized spec.
14431445
"""
1446+
if force is None:
1447+
force = spack.config.get("concretizer:force")
1448+
14441449
if force:
14451450
# Clear previously concretized specs
14461451
self.concretized_user_specs = []
@@ -1524,7 +1529,9 @@ def _get_specs_to_concretize(
15241529
]
15251530
return new_user_specs, kept_user_specs, specs_to_concretize
15261531

1527-
def _concretize_together_where_possible(self, tests: bool = False) -> Sequence[SpecPair]:
1532+
def _concretize_together_where_possible(
1533+
self, tests: Union[bool, Sequence] = False
1534+
) -> Sequence[SpecPair]:
15281535
# Exit early if the set of concretized specs is the set of user specs
15291536
new_user_specs, _, specs_to_concretize = self._get_specs_to_concretize()
15301537
if not new_user_specs:
@@ -1549,7 +1556,7 @@ def _concretize_together_where_possible(self, tests: bool = False) -> Sequence[S
15491556

15501557
return ret
15511558

1552-
def _concretize_together(self, tests: bool = False) -> Sequence[SpecPair]:
1559+
def _concretize_together(self, tests: Union[bool, Sequence] = False) -> Sequence[SpecPair]:
15531560
"""Concretization strategy that concretizes all the specs
15541561
in the same DAG.
15551562
"""
@@ -1590,7 +1597,7 @@ def _concretize_together(self, tests: bool = False) -> Sequence[SpecPair]:
15901597
# Return the portion of the return value that is new
15911598
return concretized_specs[: len(new_user_specs)]
15921599

1593-
def _concretize_separately(self, tests=False):
1600+
def _concretize_separately(self, tests: Union[bool, Sequence] = False):
15941601
"""Concretization strategy that concretizes separately one
15951602
user spec after the other.
15961603
"""

lib/spack/spack/schema/concretizer.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"type": "object",
1616
"additionalProperties": False,
1717
"properties": {
18+
"force": {"type": "boolean", "default": False},
1819
"reuse": {
1920
"oneOf": [
2021
{"type": "boolean"},

lib/spack/spack/test/cmd/ci.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,13 +1068,13 @@ def test_ci_rebuild_index(
10681068
with open(tmp_path / "spec.json", "w", encoding="utf-8") as f:
10691069
f.write(concrete_spec.to_json(hash=ht.dag_hash))
10701070

1071-
install_cmd("--fake", "--add", "-f", str(tmp_path / "spec.json"))
1071+
install_cmd("--fake", str(tmp_path / "spec.json"))
10721072
buildcache_cmd("push", "-u", "-f", mirror_url, "callpath")
10731073
ci_cmd("rebuild-index")
10741074

10751075
with capsys.disabled():
1076-
output = buildcache_cmd("list", "--allarch")
1077-
assert "callpath" in output
1076+
output = buildcache_cmd("list", "-L", "--allarch")
1077+
assert concrete_spec.dag_hash() + " callpath" in output
10781078

10791079

10801080
def test_ci_get_stack_changed(mock_git_repo, monkeypatch):

lib/spack/spack/test/cmd/install.py

Lines changed: 11 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -339,40 +339,6 @@ def test_install_invalid_spec():
339339
install("conflict%~")
340340

341341

342-
@pytest.mark.usefixtures("noop_install", "mock_packages", "config")
343-
@pytest.mark.parametrize(
344-
"spec,concretize,error_code",
345-
[
346-
(Spec("mpi"), False, 1),
347-
(Spec("mpi"), True, 0),
348-
(Spec("boost"), False, 1),
349-
(Spec("boost"), True, 0),
350-
],
351-
)
352-
def test_install_from_file(spec, concretize, error_code, tmpdir):
353-
if concretize:
354-
spec = spack.concretize.concretize_one(spec)
355-
356-
specfile = tmpdir.join("spec.yaml")
357-
358-
with specfile.open("w") as f:
359-
spec.to_yaml(f)
360-
361-
err_msg = "does not contain a concrete spec" if error_code else ""
362-
363-
# Relative path to specfile (regression for #6906)
364-
with fs.working_dir(specfile.dirname):
365-
# A non-concrete spec will fail to be installed
366-
out = install("-f", specfile.basename, fail_on_error=False)
367-
assert install.returncode == error_code
368-
assert err_msg in out
369-
370-
# Absolute path to specfile (regression for #6983)
371-
out = install("-f", str(specfile), fail_on_error=False)
372-
assert install.returncode == error_code
373-
assert err_msg in out
374-
375-
376342
@pytest.mark.disable_clean_stage_check
377343
@pytest.mark.usefixtures("mock_packages", "mock_archive", "mock_fetch", "install_mockery")
378344
@pytest.mark.parametrize(
@@ -483,6 +449,11 @@ def test_junit_output_with_errors(
483449
assert f'error message="{msg}"' in content
484450

485451

452+
@pytest.fixture(params=["yaml", "json"])
453+
def spec_format(request):
454+
return request.param
455+
456+
486457
@pytest.mark.usefixtures("noop_install", "mock_packages", "config")
487458
@pytest.mark.parametrize(
488459
"clispecs,filespecs",
@@ -494,15 +465,15 @@ def test_junit_output_with_errors(
494465
[["cmake", "libelf"], ["mpi", "boost"]],
495466
],
496467
)
497-
def test_install_mix_cli_and_files(clispecs, filespecs, tmpdir):
468+
def test_install_mix_cli_and_files(spec_format, clispecs, filespecs, tmpdir):
498469
args = clispecs
499470

500471
for spec in filespecs:
501-
filepath = tmpdir.join(spec + ".yaml")
502-
args = ["-f", str(filepath)] + args
472+
filepath = tmpdir.join(spec + f".{spec_format}")
473+
args = [str(filepath)] + args
503474
s = spack.concretize.concretize_one(spec)
504475
with filepath.open("w") as f:
505-
s.to_yaml(f)
476+
s.to_yaml(f) if spec_format == "yaml" else s.to_json(f)
506477

507478
install(*args, fail_on_error=False)
508479
assert install.returncode == 0
@@ -638,7 +609,6 @@ def test_cdash_install_from_spec_json(
638609
"--cdash-build=my_custom_build",
639610
"--cdash-site=my_custom_site",
640611
"--cdash-track=my_custom_track",
641-
"-f",
642612
spec_json_path,
643613
)
644614

@@ -851,11 +821,11 @@ def test_install_no_add_in_env(tmpdir, mutable_mock_env_path, mock_fetch, instal
851821

852822
# Make sure we can install a concrete dependency spec from a spec.json
853823
# file on disk, and the spec is installed but not added as a root
854-
mpi_spec_json_path = tmpdir.join("{0}.json".format(mpi_spec.name))
824+
mpi_spec_json_path = tmpdir.join(f"{mpi_spec.name}.json")
855825
with open(mpi_spec_json_path.strpath, "w", encoding="utf-8") as fd:
856826
fd.write(mpi_spec.to_json(hash=ht.dag_hash))
857827

858-
install("-f", mpi_spec_json_path.strpath)
828+
install(mpi_spec_json_path.strpath)
859829
assert mpi_spec not in e.roots()
860830

861831
find_output = find("-l", output=str)

0 commit comments

Comments
 (0)