Skip to content

Commit cfa74e2

Browse files
committed
fix mount_specific behavior; remove "enabled" toggle
always pass the mount when creating Mirrors - the choice about whether to use a mount-specific url for the build cache is decided using the mount_specific boolean flag the "enabled" boolean flag for turning caches on and off was redundant - the mirrors.yaml file describes the mirrors to use - mirrors.yaml will typically be a static file injected into pipelines, and the only use case where you might want to modify its behavior is when doing manual testing - in which case comment out. flipping a switch. - if we want, we can add CLI flags for turning mirror selection.
1 parent 4ce0564 commit cfa74e2

10 files changed

Lines changed: 402 additions & 64 deletions

File tree

CLAUDE.md

Lines changed: 329 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,329 @@
1+
# Stackinator
2+
3+
Stackinator is a Python CLI tool that generates build configurations for scientific software stacks on HPE Cray EX (Alps) systems. It acts like `cmake`/`configure`: given a **recipe** and a **cluster configuration**, it produces a build directory with Makefiles and Spack YAML files. The actual build is then performed by `make`.
4+
5+
## Two-Phase Workflow
6+
7+
```
8+
stack-config -b BUILD -r RECIPE -s SYSTEM [-c CACHE] [-m MOUNT]
9+
→ generates BUILD/ directory with Makefiles + spack.yaml files
10+
11+
cd BUILD
12+
env --ignore-environment PATH=/usr/bin:/bin:`pwd -P`/spack/bin make store.squashfs -j64
13+
→ clones Spack, concretises, builds, creates store.squashfs
14+
```
15+
16+
The `store.squashfs` SquashFS image is the final artifact, intended to be mounted at the recipe's `store` path (default `/user-environment`) as a uenv image.
17+
18+
## Repository Layout
19+
20+
```
21+
stackinator/ # Python package
22+
main.py # CLI entry point (stack-config)
23+
recipe.py # Recipe class: parses and validates all recipe YAML
24+
builder.py # Builder class: writes all files to the build path
25+
cache.py # Build cache configuration helpers
26+
spack_util.py # Tiny helper: checks if a path is a spack package repo
27+
schema.py # JSON schema validators with default-injection
28+
schema/ # JSON schemas for each YAML file type
29+
config.json
30+
compilers.json
31+
environments.json
32+
cache.json
33+
modules.json
34+
templates/ # Jinja2 templates for all generated files
35+
Makefile # Top-level build orchestration
36+
Makefile.compilers # Compiler build steps
37+
Makefile.environments # Environment build + view generation steps
38+
Makefile.generate-config # Generates upstream spack config for the uenv
39+
Make.user # Build path / store / sandbox variable definitions
40+
repos.yaml # Generated spack repos.yaml
41+
stack-debug.sh # Debug helper script
42+
compilers.*.spack.yaml # Per-compiler spack.yaml configs
43+
environments.spack.yaml # Environment spack.yaml config
44+
etc/
45+
Make.inc # Shared make rules (concretize, depfile, compiler_bin_dirs)
46+
bwrap-mutable-root.sh # Bubblewrap sandbox wrapper
47+
envvars.py # CLI tool: generates env.json for views and uenv metadata
48+
docs/ # MkDocs documentation source
49+
unittests/ # pytest test suite
50+
test_schema.py # Schema validation tests (primary test coverage)
51+
recipes/ # Example recipes used by tests
52+
yaml/ # Example YAML snippets for testing
53+
```
54+
55+
## Recipe Format (input)
56+
57+
A recipe is a directory containing YAML files:
58+
59+
### `config.yaml` (required)
60+
```yaml
61+
name: prgenv-gnu
62+
store: /user-environment # mount point; default /user-environment
63+
version: 2 # must be 2 for Spack 1.0 (Stackinator 6+)
64+
spack:
65+
repo: https://github.com/spack/spack.git
66+
commit: releases/v1.0 # branch, tag, or SHA; null = default branch
67+
packages:
68+
repo: https://github.com/spack/spack-packages.git
69+
commit: develop
70+
description: "optional text"
71+
default-view: develop # optional: view loaded when no view is specified
72+
```
73+
74+
- `version: 1` (the default) targets Spack v0.23 and is only supported by Stackinator v5 (`releases/v5` branch). **Current `main` requires `version: 2`.**
75+
- `store` can be overridden at configure time with `-m/--mount`.
76+
77+
### `compilers.yaml` (required)
78+
```yaml
79+
gcc:
80+
version: "13" # required; must be quoted string
81+
nvhpc: # optional
82+
version: "25.1"
83+
llvm: # optional
84+
version: "16"
85+
llvm-amdgpu: # optional
86+
version: "6.0"
87+
intel-oneapi-compilers: # optional
88+
version: "2024.1"
89+
```
90+
91+
Build order: `gcc` is built first (using system compiler), then `nvhpc`/`llvm`/`llvm-amdgpu`/`intel-oneapi-compilers` are built using the gcc toolchain. Stackinator appends opinionated variants (e.g. `gcc@13 +bootstrap`, `nvhpc@25.1 ~mpi~blas~lapack`, `llvm@16 +clang ~gold`).
92+
93+
### `environments.yaml` (required)
94+
```yaml
95+
my-env:
96+
compiler: [gcc] # required; list from compilers.yaml keys; first = default
97+
specs: # required; list of spack specs
98+
- cmake
99+
- hdf5+mpi
100+
network: # optional; null = no MPI
101+
mpi: cray-mpich # full spack spec for MPI (cray-mpich or openmpi)
102+
specs: ['libfabric@1.22'] # optional; overrides network.yaml defaults
103+
unify: true # concretizer: true | false | when_possible (default true)
104+
duplicates:
105+
strategy: minimal # minimal | full | none (default minimal)
106+
deprecated: false # allow deprecated spack versions (default false)
107+
variants: # applied to all packages (packages:all:variants)
108+
- +cuda
109+
- cuda_arch=80
110+
prefer: null # packages:all:prefer; auto-set if null
111+
packages: # external packages to discover via `spack external find`
112+
- perl
113+
- git
114+
views: # optional filesystem views
115+
default: null # view name → view config (null = defaults)
116+
no-python:
117+
exclude: [python]
118+
uenv:
119+
add_compilers: true # default true; adds compiler symlinks to view/bin
120+
prefix_paths:
121+
LD_LIBRARY_PATH: [lib, lib64]
122+
env_vars:
123+
set:
124+
- MYVAR: "value"
125+
- MYVAR2: null # unsets the variable
126+
prepend_path:
127+
- PATH: "/some/path"
128+
append_path:
129+
- PKG_CONFIG_PATH: "/usr/lib/pkgconfig"
130+
```
131+
132+
**Key constraints:**
133+
- Do not include MPI or compilers in `specs`; they are handled by `network.mpi` and `compiler`.
134+
- Spec matrices are not supported.
135+
- Only one MPI per environment; create separate environments for multiple MPIs.
136+
- The `prefer` field is auto-generated if `null`: it nudges Spack to use the first compiler for all packages.
137+
138+
#### Environment variable special syntax
139+
- `${@VAR@}` — deferred expansion: expands `VAR` at uenv load time (e.g. `${@HOME@}`)
140+
- `$@key@` — substitution at configure time: `mount`, `view_name`, `view_path`
141+
142+
#### Supported prefix-path variables (hardcoded in `etc/envvars.py`)
143+
`ACLOCAL_PATH`, `CMAKE_PREFIX_PATH`, `CPATH`, `LD_LIBRARY_PATH`, `LIBRARY_PATH`, `MANPATH`, `MODULEPATH`, `PATH`, `PKG_CONFIG_PATH`, `PYTHONPATH`
144+
145+
### `modules.yaml` (optional)
146+
Presence of this file enables module generation. Follows Spack's module config format with two differences:
147+
- `modules:default:arch_folder` must be `false` (Stackinator doesn't support `true`)
148+
- `modules:default:roots:tcl` is ignored and overwritten by Stackinator
149+
150+
### `packages.yaml` (optional)
151+
Standard Spack `packages.yaml` with recipe-specific external package overrides.
152+
153+
### `repo/` (optional)
154+
Custom Spack package definitions. Must contain a `packages/` subdirectory.
155+
Merged into a single `alps` namespace repo alongside system and site packages.
156+
Precedence: recipe repo > site repos (from cluster config `repos.yaml`) > Spack builtin.
157+
158+
### `post-install` / `pre-install` (optional)
159+
Shell scripts (any language) run inside the bwrap sandbox:
160+
- `pre-install`: after Spack is set up, before first compiler build
161+
- `post-install`: after all packages are built, before squashfs generation
162+
163+
Both are Jinja-templated with variables: `env.mount`, `env.config`, `env.build`, `env.spack`.
164+
165+
### `extra/` (optional)
166+
Arbitrary files copied to `meta/extra/` in the final image (used for CI metadata).
167+
168+
## Cluster Configuration (input)
169+
170+
A directory (passed via `-s/--system`) containing:
171+
172+
```
173+
cluster-config/
174+
packages.yaml # Spack external packages; must include gcc
175+
network.yaml # MPI defaults and network library package configs
176+
repos.yaml # optional; list of relative paths to site-wide spack repos
177+
```
178+
179+
`network.yaml` structure:
180+
```yaml
181+
mpi:
182+
cray-mpich:
183+
specs: [libfabric@1.22] # default specs injected when cray-mpich is chosen
184+
openmpi:
185+
specs: [libfabric@2.2.0]
186+
packages: # standard spack packages.yaml content
187+
libfabric: ...
188+
cray-mpich: ...
189+
```
190+
191+
Package precedence (recipe.py merges these): recipe `packages.yaml` > `network.yaml` packages > `packages.yaml` (minus gcc). The `gcc` entry from `packages.yaml` is isolated and used only for the gcc compiler build step.
192+
193+
## Build Directory Structure (output)
194+
195+
```
196+
BUILD/
197+
Makefile # top-level orchestration
198+
Make.user # variables: BUILD_ROOT, STORE, SANDBOX, etc.
199+
Make.inc # shared make rules (copied from etc/)
200+
bwrap-mutable-root.sh # sandbox wrapper (copied from etc/)
201+
envvars.py # view/meta generator (copied from etc/)
202+
spack/ # cloned Spack repository
203+
spack-packages/ # cloned spack-packages repository
204+
config/ # global spack configuration scope
205+
packages.yaml
206+
mirrors.yaml # only if --cache provided
207+
repos.yaml
208+
compilers/
209+
Makefile
210+
gcc/
211+
spack.yaml
212+
packages.yaml # generated by spack external find
213+
nvhpc/ # if nvhpc in recipe
214+
spack.yaml
215+
environments/
216+
Makefile
217+
my-env/
218+
spack.yaml
219+
generate-config/ # generates the upstream spack config for the final image
220+
Makefile
221+
modules/ # only if modules.yaml in recipe
222+
modules.yaml
223+
store/ # installation root (bind-mounted to recipe.store during build)
224+
meta/
225+
configure.json # build metadata
226+
env.json.in # view metadata template
227+
recipe/ # copy of the recipe
228+
repos/spack_repo/alps/ # consolidated custom package repo
229+
repos/spack_repo/builtin/ # copy of spack builtin repo
230+
env/ # filesystem views (created during build)
231+
view-name/
232+
activate.sh
233+
env.json
234+
bin/ lib/ ...
235+
store.squashfs # final compressed image
236+
stack-debug.sh # debug helper: opens shell in build environment
237+
```
238+
239+
## Python Architecture
240+
241+
### `Recipe` class (`recipe.py`)
242+
Parses and validates all recipe inputs in `__init__`. Key responsibilities:
243+
- Validates each YAML file against its JSON schema (with default injection)
244+
- Merges packages from cluster config, network.yaml, and recipe
245+
- Generates full compiler specs (e.g. `gcc@13 +bootstrap`) from `compilers.yaml`
246+
- Processes environments: resolves MPI specs from `network.yaml` templates, sets default `prefer` constraints, builds view metadata
247+
- Provides `compiler_files` and `environment_files` properties (Jinja-rendered Makefiles and spack.yaml files)
248+
249+
### `Builder` class (`builder.py`)
250+
Writes all files to the build path. Key responsibilities:
251+
- Creates directory structure
252+
- Clones Spack and spack-packages repositories
253+
- Merges and writes the consolidated `alps` spack package repo
254+
- Renders all Jinja templates into build path files
255+
- Writes metadata JSON files
256+
257+
### `schema.py`
258+
JSON schema validation using `jsonschema`. The `validator()` function extends the validator to auto-inject `default` values from schemas into parsed instances, so downstream code can rely on optional fields always being present.
259+
260+
### `etc/envvars.py`
261+
A standalone CLI tool (copied into the build directory) with two subcommands:
262+
- `envvars.py view <root> <build_path> [--compilers] [--prefix_paths]`: reads a Spack-generated `activate.sh`, parses env vars, adds compiler symlinks and prefix paths, writes `env.json` for the view
263+
- `envvars.py uenv <mount> [--modules] [--spack]`: merges view `env.json` files with recipe `env_vars` config, writes the final `meta/env.json`
264+
265+
The `EnvVarSet` class in `envvars.py` is also imported by `recipe.py` for processing `env_vars` at configure time.
266+
267+
## Build Pipeline (Make targets)
268+
269+
The top-level `Makefile` orchestrates in order:
270+
1. `spack-setup` — sanity check, bootstrap concretizer
271+
2. `pre-install` — run `pre-install-hook` if provided
272+
3. `mirror-setup` — configure build cache keys
273+
4. `compilers` — build gcc, then nvhpc/llvm/etc. (parallel within each stage)
274+
5. `environments` — build all user environments (parallel)
275+
6. `generate-config` — generate the upstream spack config files for the installed image
276+
7. `modules-done` — generate TCL module files (if `modules.yaml` present)
277+
8. `env-meta` — run `envvars.py uenv` to produce final `meta/env.json`
278+
9. `post-install` — run `post-install-hook` if provided
279+
10. `store.squashfs` — create the final squashfs image
280+
281+
Key Make.inc rules:
282+
- `%/spack.lock`: concretize a spack environment
283+
- `%/Makefile`: generate a depfile from a lock file (enables parallel package builds)
284+
- `compiler_bin_dirs`: helper to find compiler binaries given install prefixes
285+
286+
The build runs inside a bwrap sandbox (`bwrap-mutable-root.sh`) that:
287+
- Bind-mounts `BUILD/store` → `STORE` (the recipe mount point)
288+
- Bind-mounts `BUILD/tmp` → `/tmp`
289+
- Puts a tmpfs over `$HOME` (isolates user config)
290+
291+
## Build Cache
292+
293+
Optional binary cache configured via YAML file passed to `-c/--cache`:
294+
```yaml
295+
root: /path/to/cache # directory; env vars expanded
296+
key: /path/to/pgp.key # optional; omit for read-only cache
297+
```
298+
299+
Cache is stored in a subdirectory named after the mount point (e.g. `cache/user-environment/`) to avoid relocation issues. Packages are pushed per-environment after a successful build. Large binary packages (`cuda`, `nvhpc`, `perl`) are excluded from cache pushes.
300+
301+
## Testing
302+
303+
```bash
304+
uv run pytest # run tests
305+
./lint # ruff format + ruff check --fix
306+
```
307+
308+
Tests live in `unittests/test_schema.py` and cover schema validation and default injection. Test recipes are in `unittests/recipes/`, example YAML in `unittests/yaml/`.
309+
310+
The test coverage is limited — the schema validators and their default-injection are well tested, but `Recipe`, `Builder`, and `envvars.py` have minimal test coverage.
311+
312+
## Code Style
313+
314+
- Python 3.12+
315+
- Linting: `ruff` (line length 120, E + F rules, E203 ignored)
316+
- Format: `ruff format`
317+
- Run both via `./lint`
318+
319+
## Key Invariants and Pitfalls
320+
321+
- **Build path restrictions**: cannot be in `/tmp`, `$HOME`, or root `/`. The bwrap sandbox rebinds these.
322+
- **Version 2 is required**: `config.yaml` must have `version: 2` for current `main`. Version 1 recipes require the `releases/v5` branch.
323+
- **gcc is required**: `packages.yaml` in cluster config must define an external `gcc`. It is handled separately from other system packages for the bootstrap build step.
324+
- **MPI validation**: the MPI name in `network.mpi` must match a key in `network.yaml:mpi` templates from the cluster config. Unknown MPI implementations raise an error.
325+
- **View names are globally unique**: view names must be unique across all environments in a recipe.
326+
- **`mirrors.yaml` in recipes is unsupported**: use `--cache` CLI flag instead.
327+
- **`default-view` must exist**: if set in `config.yaml`, the named view must be defined in `environments.yaml` (or be `modules`/`spack`).
328+
- **`prefer` is auto-set**: if `null` in the recipe, Stackinator generates a `prefer` constraint using Spack's `%[when=...]` syntax to pin the default compiler.
329+
- **Spack `uenv_tools` environment**: an internal environment named `uenv_tools` is injected into every build to install `squashfs`. Recipe authors must not use this name.

bin/stack-config

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
# /// script
33
# requires-python = ">=3.12"
44
# dependencies = [
5-
# "requests",
65
# "python-magic",
76
# "jinja2",
87
# "jsonschema",

docs/cluster-config.md

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -103,22 +103,20 @@ On air-gapped systems, Spack is unable to reach its default mirror to fetch pack
103103

104104
If using a buildcache, a private key must be provided for signing packages. An optional public key may be specified with any type of mirror to verify packages.
105105

106+
To stop using a mirror, remove (or comment out) its entry from `mirrors.yaml`.
107+
106108
```yaml title="mirrors.yaml"
107109
bootstrap:
108110
url: file:///home/username/spack-mirror-2014-06-24
109-
enabled: true
110111
buildcache:
111112
url: https://example.com/some/buildcache/mirror
112-
enabled: true
113113
private_key: /user-home/.gnupg/private-keys-v1.d/my-private-key.asc
114114
sourcecache:
115115
mirror1:
116116
url: https://example.com/some/web-hosted/directory
117117
public_key: ../buildcache-key.public.gpg
118-
enabled: true
119118
mirror2:
120119
url: https://example.com/some/other-web-hosted/directory
121-
enabled: true
122120
```
123121

124122
## Site and System Configurations

pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@ license-files = ["LICENSE"]
1111
dynamic = ["version"]
1212
requires-python = ">=3.12"
1313
dependencies = [
14-
"requests",
1514
"python-magic",
1615
"Jinja2",
1716
"jsonschema",

0 commit comments

Comments
 (0)