Skip to content

ARCHITECTURE · ANATOMY

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.

SHIPPEDeverything 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 0END OF FILEbootstrapRust loader · < 3 MBthe process entry pointslot 0 · app payload.tfs image — code, gems, manifestmounts at the app rootslot 1 · toolkite.g. inkscape, a JRE, fontsmounts at its declared pathslot table280 B per slotoffset·size·format·mountextpackage manifestsignaturetrailer166 B, at EOFmagic·flags·refsidentical bytes in everypackage for the platform1–8 slots · mounted,never extractedcomposition + trust liveoutside the imagesruntime reference +package flagsLEAN — the defaultthe trailer names the runtime; the first run downloads itonce into the shared cache, verified — every later packageon the machine reuses itFAT — zero network, everthe runtime rides in a slot, pinned by its SHA-256 in theruntime reference — the package runs on an air-gappedmachine, at the cost of carrying the runtime in every copyThe format version is 1 and stays 1: new capabilities arrive as flags and extension blocks, so older readerskeep 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

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

ruby-lang.orgofficial source tarball+ published SHA-25637 versions · 3.1 – 4.0source factorytamatebako/rubycanonical patches → patched-srctarballs per version × scenario+ SHA256SUMSruntime factorytebako-runtime-rubyper version × platform triplet:interpreter exe + env .tfs image+ manifest.json + SHA256SUMSthe producttamatebako/tebakobootstrap · shim · CLI · tfspress resolves runtimes fromthe factory indexyour machinedownloads once,verifies, caches,shares machine-wideEVERY STAGE PUBLISHES PER PLATFORM — THE SEVEN TRIPLETSaarch64-macos · arm64x86_64-macos · target 11.0x86_64-linux-gnu · glibc 2.31+aarch64-linux-gnu · glibc 2.31+x86_64-linux-musl · staticaarch64-linux-musl · staticx86_64-windows-ucrt · Win 10+aarch64-windows-ucrt · reservedThe 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.

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 invocationTHE RUNTIME PROCESS — the driver inside the interpreter exeargv0 selects the payloadthe command name IS the selector — one dispatcher,one link per command in ~/.tebako/shimsversion chain — first hit wins1 · TEBAKO_<TOOL>_VERSION environment override2 · nearest .tebako-tools.yaml walking up from cwd3 · your default in ~/.tebako/config.yaml4 · the registry's declared defaultruntime resolution — cache firstthe entrypoint's constraint → newest COMPATIBLEcached runtime (no download) → else download once,SHA-256 verify, cache, share machine-widenative-extension payloads lock to the ruby ABI linethe handoff — execruntime-exe --tebako-image img:slot:mount … --tebako-entry <entrypoint> <your args…>+ TEBAKO_RUNTIME_IMAGE (the env image) + the jail policyunix: the process BECOMES the runtime — signals and exitmount the env image, wholeTEBAKO_RUNTIME_IMAGE at the interpreter's compiled-inroot — /__tfs__ (POSIX) · A:/__tfs__ (Windows)stdlib + gems appear; nothing is extractedmount each payload, in orderevery --tebako-image triple at its declared mount point;nesting allowed · duplicate mount point refused ·any failure unmounts everything — never a partial mountinstall the jail, after the mountsthe declared host-filesystem policy ∩ your tightening —fail-closed: a malformed policy stops the run, not the policyargv rewrites to the entrypointresolved inside the first image mount; the interpreter runsyour program — the loader is gone from the pictureEvery 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

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 fileLONGEST-PREFIX DISPATCHa lookup for/opt/inkscape/bin/inkscapematches two mounts; the longestprefix on a path-componentboundary wins:/ — the app payload/__tfs__ — not a prefix here/opt/inkscape — WINSRules 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 theconsumer, never by the slice itself

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.

ExitRefusalWhat it means
65manifestthe trailer or a manifest is missing, corrupt, or names no usable entrypoint
66launcher ABIthe package speaks a handoff wire format this runtime predates
67runtime referencethe runtime reference in the trailer cannot be parsed or honored
69unavailablethe runtime cannot be resolved — offline miss, download failure, or a reference to a release that does not exist
70checksuma SHA-256 verification failed; nothing was installed
71signaturea signature is invalid — or the package is unsigned and TEBAKO_REQUIRE_SIGNED=1 was set
72trustthe signing key is not in the trusted keyring, or key continuity broke
73jailthe jail policy is malformed; the run fails closed, nothing mounts
74I/Oa filesystem, lock, or install operation failed
75contractthe runtime speaks a handoff contract this loader does not — it names both generations and the remedy
76installinstallation was refused — the publisher froze the package, or the verb belongs to the CLI
77erathe artifact comes from a different tebako generation, in either direction
78layoutthe env image does not match the layout the interpreter was compiled for

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