Builds and publishes the prebuilt tebako CPython runtime packages
(tebako-runtime-<tebako-version>-<python-version>-<triplet>[.exe]) that
the tebako bootstrap/shim resolves at press/run time. Modeled on
tebako-runtime-ruby.
Status: live. Releases (v*) publish the per-platform runtime
packages (latest line: v0.1.x); consumers pin them by contract.yml +
the registry. The full chain — fetch/verify → configure/make → driver
link → env-image pack → packaging → provenance gate → boot smoke — is
implemented in build/lib/tebako_python_builder/ and wired into CI
(build-*.yml × 4 over _build-platform.yml, the ruby factory's
coordinator shape; publish.yml carries the release machinery via
scripts/upload_release.rb). The matrix is green end to end —
linux-gnu / linux-musl / macos (x86_64 + arm64) and windows-ucrt64. The
boot smoke asserts imports of json/ssl/zlib off the mounted image, the
dlopen extension path, and the TEBAKO_MOUNT_ROOT 65/78 parity cases
on POSIX, and the bare + named-error trio (65/69/78) on windows, plus
the symbol-provenance gate everywhere.
Python runtimes ship the spec-17 driver linked into the python3
executable (the ruby pattern), not the java-style wrapper exe
(spec 29).
Rationale:
- The wrapper pattern exists for runtimes that arrive as third-party binaries we cannot relink (openjdk). CPython here is source-built by us — tamatebako/python publishes the verified source tarballs and this factory compiles them — so the link step is ours, and the driver goes inside the exe: one process, no wrapper layer, no second argv/env contract.
- The boot/exit-code contract is then literally the ruby driver's:
TEBAKO_RUNTIME_IMAGEhandoff,TEBAKO_MOUNT_ROOTredirect with exit 65 (malformed) / 78 (ungranted), the same named errors. Parity is assertable in tests against the ruby driver's contract suite where the cases overlap. - CPython is relocatable via
PYTHONHOME/PYTHONPATH— the relocation probe built the pinned lines and verified a moved tree resolves stdlib + ssl against the relocated prefix with zero patches. The fs TU setsPYTHONHOMEfrom the driver's effective mount root at boot; the interpreter itself is never patched. - Contract 2 from day one: this factory has no contract-1 era (no merged
env+app images, no embedded incbin image). The env image is always the
standalone
.tfsthe driver mounts fromTEBAKO_RUNTIME_IMAGE.
The architectural difference from ruby: tamatebako/python's zero-patch
contract means CPython's own libc file IO cannot see the driver's
mounts (ruby carries patch literals that reroute its IO; python does
not). Visibility is spec 22's tier-1 preload interposition, composed by
build/resources/tebako_python_main.c (the fs TU, which replaces
Programs/python.o in the interpreter link) in two process
incarnations:
- The first incarnation boots the driver in-process
(
tebako_driver_boot): mounts the env image fromTEBAKO_RUNTIME_IMAGEand every--tebako-imagepayload triple, verifies the image's layout card (lib/tebako/layout.yaml), applies the jail, rewrites argv to the resolved entry, and arms the preload-shim injection env from the layout grant. A preload library binds only at exec, so — - when the boot mounted anything,
mainre-execs itself with the rewritten argv and the driver-armed env (LD_PRELOAD/DYLD_INSERT_LIBRARIES+TEBAKO_PRELOAD_SHIM+TEBAKO_TFS_MOUNTS, sentinelTEBAKO_PYTHON_BOOTED). The shim's constructor re-mounts the serialized mount table in the child; the child skips the boot and runs the interpreter, whose libc IO the shim now serves from the VFS.
PYTHONHOME is set from tebako_mount_point() (the driver's effective
root — a TEBAKO_MOUNT_ROOT override already applied) whenever an env
image is named; an inherited PYTHONPATH rides along (the ruby runtime's
RUBYLIB parity). A bare exe (no TEBAKO_RUNTIME_IMAGE) is dev
mode: PYTHONHOME stays untouched and getpath resolves from the exe's
own path — the stdlib is image-resident, so a bare exe is NOT a working
interpreter (the boot smoke asserts this contract honestly: non-zero
exit, the driver's warning on stderr).
Named exits are the driver's, surfaced unmodified: 65
(TEBAKO_MOUNT_ROOT malformed), 78 (ungranted override, or an env image
with no preload_shim grant — an unpatched CPython would boot blind),
74 (re-exec failure), 69 (windows with any mount — see the windows
boundary below).
Modules/Setup.local pins the deterministic contract
(PythonBuild::STATIC_MODULES / DISABLED_MODULES):
- Static, in the exe:
_ssl,_hashlib(both against static openssl),zlib,binascii(static zlib) — their deps bind via the link unit's Mlibs rewrites. - Disabled:
_bz2,_lzma,_sqlite3,_ctypes,_ctypes_test,readline,_curses,_curses_panel,_gdbm,_dbm,nis,_tkinter,_uuid— the host-asymmetric extensions. Their python sides (test/,idlelib,tkinter/turtle) are pruned from the image: the runtime answers "no such module" by absence, never by a broken import. - Everything else configure detects rides the image as dynamic
extensions in
lib-dynload, mounted with the stdlib.
The env image ships the stdlib plus a declarative site-packages
whitelist (build/site-packages.yml; v1 keeps pip only, with its
dist-info — pip's importlib.metadata self-check reads it). pip reaches
the staged site-packages via make install's ensurepip on POSIX and via
an explicit bundled-wheel placement on msys (the install's --root
rebase drive-strips the build-tree prefix — PythonBuild#place_pip).
Anything else the install staged is pruned at image assembly.
The pip form is python3 -m pip. The image's bin/ directory is
pruned wholesale: the ensurepip console scripts' shebangs spell the
build prefix — dead links in any mounted layout — so no pip3 script
ships. This matches the ruby factory's no-entrypoints shape: the L1
manifest declares no entrypoints and the release shard names the
interpreter by convention.
The windows leg (ucrt64, --enable-shared — issue 40's answer: on PE a
loadable module cannot carry undefined symbols, so the extensions link
libpython<X.Y>.dll; the ruby factory ships the same shape) is the
driver-contract surface only: the exe boots the driver, answers
--tebako-image/TEBAKO_RUNTIME_IMAGE with the same named errors, and
runs the interpreter only when nothing was mounted (bare/dev mode).
There is no preload tier on windows — with any mount the fs TU exits
69 with a named error. The shared build ships
the DLL as a <package>.dll release facet (the release manifest's
dll.install_as names the PE spelling the store materializes beside
the exe), and the mingw support set (libgcc/libwinpthread) is
statically linked into the runtime's own PE modules — a bare machine
installs nothing.
The publish layout mirrors tebako-runtime-ruby's current shape (its issue-139 layout from day one — no legacy monolith-only mode):
tebako-runtime-<tebako>-<python>-<triplet>[.exe]— the interpreter:python3with the spec-17 driver linked in (the fs TU asmain).tebako-runtime-<tebako>-<python>-<triplet>.tfs— the env image: stdlib + lib-dynload + the whitelisted site-packages + the preload shim (POSIX — REQUIRED here: the unpatched interpreter cannot read its own mounted image without it, so a missing shim is a hard build error, never the ruby factory's degrade) + the layout card (lib/tebako/layout.yaml: era 2,mount_root_overridegranted,preload_shimpath) + the L1 payload manifest (__tpkg__/manifest.yaml). Packed bytfs mkimage(the limnifs writer — the current default format; a build-time factory tool, never a runtime dependency of the shipped package).<package>.manifest.json— the package shard: the manifest entry (tebako_version/contract_era/contract_version/python_version/platform/filename/sha256/size_bytes/mount_root/image_layout/built_from), plus the additiveimagekey and, when the sidecars are present,abi(the build's own EXT-SUFFIX stem — exactly the string native-extension wheels pin) anddll(windows shared builds only; see the windows boundary).<package>.abi/<package>.contract.yaml— the builder-emitted sidecars the shard folds in (the era-2 provenance card:contract_era/mount_root/image_layout/built_from).<asset>.sha256— the checksum sidecar next to every payload asset, in the tebako store's trust-anchor shape ("<sha256> <filename>\n").- Derived conveniences: the monolithic
manifest.jsonandSHA256SUMSare regenerated from the shards + the asset listing by one finalize pass after every platform lands — never read-modify-written per platform.
tamatebako/python release tamatebako/tebako release
tfs-python-<v>-src.tar.gz link-unit-<ver>-<pid>.tar.gz
+ SHA256SUMS (trust anchor) (spec-17 driver + tfs + closure)
| |
v v
fetch + verify ────────► link the driver into python3
configure && make (relocatable; per-triplet toolchain)
|
v
assemble env layout (stdlib + lib-dynload + pip + shim
+ layout card + L1 manifest)
pack <package>.tfs (tfs mkimage — limnifs writer)
|
v
provenance gate (ci/check_symbol_provenance.sh)
boot smoke (tools/boot_smoke: imports off the mounted image,
dlopen ext path, TEBAKO_MOUNT_ROOT 65/78 parity, bare-exe
contract) → publish (scripts/upload_release.rb)
Both inputs are published release artifacts, consumed by pin from
contract.yml — never source checkouts of sibling repos (prebuilt
artifacts flow downward).
Same grammar as the ruby factory. Seven legs: every (python × env) cross
of the catalog under the dispatch filters
(python_filter=full|tidy|catalog|<list>, platform=all|windows| linux-gnu|linux-musl|macos, arch_filter=all|x86_64|arm64).
| os | arch | host | container (ghcr.io/tamatebako/…) | link-unit pid |
|---|---|---|---|---|
| linux-gnu | x86_64 | ubuntu-22.04 | tpkg-builder-x86_64-linux-gnu | linux-gnu-x86_64 |
| linux-gnu | arm64 | ubuntu-22.04-arm | tpkg-builder-aarch64-linux-gnu | linux-gnu-arm64 |
| linux-musl | x86_64 | ubuntu-22.04 | tpkg-builder-x86_64-linux-musl | linux-musl-x86_64 |
| linux-musl | arm64 | ubuntu-22.04-arm | tpkg-builder-aarch64-linux-musl | linux-musl-arm64 |
| macos | x86_64 | macos-15-intel | — (runner-native) | macos-x86_64 |
| macos | arm64 | macos-14 | — (runner-native) | macos-arm64 |
| windows (ucrt64) | x86_64 | windows-2022 | — (runner-native) | x86_64-windows-gnu |
Containers come from
tebako-ci-containers
(the tpkg-builder-<triplet> family; windows/macOS are runner-native by
design). The linux legs docker-run the image per step (the
alpine-based musl image cannot host node actions, so no job-level
container:). The version catalog lives in contract.yml (the SSOT),
the env vocabulary in .github/matrix.json — versions, tags, and SHAs
never appear in workflow YAML.
contract_version: 2— the bootstrap ↔ runtime contract (spec 17 grammar). Floored at 2 by the schema: no contract-1 era exists here. Bump rules mirror the ruby factory's (+1 in lockstep with the compiled-inTEBAKO_CONTRACT_VERSION, same commit — enforced by the driver-source parity arm ofscripts/check_contract.rb).container_version: "v1"— the tpkg-builder tag line. Per-leg digest pinning is a follow-up.link_unit_release: "v2.3.2"— the tamatebako/tebako release whose prebuilt link unit the legs consume (the v2.3 arc: spec-30 dispatch, shim routing, the spec-29 wrapper driver, the dup-class interpose + the aarch64 dup2 repair; NEVER pin v2.3.1 — it published without the linux-arm64 units).source_release: "v0.2.0"— the tamatebako/python source release pin (v0.1.0's set plus 3.11.16).python:— the version catalog (catalog/full/tidysets), mirroring tamatebako/python'sversions.yml. Flavor lines ride the version string, never a new selector axis (spec 28 §8's truffleruby native/jvm precedent):3.13.15-jit/3.14.7-jitbuild the same pristine source with--enable-experimental-jit(PEP 744). The exact-major LLVM toolchain + host python ≥ 3.11 the JIT's stencil regen needs are build-time-only — the shipped runtime gains no system dependency. CI provisions them per leg (ci/provision_jit_toolchain.shin the containers,brew install llvm@Non macos) from the matrix'sjit_llvmplan value, and the build gate re-verifies the major against the extracted source'sTools/jit/_llvm.py(a drifted plan table is a named build error, exit 113). Flavor lines sit incatalog/full, nevertidy(the smoke set stays minimal). The jit legs are linux-gnu- macos only: CPython's JIT target whitelist (
Tools/jit/_targets.py) rejects*-linux-muslupstream, so the matrix engine skips musl jit legs with a loud note (a musl enablement patch would belong to tamatebako/python, not this factory).
- macos only: CPython's JIT target whitelist (
VERSION— the package version: package names and the release tag follow it (v$(cat VERSION)).0.0.0was the never-published placeholder; the real line opened at 0.1.0.contract.yml+schema/— the pins and the version catalog, and their JSON Schema;scripts/check_contract.rbvalidates (CI), including the driver-source parity arm (contract.yml ↔ the tebako driver's compiled-in contract version).scripts/versions— emits the catalog / resolves the dispatch filter grammar / reads the pins (contract.yml is the SSOT).scripts/compute_matrix.rb— the matrix engine (--format matrix|env|pythons): catalog × env vocabulary under the dispatch filters → the leg matrix (withhost_id, andjit_llvmfor the flavor lines), the env/python expectation rowspublish.ymllater asserts.scripts/upload_release.rb— the release upload/finalize port: sidecars, shards, the idempotent re-upload skip, the FINALIZE_ONLY pass.publish.yml's release job drives it per platform with theEXPECTED_ENV_MATRIX/EXPECTED_PYTHON_MATRIXrows.tools/build_runtime— the build entry point (fetch → verify → build → link → pack → package → sidecars).tools/boot_smoke— the post-build acceptance gate (8 scenarios, 16 checks): stdlib + ssl + zlib imports off the mounted image, the dlopen extension path, theTEBAKO_MOUNT_ROOT65/78 parity cases, the bare-exe dev-mode contract, and thejitflavor probe (a jit line reports the JIT capability underPYTHON_JIT=1, a plain line reports its absence). NeedsTEBAKO_TFS(the tfs CLI, for the mount probe) and the runtime-packages tree.build/lib/tebako_python_builder/— the build model (the tebako-runtime-rubybuild/libport): Contract, Platform, PythonVersion, SourceFetcher, LinkUnit, Mlibs, PythonBuild, ImageBuilder, ImageManifest, ImagePackager, TfsTool, Builder.build/resources/tebako_python_main.c— the fs TU template (the interpreter's realmain; see the re-exec section).build/site-packages.yml— the declarative site-packages whitelist.ci/check_symbol_provenance.sh— the symbol-provenance gate: the exe definestebako_driver_boot/tebako_mount_point/tebako_driver_contract_version/main, andmainforwards totebako_driver_boot.ci/provision_jit_toolchain.sh— the jit legs' per-leg toolchain provisioning inside the tpkg-builder containers (the image's own apt sources first, apt.llvm.org only as fallback, + a pinned, sha256-verified python-build-standalone host python on gnu — deadsnakes' focal dist is emptied; apk on musl; dispatches on the bakedTPKB_FAMILY)..github/workflows/_build-platform.yml— the reusable per-platform build leg (workflow_call): compute → contract check → matrix build → provenance → boot smoke → artifact upload..github/workflows/build-{linux-gnu,linux-musl,macos,windows}.yml— the four thin triggers (push main/PR/dispatch,python_filter/arch_filter)..github/workflows/publish.yml— the release coordinator (workflow_dispatch only; the ruby factory's shape — outputs flow from the four reusable calls, one release job assembles per-platform). Deliberate deviations:publishdefaults to false; norepository_dispatchtrigger yet..github/workflows/lint.yml— the static gate: schema validation, catalog/matrix resolution, YAML/JSON parse checks, actionlint, theruby -csweep.Brewfile— macOS host build dependencies (CI).
- The boot-contract parity suite (
spec/) — exit-code parity against the ruby driver's contract suite where the cases overlap (the boot smoke covers the local acceptance; the cross-runtimespec/port is a separate change). - Container digest pinning.
- Build-graph diff-awareness for the four build triggers (they fan out 21 legs per event today; the ruby factory's plan job computes the diff — the headers note the follow-up).