Design Document: Spack 1.x in mache
Summary
mache builds Spack environments from E3SM-Project/spack, a fork of Spack
0.23.1 carrying about a hundred E3SM commits, checked out from a
spack_for_mache_<version> branch. Spack 1.0 (July 2025) moved every package
into a separate repository, spack/spack-packages, turned compilers into
ordinary packages, and fixed a versioned package API. The fork cannot follow:
its changes sit in the old in-tree package layout, and Spack 0.23 needs a
workaround to run on Python 3.12 or newer.
This design replaces the fork with three pinned pieces: an unmodified Spack
release, a spack-packages release, and a small E3SM package repository,
E3SM-Project/e3sm-spack-packages (namespace e3sm), searched ahead of
builtin. mache pins all three per release, keeps the on-disk instance
layout, and migrates its machine templates to the Spack 1.x compiler model.
Load scripts stop running spack env activate and instead source an
activation snippet captured at build time. Support for the legacy Intel
classic compilers ends. The change ships as mache 5.0.0.
Andrew Nolan’s spack-v1.0.0 branch
(PR #281) is the starting
point. What was kept and what was changed is listed in Prior work.
Open Questions
Date last modified: Sep 22, 2026
None remain; resolved questions are recorded in Decisions. The
two items this document left for verification on the pinned Spack, that a
toolchain name is accepted inside packages:all:require and the set of
variables spack env activate --sh emits for a view, both held, so neither
fallback was needed.
Requirements
Date last modified: Sep 19, 2026 Contributors: Xylar Asay-Davis, with Claude Code
Requirement: Build with unmodified upstream Spack
mache must build environments with a released Spack 1.x, checked out from
spack/spack at a tag. mache must not depend on any fork of the Spack tool.
Design resolution
The build script clones https://github.com/spack/spack.git at the tag in
pins.yaml into spack_path. The pinned Spack must be 1.2
or newer, for spack isolate.
Requirement: Upstream packages come from a released spack-packages
Package recipes must come from a released spack/spack-packages version.
mache must not carry a copy of any package that the pinned release already
provides in the form E3SM needs.
Design resolution
The builtin repository is a clone of spack/spack-packages at a release tag,
registered second in the instance’s repos.yaml. See
Package repositories.
Requirement: E3SM-specific packages and early updates
E3SM must be able to build packages that upstream does not have (albany,
trilinos-for-albany), and versions or fixes that the pinned upstream release
does not yet have. These must take precedence over the upstream package of the
same name.
Design resolution
E3SM-Project/e3sm-spack-packages, namespace e3sm, is registered first in
repos.yaml. Spack resolves a package name to the first repository that
provides it, so e3sm shadows builtin. Packages that extend an upstream
package subclass it instead of copying it. See
The e3sm package repository and
Decisions 1 and 7.
Requirement: Reproducible pins per mache release
A mache release must fully determine the Spack version, the builtin
release and the e3sm release used to build environments. Two deployments
from the same mache release must concretize the same package sources.
Design resolution
mache/spack/pins.yaml pins each repository to exactly one of a tag, a commit
or a branch. Releases pin tags or commits. See Version pins.
Requirement: Pins can be overridden for development
A developer must be able to build against a branch or commit of any of the three repositories without editing packaged files, to test a recipe before it is tagged.
Design resolution
Overrides are accepted as a file (--spack-pins <file> in mache deploy, or
pins= in make_spack_env), from a deployment hook, or from
spack.pins in deploy/config.yaml.j2. See Precedence.
Requirement: Isolation from user configuration and between releases
Building and activating environments must not read or write the invoking
user’s ~/.spack. Environments built by different mache releases must not
share a Spack instance, package repositories or install tree.
Design resolution
The build script runs spack isolate --self on the instance, which moves the
user scope, caches, stages and bootstrap store under $SPACK_ROOT. Package
repositories are cloned under $SPACK_ROOT/var/spack/package_repos. Loading
an environment sources a static file and runs no Spack at all. One instance
per mache release is what downstream already does by putting the mache
version in spack_path. See Spack instance layout.
Requirement: Machine templates valid for Spack 1.x
Every <machine>_<compiler>_<mpi>.yaml template must render to a spack.yaml
that Spack 1.x accepts with no deprecated sections, and must select the E3SM
compiler and MPI for every package that needs one.
Design resolution
Templates drop the compilers: section and packages:all:compiler, declare
the compiler as an external with extra_attributes.compilers, and select it
through a toolchains: entry required for all packages. The three templates
for Intel classic compilers are retired. See
Environment templates.
Requirement: Loading an environment is fast and needs no Spack
A load script must set up a Spack environment without running Spack, so that loading costs milliseconds rather than seconds and does not depend on the user’s Spack configuration.
Design resolution
At build time mache captures spack env activate --sh <env> in a clean
shell, rewrites path-like variables as prepends relative to the shell that
sources the file, and writes activate.sh and activate.csh into the
environment directory. Load scripts source that file. See
Captured activation.
Requirement: Existing entry points keep working
make_spack_env, get_spack_script, get_modules_env_vars_and_mpi_compilers
and the spack: section of deploy/config.yaml.j2 must keep their signatures
and meanings, with additions only. Load scripts generated by earlier mache
releases must keep working for the environments they were generated for.
Design resolution
spack_path remains $SPACK_ROOT and environments remain managed
environments under var/spack/environments, so view_path and every path a
downstream hook computes are unchanged. get_spack_script(load_spack_env=True)
now emits a source of the captured file instead of spack env activate.
Old instances are untouched because new releases use new instance paths. See
Integration.
Requirement: Legacy instances are refused
mache must refuse to reuse a spack_path that holds a Spack 0.x checkout,
and tell the user to choose a new path.
Design resolution
Before touching an existing checkout, the build script reads
lib/spack/spack/__init__.py and exits with a message if __version__ is
below 1.0. Resetting such a checkout to a 1.x tag would leave a 0.x database
and environments underneath it.
Desired: Provenance for each build
Each environment build records the resolved commit of the three repositories
and the resulting spack.lock.
Design resolution
After spack install, the build script copies spack.lock and writes
provenance.yaml next to the rendered environment YAML in the work directory.
Package repositories
Date last modified: Sep 20, 2026
Repository roles
Three git repositories, listed in Spack’s search order:
Role |
Repository |
Namespace |
Pinned by |
|---|---|---|---|
E3SM overlay |
|
|
tag or commit |
upstream packages |
|
|
release tag, e.g. |
tool |
|
– |
release tag, e.g. |
spack-packages releases are tied to Spack minor releases (v2026.06.0 with
Spack 1.2) and state which Spack versions they support; the v2026.06.0 notes
allow any Spack 1.0 or newer. The e3sm tag records which builtin release
it was tested against.
The e3sm package repository
E3SM-Project/e3sm-spack-packages exists as an empty repository (created
Sep 20, 2026, no initial commit) and is seeded from Andrew Nolan’s
open_PR_rebase branch of andrewdnolan/spack-packages; see
Repository setup. Its maintainers are Xylar Asay-Davis
and Andrew Nolan, with others added as needed. Layout, following the Spack v2
repository layout and the monorepo index:
e3sm-spack-packages/
├── README.md # one line per package: why it is here, upstream PR
├── LICENSE
├── spack-repo-index.yaml # repo_index: paths: [repos/spack_repo/e3sm]
├── ci/
│ ├── versions.yaml # (spack tag, builtin tag) pairs CI tests against
│ └── specs.txt # the specs downstream software actually requests
├── .github/workflows/ci.yaml
└── repos/spack_repo/e3sm/
├── repo.yaml # repo: {namespace: e3sm, api: v2.2}
└── packages/
├── albany/package.py
├── trilinos_for_albany/package.py
├── e3sm_scorpio/package.py
└── ...
Rules for packages in this repository:
Directory names use the v2 convention (
trilinos_for_albany, nottrilinos-for-albany).Build systems are imported from
spack_repo.builtin.build_systems; nothing is imported fromspack.pkgor fromspack.*other thanspack.package.Every package lists
maintainers("xylar", "andrewdnolan")plus the package’s upstream owner where one exists (Albany’s developers foralbanyandtrilinos-for-albany).A package that exists upstream is extended by subclassing, not by copying:
from spack_repo.builtin.packages.parallel_netcdf.package import ( ParallelNetcdf as BuiltinParallelNetcdf, ) from spack.package import * class ParallelNetcdf(BuiltinParallelNetcdf): version('1.15.0', sha256='...')
A change inside a builder (for example
esmf’ssetup_build_environment) is made by defining a builder subclass with the build system’s class name in the same module, which Spack looks up before the upstream builder. Verified on Spack 1.2.2 (Sep 21, 2026).Directives are inherited and re-executed by a subclass, so one cannot remove an upstream
depends_on.esmfneeds that (upstream declares run-timepythonandpy-pyyamldependencies for ESMX, which would put a Spack python in the view), so its module deletes the two entries from the class’sdependenciestable after the class is created, with a comment; the overlay’s CI catches a change to that table’s shape.A package or version that is not E3SM-specific is submitted upstream when it is added here. It is deleted from
e3smwhen the pinnedbuiltinrelease contains it.
Rationale
Subclassing keeps the upstream recipe evolving underneath and leaves only the
delta to maintain. The fork’s copy of esmf had removed 400 lines of upstream
code and re-added machine-specific NERSC_HOST checks; the rebase problem
that closed PR #281 came from exactly this kind of drift.
Repository setup
The first commit is mache’s LICENSE (BSD 3-Clause, copyright Energy
Exascale Earth System Model Project) together with the layout above, the CI
workflow and the tag rules, so that every later commit is checked.
Files derived from
spack-packages, whether a copied full package or a subclass module that reuses upstream code, keep their upstream header,SPDX-License-Identifier: (Apache-2.0 OR MIT). E3SM-authored files carry a BSD 3-Clause header. TheREADMEstates both. Apache-2.0 and MIT allow redistribution inside a BSD-licensed repository, so no relicensing is needed.No tag is cut until
mache5.0.0 is ready to pin one; the first tag isv2026.06.0. Until thenmache’s development branch pins the overlay bycommit, and developers testing an overlay branch use--spack-pinswith abranchentry rather than editing the packaged file.A
macheunit test guards the release: when__version__is not a pre-release (packaging.version.Version.is_prerelease), everygitURL inpins.yamlmust be undergithub.com/spack/orgithub.com/E3SM-Project/and every ref must be atag. Thespack-v1development branch carries a release-candidate version, asmachedid for3.3.0rc1, so commit pins pass until the release is cut.Personal forks such as
xylar/e3sm-spack-packagesare ordinary forks for pull requests, not development homes (Decisions 16).
Continuous integration for e3sm-spack-packages
Recommended: concretize every package against the pinned builtin on GitHub
runners for every pull request. It is cheap, because nothing is built.
.github/workflows/ci.yaml has three jobs:
style:spack style(Ruff-based in Spack 1.2) andspack audit packagesoverrepos/spack_repo/e3sm.concretize: a matrix over the(spack, builtin)pairs inci/versions.yaml. Each job clones Spack at the tag, registersbuiltinat its tag ande3smfrom the checkout, runsspack compiler findfor the runner’s GCC, and runsspack spec -Non every package directory and on every line ofci/specs.txt. The bootstrap store is cached withactions/cacheso clingo is downloaded once.install: on manual dispatch or weekly,spack installof the cheap packages (parallel-netcdf,tempestremap,tempestextremes,e3sm-scorpio) to catch bad checksums and patches.albanyandtrilinos-for-albanyare too slow for a runner and are covered by deployments.
ci/specs.txt mirrors the specs in the three downstream spack.yaml.j2
files, so a recipe change that breaks the variants E3SM asks for fails here
rather than on a machine. The README “tested against” statement is derived
from ci/versions.yaml, and mache’s pins.yaml must name a pair listed
there.
Package inventory
What the fork carries on top of Spack 0.23.1, against spack-packages
v2026.06.0, and where each piece goes:
Package |
In the fork |
Upstream |
Disposition |
|---|---|---|---|
|
compass tags, |
|
|
|
full package, 3 patches |
absent |
|
|
1.9.0–2.0.3, three variants, cce fix |
up to 1.8.1 |
upstream PR; |
|
no Python dependency; NetCDF handling on Perlmutter, Chicoma, Frontier; oneAPI |
8.9.1 |
|
|
2.2.2–2.4.2 as |
2.3, 2.3.1 (package added by Andrew, #853) |
upstream PR for 2.4.x; |
|
version for MOAB master, grid-tolerance patch |
2.2.0 (Andrew, #858), but missing |
|
|
1.15.0 |
1.14.1 (Andrew, #936) |
upstream PR; |
|
4.10.1 |
4.10.0 |
upstream PR; |
|
5.6.0, tempest variant |
5.6.0, tempest (Andrew, #872) |
drop |
|
up to 5.3.9 |
5.3.9 |
drop |
|
4.6.2 |
4.6.2 (Andrew, #835); polaris pins 4.6.3, which neither has |
upstream PR for 4.6.3; |
|
1.14.6 preferred |
present |
drop |
|
3.4.0, 3.4.1 |
3.4.1 |
drop |
|
|
– |
|
|
|
– |
drop |
|
Intel classic build patches |
– |
drop; Intel classic is retired (Decisions 9) |
Version pins
Date last modified: Sep 20, 2026
mache/spack/pins.yaml is packaged with mache (added to MANIFEST.in):
spack:
git: https://github.com/spack/spack.git
tag: v1.2.2
repos:
e3sm:
git: https://github.com/E3SM-Project/e3sm-spack-packages.git
tag: v2026.06.0
builtin:
git: https://github.com/spack/spack-packages.git
tag: v2026.06.0
Each entry has exactly one of
tag,commitorbranch;macheraises an error otherwise.The order of
reposis Spack’s search order and is written as such into the instance’srepos.yaml.The
reposentries use the keys of Spack’s ownrepos.yamlschema, so an entry can be pasted into a Spack configuration unchanged.A release of
machepins tags only, fromgithub.com/spack/orgithub.com/E3SM-Project/; a unit test enforces this for non-pre-release versions (Repository setup). Development branches may pin commits.Bumping a pin is an ordinary pull request. The release checklist adds: bump pins, run a test deployment on at least one machine.
Precedence
Highest first:
--spack-pins <file>on themache deploycommand line, orpins=(a mapping or a path) passed tomake_spack_env.ctx.runtime['spack']['pins']set by apre_spackhook.spack.pinsindeploy/config.yaml.j2.The packaged
pins.yaml.
Overrides merge per repository. Naming a branch for a repository replaces
that repository’s tag or commit. Overrides cannot add or remove
repositories.
Spack instance layout
Date last modified: Sep 19, 2026
spack_path stays the Spack checkout, $SPACK_ROOT (Decisions
3 and 8). Everything mache manages lives under it:
<spack_path>/ # spack/spack at the pinned tag
├── etc/spack/repos.yaml # written by mache on every build
├── var/spack/package_repos/
│ ├── e3sm/ # e3sm-spack-packages at the pinned ref
│ └── builtin/ # spack-packages at the pinned ref
├── var/spack/environments/<env_name>/ # managed environments
│ ├── spack.yaml
│ ├── spack.lock
│ ├── activate.sh # captured activation, sourced by load scripts
│ ├── activate.csh
│ └── .spack-env/view/ # view path, unchanged
└── opt/spack/ # install tree, unchanged
etc/spack/repos.yaml names the two repositories by path, e3sm first
(Decisions 2):
repos:
e3sm: <spack_path>/var/spack/package_repos/e3sm/repos/spack_repo/e3sm
builtin: <spack_path>/var/spack/package_repos/builtin/repos/spack_repo/builtin
In Spack 1.x the spack scope ($SPACK_ROOT/etc/spack/) outranks the user
scope, so a user’s own repos.yaml cannot replace either entry. spack isolate --self then moves the user scope, misc cache, source cache, build
stages and bootstrap store under $SPACK_ROOT/etc/spack/isolate/ for
everyone who sources the instance.
spack isolate --self (Spack 1.2.2) is not idempotent: it exits 3 when
etc/spack/isolate exists, --overwrite deletes that directory including
the bootstrap store, and it rewrites the tracked etc/spack/include.yaml,
which the git reset --hard in step 3 reverts. The build script therefore
always resets the checkout, then moves etc/spack/isolate aside, runs
spack isolate --self, copies the freshly written *.yaml into the old
directory and moves it back, so the bootstrap store survives rebuilds.
Build flow
One Jinja template, mache/spack/templates/spack_install.bash.j2, replaces
both build_spack_env.template and mache/deploy/templates/spack_install.bash.j2.
Rendered, it does the following in order:
Module loads and environment variables from
get_spack_script(load_spack_env=False), andTMPDIR, as today. This prologue is also written to<env_name>.prologue.shin the work directory for the capture step (one per environment, because a deploy run builds every toolchain pair before capturing any of them).Refuse an existing
spack_pathwhose Spack is older than 1.0.For each of Spack,
e3smandbuiltin: clone if absent, otherwisegit fetch; then check out the pinned tag or commit detached, orreset --hard origin/<branch>for a branch.Write
etc/spack/repos.yaml.source share/spack/setup-env.sh,spack isolate --self, thenspack repo list, which must show both repositories as available.Add the source mirror if
spack_mirroris set, as today.Remove and recreate the environment from the rendered YAML, activate it,
spack install, as today.Copy
spack.lockto<env_name>.spack.lockand write<env_name>.provenance.yamlwith the three resolved commits into the work directory.Run
custom_spack.
Activation is captured afterwards, in a separate step; see Captured activation.
Removed from the build script: the SPACK_PYTHON search for a Python older
than 3.12, and the SSH clone URL in the legacy template.
Spack 1.2’s installer schedules package builds concurrently and draws a
terminal UI. The UI is off when output goes to a pipe or log file (verified
on 1.2.2), and spack.build_jobs from deploy config (or build_jobs= in
make_spack_env) is passed as spack install -j.
Environment templates for Spack 1.x
Date last modified: Sep 19, 2026
All 25 packaged templates carry a compilers: section and
packages:all:compiler, both of which Spack 1.x deprecates or ignores. Three
are retired rather than migrated (Decisions 9):
compy_intel_impi.yaml, chrysalis_intel-classic_openmpi.yaml and
chrysalis_intel-classic_impi.yaml. Their compiler, Intel 20.x, has no
package in any spack-packages release. Retiring them removes the templates
only; MPI_COMPILERS and the config_machines.xml-derived shell snippets for
intel-classic are unchanged.
The migrated chrysalis_gnu_openmpi.yaml, abridged:
{%- set compiler = "gcc@11.2.0" %}
{%- set mpi = "openmpi@4.1.6" %}
spack:
specs:
- {{ mpi }}
{%- if e3sm_lapack %}
- intel-oneapi-mkl
{%- endif %}
- hdf5
- netcdf-c
- netcdf-fortran
- parallel-netcdf
{%- for spec in specs %}
- "{{ spec }}"
{%- endfor %}
concretizer:
unify: true
toolchains:
mache:
- spec: "%c={{ compiler }}"
when: "%c"
- spec: "%cxx={{ compiler }}"
when: "%cxx"
- spec: "%fortran={{ compiler }}"
when: "%fortran"
packages:
all:
require: ["%mache"]
providers:
mpi: [{{ mpi }}]
{%- if e3sm_lapack %}
lapack: [intel-oneapi-mkl@2022.1.0]
{%- endif %}
gcc:
externals:
- spec: {{ compiler }}
prefix: /gpfs/fs1/soft/chrysalis/spack/opt/spack/linux-centos8-x86_64/gcc-9.3.0/gcc-11.2.0-bgddrif
modules:
- gcc/11.2.0-bgddrif
extra_attributes:
compilers:
c: /gpfs/fs1/soft/chrysalis/spack/opt/spack/linux-centos8-x86_64/gcc-9.3.0/gcc-11.2.0-bgddrif/bin/gcc
cxx: /gpfs/fs1/soft/chrysalis/spack/opt/spack/linux-centos8-x86_64/gcc-9.3.0/gcc-11.2.0-bgddrif/bin/g++
fortran: /gpfs/fs1/soft/chrysalis/spack/opt/spack/linux-centos8-x86_64/gcc-9.3.0/gcc-11.2.0-bgddrif/bin/gfortran
buildable: false
openmpi:
externals:
- spec: {{ mpi }}
prefix: /gpfs/fs1/soft/chrysalis/spack/opt/spack/linux-centos8-x86_64/gcc-11.2.0/openmpi-4.1.6-ggebj5o
modules:
- openmpi/4.1.6-ggebj5o
buildable: false
# remaining externals unchanged
The changes, applied to every remaining template:
The
compilers:section is deleted. Itsenvironment,flagsandextra_rpathsmove under the compiler external’sextra_attributes, for example thePKG_CONFIG_PATHprepend on Perlmutter.packages:all:compileris deleted. A toolchain namedmacheselects the compiler forc,cxxandfortranonly where a package depends on that language, andpackages:all:requireapplies it to everything. The namemachecannot collide with a package name; compiler names such asintelorgnucould.%{{ compiler }}is removed from every spec: root specs, externals and providers (Decisions 4). The compiler is no longer a root spec, so it no longer appears in the view; MPI stays a root so its wrappers do.Compiler package names follow
spack-packages:gcc,nvhpc,cce, andintel-oneapi-compilersfor both today’sintel@2025.xandoneapi@x.extra_attributes.compilerskeeps the keysc,cxxandfortranthat the templates onmainalready use.The
e3sm_hdf5_netcdfandexclude_packageshandling is unchanged;_filter_yaml_dataalready removes root specs, externals and providers by package name.
Implementation may use the explicit conditional form
require: ["%[when=%c]c={{ compiler }} %[when=%cxx]cxx={{ compiler }} %[when=%fortran]fortran={{ compiler }}"]
if a toolchain name inside require is not accepted by the pinned Spack.
Spec semantics for downstream
mache no longer appends %{{ compiler }} to specs from
deploy/spack.yaml.j2 or spack_specs (Decisions 4). Specs
pass through unchanged. In Spack 1.x % means “direct dependency”, and
anything after it binds to that dependency: trilinos %gcc +mpi asks for
gcc+mpi. Downstream specs must therefore put variants before any %, and
normally need no % at all. spack style --spec-strings reports the old
ordering.
Validation of rendered YAML
After rendering, mache fails fast if the YAML still contains a top-level
compilers key or packages.all.compiler, naming the template. This catches
un-migrated deploy/spack/<machine>_<compiler>_<mpi>.yaml overrides and
yaml_template files before Spack produces a less direct error.
Captured activation
Date last modified: Sep 19, 2026
spack env activate runs Python, loads the package repositories and calls
every package’s setup_run_environment on each shell start. Polaris users
pay that on every source load_polaris_*.sh. Instead, mache captures the
result once at build time (Decisions 10 and 11).
What Spack emits
spack env activate --sh <env> prints export NAME=<value>; for every
variable that activation changes, with the full new value computed from
the calling process’s environment, plus export SPACK_ENV=...; and an alias.
For PATH that value is the view’s bin followed by the whole PATH of the
capturing shell. Run-time activation does not load external modules; the
variables come from prefix inspections of the view (PATH, MANPATH,
PKG_CONFIG_PATH, CMAKE_PREFIX_PATH, ACLOCAL_PATH, plus anything added
by modules:prefix_inspections), from packages’ setup_run_environment, and
from env_vars: in spack.yaml.
Capture step
After post_spack hooks have run, so that a hook such as compass’s
_set_ld_library_path_for_spack_env is reflected, mache runs one fresh
login shell per environment:
env -i bash -l -c '
source <work_dir>/spack/<env>.prologue.sh # same modules as the build
source <spack_path>/share/spack/setup-env.sh
env -0 > <work_dir>/spack/<env>.env_before
spack env activate --sh <env> > <work_dir>/spack/<env>.raw_activate.sh
'
mache then parses the raw output with shlex and rewrites it:
aliaslines are dropped.For a variable in
PATH_LIKE_ENV_VARS(already defined inmache/spack/shared.py), or one of the extension paths Spack sets such asPYTHONPATHandPERL5LIB, or any variable whose new value ends with its value before activation, the new value is split on:and every element not present in theenv_beforevalue is kept, in order, asX. The snippet prependsX:export PATH="/path/to/view/bin${PATH:+:$PATH}"
if ($?PATH) then setenv PATH '/path/to/view/bin'":$PATH" else setenv PATH '/path/to/view/bin' endif
csh values are single-quoted, since csh does not honour backslash escapes inside double quotes.
Elements that were present before and are absent afterwards are ignored with a warning in the build log. An empty
Xemits nothing. Spack givesMANPATHa trailing colon somankeeps its default search path; the rendered prepend keeps that when the variable was unset (export MANPATH="X:${MANPATH:-}"). Prefix inspections of externals withprefix: /usradd entries such as/usr/share/pkgconfig; these are new elements too and are kept in Spack’s order.Every other variable is set to its literal value,
SPACK_ENVincluded.The snippet starts by setting
SPACK_ROOTand prepending$SPACK_ROOT/bintoPATH, whichsetup-env.shused to do.spackis then a plain executable, not the shell function, and withSPACK_ENVset commands such asspack findandspack config get modulesact on the loaded environment;spack env activateandspack loadare not available. No downstream package runsspackafter loading, so this is for maintainers only (Decisions 13).
Both activate.sh and activate.csh are rendered from the same parsed list
and written into var/spack/environments/<env>/. Recreating the environment
removes them; a build that fails before the capture step leaves none, and
load_existing_spack_envs errors if they are missing.
Loading
get_spack_script(load_spack_env=True) emits
source <spack_path>/var/spack/environments/<env_name>/activate.<shell>
in place of the setup-env and spack env activate lines, followed by the
config_machines.xml-derived modules and variables in the same order as
today. Because the snippet prepends rather than assigns, the final PATH
order is the same as with dynamic activation.
The “software” environment keeps its PATH-only handling in
mache/deploy/spack.py; it never activated.
What hooks see
post_spack hooks run before the capture step and may need an activated
environment: E3SM-Unified’s hook appends spack_result['activation'] to a
build script and then compiles mpi4py against the view’s mpicc. The
activation string in ctx.runtime['spack']['results'] is therefore the
dynamic form (source setup-env.sh + spack env activate) during a deploy
run, regardless of spack.activation. Only the generated load scripts use
the captured source line (Decisions 14).
spack.activation: captured | dynamic in deploy config, and
activation= in make_spack_env, select the old behaviour for debugging.
captured is the default.
Integration with mache.spack and mache.deploy
Date last modified: Sep 19, 2026
New module mache/spack/pins.py:
load_pins(overrides=None)reads the packagedpins.yaml, merges overrides (a mapping, a path, or a sequence of these in precedence order, lowest first), and validates one ref per repository.merge_pins,validate_pins,checkout_command,release_pins_are_valid.render_repos_yaml(spack_path, pins)returns the instancerepos.yaml.
New module mache/spack/install.py:
render_install_script(...)renders the shared build template for both callers;write_prologue/prologue_pathhandle<env_name>.prologue.sh.
New module mache/spack/activation.py:
capture_activation(spack_path, env_name, prologue_path, work_dir)runs the capture shell and returns the rewritten modifications.parse_raw_activation,parse_env_before,rewrite_modificationsare the testable pieces of that;render_activation(modifications, shell, spack_path)returns the snippet text.write_activation_files(spack_path, env_name, modifications)writes both files;activation_source_linegives thesourceline for a shell.
mache.spack.env.make_spack_env gains keywords pins=None (mapping or path),
activation='captured' and build_jobs=None, and calls the capture step
after custom_spack. get_spack_script gains activation='captured'. Its
docstring no longer describes the spack_for_mache_<version> branch.
mache.deploy:
spack.pinsandspack.activationare accepted indeploy/config.yaml.j2and merged into the effective Spack config with the existing runtime override mechanism.--spack-pins <file>is added tocli_spec.json.j2, routed todeploy._install_spack_envstops computing a branch frommache.versionand renders the shared template.run.pycalls the capture step for every library environment between thepost_spackandpre_publishhooks.SpackDeployResult.activationkeeps the dynamic form, so hooks that source it keep working; a newSpackDeployResult.load_activationholds the capturedsourceline (the path is known before capture) and replacesresult.activationin_write_load_script. Underspack.activation: dynamicboth fields are the same.load_existing_spack_envserrors ifactivate.shis missing undercaptured.
Removed: mache/spack/templates/build_spack_env.template and
mache/deploy/templates/spack_install.bash.j2, replaced by the one template.
Removed: compy_intel_impi.yaml, chrysalis_intel-classic_openmpi.yaml,
chrysalis_intel-classic_impi.yaml.
Unchanged: list_machine_compiler_mpilib, MPI_COMPILERS, cray_compilers,
extract_spack_from_config_machines and the shell-template overrides.
Downstream migration
Date last modified: Sep 19, 2026
E3SM-Unified, compass and polaris all deploy through mache deploy today.
None passes a yaml_template. Their deploy/spack.yaml.j2 specs contain no
%, so they need no spec changes. Compass and polaris ship one identical
override, deploy/spack/katara_gnu_openmpi.yaml, in the 0.x form (a
compilers: block, packages:all:compiler, %{{ compiler }} on every
spec); E3SM-Unified ships none.
For all three:
Use a new
spack_path. Downstream already keys it on themacheversion; a 0.x checkout at the old path is refused, not converted.Bump
macheindeploy/pins.cfgto 5.0.0 and runmache deploy update.Load-script consumption does not change;
MACHE_DEPLOY_SPACK_LIBRARY_VIEWand the view-relative paths indeploy/load.share the same. None of the threedeploy/load.shsnippets runsspack; they locate tools withcommand -von the view’sbin.post_spackhooks that sourcespack_result['activation'](E3SM-Unified) orsetup-env.shdirectly (compass) keep working; see What hooks see.Maintainer docs that say to run
spack findorspack config get ...after loading keep working, because$SPACK_ROOT/binis onPATHandSPACK_ENVis set. Docs that want the shell function (forspack env activateorspack load) should saysource $SPACK_ROOT/share/spack/setup-env.shfirst. References to thespack_for_mache_<version>branch in those docs need updating.
For compass and polaris additionally:
Migrate
deploy/spack/katara_gnu_openmpi.yamlas in Environment templates. It usesprefix: /usrfor bothgccandopenmpi, so it also needsextra_attributes.compilerspaths.
For compass additionally:
The
post_spackhook that addsLD_LIBRARY_PATHtomodules:prefix_inspectionskeeps working because capture runs afterpost_spack. Moving that setting into the katara override or intospack.yaml.j2is optional.
The E3SM-Project/spack fork is archived once the last mache 4.x release
that uses it is no longer deployed. Its spack_for_mache_* branches stay for
existing instances.
Testing
Date last modified: Sep 22, 2026
Unit tests in mache:
pins: merge precedence, one-ref validation, branch replacing tag.
repos.yamlrendering and search order.Every packaged template renders for its
(machine, compiler, mpi)and contains amachetoolchain,packages.all.require, nocompilerskey and nopackages.all.compiler.Build-script rendering for tag, commit and branch pins; legacy refusal.
Activation rewrite: prepend of new elements, unset baseline,
MANPATHtrailing colon, literal non-path variables, alias dropped, csh rendering, from a recordedraw_activate.shandenv_beforepair.
Deployments, each compared against spack env activate --sh run
interactively and followed by a rerun on the existing instance:
Chrysalis with polaris: the default path, Cray-free.
Chrysalis with E3SM-Unified: the
e3smsubclass packages (e3sm-scorpio,esmf,tempestremap).Chrysalis with compass and Albany: the E3SM-only packages and the
LD_LIBRARY_PATHhook.Perlmutter
pm-cpugnu: Cray wrapperscc/CC/ftnas compiler paths,gcc-runtimedetection through them, and the movedPKG_CONFIG_PATHprepend.Aurora
intelandintelgpu: the oneAPI compiler model, and an Omega build through the load script.
The results are in the Testing comment of
PR #492.
Prior work
Date last modified: Sep 19, 2026
Andrew Nolan’s spack-v1.0.0 branch (PR #281, opened Aug 2025, closed Apr
2026 as a reference rather than a base) and his branches of
andrewdnolan/spack-packages.
Kept:
A separate E3SM package repository in the v2 layout with namespace
e3smand aspack-repo-index.yaml. Hisopen_PR_rebasebranch is the seed ofe3sm-spack-packages:albany,trilinos_for_albany,esmf,moab,parallel_netcdf,tempestextremes,tempestremap, already converted to v2 imports and directory names.A pin file with exactly one of tag, branch or commit per repository, and the matching validation.
Removing
compilers:andpackages:all:compilerfrom templates, and moving compilerenvironmentsettings intoextra_attributes.Renaming Intel compiler externals to the
intel-oneapi-compilers*packages.Six upstream PRs (#833, #835, #853, #858, #872, #936) that make
nco,netcdf-fortran,tempestextremes,tempestremap,moabandparallel-netcdfunnecessary in the overlay at the versions they added.
Changed:
One Spack instance per
macherelease, not one clone per(compiler, mpi)(spack/spack_for_{compiler}_{mpi}in his branch; Decisions 5).macheclones the package repositories itself and writes a path-basedrepos.yaml, instead ofspack repo addandspack repo update --scope siteat build time.Pins in YAML using Spack’s
repos.yamlkeys, instead ofconfig.cfg.No
%{{ compiler }}on externals; the toolchain selects compilers.extra_attributes.compilerskeysc/cxx/fortran, notcc/f77/fc.https://clone URLs, notgit@github.com:.
Decisions
Date last modified: Sep 19, 2026
Rejected alternatives and resolved questions, cited from the sections they affect.
Packages inside
mache(mache/spack/repo/spack_repo/e3sm/...), versioned withmachelike the fork branches were. Rejected: a recipe fix would need amacherelease on conda-forge; the overlay is useful to Spack users who do not usemache;package.pyfiles needfrom spack.package import *, which the repo’s lint forbids. See Package repositories.Git-based
repos.yamlentries (git:plustag:anddestination:) with Spack doing the cloning. Rejected:macheand Spack would both own the clone, and Spack’s behaviour when the pinned ref changes under an existing clone, or when only a mirror is reachable, is undocumented. Path-based entries makespack repo listshow the same thing either way. See Spack instance layout.An instance root above the Spack checkout (
<spack_path>/spack,<spack_path>/spack-packages,<spack_path>/envs). Rejected: it changesview_path, environment paths and every downstream hook that derives them, for no gain;spack isolate --selfalready targets$SPACK_ROOT. See Spack instance layout.Appending
%{{ compiler }}to every spec, as today. Rejected: in 1.x variants after%bind to the compiler, externals do not take a compiler, and downstream specs would break silently. See Environment templates.One Spack instance per toolchain (Andrew’s branch). Rejected: disk and bootstrap cost per toolchain; Spack supports many environments per instance and reuses common builds across them. See Prior work.
Keeping a Spack 0.23 code path behind a pin. Rejected: it keeps the fork alive, keeps the Python 3.12 workaround, and doubles the templates.
mache4.0.0 was released in Sep 2026 before this design landed, so this design ships as 5.0.0.Copying upstream packages into
e3smrather than subclassing. Rejected: this is how the fork drifted. See The e3sm package repository.Independent environments (
spack env create -d <dir>) outside$SPACK_ROOT. Rejected for now: managed environments keep every existing path; independent environments add nothing until instances are shared acrossmachereleases, which this design forbids. See Spack instance layout.Porting the legacy
intelcompiler package intoe3smto keepcompy_intel_impiandchrysalis_intel-classic_*. Rejected:machedrops Intel classic support with this transition; the three templates and theboostandrhashpatches go. See Environment templates.Dynamic activation (
spack env activatein load scripts), as today. Rejected as the default: seconds per shell, and a dependency on Spack and the package repositories at load time. Kept behindspack.activation: dynamicfor debugging. See Captured activation.Capturing
spack env activate --shverbatim, and havingmachecompute the activation itself from the view layout. Rejected: verbatim output assigns the capturing shell’s fullPATHand would clobber the user’s; amache-computed snippet would lose packages’setup_run_environment(for exampleESMFMKFILEfromesmf) andenv_vars:. The rewrite keeps both. See Captured activation.A fork of
spack/spack-packagesas the overlay’s home. Rejected: the overlay shares no history with upstream, so a fork only confuses GitHub’s fork tooling.E3SM-Project/e3sm-spack-packagesis a standalone repository; upstream contributions go from personal forks as Andrew’s did.spackonPATHafter loading. Resolved on Sep 19, 2026: keep$SPACK_ROOT/binonPATHand exportSPACK_ENV. A survey of E3SM-Unified, compass, polaris, MPAS-Analysis, MPAS-Tools, zppy, e3sm_diags and Omega found no code that runsspackafter a load script is sourced; the only uses are build-time hooks, which sourcesetup-env.shthemselves or use the hook-time activation string, and maintainer troubleshooting docs, which the plain executable satisfies. DroppingspackfromPATHentirely was the alternative; it saves nothing and breaks those docs. See Captured activation.Giving
post_spackhooks the capturedsourceline. Rejected: capture must run afterpost_spackso that compass’sprefix_inspectionschange is reflected, but E3SM-Unified’s hook sources the activation string before that, whenactivate.shdoes not yet exist. Hooks get dynamic activation; load scripts get the captured form. Capturing twice (before and after hooks) was the other option and doubles the slowest step for no benefit. See What hooks see.Tagging
e3sm-spack-packageswith themacheversion that pins it. Rejected on Sep 19, 2026:machereleases far more often than recipes change, so most tags would be either no-op tags or fixes with no honestmachenumber; the tag would have to be chosen beforemacheis released and would lie if the release slipped; and the number means nothing to Spack users outsidemache. Independent SemVer was also considered and rejected: “breaking” is ill-defined for a recipe overlay and collapses into “thebuiltinera changed”, which CalVer already says. Tags follow upstream’svYYYY.MM.N, withYYYY.MMthe oldest supportedbuiltinera. See Tags for e3sm-spack-packages.Starting the overlay in a personal repository (
xylar/e3sm-spack-packages) and pushing its history to the organization later. Considered on Sep 20, 2026 while the organization repository did not exist; superseded on Sep 21, 2026 whenE3SM-Project/e3sm-spack-packageswas created empty. Development starts there directly. What was kept from the plan is the license handling for upstream-derived files and the release guard onpins.yaml. See Repository setup.