Skip to content
All posts
6 min readtebakopackagingarchitecturerelease

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/tamatebako

Tebako 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 .tfs image;

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

One process, mounted images, an empty PATH one process on your machine ruby runtime interpreter exe + mounted env image metanorma payload 260 gems, one .tfs image inkscape payload mounted at /opt/inkscape openjdk payload JRE, mounted at /opt/openjdk everything above is mounted in memory — nothing is extracted to disk PDF 1.7 output the host PATH= (empty) host java host inkscape system toolchain none of it is used — so none of it can break the build

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:

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

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

  3. Payload slices — bare .tfs images: 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 install from a registry, shims on PATH, 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 dlmap2file now 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 an fopen interposition (the JVM reads some configuration files without going through libc open) 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 install places 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

aarch64-macos, x86_64-macos

Runs on macOS 11.0 and newer

x86_64-linux-gnu, aarch64-linux-gnu

Built on Ubuntu 20.04 (glibc 2.31); runs there and newer

x86_64-linux-musl, aarch64-linux-musl

Fully static — the universal Linux artifacts, no minimum

x86_64-windows-ucrt

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 press stays tebako press — now a prebuilt binary, with no CMake and no compilers on your machine. Trailer-level operations live in tebako-pkg, and image work in tfs.

  • Images are v2-only. v1 embedded images do not load in v2; press fresh payloads with the new tooling. The .tfs format 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 VERSION pin maps to a runtime constraint like ~> 3.3.0 in 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!