Skip to content

Commit 840d85a

Browse files
committed
misc cache -> explicit concretizer cache
1 parent b8f847d commit 840d85a

9 files changed

Lines changed: 218 additions & 86 deletions

File tree

CLAUDE.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ stackinator/ # Python package
2929
config.json
3030
compilers.json
3131
environments.json
32-
mirror.json # mirrors.yaml schema (buildcache/bootstrap/sourcemirror/sourcecache/misccache)
32+
mirror.json # mirrors.yaml schema (buildcache/bootstrap/sourcemirror/sourcecache/concretizer)
3333
cache.json # legacy -c/--cache cache.yaml schema
3434
modules.json
3535
templates/ # Jinja2 templates for all generated files
@@ -208,7 +208,8 @@ BUILD/
208208
packages.yaml
209209
repos.yaml
210210
mirrors.yaml # if --mirror provided: buildcache/sourcemirror entries
211-
config.yaml # if --mirror provided with a sourcecache/misccache (config:source_cache/misc_cache)
211+
config.yaml # if --mirror provided with a sourcecache (config:source_cache)
212+
concretizer.yaml # if --mirror provided with a concretizer cache (concretizer:concretization_cache), spack >= 1.1
212213
bootstrap.yaml # if --mirror provided with a bootstrap mirror
213214
key_store/ # if --mirror provided with gpg keys (decoded *.gpg)
214215
compilers/
@@ -262,8 +263,8 @@ Writes all files to the build path. Key responsibilities:
262263
263264
### `Mirrors` class (`mirror.py`)
264265
A clean exemplar of the "recipe validates & renders, builder just prints" pattern. Constructed by `Recipe` from the `--mirror` file path; does ALL mirror input processing eagerly in `__init__` (loads + schema-validates `mirrors.yaml`, validates mirror urls, decodes/validates gpg keys to in-memory bytes, checks cache paths are absolute and expands env vars). Then presents pure static artifacts:
265-
- typed members: `buildcache`, `bootstrap`, `source_mirrors`, `source_cache`, `misc_cache`
266-
- `config_files(config_root) -> {abs_path: bytes}` — the `mirrors.yaml`, `config.yaml`, `bootstrap.yaml`, and gpg key files the builder writes verbatim
266+
- typed members: `buildcache`, `bootstrap`, `source_mirrors`, `source_cache`, `concretizer_cache`
267+
- `config_files(config_root) -> {abs_path: bytes}` — the `mirrors.yaml`, `config.yaml`, `concretizer.yaml`, `bootstrap.yaml`, and gpg key files the builder writes verbatim
267268
- `gpg_key_paths(config_root)` and the `build_cache_mirror` / `push_to_build_cache` properties (the latter is `None` for a keyless, read-only build cache)
268269
269270
Mirror/cache config is supplied ONLY via `--mirror`; a `mirrors.yaml` found in the system config dir is rejected with an error (it was never a system-config artifact). Relative gpg-key paths resolve against the `--mirror` file's own directory.
@@ -310,9 +311,9 @@ Spack mirrors and caches are configured in a single `mirrors.yaml` supplied with
310311
- **`bootstrap`** (one): for bootstrapping Spack itself (clingo etc.). The `url` is either a local `spack bootstrap mirror` directory (referenced via its own `metadata/sources`+`metadata/binaries`) or a remote url (source-only). Needs **no key** (bootstrap binaries are sha256-verified) → emitted as `config/bootstrap.yaml` (+ a generated `metadata.yaml` only for the remote case); it is NOT a `mirrors.yaml` entry.
311312
- **`sourcemirror`** (many): read-only mirrors providing package source archives.
312313
- **`sourcecache`** (one): a writable local dir Spack fills as it fetches sources → emitted as `config:source_cache`.
313-
- **`misccache`** (one): a writable local dir for Spack's misc cache (package/build-cache indices and the **concretization cache** that lives under it) → emitted as `config:misc_cache`. Useful to persist across ephemeral builds. Concretization caching is on by default in Spack ≥ 1.2, opt-in in 1.1.
314+
- **`concretizer`** (one): a writable local dir persisting Spack's **concretization results** → emitted as `concretizer:concretization_cache:{enable,url}` in `config/concretizer.yaml`. Useful to persist across ephemeral builds. The config key requires Spack ≥ 1.1; Stackinator infers the Spack version from `config.yaml:spack.commit` (via `Recipe.find_spack_version`, defaulting to the latest supported version when the commit can't be pinned) and **skips the cache with a warning** for Spack 1.0 (which rejects the key).
314315
315-
`sourcecache`/`misccache` are emitted to `config/config.yaml`; `bootstrap` to `config/bootstrap.yaml`; `buildcache`/`sourcemirror` to `config/mirrors.yaml` (+ decoded keys under `config/key_store/`).
316+
`sourcecache` is emitted to `config/config.yaml`; `concretizer` to `config/concretizer.yaml`; `bootstrap` to `config/bootstrap.yaml`; `buildcache`/`sourcemirror` to `config/mirrors.yaml` (+ decoded keys under `config/key_store/`).
316317
317318
**Legacy:** a binary cache can still be configured with a `cache.yaml` (`root` + optional `key`) passed to `-c/--cache`. This path is deprecated in favour of a `buildcache` entry and will be removed.
318319

docs/build-caches.md

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ A `mirrors.yaml` can describe five kinds of entry, each optional and each docume
1818
| [`bootstrap`](#bootstrap-mirror) | one | mirror used to bootstrap Spack itself |
1919
| [`sourcemirror`](#source-mirrors) | many | read-only mirrors that provide package sources |
2020
| [`sourcecache`](#source-cache) | one | writable local cache that fills with sources as you build |
21-
| [`misccache`](#misc-cache) | one | writable local cache for Spack's indices and concretization results |
21+
| [`concretizer`](#concretizer-cache) | one | writable local cache that persists concretization results |
2222

2323
A complete example:
2424

@@ -34,8 +34,8 @@ sourcemirror:
3434
url: https://example.com/spack-sources
3535
sourcecache:
3636
path: /capstor/scratch/$USER/spack-sources
37-
misccache:
38-
path: /capstor/scratch/$USER/spack-misc
37+
concretizer:
38+
path: /capstor/scratch/$USER/spack-concretizer
3939
```
4040
4141
To stop using any entry, remove (or comment out) it from `mirrors.yaml`.
@@ -184,21 +184,31 @@ sourcecache:
184184
|-------|----------|-------------|
185185
| `path` | yes | absolute path to a local directory (environment variables are expanded) |
186186

187-
## Misc cache
187+
## Concretizer cache
188188

189-
The misc cache is a single, **writable** local directory for Spack's "misc" cache: the package and build-cache indices, and — importantly — the **concretization cache**, which stores the result of concretizing a set of specs so it does not have to be recomputed.
189+
The concretizer cache is a single, **writable** local directory in which Spack persists its **concretization results** — the output of concretizing a set of specs, so it does not have to be recomputed.
190190
Concretization can be a large fraction of build time, so pointing this at a persistent location is worthwhile when build directories are ephemeral (e.g. created in `/dev/shm` and deleted after each build).
191191

192192
```yaml title="mirrors.yaml"
193-
misccache:
194-
path: /capstor/scratch/$USER/spack-misc
193+
concretizer:
194+
path: /capstor/scratch/$USER/spack-concretizer
195195
```
196196

197197
| Field | Required | Description |
198198
|-------|----------|-------------|
199199
| `path` | yes | absolute path to a local directory (environment variables are expanded) |
200200

201-
The concretization cache lives under this directory. Spack populates it automatically: concretization caching is on by default in Spack 1.2 and later, and is opt-in (`concretizer:concretization_cache:enable`) in Spack 1.1. It is keyed by the hash of the solver inputs, so a persistent cache can be reused safely across builds — stale entries simply miss.
201+
This emits a `concretizer.yaml` that sets `concretizer:concretization_cache:{enable: true, url}`.
202+
The cache is keyed by the hash of the solver inputs, so it can be reused safely across builds — stale entries simply miss.
203+
204+
!!! warning "It does not eliminate concretization time"
205+
The cache stores only the *result of the solve* for a given set of inputs. Before it can be consulted, Spack still has to rebuild the full concretization problem on every run — loading the package recipes and enumerating the reusable packages — and that setup work is often the larger part of concretization. So a cache hit skips the solver but not the setup: concretization gets faster, not free.
206+
207+
The win is therefore largest for repeated builds of the same stack against a stable build cache (the previous solve is replayed), and smallest when the bulk of concretization time is in setup. The cache never makes concretization slower.
208+
209+
!!! note "Requires Spack ≥ 1.1"
210+
The `concretizer:concretization_cache` config key was introduced in Spack 1.1, and Spack 1.0 rejects it.
211+
Stackinator infers the Spack version from the `spack.commit` in `config.yaml` (defaulting to a supported version when the commit is a branch or arbitrary SHA that cannot be pinned). When it detects Spack 1.0 it skips the concretizer cache with a warning rather than producing a config that would fail the build.
202212

203213
## Keys
204214

stackinator/mirror.py

Lines changed: 61 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,17 @@
2727
)
2828

2929

30+
def _supports_concretization_cache(spack_version: str) -> bool:
31+
"""Whether the given "major.minor" spack version supports the concretizer cache.
32+
33+
The concretizer:concretization_cache config key was introduced in spack 1.1;
34+
spack 1.0 rejects it (its concretizer schema forbids unknown keys).
35+
"""
36+
37+
major_minor = tuple(int(x) for x in spack_version.split("."))
38+
return major_minor >= (1, 1)
39+
40+
3041
class MirrorError(RuntimeError):
3142
"""Exception class for errors thrown by mirror configuration problems."""
3243

@@ -52,11 +63,12 @@ class Mirrors:
5263
it fetches sources (spack config:source_cache). None if
5364
absent. This is not a mirror: it has no key and no url, and
5465
is emitted to config.yaml rather than mirrors.yaml.
55-
* misc_cache - at most one, a writable local directory for spack's misc
56-
cache (package/build-cache indices and the concretization
57-
cache that lives under it) (spack config:misc_cache). None
58-
if absent. Like source_cache it is not a mirror and is
59-
emitted to config.yaml.
66+
* concretizer_cache - at most one, a writable local directory persisting spack's
67+
concretization results (concretizer:concretization_cache).
68+
None if absent. Like source_cache it is not a mirror; it is
69+
emitted to concretizer.yaml. It is only emitted for spack
70+
>= 1.1 (spack 1.0 rejects the config key); when requested
71+
against spack 1.0 it is skipped with a warning.
6072
6173
All input processing - loading and schema-validating the system mirrors.yaml,
6274
validating urls, and reading/decoding/validating gpg keys - happens eagerly in
@@ -70,18 +82,22 @@ class Mirrors:
7082
MIRRORS_YAML = "mirrors.yaml"
7183
CONFIG_YAML = "config.yaml"
7284
BOOTSTRAP_YAML = "bootstrap.yaml"
85+
CONCRETIZER_YAML = "concretizer.yaml"
7386

7487
def __init__(
7588
self,
7689
system_config_root: pathlib.Path,
7790
mount_path: pathlib.Path,
91+
spack_version: str,
7892
mirror_file: Optional[pathlib.Path] = None,
7993
cmdline_cache: Optional[pathlib.Path] = None,
8094
):
8195
"""Load and fully resolve the mirror configuration.
8296
8397
Mirrors are supplied with the --mirror command line option (mirror_file).
8498
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
100+
concretizer cache (only emitted for spack >= 1.1).
85101
cmdline_cache is an optional legacy cache.yaml passed on the command line (--cache).
86102
87103
Relative paths in the mirror file (e.g. gpg keys) are resolved relative to the
@@ -96,7 +112,7 @@ def __init__(
96112
self.bootstrap: Optional[Dict] = None
97113
self.source_mirrors: Dict[str, Dict] = {}
98114
self.source_cache: Optional[Dict] = None
99-
self.misc_cache: Optional[Dict] = None
115+
self.concretizer_cache: Optional[Dict] = None
100116

101117
# The mirror configuration is supplied with --mirror, not the system
102118
# configuration. Reject a mirrors.yaml in the system config so it is not
@@ -164,28 +180,42 @@ def __init__(
164180
f"{yaml.dump([self.buildcache], default_flow_style=False)}"
165181
)
166182

167-
# The bootstrap mirror, the read-only source mirrors, and the writable
168-
# source and misc caches, if any are defined.
183+
# The bootstrap mirror, the read-only source mirrors, the writable source
184+
# cache, and the writable concretizer cache, if any are defined.
169185
self.bootstrap = raw_mirrors.get("bootstrap")
170186
self.source_mirrors = dict(raw_mirrors["sourcemirror"])
171187
self.source_cache = raw_mirrors.get("sourcecache")
172-
self.misc_cache = raw_mirrors.get("misccache")
188+
self.concretizer_cache = raw_mirrors.get("concretizer")
173189

174190
# Validate that every mirror url is well-formed (see _validate_url).
175191
for name, mirror in self._iter_mirrors():
176192
self._validate_url(mirror["url"], name)
177193

178-
# The source and misc caches are single writable local directories (spack
179-
# config:source_cache / config:misc_cache), not mirrors: validate that each is
180-
# an absolute path. Expand env vars now, because the build sandbox runs
181-
# `env --ignore-environment` and so would not expand them at build time.
182-
for cache_name, cache in (("source", self.source_cache), ("misc", self.misc_cache)):
194+
# The source and concretizer caches are single writable local directories
195+
# (spack config:source_cache / concretizer:concretization_cache), not mirrors:
196+
# validate that each is an absolute path. Expand env vars now, because the
197+
# build sandbox runs `env --ignore-environment` and so would not expand them
198+
# at build time.
199+
for cache_name, cache in (("source", self.source_cache), ("concretizer", self.concretizer_cache)):
183200
if cache is not None:
184201
path = os.path.expandvars(cache["path"])
185202
if not pathlib.Path(path).is_absolute():
186203
raise MirrorError(f"The {cache_name} cache path '{path}' is not absolute")
187204
cache["path"] = path
188205

206+
# The concretizer cache (concretizer:concretization_cache) was introduced in
207+
# spack 1.1; spack 1.0 rejects the config key. Determine whether to emit it
208+
# based on the best-effort spack version, and warn (not error) if it was
209+
# requested but cannot be configured.
210+
self._emit_concretizer_cache = self.concretizer_cache is not None and _supports_concretization_cache(
211+
spack_version
212+
)
213+
if self.concretizer_cache is not None and not self._emit_concretizer_cache:
214+
self._logger.warning(
215+
f"The concretizer cache is not supported by spack {spack_version} (requires >= 1.1) "
216+
"and will not be configured."
217+
)
218+
189219
# Resolve the bootstrap mirror. It is either a remote url, or a local spack
190220
# bootstrap mirror directory (a `spack bootstrap mirror` output) whose own
191221
# metadata/{sources,binaries} directories we reference directly.
@@ -391,17 +421,26 @@ def config_files(self, config_root: pathlib.Path) -> Dict[pathlib.Path, bytes]:
391421
spack_mirrors, default_flow_style=False, sort_keys=False
392422
).encode()
393423

394-
# the spack config.yaml setting the writable local caches: the populate-as-you-go
395-
# source cache and the misc cache (which holds the concretization cache)
396-
config_section = {}
424+
# the spack config.yaml setting the populate-as-you-go source cache (spack
425+
# config:source_cache).
397426
if self.source_cache is not None:
398-
config_section["source_cache"] = self.source_cache["path"]
399-
if self.misc_cache is not None:
400-
config_section["misc_cache"] = self.misc_cache["path"]
401-
if config_section:
402-
config_yaml = {"config": config_section}
427+
config_yaml = {"config": {"source_cache": self.source_cache["path"]}}
403428
files[config_root / self.CONFIG_YAML] = yaml.dump(config_yaml, default_flow_style=False).encode()
404429

430+
# the spack concretizer.yaml persisting concretization results. enable is set
431+
# explicitly because it is opt-in in spack 1.1 (on by default only in >= 1.2).
432+
# _emit_concretizer_cache gates this on the spack version (>= 1.1).
433+
if self._emit_concretizer_cache:
434+
concretizer_yaml = {
435+
"concretizer": {
436+
"concretization_cache": {
437+
"enable": True,
438+
"url": self.concretizer_cache["path"],
439+
}
440+
}
441+
}
442+
files[config_root / self.CONCRETIZER_YAML] = yaml.dump(concretizer_yaml, default_flow_style=False).encode()
443+
405444
# the spack bootstrap.yaml, if a bootstrap mirror is set. Bootstrapping reads
406445
# bootstrap:sources (not the mirrors list), and each source's `metadata` is a
407446
# directory describing it. No gpg key is involved (bootstrap binaries are

stackinator/recipe.py

Lines changed: 30 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -176,12 +176,19 @@ def __init__(self, args):
176176
)
177177
raise RuntimeError("Ivalid default-view in the recipe.")
178178

179+
# determine the version of spack being used: it is inferred (best effort)
180+
# from the spack commit in config.yaml, defaulting to the latest supported
181+
# version when it cannot be determined. This must precede the mirror
182+
# configuration, which gates the concretizer cache on the spack version.
183+
self.spack_version = self.find_spack_version(args.develop)
184+
179185
# resolve the mirror configuration provided with --mirror. --cache is the
180186
# legacy path.
181187
self._logger.debug("Configuring mirrors.")
182188
self.mirrors = mirror.Mirrors(
183189
self.system_config_path,
184190
self.mount,
191+
self.spack_version,
185192
pathlib.Path(args.mirror) if args.mirror else None,
186193
pathlib.Path(args.cache) if args.cache else None,
187194
)
@@ -198,11 +205,6 @@ def __init__(self, args):
198205
else:
199206
self._logger.debug("no pre install hook provided")
200207

201-
# determine the version of spack being used:
202-
# currently this just returns 1.0... develop is ignored
203-
# --develop flag will imply the next release of spack after 1.0 is supported properly
204-
self.spack_version = self.find_spack_version(args.develop)
205-
206208
# Returns:
207209
# Path: if the recipe contains a spack package repository
208210
# None: if there is the recipe contains no repo
@@ -276,10 +278,30 @@ def config(self, config_path):
276278
def with_modules(self) -> bool:
277279
return self.modules is not None
278280

279-
# In Stackinator 6 we replaced logic required to determine the
280-
# pre 1.0 Spack version.
281+
# Make a best-effort determination of the "major.minor" version of spack being
282+
# used, inferred from the spack commit in config.yaml. This is only a hint: the
283+
# commit can be an arbitrary branch/tag/sha, so when the version cannot be pinned
284+
# we default to the latest supported version ("1.1"). Returns a "major.minor"
285+
# string (e.g. "1.0", "1.1").
281286
def find_spack_version(self, develop):
282-
return "1.0"
287+
# the latest supported version, used when the version cannot be determined
288+
# (an explicit --develop, the default branch, or an unrecognised commit).
289+
default = "1.1"
290+
291+
if develop:
292+
return default
293+
294+
commit = self.config["spack"]["commit"]
295+
if commit is None or commit in ("develop", "main"):
296+
return default
297+
298+
# match a release branch/tag (releases/v1.0, v1.1, v1.1.2) or a bare "1.0",
299+
# and extract the major.minor version.
300+
match = re.search(r"v?(\d+)\.(\d+)(?:\.\d+)?", commit)
301+
if match:
302+
return f"{match.group(1)}.{match.group(2)}"
303+
304+
return default
283305

284306
@property
285307
def default_view(self):

stackinator/schema/mirror.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@
5656
"additionalProperties": false,
5757
"required": ["path"]
5858
},
59-
"misccache": {
59+
"concretizer": {
6060
"type": "object",
6161
"properties": {
6262
"description": {"type": "string", "default": ""},

unittests/data/systems/mirror-bad-misccache/mirrors.yaml renamed to unittests/data/systems/mirror-bad-concretizer/mirrors.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
misccache:
1+
concretizer:
22
path: relative/not/absolute

0 commit comments

Comments
 (0)