Skip to content

Repository files navigation

tebako-runtime-ruby

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.

How a runtime is built

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.7

produces 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).

Runtime filesystem image (item 30)

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 layout

Without 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:

  1. An explicit --tfs PATH or TEBAKO_TFS setting wins (fail-closed: a request that does not resolve is a named error, never a fallback).
  2. Otherwise the builder fetches the CLI published with the pinned tamatebako/tebako release (contract.yml's link_unit_release), verified against its published .sha256 sidecar 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 ruby DLL (issue 40)

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>.dll in 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 additive dll key flows the mapping (filename = the asset, install_as = the PE name, plus sha256/size_bytes; consumers ignoring the key keep working, same rule as image), and <asset>.sha256 carries 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 build matrix and the windows/arm64 leg

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):

  1. 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 ship x86_64-windows-gnu only). 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).
  2. 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_ARM64 is true. 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).

Bootstrap ↔ runtime contract version

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 additive contract_version key in every package's manifest entry (the <package>.manifest.json shard; consumers ignoring the key keep working, same rule as image).
  • TEBAKO_CONTRACT_VERSION in build/src/tebako-main.cpp — the constant compiled into the runtime itself. The driver exports it as the TEBAKO_CONTRACT_VERSION environment 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.

Release metadata: per-asset sidecars, per-package shards

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 additive abi / image / dll keys). The non-derivable fields (the windows DLL's install_as, the image sibling, the contract version) live here and nowhere else. On signing-enabled lines the entry also declares its signature block ({keyid, asc} — the exact .asc asset 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.

Layout

  • 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 in contract.yml.)
  • contract.yml + schema/ — the bootstrap ↔ runtime contract version and its JSON schema; scripts/check_contract_version.rb locks 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 in resources/, and the Ruby build tooling in lib/ + 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), and publish.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 at contract.yml's release_tooling); tools/registry_update.rb renders the tpkg-registry.yaml mirror from a release's shards. The architecture and the cache/determinism invariants are documented in docs/build-chain.md — read it before touching any workflow, the roll tooling, or a cache key.
  • Brewfile — macOS host build dependencies (CI).

Specs

bundle install
bundle exec rspec

Runtime boot smoke

spec/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_smoke

Without 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).

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

4 watching

Forks

Releases

Packages

Used by

Contributors

Languages