Builds and publishes the prebuilt tebako Ruby runtime packages
(tebako-runtime-<tebako-version>-<ruby-version>-<platform>) that the
tebako gem resolves at press/run time.
The build input is the pre-patched ruby source published by
tamatebako/ruby as the
tfs-ruby-<version>-src.tar.gz release assets (verified against the
release SHA256SUMS). The runtime links the prebuilt
libtfs package and embeds the
modern tebako_fs_* entry driver (vendored in build/src).
tools/build_runtime --ruby 3.3.7produces runtime-packages/tebako-runtime-$(cat VERSION)-3.3.7-<platform>
(see tools/build_runtime --help for options: output path, build prefix,
--src-release/--src-mirror overrides, --patchelf, --jobs).
Every build also packs the assembled runtime layout tree — the exact tree the v1 runtime executable embedded as its memfs image — as a standalone DwarFS image next to the executable:
runtime-packages/tebako-runtime-$(cat VERSION)-3.3.7-<platform>.tfs
Image era (item 30b, the default): the executable ships WITHOUT the
embedded incbin image — the standalone .tfs is the runtime's only
filesystem image, and the entry driver mounts the file
TEBAKO_RUNTIME_IMAGE names (an image-era tebako bootstrap sets it after
resolving the sha256-verified .tfs into the shared cache; the v1 handoff
is unchanged). Standalone use — including --tebako-extract — therefore
takes the variable explicitly:
TEBAKO_RUNTIME_IMAGE=$PWD/runtime-packages/tebako-runtime-$(cat VERSION)-3.3.7-<platform>.tfs \
runtime-packages/tebako-runtime-$(cat VERSION)-3.3.7-<platform> --tebako-extract layoutWithout the variable (and no embedded image) the driver fails startup with
a message naming the expected handoff; v1 runtimes — the published 0.15.9
executables, or anything built --embed-image — ignore the variable and
mount the embedded image exactly as before (graceful degradation, no
republish needed). The variable wins wherever it is set, so an embedded
build also mounts the named image.
The image is written by our own factory toolchain — the tfs CLI's
mkimage with its default format (limnifs), resolved in exactly one way:
- An explicit
--tfs PATHorTEBAKO_TFSsetting wins (fail-closed: a request that does not resolve is a named error, never a fallback). - Otherwise the builder fetches the CLI published with the pinned
tamatebako/tebako release (
contract.yml'slink_unit_release), verified against its published.sha256sidecar and cached per digest. An empty pin means a source-built driver — a named error asking for--tfs.
Either way it is a build-time factory tool, never a runtime dependency of
the shipped packages. --no-image skips the step (only meaningful with
--embed-image, the v1 shape — an image-era executable without the .tfs
cannot boot); --embed-image embeds the image into the executable instead
(v1 backward-compat shape: the variable is honored when set, the embedded
image otherwise). Both artifacts are
uploaded to the release; the package's <package>.manifest.json shard
carries the image as an additive image key
(filename/sha256/size_bytes), and each asset's <asset>.sha256
sidecar carries its checksum line (see "Release metadata", below).
Image layout (same as the embedded memfs tree): /lib/ruby/<api> (stdlib),
/lib/ruby/gems/<api> (gem home — spec 22 phase M2: the env image ships
NO tebako-runtime gem; the Rust driver covers the VFS),
/local/stub.rb (the runtime's compiled-in entry point), /bin (empty —
the ruby executable and the bin shims are stripped from the layout; the
interpreter is the outer driver executable that mounts the image, exactly
like the packaged-app path).
The windows-ucrt64 runtime is --enable-shared (the standard ruby-mingw
shape; every other platform stays --disable-shared): the ruby core and
the tebako closure link into x64-ucrt-ruby<ABI>.dll, and the runtime
executable imports it — a --disable-shared exe exports zero symbols and
ships no DLL, so no dynamically linked native extension could ever bind.
The memfs mount table exists exactly once per process, in the DLL; the
exe's driver reaches it through the DLL's tebako_fs_* exports.
The DLL is the third artifact of a windows package:
- it is built as
x64-ucrt-ruby<ABI>.dllin the ruby tree and staged as<runtime>.dll(the package name — two same-ABI legs share the PE name and would collide in the merged release workspace); - the store entry holds it next to the exe under the PE name
(
x64-ucrt-ruby<ABI>.dll): the PE loader resolves the exe's imports against the exe's own directory first, so interpreter and extensions bind without PATH games. The package shard's additivedllkey flows the mapping (filename= the asset,install_as= the PE name, plussha256/size_bytes; consumers ignoring the key keep working, same rule asimage), and<asset>.sha256carries the line; - the env image does NOT carry the DLL (
bin/is stripped from the layout — a DLL inside the read-only memfs would be dead weight: PE imports never resolve against it).
The leg proves the wiring before the artifacts leave CI: the windows
boot smoke materializes the PE-named copy next to the exe (the store
entry's shape) and loads racc's cparse.so from the image — a real
LoadLibrary bind of an in-image PE extension against the DLL
(spec/boot_smoke_spec.rb, the native_ext scenario).
The env vocabulary lives in .github/matrix.json (versions, runners,
arches — workflow YAML branches on matrix.env.arch only, never on a
duplicated list). Today: linux-gnu / linux-musl / macos (x86_64 + arm64),
windows-ucrt64 on windows-2022, and a windows/arm64 row
(windows-11-arm hosted runner + msys2's native clangarm64
environment, triple aarch64-w64-mingw32) that is wired but disabled
until the product side catches up. Two gates keep that honest, both in
the matrix planner (scripts/compute_matrix.rb, so every trigger path
— dispatch, pin bumps, validation, release runs — gets them):
- The artifact gate (every run). A build consumes the driver stack
from the pinned
link_unit_release(contract.yml) — and no tamatebako/tebako release publishes an arm64 windows link unit yet (link-unit-<version>-aarch64-windows-gnu.tar.gz; today's releases shipx86_64-windows-gnuonly). The planner skips the leg with a loud note naming the exact missing asset — the factory never builds the driver stack from source on arm64. When a product release publishes the unit, the leg builds automatically in build CI (push/PR/dispatch), natively on the arm64 runner, boot smoke included (the one expected first-run follow-up: the windows DLL-grammar checks below). - The publish gate (publish runs only). A green build still does
not serve: publish runs exclude windows/arm64 until the repository
variable
TEBAKO_SERVE_WINDOWS_ARM64istrue. The env and link-unit matrices and the coordinator's audit expectations all derive from the same planner walk, so a gated leg cannot half-serve a release.
The leg's package name rides the product's reserved release-asset
spelling (windows-ucrt-arm64 — the aarch64-windows-ucrt triplet,
which the product parses but rejects in served payload manifests until
the platform ships).
The bootstrap (released from tamatebako/tebako) and the runtime images published here version independently, so the protocol between them — the env vars passed down, the argv layout, the filesystem-image handoff — is versioned as an integer contract. Contract 1 pins today's semantics exactly; current behavior IS the contract.
Two representations, locked in agreement by CI
(scripts/check_contract_version.rb, run in the prepare job before the
matrix builds, and by spec/contract_spec.rb):
contract.yml(schema:schema/contract.schema.yml) — the release pipeline's single source of truth. The tebako-release gem's uploader emits it as an additivecontract_versionkey in every package's manifest entry (the<package>.manifest.jsonshard; consumers ignoring the key keep working, same rule asimage).TEBAKO_CONTRACT_VERSIONinbuild/src/tebako-main.cpp— the constant compiled into the runtime itself. The driver exports it as theTEBAKO_CONTRACT_VERSIONenvironment variable before the entry dispatch, so the packaged context (and any driver-stage tooling) can read the contract the runtime speaks.
Bump rules: any change to env/argv/handoff semantics bumps the integer
by exactly +1 in BOTH places, same commit — the agreement check fails the
build otherwise. The bootstrap side (negotiation, min_contract..max_contract
range, ContractMismatch named error) lives in the tebako-rs workspace; the
version → semantics changelog table is spec 06's.
The release's asset listing IS the package index. A build leg publishes and signs IN-LEG (spec 13 §2a's de-rendezvous): the leg that built a package uploads ONLY the write-once names it owns — its payload assets plus, for each package:
<asset>.sha256— the checksum sidecar next to every payload asset (exe,.tfs, windows.dll), in the tebako store's own trust-anchor shape ("<sha256> <filename>\n", spec 00 §8). This is the authority a resolver verifies a download against.<package>.manifest.json— the package's shard: exactly its manifest entry (ruby_version/platform/filename/sha256/size_bytes/mount_root/image_layout/built_from/contract_era/contract_version, plus the additiveabi/image/dllkeys). The non-derivable fields (the windows DLL'sinstall_as, the image sibling, the contract version) live here and nowhere else. On signing-enabled lines the entry also declares itssignatureblock ({keyid, asc}— the exact.ascasset name within the release, spec 09 §5), at the entry and facet levels.<asset>.asc— on signing-enabled lines, every served name (payload, sidecar, shard, contract card) carries its own detached OpenPGP signature, made in-leg from the fresh bytes (spec 09 §5's no-fold rule: nothing is ever "covered by" another artifact's signature).
Payload assets stay byte-immutable per name; metadata is DERIVABLE, so it replaces on drift (and a settled package's metadata describes the served bytes — the previous entry — never the fresh bytes that did not land). No leg ever read-modify-writes a shared file, so N legs publish concurrently with zero rendezvous; the release notes are written once at release creation and never rewritten.
The monolithic manifest.json and SHA256SUMS.txt are GONE as
release assets: both are derivable conveniences, computed consumer-side
from the shards + the asset listing. The
machine-readable resolution index is this repo's tpkg-registry.yaml
(spec 04 §2), rendered from the release's shards by the publish
coordinator's audit+registry job (tools/registry_update.rb) and landed
on main by bot PR — never hand-edited except status: withdrawn marks.
The same job AUDITS the whole matrix against the release (read-only) —
on signing-enabled lines it requires every served name's .asc.
BACKFILL_METADATA=true is the one-shot migration / repair pass for a
pre-shard release: it writes the missing sidecars from the listing's
server-computed digests (the served bytes' truth — a disagreement with
the monolith's record is named loudly and the digest wins) and the
missing shards from the monolith's entries (sha fields re-anchored to
the digests). It never touches a monolith or the notes.
VERSION— the package version: package names and the release tag follow it (v$(cat VERSION)), and the gem's RuntimeManager resolves packages by exactly this version. Bump it in lockstep with the tebako gem version the produced runtimes serve. (Not the bootstrap contract version — that one lives incontract.yml.)contract.yml+schema/— the bootstrap ↔ runtime contract version and its JSON schema;scripts/check_contract_version.rblocks it against the compiled-in constant (see the contract section above).build/— the self-contained CMake build project (vendored from the tebako gem's runtime press driver, adapted to the pre-patched source):CMakeLists.txt,cmake/,cmake-scripts/,src/tebako-main.cpp,include/tebako/, codegen templates inresources/, and the Ruby build tooling inlib/+tools/build_pass.rb.tools/build_runtime— the build entry point (fetch → verify → build → package)..github/workflows/— the multi-staged hierarchy:_build-platform.yml(the one per-platform build/publish unit), the four thin platform triggers (build-<platform>.yml), andpublish.yml(the release coordinator — one version everywhere / one platform all versions / one version on one platform, via workflow dispatch).scripts/holds the dependency-tree matrix computer (compute_matrix.rb, walking.github/build-graph.yaml) and this factory's release declaration (release_adapter.rb— the tebako-release gem's adapter seam: the per-leg publish jobs' upload + sign and the coordinator's audit run the gem's machinery, pinned atcontract.yml'srelease_tooling);tools/registry_update.rbrenders thetpkg-registry.yamlmirror from a release's shards. The architecture and the cache/determinism invariants are documented indocs/build-chain.md— read it before touching any workflow, the roll tooling, or a cache key.Brewfile— macOS host build dependencies (CI).
bundle install
bundle exec rspecspec/boot_smoke_spec.rb (tag :boot_smoke) boots a built runtime
executable and exercises the memfs syscall surface from inside the
packaged context — stat/lstat/fstat + btime (the ruby-4.0-linux statx
case), image IO and $LOAD_PATH resolution, gem home + bundler (incl.
bundler's process lock degrading to no-lock on the read-only gem home),
and File#flock — the statx/fcntl/flock drift class, caught at build
time.
Point TEBAKO_RUNTIME_ROOT at a runtime root — a directory holding
exactly one tebako-runtime-* executable (a build leg's
runtime-packages/, a tebako-home runtime cache dir) or the executable
path itself (a bare layout tree or a mounted filesystem image carries no
interpreter, so it is never a valid root) — and run:
TEBAKO_RUNTIME_ROOT=runtime-packages bundle exec rspec --tag boot_smokeWithout the variable the class skips in a plain run and fails loudly when
targeted explicitly. CI runs the tag against each freshly built runtime
before the artifact upload (.github/workflows/_build-platform.yml).