docs: the ip65 blob is not committed — document the fresh-clone build order - #86
Conversation
CLAUDE.md's Build section described `ip65-build/ip65-c64.bin` as "the committed blob" and `make ip65-libs` as "only needed if the ip65 submodule changes". Both are wrong, and they reinforce each other: the reader is told the artifact ships with the repo (so no rebuild) and that the library build is conditional (so skip it) — which routes them past the only step that works. The blob is gitignored (`.gitignore: ip65-build/*.bin`; `git ls-files ip65-build/` returns only ip65.cfg and ip65_stub.s). A fresh clone has no blob, `make` tries to build one, and the link step consumes ip65 `.lib` archives the submodule does not ship, so it dies with: ld65: Error: Input file '../ip65/ip65/ip65_tcp.lib' not found which reads as a broken checkout rather than a missing one-time step. Reproduced in two independent fresh worktrees; `make ip65-libs && make` then links clean and yields the 47,105 B ip65 PRG. The blob rebuild is deterministic — 6,951 B, sha256 cf1a5ff7…0c2d90dd in both worktrees. - Build gains a "first build in a fresh clone" block with the working order, and states what "normally reused" actually means: `clean` only removes `build/`, so a once-built blob persists. - ip65-libs / ip65-blob target descriptions corrected. - Same false claim removed from the Makefile's own comments (two sites), which is where it was being copied from. - The VICE ip65 rig's prerequisite list gains the blob step; it is the one rig that needs an ip65 PRG. - While in the section: the C64_SKIP_BUILD count said 7, it is 14 (13 under tools/, plus tests/test_vice_https_macos.py). Replaced the number with the grep that regenerates the list. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
… two Correction routed from Lane G, verified here before restating: the determinism evidence for ip65-c64.bin is stronger than the sentence claimed. Three independent worktrees rebuilt it byte-identically on 2026-08-13 (Lane C's, Lane G's, mine), and the 2026-05-06 copies carried in three repo snapshots hash the same cf1a5ff7...0c2d90dd — I hashed those three directly rather than take the report. The sentence carries the load of the whole section: if the rebuild is deterministic then nobody ever needed a committed copy, and the fix is to name the one-time prerequisite. Four agreeing artifacts across three months make that point better than two, so it is worth the accuracy. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Found while verifying my own branch's PRG-neutrality: an incremental `make` after a `BACKEND=uci` build produced an ip65 PRG of exactly the right size (47,105 B) and the wrong contents — d483d46f… against the clean tree's db311110…, with no warning. The final make ran ld65 alone; every object was "up to date" by timestamp. The mechanism is worth naming because it is wider than the flag rule the Packaging section already states. BACKEND= is not a -D flag, it selects `-I src/net/$(BACKEND)`, and src/tls13.s includes net_tuning.inc from that path. So the stale object carries the *other* backend's post-ServerHello drain budget: an ip65 PRG built this way gets UCI's 1x16 instead of 8x250, which is precisely the #73 regression that #74 fixed — reintroduced by a build order rather than by a code change, in the one backend where the drain is load-bearing (ip65 sends no MSS and ACKs only when polled). Documented at the top of Variables, where someone about to type `make BACKEND=...` will read it, with the two hashes as evidence. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
JC-000
commented
Aug 13, 2026
Reproduced independently by the supervising session — and the hazard is worse than commit 3 documents. Fresh copy of the tree at Commit 3 describes a mixed binary — right size, wrong contents, because Both failures share the root cause this commit identifies — Worth adding to the note, because it changes what a reader should conclude: after a This also retroactively justifies the |
…ntirely Lane G reproduced the mixed link deliberately from a third worktree, so I went back to reproduce it cleanly here rather than rely on the accidental first sighting — and the deliberate run failed a different way, which is worth recording because it defeats the obvious check. make clean && make BACKEND=uci then plain make -> 4 ca65 lines, NO ld65 line, exit 0, build/c64-https.prg still the 62,977 B UCI image macOS ships GNU Make 3.81, which compares mtimes at 1-second resolution. The recompiled ip65 objects were 39 ms newer than the previous link; make's own -d output says "Prerequisite build/net/ip65/net.o is older than target build/c64-https.prg" and skips the link. A second `make` then says "Nothing to be done for `all'". So a BACKEND switch without clean gives you either a mixed PRG at the right size (the first mode, d483d46f... vs db311110...) or no rebuild at all and the wrong backend's PRG entirely — which of the two depends on whether the compile crosses a second boundary. Size catches the second, hash catches both, exit code catches neither. Object hashes are deliberately not cited: --debug-info embeds source paths, so .o hashes differ across worktrees even when the linked PRG is identical (confirmed — my clean ip65 tls13.o does not match Lane G's, while both link to db311110...). PRG hashes are the portable evidence. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Correcting my own reasoning in a0039b1. That commit said object hashes differ across worktrees because `--debug-info` embeds source paths. That is wrong, and I checked it this time instead of reasoning about it: `strings build/tls13.o` shows only relative paths (`src/tls13.s`, `src/net/ip65/net_tuning.inc`) and zero absolute ones, so two worktrees would agree. The real mechanism is worse for the conclusion's sake, and better for the advice. ca65 stamps the build's wall-clock time into the object header — a little-endian 32-bit field at offset 107 in build/tls13.o that matches `date +%s` of the build exactly (searched the header for a LE32 within +/-20 s of the build time; one hit, delta 0). So rebuilding identical source in the SAME worktree changes the object hash: 6cd91553... -> cfef8378... over a `touch src/tls13.s && make`, four bytes differing. Object hashes are therefore not weak evidence, they are not evidence: nobody can reproduce their own twice. The useful half is the asymmetry — ld65 does not propagate that field, and build/c64-https.prg stayed at db31111031e2... across every one of those rebuilds. PRG-hash comparison works because of a checkable toolchain property, which is worth stating where the neutrality checks in this session all quietly relied on it. Credit to Lane G for catching the wrong mechanism; the timestamp offset and the same-worktree reproduction are measured here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
dc016a7 quoted "LE32 at offset 107 in build/tls13.o". The offset should not be in the doc: it is a cc65-version detail, and someone reproducing on another toolchain who finds nothing at 107 would reasonably conclude the whole finding was wrong. The reproduction that always works is two clean builds and a hash comparison, so that is what the section now says. For the record, since three readings of this effect are now in circulation (100, 100/101, 107): measured across three objects in one tree — build/tls13.o, build/boot.o, build/crypto/sha256.o — every build changes bytes at 0-based 100-101 AND 107-108, and in all three objects the LE32 at 107 equals `date +%s` of the build exactly. So the offsets were consistent across objects here, and the divergent readings look like different bytes of the same effect rather than an object-dependent layout. That detail is worth knowing and not worth documenting, which is the point. Conclusion unchanged and now confirmed three ways: object hashes are not evidence, the PRG hash is deterministic, and every neutrality claim in this remediation rests on the latter. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-up to #80, kept on its own branch so #80 stays merge-ready — the two
touch different parts of CLAUDE.md and do not conflict. Routed from Lane C via
the coordinator, reproduced independently by Lane G and by me.
The defect
CLAUDE.md's Build section described
ip65-build/ip65-c64.binas "the committedblob", and
make ip65-libsas "only needed if the ip65 submodule changes".Both are false, and they compound: the reader is told the artifact ships with
the repo (so no rebuild needed) and that the library build is conditional (so
skip it) — which is precisely the path that does not work.
The blob is gitignored:
Reproduction (fresh worktree, verbatim)
The submodule ships ip65's sources; the blob link wants
.libarchives thatonly
make ip65-libsproduces. Lane G saw an earlier variant of the same wallwith submodules uninitialised (
Cannot open include file '../ip65/inc/common.inc').Recovery, verified here end to end:
47,105 B matches the audit's ip65 baseline PRG. The blob is byte-identical
across three independent worktrees rebuilt today — Lane C's, Lane G's and
mine — at sha256
cf1a5ff7809af4e4655e385b378b936054f41046ff2b7604828af3240c2d90dd, and the2026-05-06 copies preserved in three repo snapshots hash the same value (I
hashed those directly). Four agreeing artifacts spanning three months: the
rebuild path is deterministic, so nobody needs a committed copy — they need the
one-time step named. A stale blob is not a failure mode worth designing around;
a missing one is.
What changed
working order (
git submodule update --init --recursive→make ip65-libs→make), states that the blob is a gitignored artifact, quotes the exactld65error so a search lands here, and explains what "normally reused"actually meant:
make cleanisrm -rf build, so a once-built blob survivesand is never rebuilt. That persistence — not a committed file — is why the
rebuild targets were invisible for so long.
make ip65-libs/make ip65-blobdescriptions corrected.("the prebuilt blob is committed to ip65-build/", "The resulting file is
committed to the repo"). That is where CLAUDE.md was copying it from, so both
are fixed; leaving them would have re-seeded the error.
lose") gains the blob step — it is the one rig that needs an ip65 PRG.
C64_SKIP_BUILDclaim said 7 scripts honor thevariable and pointed at a commit for the list. It is 14 (13 under
tools/,plus
tests/test_vice_https_macos.py), so the number is replaced with thegrep that regenerates the list — a count in prose is a claim that decays.
Third commit: a mixed-binary hazard found while verifying this PR
Lane G's cross-PR matrix asserts every remediation branch is PRG-neutral. I went
to confirm that for my own branches rather than accept it, and my first build
disagreed: 47,105 B, but
d483d46f…against theirdb311110….make clean && makein the same tree then reproduced their hash exactly.The first build was mixed. I had built
BACKEND=uciearlier in the session;the later plain
makefound every object up to date by timestamp and ranld65alone:That
tls13.owas compiled with-I src/net/uci.BACKEND=is not a-Dflag — it selects the include path, and
src/tls13.sdoes.include "net_tuning.inc". So the ip65 PRG carried UCI'spost-ServerHello drain budget,
NET_SH_DRAIN 1x16instead of8x250: issue#73's regression, reintroduced by build order rather than by a code change, in
the one backend where that drain is load-bearing (ip65 sends no MSS option and
ACKs only when polled, so the budget is what keeps an impatient peer from
RSTing during the multi-minute crypto stall).
Same size, different bytes, no warning. CLAUDE.md stated the
make cleanruleonly in the Packaging section and only about flags; commit 3 states it at the
top of Variables, where someone about to type
make BACKEND=…will read it,with both hashes as evidence.
Fourth commit: a second mode, found trying to reproduce the first
Lane G reproduced the mixed link deliberately from a third worktree, so I went
back to reproduce it cleanly here rather than rest on an accidental sighting.
The deliberate run failed a different way:
macOS ships GNU Make 3.81, which compares mtimes at 1-second resolution.
The recompiled ip65 objects were 39 ms newer than the previous link
(
1786622893.195373vs…893.156535), and make's own-doutput saysPrerequisite 'build/net/ip65/net.o' is older than target 'build/c64-https.prg'. So it skips the link and leaves the wrong backend'sPRG in place.
A
BACKENDswitch withoutcleantherefore yields either a mixed PRG atexactly the right size, or no rebuild at all and the other backend's PRG
entirely — which one depends on whether the compile crosses a second boundary.
Size catches the second, hash catches both, exit code catches neither.
Fifth commit: why PRG hashes are checkable and object hashes are not
Object hashes were floated as a proof chain for the trap above. They cannot
serve. My first explanation for that was wrong too — I said
--debug-infoembeds source paths, making
.ohashes worktree-local — and commit 5 correctsit with a measurement rather than a second guess:
strings build/tls13.ocontains only relative paths (src/tls13.s,src/net/ip65/net_tuning.inc) and zero absolute ones. Worktrees wouldagree.
touch src/tls13.s && makechanges the object hash in the same worktree —6cd91553…→cfef8378…, four bytes differing. Nobody can reproduce theirown object hash twice.
ld65does not propagate it.build/c64-https.prgstayed atdb31111031e2…across every one of those rebuilds.
That asymmetry is the reason PRG-hash comparison works at all, and every
neutrality check in this remediation quietly depended on it. It is now stated
in the Build section rather than assumed.
Commit 6 removes the byte offset commit 5 had quoted, and the reason it was
worth removing turned out to be better than the reason I gave. Three of us read
three different offsets out of this effect. The resolution, measured:
continuation in bit 7) decoding to
date +%sof the build exactly — 5 bytes,delta 0. Lane G decoded this; I confirmed it in my tree.
the build time in my earlier samples only because my method was
touch src/tls13.s && make, which set the source mtime to the build second.My own test manufactured the coincidence it then reported.
make clean && makewith notouchchanges two bytes, both inthe varlen field; the count varies with how many 7-bit groups roll over
between builds, which is why one observer saw 1 byte and I saw 4.
Verified here:
LE32@107 == src mtime(true)== build time(false);varlen@100 == build time(delta 0). Confirmed independently in a second tree,where
LE32@107likewise equals that tree's source mtime and stays constantacross builds. So the doc quotes no offset and gives the reproduction —
make clean && maketwice and compare — which holds whatever the encoding. The conclusion is untouched by all of it:tls13.ohad adifferent hash on every one of ~8 builds today;
c64-https.prgwasdb31111031e2…on every one.Blast radius
Anyone building
BACKEND=ip65from a clean checkout, including thehardware-free VICE ip65 rig.
BACKEND=uciis unaffected (no blob). Cost isalready measured, not hypothetical: two lanes in this session lost time to it,
and one propagated a wrong workaround before it was corrected.
Not fixed here, flagged for whoever next passes through:
src/constants.asmand
src/entropy.asmare tracked but dead — the build globssrc/*.s— andtools/test_http.py:87still explains its expectations in terms of "what ACMEactually emitted". Neither misleads the way this one did, so they did not seem
worth widening the diff for.