Skip to content

ARCHITECTURE · 04

How a Tebako package works.

One file, three parts, no installation. This is the whole mechanism — what the file contains, how the parts meet at run time, and what happens when something does not fit.

Note

Status: shipped — everything on this page is the shipping v2 behavior.

Three parts, glued by a trailer.

A tebako package is a composition of independently distributable parts. Each part has its own release line, its own publisher, and its own trust anchor; the executable you download is where they meet.

A — the bootstrap, the loader.

A small, static Rust binary — under 3 MB, a budget enforced in CI on every platform — that is the process entry point of every package. It reads the trailer at the end of its own file, verifies what it finds, resolves the runtime (from the machine-wide cache, or by downloading it once), and hands off. It carries its own HTTP, TLS, and archive handling: nothing shells out to curl, git, or any tool on your machine. The bootstrap is a published artifact — we build it, you never do — and the same bytes are shared by every package pressed for a platform.

B — the runtime, two artifacts.

A runtime is the interpreter plus everything its load paths expect, and v2 ships it as two artifacts: the interpreter executable itself, and an env image — a single .tfs file holding the standard library, gems, and support files. (The windows runtime adds a third: the shared ruby DLL the exe imports, installed beside it under its declared PE name.) The set is downloaded once per machine into the shared cache, SHA-256 verified, and mounted, never extracted. Every tebako application on the machine reuses the same copy; upgrading the runtime never rebuilds an application. Ruby is the first runtime; the model is language-agnostic.

C — the payload slices, bare images.

Your application, your data, your native tools — each a bare .tfs image, self-describing through a manifest inside the image at /tpkg/manifest.yaml. The manifest declares what the slice is (app, data, toolkit, runtime), what it provides (commands, libraries, a runtime), and what it requires (a runtime constraint, other slices at mount points). Slices are published, versioned, and verified independently — and they co-mount into one process.

One executable, laid out.

The package file is the bootstrap followed by payload slots, a slot table, optional extension blocks, and a 166-byte trailer header at the very end — read last, first. A fat package carries its runtime in a slot and never touches the network; a lean package references the runtime and resolves it at run time.

OFFSET 0 END OF FILE bootstrap Rust loader · < 3 MB the process entry point slot 0 · app payload .tfs image — code, gems, manifest mounts at the app root slot 1 · toolkit e.g. inkscape, a JRE, fonts mounts at its declared path slot table 280 B per slot offset·size·format·mount ext package manifest signature trailer 166 B, at EOF magic·flags·refs identical bytes in every package for the platform 1–8 slots · mounted, never extracted composition + trust live outside the images runtime reference + package flags LEAN — the default the trailer names the runtime; the first run downloads it once into the shared cache, verified — every later package on the machine reuses it FAT — zero network, ever the runtime travels in a slot, pinned by its SHA-256 in the runtime reference — the package runs on an air-gapped machine, at the cost of carrying the runtime in every copy The format version is 1 and stays 1: new capabilities arrive as flags and extension blocks, so older readers keep reading newer packages — and refuse, by name, only what they genuinely cannot honor. inspect any package: tebako inspect <file> — slots, mounts, runtime reference, trust state

Figure 1 — Byte layout of a tebako package: bootstrap, payload slots, slot table, extension blocks, and the trailer header at end of file.

Where the runtime comes from.

Runtimes are factory products, not local builds. An official Ruby release flows through two factories before any user machine sees it, and every boundary is a published, checksummed release — each stage consumes the previous stage’s artifacts, never its source tree. The full stage-by-stage table is on the factories page.

ruby-lang.org official source tarball + published SHA-256 37 versions · 3.1 – 4.0 source factory tamatebako/ruby canonical patches → patched-src tarballs per version × scenario + SHA256SUMS runtime factory tebako-runtime-ruby per version × platform triplet: interpreter exe + env .tfs image + manifest.json + SHA256SUMS the product tamatebako/tebako bootstrap · shim · CLI · tfs press resolves runtimes from the factory index your machine downloads once, verifies, caches, shares machine-wide EVERY STAGE PUBLISHES PER PLATFORM — THE SEVEN TRIPLETS aarch64-macos · arm64 x86_64-macos · target 11.0 x86_64-linux-gnu · glibc 2.31+ aarch64-linux-gnu · glibc 2.31+ x86_64-linux-musl · static aarch64-linux-musl · static x86_64-windows-ucrt · Win 10+ aarch64-windows-ucrt · reserved The musl builds are fully static — the universal Linux artifacts. Pure-language payloads ship once as universal; only payloads with native code multiply across the matrix.

Figure 2 — The release chain: ruby-lang.org tarball through the source and runtime factories to the product and the user machine, with the seven platform triplets.

How a run works.

There are two front doors and one mechanism behind them. Standalone: you execute a package file directly — the bootstrap inside reads its own trailer and resolves the runtime. Managed: you invoke a command name on PATH — a shim link to the dispatcher, which picks the payload version and the runtime per invocation. Either way, the same handoff happens at the end: the runtime executable starts with a list of images to mount and an entrypoint to run.

THE LOADER — shim or bootstrap, per invocation THE RUNTIME PROCESS — the driver inside the interpreter exe argv0 selects the payload the command name IS the selector — one dispatcher, one link per command in ~/.tebako/shims version chain — first hit wins 1 · TEBAKO_<TOOL>_VERSION environment override 2 · nearest .tebako-tools.yaml walking up from cwd 3 · your default in ~/.tebako/config.yaml 4 · the registry's declared default runtime resolution — cache first the entrypoint's constraint → newest COMPATIBLE cached runtime (no download) → else download once, SHA-256 verify, cache, share machine-wide native-extension payloads lock to the ruby ABI line the handoff — exec runtime-exe --tebako-image img:slot:mount … --tebako-entry <entrypoint> <your args…> + TEBAKO_RUNTIME_IMAGE (the env image) + the jail policy unix: the process BECOMES the runtime — signals and exit mount the env image, whole TEBAKO_RUNTIME_IMAGE at the interpreter's compiled-in root — /__tfs__ (POSIX) · A:/t (Windows) stdlib + gems appear; nothing is extracted mount each payload, in order every --tebako-image triple at its declared mount point; nesting allowed · duplicate mount point refused · any failure unmounts everything — never a partial mount install the jail, after the mounts the declared host-filesystem policy ∩ your tightening — fail-closed: a malformed policy stops the run, not the policy argv rewrites to the entrypoint resolved inside the first image mount; the interpreter runs your program — the loader is gone from the picture Every step names its failure. Nothing guesses, nothing falls back silently, nothing half-mounts. preview any dispatch: tebako-shim which <tool> windows: spawn + wait, child's exit code propagated

Figure 3 — The dispatch sequence: invocation name, version chain, runtime resolution, the exec handoff, and the driver mounting images before the entrypoint runs.

Two consequences worth internalizing. First, a run is a run: executing a package never installs its slices — installation is the explicit verb, tebako install. Second, verification happens at fetch and install, never per run — a cached, verified artifact starts at full speed every time.

One process, many images: the mount model.

Inside the running process there is one virtual filesystem. The env image mounts whole at the runtime root — the path the interpreter was compiled to live at — and every payload mounts at the path its consumer declared. A path lookup resolves against the longest matching mount prefix; mounts may nest; and the host filesystem is visible only where the jail allows it. Payload images are always read-only.

WHAT THE PROCESS SEES — ONE NAMESPACE / ◀ slot 0: the app payload — the entrypoint lives here ├── bin/metanorma ├── lib/metanorma/… ├── /__tfs__ ◀ env image, mounted WHOLE at the runtime root │   ├── bin/ruby │   ├── lib/ruby/3.3.0/… │   └── lib/ruby/gems/… ├── /opt/inkscape ◀ toolkit slice, mount declared by the consumer │   └── bin/inkscape ├── /opt/openjdk ◀ toolkit slice, independently published + versioned │   └── bin/java └── /work ◀ the host, only where the jail grants it (here: ro)     └── document.adoc — your file LONGEST-PREFIX DISPATCH a lookup for /opt/inkscape/bin/inkscape matches two mounts; the longest prefix on a path-component boundary wins: / — the app payload /__tfs__ — not a prefix here /opt/inkscape — WINS Rules that keep this sane: · env image first, payloads in order · nested mounts are legal · duplicate mount point: refused by name · any failure unmounts everything · payloads are always read-only · a mount point is declared by the consumer, never by the slice itself

Figure 4 — The mount model: the env image at the runtime root, payloads at consumer-declared mounts, and longest-prefix dispatch.

The consumer-declared mount is the dependency mechanism: an application’s manifest says I need inkscape, this version range, at `/opt/inkscape` — the slice itself stays location-agnostic, and one installed copy serves every application that mounts it. The worked example on the audience page runs this end to end; the manifest grammar is on the feature surface.

Generations meet fail-closed: the contract model.

The loader and the runtime are separate artifacts with separate release lines, so they can drift apart. Tebako’s answer is a declared handoff contract: every runtime release states which handoff semantics it speaks, and the loader checks the declaration before downloading, before mounting, before running anything. A combination that cannot work is refused by name — the error tells you both generations and the remedy — instead of failing later as a corrupted environment or a segfault.

The same rule covers packages and payloads: every artifact belongs to a declared generation, and anything that predates declarations is treated as the oldest one and refused by name when it no longer fits. The failure you meet in practice is a sentence like "this package was pressed by a newer tebako — upgrade your CLI" or "this runtime is too old for this package — pin an older payload or upgrade the runtime": a named error with an exit code, never a silent fallback and never a half-installed cache.

Every failure has a name.

The loader and the runtime’s mount driver share a small, stable set of refusal codes. Scripts can branch on them; humans get a one-line message that says what was refused and why.

Exit Refusal What it means

65

manifest

the trailer or a manifest is missing, corrupt, or names no usable entrypoint

66

launcher ABI

the package speaks a handoff wire format this runtime predates

67

runtime reference

the runtime reference in the trailer cannot be parsed or honored

68

overlay

an overlay/decrypt binding failed — an unbound retained store, missing or non-opening key material, an unwritable store, an orphan binding, or a malformed TEBAKO_OVERLAYS / TEBAKO_DECRYPT

69

unavailable

the runtime cannot be resolved — offline miss, download failure, or a reference to a release that does not exist

70

checksum

a SHA-256 verification failed; nothing was installed

71

signature

a signature is invalid — or the package is unsigned and TEBAKO_REQUIRE_SIGNED=1 was set

72

trust

the signing key is not in the trusted keyring, or key continuity broke

73

jail

the jail policy is malformed; the run fails closed, nothing mounts

74

I/O

a filesystem, lock, or install operation failed

75

contract

the runtime speaks a handoff contract this loader does not — it names both generations and the remedy

76

install

installation was refused — the publisher froze the package, or the verb belongs to the CLI

77

era

the artifact comes from a different tebako generation, in either direction

78

layout

the env image does not match the layout the interpreter was compiled for

79

check

a payload check FAILed — the tebako check aggregate; the verdict line names the check and the failed expectation

Once the interpreter is running, its own exit codes are the program’s — the loader is out of the picture by then.