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 a Makefile and a single spack.yaml. The actual build is then performed by make.
Stackinator v7 (this branch) supports only version 3 recipes (Spack 1.2+). For version 2 recipes use the releases/v6 branch.
stack-config -b BUILD -r RECIPE -s SYSTEM [-c CACHE] [-m MOUNT]
→ generates BUILD/ directory with Makefile + spack.yaml
cd BUILD
env --ignore-environment PATH=/usr/bin:/bin:`pwd -P`/spack/bin make store.squashfs -j64
→ clones Spack, concretises all spec groups, builds, creates store.squashfs
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.
stackinator/ # Python package
main.py # CLI entry point (stack-config)
recipe.py # Recipe class: parses and validates all recipe YAML
builder.py # Builder class: writes all files to the build path
cache.py # Build cache configuration helpers
spack_util.py # Tiny helper: checks if a path is a spack package repo
schema.py # JSON schema validators with default-injection
schema/ # JSON schemas for each YAML file type
config.json
compilers.json
environments.json
cache.json
modules.json
templates/ # Jinja2 templates for all generated files
Makefile # Top-level build orchestration (single concretize + install)
Makefile.generate-config # Generates upstream spack config for the uenv
Make.user # Build path / store / sandbox variable definitions
repos.yaml # Generated spack repos.yaml
spack.yaml # Unified spack.yaml with spec groups for all compilers + envs
stack-debug.sh # Debug helper script
etc/
Make.inc # Shared make rules
bwrap-mutable-root.sh # Bubblewrap sandbox wrapper
compiler-config.py # Spack Python script: generates packages.yaml with compiler externals
envvars.py # CLI tool: generates env.json for views and uenv metadata
docs/ # MkDocs documentation source
unittests/ # pytest test suite
test_schema.py # Schema validation tests (primary test coverage)
recipes/ # Example recipes used by tests
yaml/ # Example YAML snippets for testing
A recipe is a directory containing YAML files:
name: prgenv-gnu
store: /user-environment # mount point; default /user-environment
version: 3 # must be 3; v1/v2 require Stackinator v6
spack:
repo: https://github.com/spack/spack.git
commit: releases/v1.2 # branch, tag, or SHA; null = default branch
packages:
repo: https://github.com/spack/spack-packages.git
commit: develop
description: "optional text"
default-view: develop # optional: view loaded when no view is specifiedstorecan be overridden at configure time with-m/--mount.
gcc:
version: "13" # required; must be quoted string
nvhpc: # optional
version: "25.1"
llvm: # optional
version: "16"
llvm-amdgpu: # optional
version: "6.0"
intel-oneapi-compilers: # optional
version: "2024.1"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). Each compiler becomes a separate spec group in the unified spack.yaml.
my-env:
compiler: [gcc] # required; list from compilers.yaml keys; first = default
specs: # required; list of spack specs
- cmake
- hdf5+mpi
network: # optional; null = no MPI
mpi: cray-mpich # MPI implementation name (must match network.yaml key)
specs: ['libfabric@1.22'] # optional; overrides network.yaml defaults
unify: true # concretizer: true | false | when_possible (default true)
duplicates:
strategy: minimal # minimal | full | none (default minimal)
deprecated: false # allow deprecated spack versions (default false)
variants: # applied to all packages (packages:all:variants)
- +cuda
- cuda_arch=80
prefer: null # packages:all:prefer; auto-set if null
views: # optional filesystem views
default: null # view name → view config (null = defaults)
no-python:
exclude: [python]
uenv:
add_compilers: true # default true; adds compiler symlinks to view/bin
prefix_paths:
LD_LIBRARY_PATH: [lib, lib64]
env_vars:
set:
- MYVAR: "value"
- MYVAR2: null # unsets the variable
prepend_path:
- PATH: "/some/path"
append_path:
- PKG_CONFIG_PATH: "/usr/lib/pkgconfig"Key constraints:
- Do not include MPI or compilers in
specs; they are handled bynetwork.mpiandcompiler. - Spec matrices are not supported.
- Only one MPI per environment; create separate environments for multiple MPIs.
- The
preferfield is auto-generated ifnull: it nudges Spack to use the first compiler for all packages. - The
packagesfield (list of package names forspack external find) is accepted by the schema but ignored in v3 recipes — a warning is emitted. Add external packages topackages.yamlinstead.
${@VAR@}— deferred expansion: expandsVARat uenv load time (e.g.${@HOME@})$@key@— substitution at configure time:mount,view_name,view_path
ACLOCAL_PATH, CMAKE_PREFIX_PATH, CPATH, LD_LIBRARY_PATH, LIBRARY_PATH, MANPATH, MODULEPATH, PATH, PKG_CONFIG_PATH, PYTHONPATH
Presence of this file enables module generation. Follows Spack's module config format with two differences:
modules:default:arch_foldermust befalse(Stackinator doesn't supporttrue)modules:default:roots:tclis ignored and overwritten by Stackinator
Standard Spack packages.yaml with recipe-specific external package overrides.
Custom Spack package definitions. Must contain a packages/ subdirectory.
Merged into a single alps namespace repo alongside system and site packages.
Precedence: recipe repo > site repos (from cluster config repos.yaml) > Spack builtin.
Shell scripts (any language) run inside the bwrap sandbox:
pre-install: after Spack is set up, before first compiler buildpost-install: after all packages are built, before squashfs generation
Both are Jinja-templated with variables: env.mount, env.config, env.build, env.spack.
Arbitrary files copied to meta/extra/ in the final image (used for CI metadata).
A directory (passed via -s/--system) containing:
cluster-config/
packages.yaml # Spack external packages; must include gcc
network.yaml # MPI defaults and network library package configs
repos.yaml # optional; list of relative paths to site-wide spack repos
network.yaml structure:
mpi:
cray-mpich:
specs: [libfabric@1.22] # default specs injected when cray-mpich is chosen
openmpi:
specs: [libfabric@2.2.0]
packages: # standard spack packages.yaml content
libfabric: ...
cray-mpich: ...Package precedence (recipe.py merges these): recipe packages.yaml > network.yaml packages > cluster packages.yaml. All packages (including gcc externals) go into a single global packages.yaml that is included by the unified spack.yaml.
BUILD/
Makefile # top-level orchestration (single concretize + install)
Make.user # variables: BUILD_ROOT, STORE, SANDBOX, etc.
Make.inc # shared make rules (copied from etc/)
bwrap-mutable-root.sh # sandbox wrapper (copied from etc/)
envvars.py # view/meta generator (copied from etc/)
compiler-config.py # compiler external generator (copied from etc/)
spack.yaml # unified spack.yaml with all spec groups
packages.yaml # merged system + network + recipe packages
config.yaml # spack install tree location
spack/ # cloned Spack repository
spack-packages/ # cloned spack-packages repository
config/ # SPACK_SYSTEM_CONFIG_PATH scope
repos.yaml
mirrors.yaml # only if --cache provided
generate-config/ # generates the upstream spack config for the final image
Makefile
modules/ # only if modules.yaml in recipe
modules.yaml
store/ # installation root (bind-mounted to recipe.store during build)
meta/
configure.json # build metadata
env.json.in # view metadata template
recipe/ # copy of the recipe
repos/spack_repo/alps/ # consolidated custom package repo
repos/spack_repo/builtin/ # copy of spack builtin repo
env/ # filesystem views (created during build)
view-name/
activate.sh
env.json
bin/ lib/ ...
store.squashfs # final compressed image
stack-debug.sh # debug helper: opens shell in build environment
Parses and validates all recipe inputs in __init__. Key responsibilities:
- Validates each YAML file against its JSON schema (with default injection)
- Merges packages from cluster config, network.yaml, and recipe into a single dict
- Generates full compiler specs (e.g.
gcc@13 +bootstrap) fromcompilers.yaml - Processes environments: resolves MPI specs from
network.yamltemplates, sets defaultpreferconstraints, builds view metadata - Provides
spack_yamlproperty (Jinja-rendered unified spack.yaml with spec groups) - Provides
compiler_namesproperty (list of compiler package names forcompiler-config.py)
Writes all files to the build path. Key responsibilities:
- Creates directory structure
- Clones Spack and spack-packages repositories
- Merges and writes the consolidated
alpsspack package repo - Writes the unified
spack.yaml,packages.yaml, andconfig.yamltoBUILD_ROOT - Renders Makefile, Make.user, generate-config/Makefile from Jinja2 templates
- Copies
Make.inc,bwrap-mutable-root.sh,envvars.py,compiler-config.pyfrometc/ - Writes metadata JSON files
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. check_config_version enforces version 3 and gives a clear error for v1/v2 recipes pointing to the releases/v6 branch.
Replaces spack compiler find. Run as spack -e BUILD_ROOT python compiler-config.py OUTPUT_YAML COMPILER.... Uses spack.store.STORE.db.query() to find installed compiler packages, walks the install prefix to locate binaries, and writes (or merges into) a packages.yaml with extra_attributes.compilers entries. This is how compilers become available to downstream Spack users without a compilers.yaml.
A standalone CLI tool (copied into the build directory) with two subcommands:
envvars.py view <root> <build_path> [--compilers=FILE] [--prefix_paths=STR]: reads a Spack-generatedactivate.sh, parses env vars, adds compiler symlinks and prefix paths, writesenv.jsonfor the viewenvvars.py uenv <mount> [--modules] [--spack=...]: merges viewenv.jsonfiles with recipeenv_varsconfig, writes the finalmeta/env.json
The EnvVarSet class in envvars.py is also imported by recipe.py for processing env_vars at configure time.
The top-level Makefile orchestrates in order:
spack-setup— sanity check, bootstrap concretizerpre-install— runpre-install-hookif provided (optional)mirror-setup— configure build cache keysconcretize—spack -e BUILD_ROOT concretize(all spec groups in one pass)install—spack -e BUILD_ROOT install(all groups in dependency order)compiler-config.yaml— runcompiler-config.pyto generate packages.yaml with compiler externalsviews/NAME— per-view: generateactivate.shviaspack env activate, then runenvvars.py viewviews— aggregate target for all viewsgenerate-config—make -C generate-config(writes store/config/{upstreams,packages,repos}.yaml)modules-done—spack module tcl refresh(ifmodules.yamlpresent)env-meta— runenvvars.py uenvto produce finalmeta/env.jsonpost-install— runpost-install-hookif provided (optional)cache-push— push to build cache (if cache with key configured)store.squashfs— create the final squashfs image using thesquashfspackage installed in theuenv_toolsspec group
The uenv_tools spec group (hardcoded in the spack.yaml template) installs squashfs as an implicit dependency of gcc, providing the mksquashfs binary for image creation.
All spack commands use $(SANDBOX) $(SPACK) -e $(BUILD_ROOT) — there is a single spack environment at the build root.
The build runs inside a bwrap sandbox (bwrap-mutable-root.sh) that:
- Bind-mounts
BUILD/store→STORE(the recipe mount point) - Bind-mounts
BUILD/tmp→/tmp - Puts a tmpfs over
$HOME(isolates user config)
The unified spack.yaml uses Spack 1.2 spec groups to express the build order and per-group concretizer settings. Structure:
- gcc group:
explicit: false, override sets static-library variants for gcc's dependencies (mpc, gmp, mpfr, zstd, zlib) - nvhpc/llvm/llvm-amdgpu/intel-oneapi-compilers groups:
explicit: false,needs: [gcc],reuse: false - uenv_tools group:
explicit: false,needs: [gcc], installssquashfs - user environment groups:
needs: [compiler list], override setsconcretizer.unify,concretizer.duplicates.strategy,packages.all.prefer,packages.all.variants, andpackages.mpi.requireper-environment
Per-group override: blocks are pushed as the highest-priority config scope during that group's concretization, so unify and duplicates.strategy settings are truly per-group.
Optional binary cache configured via YAML file passed to -c/--cache:
root: /path/to/cache # directory; env vars expanded
key: /path/to/pgp.key # optional; omit for read-only cacheCache is stored in a subdirectory named after the mount point (e.g. cache/user-environment/) to avoid relocation issues. Packages are pushed in a single cache-push step. Large binary packages (cuda, nvhpc, perl) are excluded from cache pushes.
uv run pytest # run tests
./lint # ruff format + ruff check --fixTests live in unittests/test_schema.py and cover schema validation and default injection. Test recipes are in unittests/recipes/, example YAML in unittests/yaml/.
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.
- Python 3.12+
- Linting:
ruff(line length 120, E + F rules, E203 ignored) - Format:
ruff format - Run both via
./lint
- Build path restrictions: cannot be in
/tmp,$HOME, or root/. The bwrap sandbox rebinds these. - Version 3 is required:
config.yamlmust haveversion: 3. Versions 1 and 2 raise a clear error pointing to thereleases/v6branch. - gcc is required: cluster
packages.yamlmust define an externalgcc. It is merged into the global packages.yaml and used by the gcc spec group's override. - MPI validation: the MPI name in
network.mpimust match a key innetwork.yaml:mpitemplates from the cluster config. Unknown MPI implementations raise an error. - View names are globally unique: view names must be unique across all environments in a recipe.
mirrors.yamlin recipes is unsupported: use--cacheCLI flag instead.default-viewmust exist: if set inconfig.yaml, the named view must be defined inenvironments.yaml(or bemodules/spack).preferis auto-set: ifnullin the recipe, Stackinator generates apreferconstraint using Spack's%[when=...]syntax to pin the default compiler.uenv_toolsis reserved: a spec group nameduenv_toolsis hardcoded in the spack.yaml template to installsquashfs. Recipe authors must not use this environment name.packagesfield in environments.yaml is ignored: in v3 recipes, thepackageslist (formerly used to drivespack external find) is silently ignored with a warning. Add external packages topackages.yamlinstead.- compiler-config.py must run inside spack python: it imports
spack.storeand must be invoked asspack -e BUILD_ROOT python compiler-config.pyto access the correct spack DB.