Skip to content

Commit 68ec45c

Browse files
authored
support spack 1.2 to stackinator v6 (#301)
Add support for spack 1.2 in stackinator 6 This adds support for quirks of Spack 1.2 to the current version of stackinator that can targets Spack 1.0 and 1.1. * disable the new Spack installer (making it work properly requires deep plumbing changes that might affect compatibility with 1.0 and 1.1. * insert the `--yes-to-all` flag to `uenv buildcache` calls when using Spack 1.2 This also improves how the Spack version is determined and used in the Jinja templates. Note that this is separate from Stackinator v7, which supports Spack 1.2 more thoroughly (with no backwards compatibility with v1.1 and v1.0): #298
1 parent 3ce4ff0 commit 68ec45c

7 files changed

Lines changed: 112 additions & 56 deletions

File tree

stackinator/builder.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -229,6 +229,16 @@ def generate(self, recipe):
229229
dest.parent.mkdir(parents=True, exist_ok=True)
230230
dest.write_bytes(content)
231231

232+
# Spack 1.2 makes the new (parallel) installer the default.
233+
# Stackinator 6 the behaviour of the old installer, so pin config:installer to
234+
# "old" for spack >= 1.2.
235+
if spack_version >= (1, 2):
236+
config_yaml_path = config_path / "config.yaml"
237+
config_doc = yaml.safe_load(config_yaml_path.read_text()) if config_yaml_path.exists() else None
238+
config_doc = config_doc or {}
239+
config_doc.setdefault("config", {})["installer"] = "old"
240+
config_yaml_path.write_text(yaml.dump(config_doc, default_flow_style=False))
241+
232242
# generate top level makefiles
233243
makefile_template = jinja_env.get_template("Makefile")
234244

stackinator/mirror.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import magic
99

1010
from . import schema, root_logger
11+
from .spack_util import Version
1112

1213

1314
# GPG keys may be presented either ASCII-armored (these headers) or as binary
@@ -27,15 +28,14 @@
2728
)
2829

2930

30-
def _supports_concretization_cache(spack_version: str) -> bool:
31-
"""Whether the given "major.minor" spack version supports the concretizer cache.
31+
def _supports_concretization_cache(spack_version: Version) -> bool:
32+
"""Whether the given spack version supports the concretizer cache.
3233
3334
The concretizer:concretization_cache config key was introduced in spack 1.1;
3435
spack 1.0 rejects it (its concretizer schema forbids unknown keys).
3536
"""
3637

37-
major_minor = tuple(int(x) for x in spack_version.split("."))
38-
return major_minor >= (1, 1)
38+
return spack_version >= (1, 1)
3939

4040

4141
class MirrorError(RuntimeError):
@@ -88,15 +88,15 @@ def __init__(
8888
self,
8989
system_config_root: pathlib.Path,
9090
mount_path: pathlib.Path,
91-
spack_version: str,
91+
spack_version: Version,
9292
mirror_file: Optional[pathlib.Path] = None,
9393
cmdline_cache: Optional[pathlib.Path] = None,
9494
):
9595
"""Load and fully resolve the mirror configuration.
9696
9797
Mirrors are supplied with the --mirror command line option (mirror_file).
9898
mount_path is the recipe mount point (used to make a build cache mount-specific).
99-
spack_version is the best-effort "major.minor" spack version, used to gate the
99+
spack_version is the best-effort spack Version, used to gate the
100100
concretizer cache (only emitted for spack >= 1.1).
101101
cmdline_cache is an optional legacy cache.yaml passed on the command line (--cache).
102102

stackinator/recipe.py

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -340,28 +340,28 @@ def config(self, config_path):
340340
def with_modules(self) -> bool:
341341
return self.modules is not None
342342

343-
# Make a best-effort determination of the "major.minor" version of spack being
343+
# Make a best-effort determination of the major.minor version of spack being
344344
# used, inferred from the spack commit in config.yaml. This is only a hint: the
345345
# commit can be an arbitrary branch/tag/sha, so when the version cannot be pinned
346-
# we default to the latest supported version ("1.1"). Returns a "major.minor"
347-
# string (e.g. "1.0", "1.1").
346+
# we default to the most recent version expected by default with this tool
347+
# ("1.1"). Returns a spack_util.Version.
348348
def find_spack_version(self, develop):
349-
# the latest supported version, used when the version cannot be determined
350-
# (an explicit --develop, the default branch, or an unrecognised commit).
351-
default = "1.1"
349+
# the most recent version expected to be used by default with this tool,
350+
# used when the version cannot be determined from an unrecognised commit.
351+
default = spack_util.Version(1, 1)
352352

353+
# the develop and main branches of spack are now at 1.2.
353354
if develop:
354-
return default
355+
return spack_util.Version(1, 2)
355356

356357
commit = self.config["spack"]["commit"]
357358
if commit is None or commit in ("develop", "main"):
358-
return default
359+
return spack_util.Version(1, 2)
359360

360-
# match a release branch/tag (releases/v1.0, v1.1, v1.1.2) or a bare "1.0",
361-
# and extract the major.minor version.
362-
match = re.search(r"v?(\d+)\.(\d+)(?:\.\d+)?", commit)
363-
if match:
364-
return f"{match.group(1)}.{match.group(2)}"
361+
# match a release branch/tag (releases/v1.0, v1.1, v1.1.2) or a bare "1.0".
362+
version = spack_util.Version.parse(commit)
363+
if version is not None:
364+
return version
365365

366366
return default
367367

stackinator/spack_util.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,35 @@
1+
import re
2+
from typing import NamedTuple, Optional
3+
4+
5+
class Version(NamedTuple):
6+
"""A minimal "major.minor" spack version that supports comparison.
7+
8+
Implemented as a NamedTuple so that the comparison operators (==, <, >=, ...)
9+
come for free via tuple ordering, e.g. ``Version(1, 2) > Version(1, 1)``.
10+
Because it is a tuple it also compares directly against plain tuples, which is
11+
convenient in Jinja templates: ``{% if spack_version >= (1, 2) %}``.
12+
"""
13+
14+
major: int
15+
minor: int
16+
17+
@classmethod
18+
def parse(cls, text: str) -> Optional["Version"]:
19+
"""Extract a "major.minor" Version from text, or None if none is present.
20+
21+
Matches a release branch/tag (releases/v1.0, v1.1, v1.1.2) or a bare
22+
"1.0", ignoring any trailing patch component.
23+
"""
24+
match = re.search(r"v?(\d+)\.(\d+)(?:\.\d+)?", text)
25+
if match is None:
26+
return None
27+
return cls(int(match.group(1)), int(match.group(2)))
28+
29+
def __str__(self) -> str:
30+
return f"{self.major}.{self.minor}"
31+
32+
133
def is_repo(path):
234
"""
335
Returns True if path contains a spack package repo, where the definition of

stackinator/templates/Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,11 @@ mirror-setup: spack-setup{% if pre_install_hook %} pre-install{% endif %}
3535

3636
{% if cache %}
3737
@echo "Pulling and trusting keys from configured buildcaches."
38-
$(SANDBOX) $(SPACK) buildcache keys --install --trust
38+
$(SANDBOX) $(SPACK) buildcache keys --install --trust{% if spack_version >= (1, 2) %} --yes-to-all{% endif +%}
3939
{% endif %}
4040
@echo "Adding mirror gpg keys."
4141
{% for key_path in gpg_keys %}
42-
$(SANDBOX) $(SPACK) gpg trust {{ key_path }}
42+
$(SANDBOX) $(SPACK) gpg trust{% if spack_version >= (1, 2) %} --yes-to-all{% endif %} {{ key_path }}
4343
{% endfor %}
4444
@echo "Current mirror list:"
4545
$(SANDBOX) $(SPACK) mirror list

unittests/test_mirrors.py

Lines changed: 33 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import stackinator.mirror as mirror
55
import yaml
66

7+
from stackinator.spack_util import Version
8+
79

810
@pytest.fixture
911
def test_path():
@@ -36,7 +38,7 @@ def mirror_ok(systems_path):
3638

3739
def test_mirror_init(clean_root, mount_path, systems_path, mirror_ok):
3840
"""Check that the three kinds of mirror are resolved into separate members."""
39-
mirrors_obj = mirror.Mirrors(clean_root, mount_path, "1.1", mirror_file=mirror_ok)
41+
mirrors_obj = mirror.Mirrors(clean_root, mount_path, Version(1, 1), mirror_file=mirror_ok)
4042

4143
with (systems_path / "../test-gpg-pub.asc").open("rb") as pub_key_file:
4244
pub_key_b64 = base64.b64encode(pub_key_file.read()).decode()
@@ -79,20 +81,20 @@ def test_system_mirrors_yaml_rejected(systems_path, mount_path):
7981

8082
# mirror-ok contains a mirrors.yaml; passing it as the system config root must raise.
8183
with pytest.raises(mirror.MirrorError):
82-
mirror.Mirrors(systems_path / "mirror-ok", mount_path, "1.1")
84+
mirror.Mirrors(systems_path / "mirror-ok", mount_path, Version(1, 1))
8385

8486

8587
def test_missing_mirror_file(clean_root, mount_path):
8688
"""A --mirror file that does not exist raises MirrorError."""
8789

8890
with pytest.raises(mirror.MirrorError):
89-
mirror.Mirrors(clean_root, mount_path, "1.1", mirror_file=clean_root / "does-not-exist.yaml")
91+
mirror.Mirrors(clean_root, mount_path, Version(1, 1), mirror_file=clean_root / "does-not-exist.yaml")
9092

9193

9294
def test_no_mirror_file(clean_root, mount_path):
9395
"""With no --mirror file there are no mirrors configured."""
9496

95-
mirrors_obj = mirror.Mirrors(clean_root, mount_path, "1.1")
97+
mirrors_obj = mirror.Mirrors(clean_root, mount_path, Version(1, 1))
9698

9799
assert mirrors_obj.buildcache is None
98100
assert mirrors_obj.bootstrap is None
@@ -105,7 +107,11 @@ def test_command_line_cache(clean_root, mount_path, systems_path, mirror_ok):
105107
"""Check that adding a cache from the command line works."""
106108

107109
mirrors = mirror.Mirrors(
108-
clean_root, mount_path, "1.1", mirror_file=mirror_ok, cmdline_cache=systems_path / "mirror-ok/cache.yaml"
110+
clean_root,
111+
mount_path,
112+
Version(1, 1),
113+
mirror_file=mirror_ok,
114+
cmdline_cache=systems_path / "mirror-ok/cache.yaml",
109115
)
110116

111117
# the command line cache overrides any build cache defined in the mirror file,
@@ -127,7 +133,11 @@ def test_keyless_command_line_cache(tmp_path, clean_root, mount_path, systems_pa
127133
"""A cache.yaml without a key configures a read-only (fetch-only) build cache."""
128134

129135
mirrors = mirror.Mirrors(
130-
clean_root, mount_path, "1.1", mirror_file=mirror_ok, cmdline_cache=systems_path / "mirror-ok/cache-nokey.yaml"
136+
clean_root,
137+
mount_path,
138+
Version(1, 1),
139+
mirror_file=mirror_ok,
140+
cmdline_cache=systems_path / "mirror-ok/cache-nokey.yaml",
131141
)
132142

133143
# the cache exists (so it is fetched from), but has no signing key ...
@@ -158,7 +168,7 @@ def test_readonly_buildcache(tmp_path, clean_root, mount_path, systems_path):
158168
"""A buildcache in the mirror file without a private_key is read-only (fetch-only)."""
159169

160170
mirrors = mirror.Mirrors(
161-
clean_root, mount_path, "1.1", mirror_file=systems_path / "mirror-readonly-cache/mirrors.yaml"
171+
clean_root, mount_path, Version(1, 1), mirror_file=systems_path / "mirror-readonly-cache/mirrors.yaml"
162172
)
163173

164174
# the cache exists (so it is fetched from), but has no signing key ...
@@ -192,7 +202,7 @@ def test_config_files(tmp_path, clean_root, mount_path, mirror_ok):
192202
so this also exercises relative key paths resolving against the mirror file's dir.
193203
"""
194204

195-
mirrors_obj = mirror.Mirrors(clean_root, mount_path, "1.1", mirror_file=mirror_ok)
205+
mirrors_obj = mirror.Mirrors(clean_root, mount_path, Version(1, 1), mirror_file=mirror_ok)
196206
files = mirrors_obj.config_files(tmp_path)
197207

198208
expected = {
@@ -236,7 +246,7 @@ def test_spack_mirrors_yaml(tmp_path, clean_root, mount_path, mirror_ok):
236246
}
237247
}
238248

239-
mirrors_obj = mirror.Mirrors(clean_root, mount_path, "1.1", mirror_file=mirror_ok)
249+
mirrors_obj = mirror.Mirrors(clean_root, mount_path, Version(1, 1), mirror_file=mirror_ok)
240250
files = mirrors_obj.config_files(tmp_path)
241251
data = yaml.safe_load(files[tmp_path / "mirrors.yaml"])
242252

@@ -250,7 +260,7 @@ def test_mount_specific_buildcache(tmp_path, clean_root, mount_path, mirror_ok):
250260
cache is namespaced per-mount-point to avoid relocation issues / collisions.
251261
"""
252262

253-
mirrors_obj = mirror.Mirrors(clean_root, mount_path, "1.1", mirror_file=mirror_ok)
263+
mirrors_obj = mirror.Mirrors(clean_root, mount_path, Version(1, 1), mirror_file=mirror_ok)
254264

255265
# mirror-ok's buildcache is mount_specific: false by default; enable it.
256266
mirrors_obj.buildcache["mount_specific"] = True
@@ -269,7 +279,7 @@ def test_mount_specific_buildcache(tmp_path, clean_root, mount_path, mirror_ok):
269279
def test_mount_specific_disabled(tmp_path, clean_root, mount_path, mirror_ok):
270280
"""A buildcache with mount_specific false is unchanged, even when a mount point is set."""
271281

272-
mirrors_obj = mirror.Mirrors(clean_root, mount_path, "1.1", mirror_file=mirror_ok)
282+
mirrors_obj = mirror.Mirrors(clean_root, mount_path, Version(1, 1), mirror_file=mirror_ok)
273283

274284
# confirm the fixture leaves the flag off
275285
assert mirrors_obj.buildcache["mount_specific"] is False
@@ -301,7 +311,7 @@ def test_remote_bootstrap_configs(tmp_path, clean_root, mount_path, mirror_ok):
301311
},
302312
}
303313

304-
mirrors_obj = mirror.Mirrors(clean_root, mount_path, "1.1", mirror_file=mirror_ok)
314+
mirrors_obj = mirror.Mirrors(clean_root, mount_path, Version(1, 1), mirror_file=mirror_ok)
305315
files = mirrors_obj.config_files(tmp_path)
306316

307317
bs_data = yaml.safe_load(files[tmp_path / "bootstrap.yaml"])
@@ -327,7 +337,7 @@ def test_local_bootstrap_configs(tmp_path, clean_root, mount_path):
327337
mirror_file.write_text(f"bootstrap:\n url: {boot}\n")
328338

329339
config_root = tmp_path / "config"
330-
mirrors_obj = mirror.Mirrors(clean_root, mount_path, "1.1", mirror_file=mirror_file)
340+
mirrors_obj = mirror.Mirrors(clean_root, mount_path, Version(1, 1), mirror_file=mirror_file)
331341
files = mirrors_obj.config_files(config_root)
332342

333343
bs_data = yaml.safe_load(files[config_root / "bootstrap.yaml"])
@@ -357,13 +367,13 @@ def test_local_bootstrap_missing_metadata(tmp_path, clean_root, mount_path):
357367
mirror_file.write_text(f"bootstrap:\n url: {boot}\n")
358368

359369
with pytest.raises(mirror.MirrorError):
360-
mirror.Mirrors(clean_root, mount_path, "1.1", mirror_file=mirror_file)
370+
mirror.Mirrors(clean_root, mount_path, Version(1, 1), mirror_file=mirror_file)
361371

362372

363373
def test_keys(tmp_path, clean_root, mount_path, systems_path, mirror_ok):
364374
"""Check that gpg keys are decoded, relocated and reported consistently."""
365375

366-
mirrors_obj = mirror.Mirrors(clean_root, mount_path, "1.1", mirror_file=mirror_ok)
376+
mirrors_obj = mirror.Mirrors(clean_root, mount_path, Version(1, 1), mirror_file=mirror_ok)
367377
files = mirrors_obj.config_files(tmp_path)
368378

369379
# the buildcache is the only mirror with keys (sources are checksum-verified)
@@ -383,7 +393,7 @@ def test_keys(tmp_path, clean_root, mount_path, systems_path, mirror_ok):
383393
def test_local_caches_config(tmp_path, clean_root, mount_path, mirror_ok):
384394
"""The source cache is emitted to config.yaml; the concretizer cache to concretizer.yaml."""
385395

386-
mirrors_obj = mirror.Mirrors(clean_root, mount_path, "1.1", mirror_file=mirror_ok)
396+
mirrors_obj = mirror.Mirrors(clean_root, mount_path, Version(1, 1), mirror_file=mirror_ok)
387397
files = mirrors_obj.config_files(tmp_path)
388398

389399
config_data = yaml.safe_load(files[tmp_path / "config.yaml"])
@@ -401,7 +411,7 @@ def test_concretizer_cache_only(tmp_path, clean_root, mount_path):
401411
mirror_file = tmp_path / "mirrors.yaml"
402412
mirror_file.write_text("concretizer:\n path: /scratch/only-concretizer\n")
403413

404-
mirrors_obj = mirror.Mirrors(clean_root, mount_path, "1.1", mirror_file=mirror_file)
414+
mirrors_obj = mirror.Mirrors(clean_root, mount_path, Version(1, 1), mirror_file=mirror_file)
405415
files = mirrors_obj.config_files(tmp_path)
406416

407417
assert mirrors_obj.source_cache is None
@@ -418,7 +428,7 @@ def test_concretizer_cache_skipped_on_spack_1_0(tmp_path, clean_root, mount_path
418428
mirror_file = tmp_path / "mirrors.yaml"
419429
mirror_file.write_text("concretizer:\n path: /scratch/only-concretizer\n")
420430

421-
mirrors_obj = mirror.Mirrors(clean_root, mount_path, "1.0", mirror_file=mirror_file)
431+
mirrors_obj = mirror.Mirrors(clean_root, mount_path, Version(1, 0), mirror_file=mirror_file)
422432
files = mirrors_obj.config_files(tmp_path)
423433

424434
# the cache is still recorded as requested, but no concretizer.yaml is written
@@ -431,7 +441,7 @@ def test_local_caches_absent(tmp_path, clean_root, mount_path, systems_path):
431441

432442
# mirror-no-sourcecache has neither a sourcecache nor a concretizer entry
433443
mirrors_obj = mirror.Mirrors(
434-
clean_root, mount_path, "1.1", mirror_file=systems_path / "mirror-no-sourcecache/mirrors.yaml"
444+
clean_root, mount_path, Version(1, 1), mirror_file=systems_path / "mirror-no-sourcecache/mirrors.yaml"
435445
)
436446
files = mirrors_obj.config_files(tmp_path)
437447

@@ -449,7 +459,9 @@ def test_s3_fetch_and_push_connections(tmp_path, clean_root, mount_path, systems
449459
buildcache and sourcemirror entries accept the same spack connection config.
450460
"""
451461

452-
mirrors_obj = mirror.Mirrors(clean_root, mount_path, "1.1", mirror_file=systems_path / "mirror-s3/mirrors.yaml")
462+
mirrors_obj = mirror.Mirrors(
463+
clean_root, mount_path, Version(1, 1), mirror_file=systems_path / "mirror-s3/mirrors.yaml"
464+
)
453465
files = mirrors_obj.config_files(tmp_path)
454466
data = yaml.safe_load(files[tmp_path / "mirrors.yaml"])
455467

@@ -492,4 +504,4 @@ def test_bad_config(clean_root, mount_path, systems_path, system_name):
492504
"""Check that MirrorError is raised at construction for bad keys or a bad cache path."""
493505

494506
with pytest.raises(mirror.MirrorError):
495-
mirror.Mirrors(clean_root, mount_path, "1.1", mirror_file=systems_path / system_name / "mirrors.yaml")
507+
mirror.Mirrors(clean_root, mount_path, Version(1, 1), mirror_file=systems_path / system_name / "mirrors.yaml")

0 commit comments

Comments
 (0)