nix-plugin: persistent DAG cache for resolveGoPackages#48
Draft
aldoborrero wants to merge 3 commits intomainfrom
Draft
nix-plugin: persistent DAG cache for resolveGoPackages#48aldoborrero wants to merge 3 commits intomainfrom
aldoborrero wants to merge 3 commits intomainfrom
Conversation
Cache the full output JSON of resolveGoPackages under $XDG_CACHE_HOME/go2nix/resolve/<sha256>.json, keyed on go.sum + go.mod + a cheap local-only `go list -e -mod=mod -json=ImportPath,Imports,... ./...` probe + GOOS/GOARCH/CGO_ENABLED/tags/subPackages/modRoot/doCheck/ resolveHashes + `go version` + a SCHEMA_VERSION constant. The probe runs without GOMODCACHE because Imports come from header parsing (go/build/read.go readGoInfo, parser.ImportsOnly), so the key is correct without realising any module sources and survives function-body edits. On a hit we return the cached string before resolve_packages runs, so the full `go list -deps` walk — and therefore GOMODCACHE — is never touched. Best-effort: any error in the probe/key/read/write degrades to the existing path. Opt-out via GO2NIX_RESOLVE_CACHE=0.
Check $GO2NIX_RESOLVE_CACHE_DIR before the writable XDG location so sandboxed/hermetic environments can supply a pre-built cache (e.g. a Nix store path produced by running one eval with XDG_CACHE_HOME=$out) without granting write access at eval time. Read-only — never written to. Falls through to XDG on miss.
- probe: include TestImports/XTestImports/TestGoFiles/XTestGoFiles so doCheck=true keys invalidate on _test.go import edits - probe: add -buildvcs=false and a self-contained GOCACHE so it works with HOME unset - key: use `go env GOVERSION GOEXPERIMENT GOAMD64 GOARM GOARM64 GOFIPS140` for toolchain identity (go version misses baked-in GOEXPERIMENT/sub-arch); propagate exec failure instead of hashing empty - read: validate cached JSON parses before returning so a truncated file degrades to miss instead of an opaque C++ parseJSON error - read: accept resolveCacheDir as a JsonInput field (Nix-trackable via string context) in addition to the GO2NIX_RESOLVE_CACHE_DIR env fallback; lookup order is arg → env → XDG
aldoborrero
added a commit
that referenced
this pull request
Apr 13, 2026
Seeds backlog/ with known deferred items for the grind workflow (.claude/commands/grind.md, untracked): - refactor: dead .has_cgo/.has_cxx markers (post-#85) - refactor: pkg/lockfile/ dead-code audit (post-#80/#81/#106) - refactor: filter *_test.go from CLI src so canary doesn't cascade - coverage: pie buildmode fixture - coverage: second dynamic-mode pkg-config fixture - feat: GOFIPS140 snapshot mode (stdlib importcfg remap) - feat: vendor/ directory support - perf: #48 DAG cache revival (blocked, needs-human gate) backlog/tried/ seeded with shipped Tier 1-3 perf work so it's not re-attempted. :house: Remote-Dev: homespace
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problem
builtins.resolveGoPackagesrunsgo list -json -depsat Nix eval time. That command must read source files from every module in the transitive closure to parse theirimportblocks, so the full GOMODCACHE has to be present on disk before the builtin can produce output — even when nothing has changed since the last eval. For large module graphs this is the dominant cost of everynix eval.Approach
Cache the full output JSON under
$XDG_CACHE_HOME/go2nix/resolve/<sha256>.json(sibling of the existingnar_cacheimpurity). On a hit we return the cached string beforeresolve_packagesruns, sogo list -deps— and therefore GOMODCACHE — is never touched.Why the cache key is correct
go listreads each.gofile only through theimportblock (go/build/read.go readGoInfo,parser.ImportsOnly); function bodies are never read. So a cheap local-only probe captures exactly the local state that can change the third-party closure:This runs with an empty
GOMODCACHEandGOPROXY=offin milliseconds —Importsis populated because it comes from header parsing, not resolution. (Do not use-find: it setsIgnoreImportsand clearsImports.)The key is
sha256of:SCHEMA_VERSIONconstantgo.sum+go.modbytesGOOS/GOARCH/CGO_ENABLEDtags, sortedsubPackages,modRootdoCheck,resolveHashesgo versionoutputInvalidation
.gobody edited (no import change)importline added/removedgo.sum/go.modchangedSemantics
GO2NIX_RESOLVE_CACHE=0disables it entirely.nar_cache.rs.Caller-side note
nix/dag/default.nix:296-315passes no modcache argument to the builtin — the plugin reads ambientGOMODCACHEfrom the evaluator's process. So a cache hit genuinely avoids GOMODCACHE. To realise the full win, whatever wrapper exportsGOMODCACHEbeforenix eval(e.g.benchmark-eval/default.nix:106-107) must become lazy — populate only on miss. That's outside this PR.Test plan
cargo build && cargo test— 44/44 (38 pre-existing + 6 new: key determinism, varies-with-tags/subpackages, sort-stable, no-adjacent-field-collision, read/write round-trip)cargo clippy— only the 2 pre-existing warnings (nar.rs:57, resolve.rs:232)--option plugin-files: cold call populates$XDG_CACHE_HOME/go2nix/resolve/<hash>.json; warm call withGOMODCACHE=/definitely/missingreturns identical JSONgo version; no-depsinvocation. Cold call execs-deps.import "fmt"→ miss (new key,-depsruns)GO2NIX_RESOLVE_CACHE=0→ eval succeeds, no cache file writtenXDG_CACHE_HOME=/proc/self/nonwritable→ write error swallowed, eval succeedsdoCheck=truevsfalse→ distinct keysRelated
-pgo=off -mod=readonlyto thego list -depsinvocations — independent quick win for the cold/miss path.Pre-building the cache (sandboxed environments)
For hermetic/sandboxed evals with no writable cache dir, point
GO2NIX_RESOLVE_CACHE_DIRat a pre-built read-only directory — checked before XDG, never written to.To produce that directory as a Nix derivation, run one eval with
XDG_CACHE_HOME=$out:Downstream evals then export
GO2NIX_RESOLVE_CACHE_DIR=${resolveCache}/go2nix/resolveand never realise GOMODCACHE on a hit.