Repository navigation
decode,graph: defer BuildDecl.target resolution to graph construction - #115
Merged
Merged
Conversation
Change BuildDecl.target from Target to Option<Target>. The decoder used to eagerly fall back to Target::host() when the NCL didn't set target explicitly, which silently captured the arch of whatever process was running the decoder - fine when the decoder and executor were the same process, catastrophic when a graph is constructed on one arch for dispatch to another. Hydration now happens at the decode to graph seam in graph::BuildSpec::from_decoded via a new Loader::for_target field, populated in Graph::ingest from self.target (which was set by new_from_chain's existing out.target = for_target call). Motivation: buildbot on amd64 dispatching graphs to res-server-arm64 was producing specs stamped amd64/linux, failing the host-match check in op::specs. See gominimal/infra#18. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
twitchyliquid64
approved these changes
Apr 13, 2026
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.
Summary
Defers target resolution on
BuildDeclfrom decode-time to graph-construction-time, so graphs constructed on one arch for execution on a different arch (e.g.buildboton amd64 dispatching tores-server-arm64) produce specs whosetargetfield matches the destination host.Tom proposed the approach on Slack; this is the implementation.
Root cause
decode::BuildDecl.targetpreviously had typeTargetand defaulted at decode time viatarget.unwrap_or(Target::host())(crates/decode/src/builds.rs:1228onmain).Target::host()is aconst fndispatched bycfg!()at compile time, so it captured the architecture of the process running the decoder, not the target the caller actually requested viagraph_from_all_packages_with_target(target).When the decoder and the executor ran on the same host (every historical invocation until now) the check at
crates/op/src/specs.rs:256silently passed. The first cross-host graph construction — an amd64build-botVM generating a graph to dispatch tores-server-arm64— hit:See the full trace in the issue / in this debugging session.
Fix
Change
BuildDecl.targetfromTargettoOption<Target>:Nonemeans "the NCL didn't set it; inherit from the graph this spec is being constructed into."Some(_)preserves the ability for an NCL author to pin a spec to an explicit target (no package currently does this, but the capability is retained).Hydration happens at the decode→graph seam in
graph::BuildSpec::from_decoded, which consults a newLoader::for_targetfield. The Loader'sfor_targetis populated inGraph::ingestby cloningself.targetbefore the graph moves into the loader'sRefCell. This works becauseGraph::new_from_chain(already onmainfrom 39d2d52) setsout.target = for_targetbefore callingingest, so by the time the loader runs,self.targetholds the correct value.Scope of changes
Only two files touched. Grepping for readers of
BuildDecl.targetacross the entire workspace:crates/graph/src/graph.rs:166bd.target.clone().unwrap_or_else(|| loader.for_target.clone())crates/decode/src/builds.rs:750Target→Option<Target>crates/decode/src/builds.rs:1228from_term)target.unwrap_or(Target::host())→target,(pass-through)crates/decode/src/builds.rs:1424-1429simple_buildspec)target == Target::host()→target.is_none()crates/decode/src/builds.rs:1569Some(...)crates/decode/src/lib.rsArena<BuildDecl>/BuildDecl::default()Option::default() = None, correct.crates/minimal/src/cmd_dep.rsedge_ref.target()etc.BuildDecl.target.No wire-protocol change. No
pkgschange. Nores-serverchange. No changes toTargettype itself.Tests
cargo test -p decode— 42/42 passing. The two test updates at:1424-1429(now assertstarget.is_none()) and:1569(now wraps inSome(...)) both pass.cargo test -p graph— the 44 non-spec_hashertests pass. The 5spec_hasher::tests::*failures are pre-existing onmainin my macOS dev environment (cargo test -p graph spec_hasherfails identically with my changes stashed). They are not caused by this PR. CI will run on Linux where they should pass.cargo check -p decode -p graph— clean.cargo check -p mctx/ anything pulling insandbox2— fails on my macOS dev host due toprocfs/capsbeing Linux-only, not related to this PR. CI covers the full workspace onubuntu-latest.cargo fmt --check -p decode -p graph— clean.cargo clippy -p decode -p graph -- -D warnings— clean.Operational note for downstream consumers
Existing entries in any
layer_cache_dir(~/.cache/minimal/...on dev hosts, and the one on thebuild-botprod VM) were written under the old schema where everyBuildDeclhad a concretetarget: Targetbaked in — almost alwaysamd64/linuxif the writer was an amd64 buildbot process. Those JSON payloads will deserialize cleanly underOption<Target>asSome(amd64/linux)(serde handles this transparently), but they will continue to carry the wrong cached target until rewritten, so arm64 res-server dispatch on a cold re-run will still fail against a stale cache.Recommended post-merge step on any host that caches layers (especially
build-boton prod):Then re-trigger a run — the fresh decode will produce
Some(None) → hydrated from graphtargets and dispatch correctly.Test plan (for reviewer)
cargo fmt --checkcleancargo clippy -p decode -p graph -- -D warningscleancargo test -p decode42/42 passcargo test -p graph(non-spec_hasher) passgominimal/build-serversrev bump + fresh buildbot binary +layer_cacheclear: arm64 dispatch throughres-server-arm64should progress pastop/src/specs.rs:256and actually run buildsRelated
gominimal/infra#18merged earlier today: deploys the second res-server instance on arm64.gominimal/build-serverswill need a follow-up bump of theminimalgit dep revs to point at this commit once it lands.🤖 Generated with Claude Code