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 with the writer defaults (mkdwarfs compression level 7) by our own factory toolchain:
tfs mkimage --format dwarfs(the tebako-rs tfs-cli binary) when one resolves —--tfs PATH, thenTEBAKO_TFS, thentfsonPATH. It is pointed at the build's own SHA256-verifiedmkdwarfsvia--mkdwarfsso the embedded image and the standalone image share one writer.- Otherwise the build's own
deps/bin/mkdwarfsdirectly — the same prebuilt binary the deploy pass already uses for the embedded image (tfs-cli'smkimageis a wrapper over exactly this invocation until it binds the writer API in-process).
Both are build-time factory tools; neither becomes 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 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.scripts/upload_release.rbemits 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), the in-leg release assembly (upload_release.rb— write-once per-leg publish + audit) and the in-leg signer (sign_release.rb);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).