magus v0.4.3 is out. See what's new
¶ View markdown source · ✎ Suggest an edit
3 min read

MGS4003: non-deterministic output

A project's declared outputs produced different content across two consecutive runs of the same dispatch. The same inputs should yield the same outputs; magus treats determinism as a build-system invariant.

[MGS4003] non-deterministic output (see .../MGS4003.md)
  project=api target=build differing_paths=[api/dist/bundle.js api/dist/manifest.json]

Why

This is an error, not a warning: it fails the run. Determinism is not a preference here, because three things magus does are unsound without it: drift gating compares regenerated output against what is committed, a cache hit replays stored bytes instead of rerunning, and a conflict in generated output is resolved by regenerating. A generator that emits different bytes for the same inputs breaks all three. The fix is always in the generator.

It is only emitted by --race=replay, which:

  1. Runs the affected set normally (concurrent, cache-aware).
  2. Content-hashes every declared output of every project.
  3. Re-executes the same set sequentially, bypassing the cache.
  4. Content-hashes the outputs again.
  5. Compares the two hash sets per project.

Any project whose outputs differ between the two runs is non-deterministic. Common causes:

  • Embedded timestamps. A build that injects time.Now() or __DATE__ into the artifact (linker build IDs, pkg/build.go ldflags, source maps with generation timestamps).
  • Map iteration order. Go and Python iterate maps in random order; a generator that emits code from a map without sorting keys will produce different bytes each run.
  • Parallel work-stealing leaking into output. Worker IDs, goroutine IDs, or the order of fan-in results landing in a manifest.
  • PID / hostname / cwd in the output. Some toolchains embed these by default (e.g. pip embeds the build tempdir path into compiled .pyc).
  • Random salt / nonce. Crypto helpers used at build time without a fixed seed.

This check is expensive: it doubles the wall-clock time of the affected build, and the second pass cannot benefit from the cache. That is why it is opt-in and belongs on a scheduled audit rather than on every push. Asking for it and getting a warning would be the worst of both, so when you ask, you get a verdict.

Resolution

1. Identify the source

The differing_paths list narrows the search. Open one of the differing files in both runs and diff them. The divergence is often visible at first glance (a timestamp, a permuted list, a tempdir path).

For binary outputs, use diffoscope (the reproducible-builds.org tool) which unpacks archives, decompresses sections, and diffs the contents recursively:

magus run build
cp -r api/dist /tmp/run1
magus run build --race=replay   # second run rebuilds
diffoscope /tmp/run1/bundle.js api/dist/bundle.js

2. Remove the source of non-determinism

Common fixes:

  • Embedded timestamps: ldflag the build time to a fixed value (-X main.BuildTime=$(git log -1 --format=%cI) ties it to the commit timestamp, which is deterministic).
  • Map iteration: sort keys before emitting (slices.Sort(maps.Keys(m))).
  • Tempdir paths: use a fixed relative path or strip the absolute prefix.
  • Build IDs / PIDs: pass -buildid= to go build and -Wl,--build-id=none to the linker.

3. If non-determinism is unavoidable

Some artifacts (e.g. compiler outputs with debug info on Windows) are non-deterministic by design. For these, exclude the path from the project's declared outputs so the cache doesn't track them.

What this is not

  • Not a runtime crash detector. It only checks that outputs are bit-for-bit identical, not that they behave the same.
  • Not run by default. You must pass --race=replay explicitly.
  • Not a substitute for reproducible-builds.org tooling. That community has stronger guarantees (cross-machine, cross-time, cross-locale). MGS4003 is a same-machine same-session check.

See also

MGS4003racedeterminismreplayreproducible buildsoutputshashing
Last updated (895d94e4)
Earlier changes on this page (3)

Full history ↗ · Blame source ↗

Glossary

Project

A directory magus recognizes as a unit of work (it has a magusfile); the unit of caching, scheduling, and dependency tracking. See workspace.

Target

A named operation (build, test, ...) you invoke with magus run <target>; it may compose a spell's tool-native operations and depend on other targets. See targets.

Op

A single tool-native command a target composes (long form: operation); the middle of the work hierarchy (Spell to Op to Target). See operations.

Manifest

The file that declares what a package is: its name, dependencies, entry points and tool config (go.mod, package.json, pyproject.toml, Cargo.toml). How the workspace builds, tests and lints the package belongs in the magusfile, not in the manifest's scripts. See spells.

Cache

The content-addressed store magus consults before running a target, so unchanged work is skipped. See cache.

Affected

The set of projects touched by a change; magus affected <target> runs a target only over them. See affected.

CI

An ordinary magusfile-defined target you compose yourself with magus\needs: magus does not hardcode its stages. Magus.RunCI treats it specially only in that it strips the rw charm, it is the anchor magus affected ci keys off, and a selected scope with no project declaring it is a load error rather than a silent no-op. See targets.

Session

An agent host's conversation, by the id the host delivers to its hooks. magus never mints one: a record with no session is unattributed, and the OS user it carries says whose account ran it.

Window

The terminal a command runs in. It keys fire-once notices for a caller no host gave a session, and is never recorded as a session.

Run

One target executing under one magus invocation, such as magus run test web or magus affected ci. A run keeps its captured output behind an output reference. Every magus run is a run whether or not any job asked for it; see Job for how the two relate.

Conventions

This page uses none of the site's convention markers. The full set is on the conventions page.