Skip to content

ip65 blob: .incbin resolves cwd-first, so every worktree build silently reads the PRIMARY checkout's blob #116

Description

@JC-000

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:

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 ip65 submodule 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:

spellingflagsboth blobs present
bare "ip65-c64.bin"-I ip65-buildERROR
"../../../ip65-build/…"--bin-include-dir ip65-buildPARENT — 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions