stout
Article

Deterministic Builds with Lock Files

Why reproducible builds matter, how Homebrew fails at them, and how stout lock ensures every machine installs the exact same packages.

Neul Labs ·
#lock-files#reproducibility#ci-cd#devops#enterprise

You’ve seen it before. A build passes in CI on Tuesday. On Wednesday, the same commit fails. Nobody changed the code. Nobody changed the pipeline. But somewhere between Tuesday and Wednesday, a system dependency was silently upgraded, and now everything is broken.

This is the reproducibility problem, and it’s one of the most common sources of wasted engineering time in organizations of any size. Language-level package managers solved this years ago with lock files — package-lock.json, Cargo.lock, poetry.lock, Gemfile.lock. But system-level package managers, the tools that install ffmpeg, openssl, protobuf, and other native dependencies, have largely ignored the problem.

Homebrew has no lock file mechanism. stout does. Here’s why it matters and how it works.

What deterministic builds actually mean

A deterministic build means: given the same inputs, you get the same outputs. Every time. On every machine.

For this to work at the system dependency level, you need to guarantee:

  1. Exact version pinning. Not “the latest 3.x” — the exact version, like 3.2.1.
  2. Dependency tree resolution. If ffmpeg depends on libx264 which depends on nasm, every version in the tree must be pinned.
  3. Artifact integrity. The binary you install on machine B must be byte-for-byte identical to the one on machine A. This requires checksum verification.
  4. Resolution stability. Running the resolver twice with the same input must produce the same output. The resolver can’t prefer “newest available” if “newest available” changes daily.

Language package managers enforce all four through lock files. The lock file captures the full resolved dependency tree with exact versions and integrity hashes. The install command reads the lock file and reproduces the exact state.

How Homebrew handles (or doesn’t handle) reproducibility

Homebrew’s design actively works against reproducibility:

No lock files. There is no brew lock command. There is no file you can commit to your repository that says “these are the exact packages and versions this project needs.”

Brewfile is not a lock file. brew bundle reads a Brewfile, but a Brewfile is a dependency declaration, not a lock. It says brew "openssl" — it doesn’t say which version. When you run brew bundle on two different machines at two different times, you can get different versions of openssl.

Rolling releases. Homebrew’s homebrew-core tap is a git repository. When you run brew update, you get the latest commit. Formula versions change constantly. There’s no concept of “install the version that was current on April 1st.”

Implicit upgrades. Running brew install foo when foo is already installed but outdated will sometimes trigger an upgrade. Running brew install bar that depends on foo might upgrade foo as a side effect. These implicit version changes break reproducibility silently.

No integrity verification for versioned installs. You can install a specific version of some packages (brew install [email protected]), but there’s no checksum pinning. You’re trusting that the formula hasn’t changed and that the bottle server returns the same binary.

The result is that Homebrew installations are inherently non-deterministic. Two machines that run the same brew install commands at different times will have different package versions. Two CI runs of the same commit can produce different environments.

How stout lock works

stout provides a stout lock command that generates a lock file capturing the exact state of your package set.

Generating a lock file

# Install your packages
stout install ffmpeg imagemagick redis [email protected]

# Generate a lock file from the current state
stout lock > stout.lock

The generated stout.lock file looks like this:

# stout.lock — generated 2026-04-02T10:30:00Z
# stout 0.9.1, index 2026-04-02-rev3

[metadata]
platform = "macos-arm64"
index_revision = "2026-04-02-rev3"

[[package]]
name = "ffmpeg"
version = "7.1"
sha256 = "a3f8c1d2e4b5678901234567890abcdef1234567890abcdef1234567890abcdef"
dependencies = ["aom", "dav1d", "fontconfig", "freetype", "lame", "libass", "libvorbis", "libvpx", "opus", "sdl2", "snappy", "theora", "x264", "x265", "xvid"]

[[package]]
name = "aom"
version = "3.9.1"
sha256 = "b4c9d2e5f6a7890123456789abcdef01234567890abcdef01234567890abcdef"
dependencies = []

# ... every transitive dependency listed with exact version and checksum

Every package in the dependency tree is listed with its exact version and SHA-256 checksum. The index revision is recorded so you can trace exactly which package metadata was used for resolution.

Installing from a lock file

# On any machine — CI, a teammate's laptop, a production builder
stout install --lockfile stout.lock

This command:

  1. Reads the lock file
  2. Downloads every listed package at the exact specified version
  3. Verifies every download against the SHA-256 checksum in the lock file
  4. Rejects any package where the checksum doesn’t match

There’s no dependency resolution step. The lock file already contains the fully resolved tree. This makes installation faster (no resolver) and deterministic (no resolution decisions to make).

If a package in the lock file is no longer available (e.g., a bottle was removed from the registry), the install fails with a clear error rather than silently substituting a different version.

Updating the lock file

When you want to update dependencies, you explicitly update and re-lock:

# Update a specific package
stout update ffmpeg

# Or update everything
stout update --all

# Re-generate the lock file
stout lock > stout.lock

You review the diff in the lock file, commit it, and every subsequent install from that lock file uses the new versions. This is the same workflow developers use with npm update / npm install or cargo update / cargo build.

Lock files in CI

Lock files eliminate an entire class of CI failures. Here’s a GitHub Actions workflow using stout with a lock file:

- name: Install system dependencies
  run: |
    curl -fsSL https://get.stout.dev | sh
    stout install --lockfile stout.lock

This step will produce the same package environment whether it runs today, next week, or next month. The only way the installed packages change is if someone updates stout.lock and commits it.

Compare this to the Homebrew equivalent, where even with HOMEBREW_NO_AUTO_UPDATE=1, the installed versions depend on when the CI runner image was last built.

Detecting drift

stout can verify that the current installation matches a lock file:

stout lock verify --lockfile stout.lock

This checks that every installed package matches the version and checksum in the lock file. If anything has drifted — a package was upgraded outside the lock file workflow, or a different version was installed — the command reports the discrepancy and exits non-zero.

You can run this as a CI check to enforce that no one has modified the package environment without updating the lock file:

- name: Verify dependency integrity
  run: stout lock verify --lockfile stout.lock

Cross-platform lock files

Some teams build on both macOS (development) and Linux (CI/production). stout lock files support multi-platform pinning:

# Generate a lock file for both platforms
stout lock --platform macos-arm64 --platform linux-x86_64 > stout.lock

The lock file includes platform-specific entries for each package. On each platform, stout installs the correct variant while maintaining the same version pins:

[[package]]
name = "ffmpeg"
version = "7.1"

[package.artifacts]
macos-arm64 = { sha256 = "a3f8c1d2..." }
linux-x86_64 = { sha256 = "e7f0a1b2..." }

The bigger picture

Lock files aren’t just a convenience feature. They’re a prerequisite for several enterprise requirements:

  • Audit compliance. Regulated industries need to know exactly what software is running. A lock file is a machine-readable bill of materials.
  • Security response. When a CVE drops for libxml2 2.12.3, you can grep your lock files to find every project that uses it — instantly.
  • Incident investigation. “What changed?” is the first question in any incident. If the lock file didn’t change, system dependencies didn’t change. That’s one variable eliminated.
  • Reproducible debugging. When a customer reports a bug, you can recreate their exact environment from the lock file they were using.

Homebrew wasn’t designed for these use cases. stout was. The lock file is the foundation that makes reproducible, auditable, verifiable builds possible at the system dependency level — the same way Cargo.lock and package-lock.json made them possible for application dependencies.

Need Rust performance engineering or AI agent expertise?

Neul Labs — the team behind stout — consults on Rust development, performance optimization, CLI tool design, and AI agent infrastructure. We build fast, reliable systems that ship.