Tebako v2 — the hermetic portable-runtime platform
Tebako v2 is here. To show what "run anywhere" means in practice, a complete standards document — figures, fonts, and the Java conversion pipeline — builds entirely from mounted disk images, with an empty PATH and nothing on the host but tebako itself.
The Tebako team
github.com/tamatebakoTebako v2 is here. Rather than tell you what it does, we would like to show you.
The demonstration: a real document, an empty PATH
We ran a complete document build with every host tool made unreachable, on an ordinary macOS machine:
$ PATH= metanorma compile site.adoc
The PATH= is not a typo. The environment had no java, no
inkscape, and no system toolchain at all. Out came a valid PDF 1.7 —
a real ISO-standard document with a rasterized figure, embedded fonts,
and the full XSL-FO conversion pipeline, byte-for-byte the same size as
the version built on a fully equipped machine.
Everything that produced that document came from three disk images mounted inside the process, plus one downloaded runtime:
-
the metanorma payload — the complete Metanorma 1.16.9 application with 260 gems, as a single
.tfsimage; -
the inkscape payload — Inkscape 1.4.3, mounted at
/opt/inkscape, which rasterized the figure; -
the openjdk payload — a Temurin 21 JRE, mounted at
/opt/openjdk, which ran validation and the PDF conversion; -
the ruby runtime — a patched Ruby 3.3.7 in two parts (an interpreter executable and its environment image), downloaded once and shared by every tebako package on the machine.
Nothing was installed with Homebrew or apt, and nothing depended on
what the machine happened to have. A user who has never heard of Java
or Inkscape gets the same result as a user who has both.
Figure 1 — The empty-PATH build: one process, three mounted payload images, one shared runtime, and a host that contributed nothing.
What Tebako v2 is
Tebako is a portable-application platform: package an application once, and run it anywhere — as a single executable your users never think about, or as a managed, versioned payload with shared runtimes.
A tebako package is a composition of independently distributable parts:
-
The bootstrap — a small Rust loader (under 3 MB, kept that size by CI). It reads the package trailer, resolves the runtime, verifies it, and hands off.
-
The runtime — two parts: the interpreter executable and its environment image (
lib/ruby, gems, everything the load paths expect). Downloaded once per machine into~/.tebako, verified against a published SHA-256, and mounted — never extracted. -
Payload slices — bare
.tfsimages: your app, your data, your native tools. Each describes itself with a manifest inside the image (/__tpkg__/manifest.yaml).
The slices co-mount in one process. An application declares what it needs — a runtime, a toolkit like Inkscape or a JRE, a data slice — and the dispatcher mounts each at its declared path. One payload can require another at a mount point; the dependency is written down, versioned, and platform-aware.
There are two ways to use the same packages:
-
Standalone: the bootstrap and the slices stitched into one executable (fat) or referenced from it (lean). Your users download a file and run it. The runtime downloads on first run, then it is shared.
-
Managed:
tebako installfrom a registry, shims onPATH, and a machine-wide store with content-addressed, verified artifacts. This is the flow behind the empty-PATH document above.
How it works under the hood
Running binaries that live inside disk images — a JVM included — needed some new machinery, all of it in Rust:
-
The spawn hook. Every patched Ruby runtime mediates process spawning: when a program tries to run something that lives in the mounts, tebako copies it to a host cache and runs it there, passing the mounts along to the child.
-
Dependency-closure extraction. The system loader (dyld, ld.so) looks for shared libraries with raw syscalls that user-space hooks cannot serve. So
dlmap2filenow reads the Mach-O/ELF dependency closure of anything it materializes — rpaths,@executable_path,$ORIGIN— and copies every in-image library into place before the exec. The JVM’s many dylibs are all in place before it starts. -
The preload shim. Spawned children see the same mount table via
DYLD_INSERT_LIBRARIES/LD_PRELOAD— including anfopeninterposition (the JVM reads some configuration files without going through libcopen) and an ABI trampoline for Darwin’s stack-passed variadic arguments. -
Native tools work directly. Tools like Inkscape or a JRE need no interpreter.
tebako installplaces their executables in the store and the shim runs them as-is — native tools are now ordinary tebako packages, not a special case.
Seven platforms, one payload model
Everything above is published per platform, and the table lists the oldest system each build runs on — no fine print:
| Platform | Notes |
|---|---|
|
Runs on macOS 11.0 and newer |
|
Built on Ubuntu 20.04 (glibc 2.31); runs there and newer |
|
Fully static — the universal Linux artifacts, no minimum |
|
Windows 10 / Server 2016 and newer, runtimes statically linked |
An aarch64-windows-ucrt build is planned but not built yet.
Pure-language payloads ship once as universal and run on all seven
platforms; only payloads with native code are built per platform.
Using it
# install tebako (prebuilt, per platform)
brew install tamatebako/tap/tebako
# point at a payload registry and install
tebako add-registry tfs:github:tebako-packages/metanorma
tebako install metanorma
# it's on PATH now — the runtime downloads on first run
metanorma compile site.adoc
# native toolkits work the same way
tebako add-registry tfs:github:tebako-packages/hello
tebako install hello
hello # Hello, world!
Runtimes download once per machine, are verified against their
published SHA-256, and are shared by every package. Payloads stay
byte-identical with what the publisher released. Running a package
never installs anything; tebako install is the explicit verb.
Migrating from Tebako v1
The C++/CMake era is archived as tebako-v1 and will not receive new development. What changes for you:
-
The gem is replaced by the Rust CLI.
tebako pressstaystebako press— now a prebuilt binary, with no CMake and no compilers on your machine. Trailer-level operations live intebako-pkg, and image work intfs. -
Images are v2-only. v1 embedded images do not load in v2; press fresh payloads with the new tooling. The
.tfsformat is the dwarfs-t image (FlatBuffers metadata — upstream DwarFS cannot read it). -
The runtime model changed. v1 merged the environment and the application into one image. v2 separates them: a shared runtime (executable + environment image) and bare payload slices. The result is leaner, cacheable, and composable.
-
Patches live per Ruby version in tamatebako/ruby, applied and released as patched-source tarballs; the runtime factory builds runtimes from them. Your
RUBY VERSIONpin maps to a runtime constraint like~> 3.3.0in the payload manifest.
Released alongside this announcement
-
tamatebako/tebako— the Rust product (bootstrap, shim, driver, tfs, tpkg, CLI): v2.0.0 -
tamatebako/tebako-runtime-ruby— the runtime factory: 0.16.0 -
tamatebako/ruby— the patch factory (36 Ruby versions, 3.1–4.0): patched-source releases -
tebako-runtime— the in-runtime adapter gem: 0.8.0 on RubyGems
More runtimes (Python next), more payload feedstocks, and the Windows builds come next, in that order.
What this means
"Package once, run anywhere" is easy to say. Our empty-PATH build is how we hold ourselves to it: nothing on the host was used, so nothing on the host can break it. That is the standard we intend to keep — and the one we hope you will hold us to.
Questions? We are always happy to hear from you in the issue tracker.
Press on with Tebako!