Repository navigation
Description
9e1c8c9 established that ca65 resolves .incbin against the current directory as well as the including source file, and #115 closed the case it caused (a missing blob assembling silently from outside the checkout) by adding the build/net/ip65/ip65_blob.o: $(IP65_BIN) edge.
The dependency edge fixes the fresh clone. It does not fix the resolution ambiguity, and I measured which side wins — it is the one that makes worktrees wrong.
Measured: cwd-relative wins; source-relative is only a fallback
ca65 V2.18, scratch tree mimicking the repo's worktree layout — a "parent checkout" with a blob, a "worktree" three levels down with its own blob, and src/net/ip65/blob.s carrying the production spelling .incbin "../../../ip65-build/ip65-c64.bin":
scratch/ip65-build/ip65-c64.bin = "PARENTPARENTPARE"
scratch/x/y/z/ip65-build/ip65-c64.bin = "WORKTREEWORKTREE"
scratch/x/y/z/src/net/ip65/blob.s = .incbin "../../../ip65-build/ip65-c64.bin"
$ cd scratch/x/y/z && ca65 src/net/ip65/blob.s -o out.o
exit=0
$ strings out.o
PARENTPA <- the PARENT's blob, not its own
Remove only the parent's copy and re-assemble, everything else identical:
$ rm ../../../ip65-build/ip65-c64.bin
$ ca65 src/net/ip65/blob.s -o out2.o
exit=0
$ strings out2.o
WORKTREE <- falls back to source-relative
So the precedence is cwd first, including-file second. The comment at src/net/ip65/ip65_blob.s:23 is correct that resolution is not source-relative-only; what is new here is that source-relative does not merely coexist, it loses.
Why that matters in this repo specifically
A git worktree lives at <repo>/.claude/worktrees/<name>/, exactly three levels below the primary checkout. From a worktree root, cwd-relative ../../../ip65-build/ is the primary checkout's ip65-build/. So for every ip65 build run in a worktree while the primary checkout has a blob:
make ip65-blobbuilds the worktree's ownip65-build/ip65-c64.bin;$(IP65_BIN)— including chore: finish the pytest-boundary rename, correct CLAUDE.md against measurement, and fix #89's fresh-clone build #115's new dependency edge — tracks that worktree file;- ca65 then reads the primary checkout's file instead, and never touches the one make just built.
Both the build graph and the operator are describing a different file from the one that lands in the PRG.
This is currently benign only by coincidence: the blob is byte-deterministic at a fixed ip65 submodule pin, so both copies are cf1a5ff7… and the resulting PRG is correct. It stops being benign the moment the two copies can differ, which is exactly the situation any of these creates:
- bumping the
ip65submodule on a branch in a worktree — the worktree rebuilds its blob, ca65 keeps reading the old one from the primary checkout, and the PRG silently contains the old network stack with no diagnostic; - any deliberate blob experiment in a worktree (different
ip65-build/ip65.cfg, a driver change); - concurrent agents in sibling worktrees, where whoever last wrote the primary checkout's blob decides what everyone else compiles.
It also silently weakens blob-related evidence taken from a worktree. My own #113 ip65 numbers are an instance: I ran make ip65-libs && make ip65-blob first and reported the blob hash, but the PRGs were in fact assembled from the primary checkout's copy. The hashes I reported are still right — the blob is deterministic, and 9e1c8c9's independent fresh-clone run reproduced the same d522e684… — but the provenance is not what I stated, and I would not have known without this measurement.
Suggested fix
CORRECTED — the first version of this section was wrong. It recommended -I ip65-build with a bare .incbin "ip65-c64.bin", and flagged that "whether ca65 applies -I to .incbin needs confirming". It does not. Measured (ca65 V2.18, same scratch layout):
$ ca65 -I ip65-build src/net/ip65/bare.s -o a.o
src/net/ip65/bare.s(2): Error: Cannot open include file 'ip65-c64.bin': No such file or directory
-I is the source-include path. cc65 ships a separate flag for binary includes, both visible in ca65 --help:
-I dir Set an include directory search path
--bin-include-dir dir Set a search path for binary includes
Full option matrix measured by LaneContract2 and posted as a comment below; the three rows that decide the patch, re-confirmed independently here:
| spelling | flags | both blobs present |
|---|---|---|
bare "ip65-c64.bin" |
-I ip65-build |
ERROR |
"../../../ip65-build/…" |
--bin-include-dir ip65-build |
PARENT — bug survives |
bare "ip65-c64.bin" |
--bin-include-dir <absolute> |
WORKTREE — correct |
The trap, and the acceptance criterion
The middle row is the one to be careful about. Adding --bin-include-dir while leaving the ../../../ operand in place resolves to the parent's blob, exits 0, and changes nothing. This is a two-part edit — flag and operand — and a half-applied version is indistinguishable from a working one by exit status, by PRG hash (the blob is deterministic today), and by the whole existing evidence bar.
So the acceptance criterion for this patch is not a green build. It is a marker test: plant distinguishable bytes in the two ip65-build/ip65-c64.bin copies, assemble from a worktree, and confirm the object contains the worktree's marker. Anything less cannot tell the fixed state from the broken one.
Recommendation
CA65FLAGS += --bin-include-dir $(abspath $(IP65_BUILD)).incbin "ip65-c64.bin"Absolute rather than relative: --bin-include-dir ip65-build also works from the repo root, but it is itself cwd-relative and would silently re-acquire a variant of this bug if any recipe ever assembles from elsewhere — and cwd sensitivity is the whole defect. Every working option fails loudly on a missing blob, so together with #115's dependency edge the file is both guaranteed present and guaranteed to be the worktree's.
Keep #115's dependency edge either way; it is orthogonal and still needed.
Also part of this patch: src/net/ip65/ip65_blob.s:23 states the rule backwards — "ca65 does NOT resolve .incbin relative to the including source file" reads as symmetric, when source-relative is specifically the loser. That comment is what makes the current ../../../ look deliberate, so a fixed path left beside it invites someone to re-derive the escape.
Worth doing regardless of the fix chosen: the caution comment at ip65_blob.s:23 should say cwd-relative takes precedence, since "does not resolve only relative to the source file" reads as symmetric and the asymmetry is the whole hazard.
Provenance
Found by LaneContract2 during #114 review as a residual on 9e1c8c9/#115 ("harmless while the blob is deterministic, but they are not the same file"); precedence measured here. Filed rather than patched because it touches Makefile and src/net/ip65/ip65_blob.s, both changed by #115 within the last few commits.
Activity
JC-000 commentedon Aug 15, 2026
Measured: the proposed
-I ip65-buildfix does not work — but--bin-include-dirdoesSettling the open caveat in the issue ("whether ca65 applies
-Ito.incbinneeds confirming first"). Confirmed, and the answer is no. ca65 V2.18, same scratch layout as the issue (primary blobPARENTPARENTPARE, worktree blobWORKTREEWORKTREE, cwd = worktree root)..incbin "../../../ip65-build/ip65-c64.bin".incbin "ip65-c64.bin"-I ip65-build.incbin "ip65-build/ip65-c64.bin".incbin "ip65-c64.bin"--bin-include-dir ip65-build.incbin "ip65-c64.bin"--bin-include-dir <ABSOLUTE>.incbin "../../../ip65-build/ip65-c64.bin"--bin-include-dir ip65-buildB fails even though the file exists at that path:
-Iis the source-include path and does not feed.incbin. cc65 ships a separate flag for binary includes —ca65 --help:So the fix as written in the issue would have failed the build outright. Loudly, at least — but it would have cost whoever implemented it a confusing detour.
Row G is the one to be careful about
Adding
--bin-include-dirwhile leaving the../../../operand in place keeps the bug — still resolves to the primary checkout. The flag only helps if the operand also drops the escape. A patch that adds the flag and stops there looks correct, builds clean, and changes nothing; it needs the two edits together, verified by the marker test above rather than by the build succeeding.Recommendation: F
Why F over C and E: all three resolve correctly today, but C and E are cwd-relative, so they silently re-acquire a variant of this bug if any recipe ever assembles from a directory other than the repo root.
$(abspath …)is cwd-independent, and make already expands it against its own working directory, which is the thing we actually mean.Every working option fails loudly when the blob is absent — none of them falls back to the primary checkout — so combined with #115's
build/net/ip65/ip65_blob.o: $(IP65_BIN)edge the file is both guaranteed present and guaranteed to be the worktree's.One more thing to fix in the same patch
src/net/ip65/ip65_blob.scurrently documents the wrong rule:Source-relative is only the fallback; cwd is tried first and wins, which is the whole defect. That comment is what makes the current spelling look deliberate and correct, so it should be replaced rather than left next to a fixed path — otherwise the next person to reorganise the tree re-derives the
../../../from it.Not patching this myself — agreed with your reasoning about not surprising LaneFollowups on
Makefile/ip65_blob.stwice in a few commits, and my own lane (#114) is merged and closed. Happy to implement if it's useful; otherwise the measurements above should make it a short patch for whoever owns it.On provenance: same correction applies to my #114 ip65 numbers as to your #113 ones — my worktree had its own blob throughout, but ca65 was reading the primary's, so the PRGs I hashed were assembled from the primary checkout's copy. The hashes stand for the same reason yours do (deterministic blob,
cf1a5ff7…reproduced independently several ways, and9e1c8c9's fresh-clone run reproducingd522e684…), but the provenance claim is weaker than it looked and is worth stating that way.JC-000 commentedon Aug 15, 2026
Implemented — PR #117.
Two things changed shape from the issue as filed, both measured:
1. Four sites, not one. #116 is scoped to the ip65 blob, but the identical escape was in both overlay TUs:
These are the worse case.
ip65-build/is deterministic, which is the only reason the blob version stayed benign;build/is branch- and flag-dependent, so a borrowed overlay image could have come from a different pin entirely. All three files also carried the same wrong comment ("resolved by ca65 relative to this source file") — replaced, since that is what made the../../../look deliberate.2. The gate had to move from the object to the PRG. My first marker test reported FAIL against a working fix. Cause: ca65 stores
.incbindata in fragments — measured, only 8 bytes of the blob appear contiguously in the.o— so a planted marker never appears as a searchable string even when the correct file was read. Anyone building this gate at object level will get a false failure. ld65 emits the blob raw, so the check lives at the linked PRG.Gate results, and it discriminates rather than merely passing:
The test never writes the primary checkout's blob — it marks only the worktree's copy and restores it byte-for-byte.
Verification: all 5 backend × profile builds PASS with hashes identical to master, i.e. output-neutral. Overlay operands verified in isolation (resolve with a stub
.binpresent, fail loudly when absent, real TUs and real defines) — because both overlay-embed builds are blocked by pre-existing defects confirmed identical on master (P-256LIB_NISTCURVES_P256_CODE overflows OVERLAY_REGION by 644 bytes; P-384 SHA-384 table). Their.incbinlines are unreachable by a real build, so I said that plainly rather than let the green matrix imply coverage they do not have.$(abspath ...)behaved normally throughout — no oddity to report, and I did not fall back to the relative form.Merge pull request #117 from JC-000/fix/incbin-worktree-escape
test: record the ip65 measurement at the finished_verify dispatch arm
docs: fix four defects an adversarial review found in this branch
docs: finish the .incbin retirement, and fix the same shape in the cf…