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

MGS4002: declared output overlap

Two or more projects in the current dispatch declare the same output path glob. Because both projects produce the same file, the build outcome depends on which project ran last.

[MGS4002] declared output overlap (see .../MGS4002.md)
  projects=[api,worker] target=build overlapping=[shared/types/generated.go]

Why

Magus records per-project outputs in magus.yaml (or via the Go registration API: WithOutputs(...)) so the cache can replay prior results and prune outdated files. When two projects declare the same output glob and run concurrently under the same target, they will both write to the same path. The second writer silently overwrites the first, making the final content depend on execution order.

This check runs at graph construction time, before any spells execute, requiring no --race flag. It operates on static declarations only: if a project declares an output glob that overlaps another project's glob exactly, the warning fires. It is observational, so magus does not block or reorder execution.

The --race runtime detector (MGS4001) observes actual file writes during execution and can catch races that involve undeclared outputs. MGS4002 runs earlier and cheaper: it catches declared overlaps before any code runs.

Two overlaps, two severities

The observational behavior above covers projects that declare overlapping globs for their own trees. A cross-project output - one project declaring ctx.writesFiles(<other>.file(...)) into another project's tree - is checked at load instead, and is fatal:

[MGS4002] site: "shared.txt" is declared as an output by two projects (p1 and
p2); they cannot be ordered against each other, and each would cache the other's
bytes as its own output

It is fatal because the consequence is worse than a nondeterministic file. Both writers snapshot the path into their own cache entries, so whichever loses the race still records the winner's bytes as its own output. Replaying the loser alone then reproduces content it never produced - cache poisoning that outlives the run that caused it, in a project that looks perfectly healthy.

It is checked at load because the run-scoped check cannot see it reliably. That check only fires when both writers land in one dispatch, it keeps just the first stage's step per project, and two separate invocations are never compared at all. A collision this expensive must not depend on what you happened to ask for.

The same check also fires when a writer claims a path the owner already declares for itself, which would have the owner's own build produce and clean a file the writer also owns.

Resolution

1. Scope outputs under the project directory

The most common cause is using a workspace-relative glob that two projects share. Use the project-relative convention instead:

# Before (workspace-relative - both projects claim shared/types/**)
outputs:
  - shared/types/**

# After (project-relative - each project claims only its own tree)
outputs:
  - "**/*.gen.go"

In Go registration: WithOutputs("**/*.gen.go") anchors to the project root.

2. Assign the shared output to exactly one project

If shared/types/generated.go is produced by one project and consumed by another, declare the output only on the producer:

# producer: api/magus.yaml
outputs:
  - ../shared/types/generated.go

# consumer: worker - no matching output declaration

Then express the ordering dependency so the consumer always runs after the producer:

// magusfile.go
boosterpack.Bind(pack.Go, specWorker.Build).After(specAPI.Build)

3. If the overlap is intentional and safe

If both projects produce the file identically (deterministic generation), the overlap is benign but still worth documenting. Suppress the check by removing the duplicate declaration from one project and declaring a dependsOn relationship instead.

What this is NOT

  • Not always a real race. If the two projects are ordered (one's dependsOn the other), they never run concurrently and the overlap is harmless. The check fires on static declarations; it does not verify ordering. Run with --race to confirm whether a concurrent write occurs at runtime.
  • Not a build failure. The warning is advisory; magus does not block the build.

See also

  • MGS4001.md: runtime filesystem race detector (requires --race).
  • types.WithOutputs / WithOutputs(...): Go API for declaring outputs.
  • magus.yaml outputs: field: YAML API for declaring outputs.
MGS4002raceoutputsstatic checkgraphoverlapdeclarations
Last updated (3da15dc9)
Earlier changes on this page (2)

Full history ↗ · Blame source ↗

Glossary

Workspace

The magus root directory that owns a set of projects and shared config; the unit magus operates over. See workspace.

Project

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

Magusfile

The magusfile.buzz that declares a project's targets (as export funs) and binds its spells. See targets.

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.

Spell

A language/runtime adapter (e.g. go, md) that maps generic targets onto a toolchain's real commands. See spells.

Cache

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

Snapshot

A point-in-time view of live state - the pool's occupancy or a tick of exported metrics - as opposed to accumulated history. See daemon.

Health

The at-a-glance daemon state derived from the pool: healthy when the pool is reporting, degraded when it reports an error, down when there is no pool. The dashboard color-codes each state. See daemon.

Conventions

Placeholders

Angle brackets mark a value you replace with your own - never type the brackets:

magus run <target>
magus completion <shell>    # e.g. bash, zsh, fish

<target>, <path>, <shell>, <name> and the like are stand-ins, not literal text.