Skip to content

feat(package): four PRG variants, per-variant D64s, and a dependency-free single-file listener - #96

Merged
JC-000 merged 1 commit into
masterfrom
feat/release-packaging
Aug 14, 2026
Merged

feat(package): four PRG variants, per-variant D64s, and a dependency-free single-file listener#96
JC-000 merged 1 commit into
masterfrom
feat/release-packaging

Conversation

@JC-000

Copy link
Copy Markdown
Owner

Lane P of the release-prep team: the packaging machinery for a tagged release
of prebuilt artifacts, aimed at people who never want to build anything.

No libs/, cfg/*.cfg or src/ changes — the two library-bump lanes are
moving those underneath this. Everything here is data-driven and re-runnable:
sizes, hashes, git HEAD and submodule pins are read at run time, and the
variant matrix is one line per shipped PRG in tools/package/_common.sh, so
re-running make package after their bumps regenerates every artifact with
zero edits.

What ships

Four PRGs — every combination of backend x crypto profile. make package
previously built the two UCI images only:

c64-https-uci-reu.prg make BACKEND=uci
c64-https-uci-onchip.prg make BACKEND=uci USE_NISTCURVES_ONCHIP=1
c64-https-ip65-reu.prg make BACKEND=ip65
c64-https-ip65-onchip.prg make BACKEND=ip65 USE_NISTCURVES_ONCHIP=1

ip65 was excluded on a link failure that the #68 refit closed. It is the only
artifact that serves a stock C64 + RR-Net cartridge, and ip65-onchip is the
only image a bone-stock machine with no REU can run end to end. Getting those
into the release is the point of the lane.

Six disk images: one per variant, plus one per backend carrying both of that
backend's profiles.

c64-https-listener.py — one self-extracting file, no dependencies.

MANIFEST.txt — sizes, sha256, git HEAD, submodule pins, and a "which one do
I want?" section keyed on the two questions that actually decide it.

Two decisions that needed making

There is no all-in-one D64, and that is arithmetic rather than preference.
A .d64 has 664 free blocks = 168,656 usable bytes; the four PRGs total 868
blocks (~220 KB). Each backend's pair does fit — UCI 496 blocks, ip65 372 —
which makes the per-backend disk the largest bundle a real 1541 can hold, so
that is where the line got drawn. File names are shared between a variant's
single disk and its backend disk, so a user only ever learns one name, and
every single-variant disk works with LOAD"*",8,1.

The listener's cryptography dependency is gone, not worked around. The
brief flagged the honest constraint: cryptography is a compiled extension,
wheels are per-platform and per-Python-version, so it cannot be embedded in a
portable source bundle. The three suggested outs were embed-and-error,
vendor a pure-Python TLS stack, or bootstrap a venv on first run.

None were taken, because the premise was wrong. cryptography was never
needed for TLS — the server has always been stdlib ssl. It was needed for
exactly one thing: minting the self-signed P-256 certificate. So
gen_certs.py now does that in pure Python (~200 lines: P-256 point
arithmetic, a minimal DER encoder, ECDSA-SHA256), and the bundle has no
third-party dependency at all
. Nothing to pip install, no venv, no network.
openssl x509 parses the result and openssl verify accepts the
self-signature.

What remains is a property of the interpreter, not of any installable
package: an ssl module with TLS 1.3, i.e. OpenSSL 1.1.1+. macOS's
/usr/bin/python3 is LibreSSL 2.8.3 and cannot serve this client at any
price. That is detected at startup, reported in one line rather than a
traceback (--debug puts the traceback back), and — per the brief — stated in
MANIFEST.txt so nobody discovers it by running the thing:

ERROR: this Python's ssl module has no TLS 1.3 support (LibreSSL 2.8.3).
The C64 client requires TLS 1.3.
Use a Python linked against OpenSSL 1.1.1+ / 3.x …

The cert generator deliberately emits the same extension set as the old
cryptography path (subjectAltName only, non-critical). The C64 parses this
with a hand-written 6502 DER walker, and that is the shape it has been
validated against; adding basicConstraints/keyUsage "for correctness" without
a hardware e2e would be a silent change to a tested interface.

Acceptance — make package-verify, 18/18

=== 1. PRG byte-reproducibility (second build from clean) ===
[PASS] uci-reu reproduces — 741f0e8cd99470a2… vs 741f0e8cd99470a2…
[PASS] uci-onchip reproduces — fe959dad5edc1dc7… vs fe959dad5edc1dc7…
[PASS] ip65-reu reproduces — 417c70859411e8b5… vs 417c70859411e8b5…
[PASS] ip65-onchip reproduces — 573561dab4af9e37… vs 573561dab4af9e37…
=== 2a. D64 contents (c1541 read-back, byte-compare) ===
[PASS] c64-https-ip65-onchip.d64 carries the built PRGs — ip65-noreu == c64-https-ip65-onchip.prg
[PASS] c64-https-ip65-reu.d64 carries the built PRGs — ip65-reu == c64-https-ip65-reu.prg
[PASS] c64-https-ip65.d64 carries the built PRGs — ip65-reu == c64-https-ip65-reu.prg; ip65-noreu == c64-https-ip65-onchip.prg
[PASS] c64-https-uci-onchip.d64 carries the built PRGs — uci-noreu == c64-https-uci-onchip.prg
[PASS] c64-https-uci-reu.d64 carries the built PRGs — uci-reu == c64-https-uci-reu.prg
[PASS] c64-https-uci.d64 carries the built PRGs — uci-reu == c64-https-uci-reu.prg; uci-noreu == c64-https-uci-onchip.prg
=== 2b. D64 boots to the banner in VICE ===
[PASS] c64-https-ip65-onchip.d64 boots to the banner — 'C64-HTTPS CLIENT V0.1' at 5s, 'RR-NET (CS8900A) ETHERNET' at 5s
[PASS] c64-https-ip65-reu.d64 boots to the banner — 'C64-HTTPS CLIENT V0.1' at 5s, 'RR-NET (CS8900A) ETHERNET' at 5s
[PASS] c64-https-ip65.d64 boots to the banner — 'C64-HTTPS CLIENT V0.1' at 5s, 'RR-NET (CS8900A) ETHERNET' at 5s
[PASS] c64-https-uci-onchip.d64 boots to the banner — 'C64-HTTPS CLIENT V0.1' at 5s, 'UCI NETWORKING' at 5s
[PASS] c64-https-uci-reu.d64 boots to the banner — 'C64-HTTPS CLIENT V0.1' at 5s, 'UCI NETWORKING' at 5s
[PASS] c64-https-uci.d64 boots to the banner — 'C64-HTTPS CLIENT V0.1' at 5s, 'UCI NETWORKING' at 5s
=== 3. Single-file listener selftest (clean temp dir, no venv) ===
== c64-https listener selftest ==
cert/key missing (…/certs/server.pem / …/certs/server.key); generating...
CN = www.foo.bar
SAN = foo.bar, www.foo.bar
key = ECDSA P-256 (secp256r1), sig = ecdsa-with-SHA256
(generated with the Python stdlib only — no 'cryptography')
server listening on 127.0.0.1:52229
connection from 127.0.0.1:52230
[stdlib client] client handshake: TLSv1.3 / TLS_AES_256_GCM_SHA384
request (37 B): b'GET / HTTP/1.1\r\nHost: www.foo.bar\r\n\r\n'
served 60 B (body 'HELLO FROM TLS SERVER')
connection from 127.0.0.1:52231
handshake OK: TLSv1.3 / TLS_CHACHA20_POLY1305_SHA256
served 60 B (body 'HELLO FROM TLS SERVER')
[PASS] stdlib client: TLS 1.3 negotiated
[PASS] stdlib client: server saw a request
[PASS] stdlib client: canonical response received
[PASS] C64 suite TLS_CHACHA20_POLY1305_SHA256 negotiates and gets the canonical body
SELFTEST PASSED (4 checks)
[PASS] listener selftest — exit 0
[PASS] selftest leaves no droppings — clean
============================================================
18/18 checks passed
RELEASE ARTIFACTS VERIFIED

Sizes and hashes at this commit, all clean-build:

c64-https-uci-reu.prg 62977 B 741f0e8cd99470a2d2f76b21d0603235eb625cded32f9ad80b9f7c93f7462e70
c64-https-uci-onchip.prg 62977 B fe959dad5edc1dc7f03f024541fb84e7095cb3434bca4b296625c3cd51de9df9
c64-https-ip65-reu.prg 47105 B 417c70859411e8b5b9d2ef7a89575c2122d263cb0a622b8fc336693a907f2444
c64-https-ip65-onchip.prg 47105 B 573561dab4af9e3776baa11e29b61702e89a365645b6f8a3d7f83ed02f2e861b

ip65-reu matches the 417c7085… recorded in CLAUDE.md for master 2ceb5b1
an independent cross-check that the ip65 build is the intended one. The ip65
blob rebuilt to the documented 6,951 B / cf1a5ff7….

Object hashes are deliberately not compared anywhere: ca65 stamps wall-clock
time into every .o header, so nobody can reproduce their own object hash
twice. ld65 does not propagate that field, which is exactly what makes the
PRG comparable.

Two traps found, both now in CLAUDE.md

Booting a .d64 in VICE needs -trapdevice8 +drive8truedrive. Under true
drive emulation the ~250-block serial load of a 63 KB PRG does not finish
inside any budget worth waiting for, and the symptom — a screen frozen on
LOADING — reads as a corrupt image rather than a slow one. With the KERNAL
load traps the banner lands at ~5 s. Image contents are byte-compared
separately, so nothing is lost by loading fast.

The menu is not a usable VICE pass criterion. A 900 s probe on
c64-https-uci-reu.d64 saw the banner at 6.0 s and never reached Q=QUIT:
boot's table init runs at emulated 1 MHz because VICE 3.10 has no usable warp.
The banner is the verdict; whether the menu appeared is reported, not
asserted. (The ip65 images also print NETWORK INIT FAILED with no network
attached — expected, and explicitly not part of the criteria.)

Also corrected

The packaging section's REU-vs-onchip guidance still said the crossover was
~7 MHz. The measured figure is ~18 MHz — on a U64E the sign flips between
the 16 and 20 MHz CPU-speed settings, and the five-point fits put it at
17.9 MHz. MANIFEST.txt and the new README section use ~18 MHz and explain
the mechanism (the REU profile carries a DMA-anchored wall-clock floor that
turbo cannot touch; the on-chip profile has none).

For the supervisor

make package PACKAGE_PYTHON=<a TLS-1.3-capable python3>
make package-verify PACKAGE_PYTHON=<same>

PACKAGE_PYTHON matters only for the listener build and its selftest; the
macOS system python3 will fail the selftest with the LibreSSL message above.
SKIP_REBUILD=1 / SKIP_VICE=1 / SKIP_LISTENER=1 narrow the gate if you
need a fast re-run.

🤖 Generated with Claude Code

dependency-free single-file listener
The release is aimed at people who never want to build anything, so
`make package` now produces every combination of backend x crypto profile
rather than the two UCI images it built before:
c64-https-uci-reu.prg make BACKEND=uci
c64-https-uci-onchip.prg make BACKEND=uci USE_NISTCURVES_ONCHIP=1
c64-https-ip65-reu.prg make BACKEND=ip65
c64-https-ip65-onchip.prg make BACKEND=ip65 USE_NISTCURVES_ONCHIP=1
ip65 was previously excluded on a link failure that the #68 refit closed.
It is the only artifact that serves a stock C64 + RR-Net cartridge, and
ip65-onchip is the only image a bone-stock machine with no REU can run at
all — that gap was the point of this change.
Disk images: one per variant plus one per backend. There is deliberately
no all-in-one image, and that is arithmetic rather than preference — the
four PRGs total 868 blocks against a .d64's 664 free. Each backend's pair
does fit (UCI 496, ip65 372).
The listener stops needing `cryptography`. That library is a compiled
extension and cannot be embedded in a portable source bundle, which is
what blocked a single-file build — but it was never needed for TLS. The
server was always stdlib `ssl`; the library was used for exactly one
thing, minting the self-signed P-256 cert. gen_certs.py now does that in
pure Python (P-256 point arithmetic, a minimal DER encoder,
ECDSA-SHA256), so dist/c64-https-listener.py is one self-extracting file
with no third-party dependency: no pip, no venv, no network.
What remains is a property of the interpreter, not an installable
package: an ssl module with TLS 1.3. macOS's /usr/bin/python3 is LibreSSL
2.8.3 and cannot serve this client at any price. That is detected at
startup and reported in one line rather than a traceback, and it is
stated in MANIFEST.txt so nobody discovers it by running the thing.
Packaging is now data-driven: tools/package/_common.sh holds the variant
matrix, one line per shipped PRG, and build_prgs.sh / build_d64.sh /
write_manifest.sh all derive from it. Nothing is version-specific —
sizes, hashes, git HEAD and submodule pins are read at run time — so
re-running `make package` after a library bump regenerates everything
with zero edits.
`make package-verify` is the new acceptance gate: it rebuilds every
variant and compares PRG hashes (object hashes are not evidence, ca65
stamps build time into every .o), reads each PRG back out of its .d64
with c1541 and byte-compares, boots every image in VICE asserting the
banner, and runs the listener selftest.
Measured at this commit, all clean-build:
c64-https-uci-reu.prg 62977 B 741f0e8cd99470a2…
c64-https-uci-onchip.prg 62977 B fe959dad5edc1dc7…
c64-https-ip65-reu.prg 47105 B 417c70859411e8b5…
c64-https-ip65-onchip.prg 47105 B 573561dab4af9e37…
All four reproduce byte-for-byte on a second build from clean; all six
disk images boot to the correct backend banner in VICE; the listener
selftest passes 4/4 from a clean temp dir with no venv.
Booting a .d64 in VICE needs `-trapdevice8 +drive8truedrive`: under true
drive emulation the ~250-block load never completes in any sane budget
and the screen sits on LOADING, which reads as a corrupt image rather
than a slow one.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@JC-000