← board

Release packaging, reproducibility manifest, and release.sh

Model — a toolchain release, not an app release

PXX is a compiler (toolchain), so it ships like gcc/FPC/rustup, not like a desktop app. AppImage/snap/flatpak solve GUI runtime dependency-hell and add sandboxing that fights a tool whose job is reading/writing arbitrary files — explicitly not used here. The PXX compiler binary is freestanding-ish (own RTL, raw syscalls), so it needs no bundling.

What a release contains

  1. Source (the tag tarball — GitHub auto-generates it). The RTL/PCL must ship as source: PXX compiles lib/rtl + lib/pcl from source on every build ("own RTL from scratch"; no precompiled .a). So source isn't bloat — it's the floor, and tinkerers get it for free.
  2. Host binaries for the 4 Linux host arches: x86_64, i386, aarch64, arm32 (all produced byte-identical by make cross-bootstrap). xtensa/riscv32 are emit-only targets, not hosts — no "xtensa compiler binary".
  3. setup.sh: detect uname -m, symlink the native binary to pxx on PATH. Nothing else — the compiler already resolves lib/rtl, lib/pcl, builtin/ relative to its own binary via ExeDir (<root>/compiler/ -> ../lib/..., parser.inc ~10752; compiler.pas ~336/349). Preserving the tree layout makes an unpacked release work from anywhere — this is the linchpin; keep it solid.

Install layout (FPC-shaped, devs recognize it)

pxx-vNN/
  bin/  pxx-x86_64  pxx-aarch64  pxx-i386  pxx-arm32
  lib/  rtl  pcl                 # source, ExeDir-resolved
  builtin/                       # frozen builtin RTL
  examples/   README   setup.sh   MANIFEST.sha256

Reproducibility — the ultimate post-install / bringup check

Codegen output is a pure function of (source, target), independent of host (proven by the cross-bootstrap triple-stage: native emits --target=T, that binary under QEMU emits --target=T, byte-identical). Consequences:

pxx --selfcheck / make verify-install — two distinct checks

  1. Self-fixedpoint, native (pxx -> gen1 -> gen2, cmp gen1 gen2): determinism on this silicon/kernel. Needs no manifest, always runs (HEAD or tag). This is the hardware/kernel/CPU probe (catches non-determinism, missing instructions, the suspected-bit-flip canary). Also the new-hardware bringup test (point it at a new arch/kernel/QEMU and it tells you instantly if codegen + ABI are sound).
  2. Reproduce-all-targets vs manifest: validates the full release reproducibility. Only meaningful when source == the tagged source. Compare embedded version / git describe to the manifest tag: match -> run (hard); mismatch (HEAD ahead) -> print "no release manifest for this source; determinism-only" and skip, don't fail. (xtensa/riscv32: reproduction verified by emit-hash; execution only via QEMU/hardware, since they don't self-host.)

Asymmetry is correct: determinism = property of (host, source) — always testable; reproducibility = property of (source == tagged source) — tag-only.

nightly != release

Versioning — semver tags, not a custom string

Tags are vMAJOR.MINOR.PATCH with the prerelease channel in the suffix: v0.1.0-alpha.1 -> -beta.1 -> -rc.1 -> v0.1.0, GitHub --prerelease until the bare v0.1.0. 0.x already means "unstable, expect breakage" — don't double-encode with preview/beta words (and 0.01 doesn't parse/sort). Semver sorts correctly and is parseable by the tag tooling / git describe selfcheck — a custom string like preview-beta-0.01 is not. Pre-1.0 semantics (loose): MAJOR = language/emitted-ABI break, MINOR = features/new targets, PATCH = bugfix. Keep the internal pin counter (stable_linux_amd64 VERSION, currently v32) separate from the public semver — pin = dev checkpoint, tag = release.

The channels (alpha/beta/rc) are just ordered labels on a monotonically increasing version; semver already orders them (alpha < beta < rc < stable), so they sort and the publish tool can enforce "strictly greater than last". A per-release codename (decorative) rides in the GitHub Release title/notes, not in the tag (tag stays pure semver). tools/release.sh drives the bump + codename interactively — see its section below.

GitHub mechanics — branch push = CI, tag = release

One x86_64 runner cross-builds all targets (host-independent codegen → no arch matrix needed; QEMU only if CI should also run each arch's self-fixedpoint).

Cutting a release = git tag v0.1.0 && git push origin v0.1.0 (fires release.yml). The tag freezes {source, manifest} — the invariant enforced by Git.

Deliverables

tools/release.sh — dry-run by default, explicit --publish

Avoid littering git/GitHub with test releases: default is a no-side-effect rehearsal.

Interactive version + codename (idiot-proof — the maintainer never hand-types a tag)

release.sh is the maintainer's publish tool; idiot-proof its own process (no malformed tags, no version regression, no skipped channel). Never accept a free-typed version — compute and confirm from a menu.

Full-suite gate + explicit xfail registry

A release runs the entire verification surface and blocks on any failure, period — unless that failure is explicitly registered as expected/WIP:

Non-goals

Now vs later (not building a release yet)

Validation — full dry-run pass (2026-06-21)

The whole release path was exercised end-to-end (no publish). Verified green:

Bugs fixed (735a8f2, 96333d2): last_tag fed a non-semver git describe tag into sv_parse; --build-for codename grep aborted under set -euo pipefail (missing || true); the demos FAIL-parser matched the dashboard caption line; a same-statement local tag=.. d="..$tag" left $tag unbound under set -u; RELEASE.md referenced a nonexistent examples/hello. Plus codename Babbage and the corrected chess xfail reason.

Remaining / decisions for the maintainer:

  1. First-prerelease from the menu — FIXED. Added npm-style prepatch / preminor / premajor ops (menu picks 7–9): bump the core AND open a prerelease channel at .1. Channel chosen via a follow-up prompt, or as a suffix on the env override (RELEASE_BUMP=preminor:beta). Wired through the previously-unused startchan param. compute_next v0.0.0 preminor beta -> v0.1.0-beta.1 (covered by --selftest); the menu path emits the same.
  2. Pinned-vs-shipped chess message. The demos gate runs the pinned stable (v32, still rejects local typed consts); the shipped binary is HEAD (advances to eng.Free). The xfail reason documents shipped behavior; a make pin would align the gate's printed message. Non-blocking.
  3. tools/progress.sh ships in the bundle but devdocs/progress/ is export-ignored, so it no-ops (prints "no .../devdocs/progress", exits 0) in a release clone. Left in intentionally (part of complete source; Makefile check target still references it). Harmless.
  4. Codename ledger (CODENAMES) is created only at publish; suggest_codename counts its lines. The hand-set codename in the release-notes file must match what publish will append — consistent for beta.1 (Babbage = pool index 0). No change.

CLOSED via triage (2026-06-30)

Validated end-to-end (tools/release.sh dry-run + --publish; dist/ holds reproducible pxx-v0.1.0[-beta.1] bundles + manifests; Makefile selfcheck; bit-for-bit reproducibility green 2026-06-21). Remaining items are non-blocking maintainer decisions. Closing.