Skip to content

gomod edits (replace) are ignored for sources nested under mounts #1131

Description

@cpuguy83

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 SourceMountSpec.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:

  1. Add a generic recursive walker over a Source's mounts (Command mounts, BuildStep mounts, and recursing into Build.Source).
  2. 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).
  3. Apply this in Spec.Preprocess, which already has sOpt + worker, so no caller changes.
  4. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Fields

    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions