Ensuring Reproducible HPC Builds with Spack Environments and Lock Files
Learn how to use Spack environments and lock files to eliminate version drift and ensure reproducible software stacks across different HPC clusters and team members.
08 Aug 2025, 05:30 UTC

The Problem: The "It Works on My Node" Syndrome
In High-Performance Computing (HPC), a software stack is rarely a single binary. It is a complex Directed Acyclic Graph (DAG) of dependencies—compilers, MPI implementations, and linear algebra libraries—where a single version mismatch or a different build variant can lead to subtle numerical drift or segmentation faults. When a team shares a list of packages to install, they often rely on a manual list of specs. However, because Spack resolves dependencies dynamically based on the current state of the repository and the local system, two researchers running the same install command on different days may end up with different dependency versions.
The solution is to move from imperative installations (running a series of install commands) to declarative environments using spack.yaml and spack.lock files. This approach pins the entire dependency tree, ensuring that every team member and every CI runner builds the exact same software stack.
Defining the Stack with spack.yaml
A Spack environment begins with a spack.yaml file. This file defines the abstract spec—the requirements you want, without necessarily specifying the exact version of every sub-dependency. You can define version ranges, required variants (features), and compiler constraints.
For example, you might require a specific version of a physics simulation tool but allow Spack to choose the best compatible version of a utility library. Using the ^ symbol indicates a dependency, while + and ~ toggle build variants (options that enable or disable specific features during compilation).
Freezing the Graph with Concretization
The transition from an abstract request to a fixed installation happens during concretization. When you run spack concretize, Spack analyzes the spack.yaml and the available packages in its database to resolve every single dependency to a specific version and hash. This result is written to a spack.lock file.
The spack.lock file is the "source of truth" for reproducibility. It contains the exact hashes of every package in the DAG. By committing both the spack.yaml and the spack.lock to a version control system (like Git), you ensure that any other user who activates that environment will resolve to the identical set of packages, bypassing the uncertainty of the solver.
Worked Example: Setting Up a Reproducible Environment
Assume you are working on a project that requires hdf5 with shared library support and a specific version of fftw. Run these commands on your head node or login node. You will need a standard Spack installation (v0.20+ recommended) and a configured compiler.
# 1. Create a new environment named 'simulation-stack'
spack env create simulation-stack
# 2. Activate the environment
spack env activate simulation-stack
# 3. Add the abstract specifications
# We want hdf5 with shared support and fftw version 3.3.10
spack add "hdf5+shared"
spack add "fftw@3.3.10"
# 4. Concretize the environment to generate spack.lock
spack concretize -f
# 5. Install the resolved dependencies
spack installVerification: To verify the lock file was created, check for the existence of spack.lock in your environment directory. You can inspect the pinned versions by running spack find while the environment is active. If you modify spack.yaml (e.g., changing +shared to ~shared), you must run spack concretize -f again to update the lock file before installing.
Trade-offs and Limitations
While lock files provide stability, they are not a complete substitute for a container image or a full system snapshot. There are two primary limitations to consider:
- System Dependencies: Spack lock files pin packages managed by Spack, but they do not pin external system libraries (like
glibcor kernel headers) provided by the OS. If the underlying OS version changes significantly between clusters, the lock file may still fail to produce a working binary. - Compiler Availability: A lock file specifies that a package was built with a specific compiler (e.g.,
gcc@11.2.0). If the target machine does not have that exact compiler version installed and configured in Spack, the environment cannot be activated or installed.
Actionable Summary
To move your project toward reproducible builds, stop sharing shell scripts full of spack install commands. Instead: (1) Create a spack.yaml to define your high-level requirements, (2) Run spack concretize -f to generate a spack.lock, and (3) Commit both files to your repository. This shifts the burden of dependency resolution from the individual user to the project configuration, drastically reducing "dependency hell" across your team.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.