"It works on my machine." These five words have caused more wasted engineering hours than perhaps any other phrase in software development. And in C++, where builds depend on compiler versions, system headers, link order, and preprocessor state, the problem is especially severe.
cmod was designed from day one to make deterministic builds the default, not an aspirational goal. Here's how and why.
What Makes a Build Non-Deterministic?
A build is non-deterministic when the same source code produces different results on different machines or at different times. In C++, the common culprits are:
- Floating dependency versions:
>=10.0resolves to10.2.1today and10.3.0tomorrow - Unpinned compiler versions: Clang 17 and Clang 18 can produce different code for the same source
- System header differences: glibc on Ubuntu 22.04 differs from Ubuntu 24.04
- Implicit dependencies: A library works because a transitive header was included — until it isn't
- Build order sensitivity: Parallel builds can produce different results if ordering isn't determined by a DAG
cmod's Approach: Pin Everything
Mandatory lockfiles
When you run cmod resolve, cmod generates a cmod.lock file that records:
[[dependencies]]
url = "github.com/fmtlib/fmt"
version = "10.2.1"
commit = "a0b8a4e19f7e8f..."
hash = "sha256:3f7c4d..."
[toolchain]
compiler = "clang"
version = "18.1.3"
std = "c++23"
The lockfile pins not just the version, but the exact Git commit hash and a content hash of the source. Even if a maintainer force-pushes over a tag (a bad practice, but it happens), the lockfile catches it.
The --locked flag
In CI, always build with --locked:
cmod build --locked --release
This tells cmod to use the lockfile exactly as-is. If the lockfile is missing or out of date with cmod.toml, the build fails immediately with a clear error message instead of silently resolving to different versions.
Toolchain pinning
The [toolchain] section in cmod.toml specifies exact compiler requirements:
[toolchain]
compiler = "clang"
version = ">=18.0"
std = "c++23"
cmod verifies the installed compiler matches these constraints before starting a build. No more "it compiled on the developer's Clang 19 but CI has Clang 17" surprises.
The Module DAG: Determined Build Order
C++20 modules introduce explicit import relationships. cmod uses clang-scan-deps to discover these relationships and construct a Directed Acyclic Graph (DAG) of module dependencies. The build order is a topological sort of this DAG — it's deterministic by construction.
# Visualize the exact build order
cmod graph --format dot
# See why a module would be rebuilt
cmod explain my_module
Unlike header-based builds where include order can affect macro expansion and therefore compilation results, module imports are unambiguous. Module A imports module B means exactly one thing, regardless of order.
Content-Addressed Caching
cmod's cache uses SHA-256 hashes computed from:
- Source file content
- Compiler version and flags
- Dependencies' BMI hashes
- Target triple
This means a cache hit is a proof of equivalence. If the cache key matches, the output is guaranteed to be identical. No stale cache bugs, no "have you tried a clean build?" troubleshooting.
# Cache is transparent — same inputs always produce same outputs
cmod cache status # See what's cached
cmod cache clean # Clear everything (rarely needed)
cmod build --no-cache # Build without cache (for verification)
Real-World Impact
CI reliability
With deterministic builds, CI failures mean something. If the build passes locally with --locked and fails in CI, you know the environment differs — not that a dependency silently updated. The lockfile becomes a contract between your local machine and CI.
Security auditing
Deterministic builds are a prerequisite for meaningful security auditing. If you can't reproduce a build exactly, you can't verify that the binary you're auditing matches the source code you reviewed. cmod's lockfile + --locked flag + cmod verify provides an auditable chain from source to artifact.
Regulatory compliance
Industries like automotive (ISO 26262), medical devices (IEC 62304), and aerospace (DO-178C) require reproducible builds as part of their certification processes. cmod's deterministic build model satisfies these requirements without additional tooling or custom scripts.
The CI Recipe
Here's the recommended CI pipeline for deterministic builds:
# .github/workflows/ci.yml
- name: Build
run: cmod build --locked --release
- name: Test
run: cmod test --locked --release
- name: Verify
run: cmod verify
- name: SBOM
run: cmod sbom --output sbom.json
If any step fails, it's a real problem — not a flaky build caused by floating dependencies or cache corruption.
Updating Dependencies Deliberately
Deterministic builds don't mean frozen builds. cmod provides controlled update mechanisms:
# Update all dependencies
cmod update
# Update only patch versions (safe)
cmod update --patch
# Update a specific dependency
cmod update fmt
# See what changed
git diff cmod.lock
Every update produces a diff in cmod.lock that you can review, test, and commit. Updates are deliberate actions, not side effects of building.
The Bottom Line
Deterministic builds aren't just a nice-to-have. They're the foundation for:
- Reliable CI pipelines
- Meaningful security audits
- Regulatory compliance
- Team confidence ("if it builds for you, it builds for me")
- Faster debugging (fewer variables to investigate)
cmod makes deterministic builds the default by pinning everything that matters: dependency versions, Git commits, compiler versions, and build order. The result is a build system you can trust.
Get started with cmod and experience deterministic C++ builds.