Summary
When a source is nested under a mount (SourceMount.Spec) instead of being a top-level entry in spec.Sources, its generate: directives are silently ignored — no error or warning. The interesting case is gomod: gomod.edits.replace (and other go mod edit transformations) never apply to a source used as a mount, so the build compiles against an unpatched go.mod.
Generators vs. edits
These are two different capabilities and should be treated differently for mounts:
- A. gomod
edits (e.g. edits.replace) are a pure content transformation of the source's files (patching go.mod/go.sum). Self-contained, needs no module cache and no top-level source name, and is meaningful for a mounted checkout. This is the correctness bug and the scope of this issue.
- B. Dependency prefetch into a shared cache (the "generator" side of gomod/cargohome/pip/nodemod) builds a module/package cache that DALEC delivers into the build. Verified: this cache is never mounted into source-generation cmds (
Command.baseState only mounts cmd.Mounts/step.Mounts, not /go/pkg/mod), and those cmds run with network anyway — so it isn't required for a mounted cmd to build, and wiring it in is a separate, larger change. Out of scope / tracked separately.
Only gomod mutates source content, so this issue is effectively gomod-edits-only.
Details (verified in code)
- Generator collection is top-level-only:
isGomod() inspects only s.Generate; gomodSources()/HasGomods()/GomodDeps() iterate only s.Sources. Same pattern for cargohome/pip/nodemod.
- Patches are top-level-only:
spec.Patches is keyed by source name; getPatchedSources() applies s.Patches[name]. A mounted source is fetched inline via SourceMount → Spec.ToMount and never touches the patch machinery — there is no existing hook to patch a mount.
- The gomod edits.replace flow is entirely keyed by top-level source name (patch source
__gomod_patch_<name>, s.Patches[name], workdir /work/src/<name>), so it cannot apply to a nameless nested mount as-is.
SourceMount.fillDefaults() carries an explicit // TODO: need to pass in generators to fill defaults for the source and drops the generators.
Reproduction (conceptual)
sources:
builder:
image:
ref: some/builder:latest
cmd:
mounts:
- dest: /src
spec:
git:
url: https://example.com/org/project.git
commit: <sha>
generate:
- gomod:
edits:
replace:
- example.com/foo => example.com/foo@v1.2.3
steps:
- command: cd /src && go build ./...
Expected: the replace is applied to /src's go.mod before the build runs.
Actual: the edit is ignored; the mounted source builds with its original go.mod.
Chosen approach — in-band transform + swap mount.Spec
Rather than inventing synthetic names to shoehorn mounts into the top-level s.Sources/s.Patches machinery, apply the edit where the mount is fetched:
- Add a generic recursive walker over a
Source's mounts (Command mounts, BuildStep mounts, and recursing into Build.Source).
- For each mounted source with gomod
edits, fetch its base state and run the existing go mod edit+tidy worker step to produce the patched source tree, then swap mount.Spec to an LLB source carrying the patched content (preserving Path/Includes/Excludes and any other generators).
- Apply this in
Spec.Preprocess, which already has sOpt + worker, so no caller changes.
- Resolve the
SourceMount.fillDefaults generator TODO (for gomod git auth on mounts).
The go-mod-edit worker step runs as our own internal LLB run (reusing the same go mod edit + go mod tidy + optional go mod vendor script the top-level flow uses), with all env attached as run options and the shared module cache mounted. This internal run needs network and the module cache to recompute go.sum, exactly like the top-level source flow. That is distinct from capability B: the mount's own build command still gets no DALEC-managed module cache and owns its own env/network.
The walk is built generically so cargohome/pip/nodemod can be slotted in later if/when they gain source-mutating behavior; for now those remain no-ops on mounts, as does a gomod mount with no edits.
Summary
When a source is nested under a
mount(SourceMount.Spec) instead of being a top-level entry inspec.Sources, itsgenerate:directives are silently ignored — no error or warning. The interesting case is gomod:gomod.edits.replace(and othergo mod edittransformations) never apply to a source used as a mount, so the build compiles against an unpatchedgo.mod.Generators vs. edits
These are two different capabilities and should be treated differently for mounts:
edits(e.g.edits.replace) are a pure content transformation of the source's files (patchinggo.mod/go.sum). Self-contained, needs no module cache and no top-level source name, and is meaningful for a mounted checkout. This is the correctness bug and the scope of this issue.Command.baseStateonly mountscmd.Mounts/step.Mounts, not/go/pkg/mod), and those cmds run with network anyway — so it isn't required for a mounted cmd to build, and wiring it in is a separate, larger change. Out of scope / tracked separately.Only gomod mutates source content, so this issue is effectively gomod-
edits-only.Details (verified in code)
isGomod()inspects onlys.Generate;gomodSources()/HasGomods()/GomodDeps()iterate onlys.Sources. Same pattern for cargohome/pip/nodemod.spec.Patchesis keyed by source name;getPatchedSources()appliess.Patches[name]. A mounted source is fetched inline viaSourceMount→Spec.ToMountand never touches the patch machinery — there is no existing hook to patch a mount.__gomod_patch_<name>,s.Patches[name], workdir/work/src/<name>), so it cannot apply to a nameless nested mount as-is.SourceMount.fillDefaults()carries an explicit// TODO: need to pass in generators to fill defaults for the sourceand drops the generators.Reproduction (conceptual)
Expected: the
replaceis applied to/src'sgo.modbefore the build runs.Actual: the edit is ignored; the mounted source builds with its original
go.mod.Chosen approach — in-band transform + swap
mount.SpecRather than inventing synthetic names to shoehorn mounts into the top-level
s.Sources/s.Patchesmachinery, apply the edit where the mount is fetched:Source's mounts (Command mounts,BuildStepmounts, and recursing intoBuild.Source).edits, fetch its base state and run the existinggo mod edit+tidyworker step to produce the patched source tree, then swapmount.Specto an LLB source carrying the patched content (preservingPath/Includes/Excludesand any other generators).Spec.Preprocess, which already hassOpt+worker, so no caller changes.SourceMount.fillDefaultsgenerator TODO (for gomod git auth on mounts).The go-mod-edit worker step runs as our own internal LLB run (reusing the same
go mod edit+go mod tidy+ optionalgo mod vendorscript the top-level flow uses), with all env attached as run options and the shared module cache mounted. This internal run needs network and the module cache to recomputego.sum, exactly like the top-level source flow. That is distinct from capability B: the mount's own build command still gets no DALEC-managed module cache and owns its own env/network.The walk is built generically so cargohome/pip/nodemod can be slotted in later if/when they gain source-mutating behavior; for now those remain no-ops on mounts, as does a gomod mount with no
edits.