DEV Community

Cover image for Embed mruby as a Single Binary That Runs Everywhere
Can Eldem
Can Eldem

Posted on

Embed mruby as a Single Binary That Runs Everywhere

I've been working on RapidForge a self hosted platform that turns scripts into webhooks, pages and scheduled jobs. RapidForge itself ships as a single binary. No dependencies. No runtime installation. You download it and run it.

I already have lua embeded into Rapidforge binary because lets admit it after certain size its very difficult to code with bash. I wanted to support mruby as well becuase I really love ruby and I think you can express complex ideas easily with Ruby. Its very ecstatic language imo.

This post is about how I compiled mruby with full HTTP/HTTPS support into a single fat binary that runs unmodified on multiple platforms installing anything.

Cosmopolitan Libc

Cosmopolitan Libc is a C library that lets you compile once and run everywhere. Binaries built with it are called Actually Portable Executables (APE). They use the .com extension not DOS COM files. They're a clever polyglot format that contains x86-64 ELF, aarch64 ELF, Mach-O, PE and shell script headers all packed into one file.

The compiler is called cosmocc. It wraps GCC and compiles for x86-64 and aarch64 simultaneously. When you compile a single .c file you get two object files:

The archiver cosmoar creates dual arch .a libraries the same way. The linker (apelink, invoked automatically) packages both architectures into one binary. It's genuinely magical.

The Goal: mruby + libcurl + OpenSSL + zlib All Static

mruby on its own is tiny so its easy to compile using cosmocc. But I wanted curl and json support so that users can script their business logic in Rapidforge. That meant I needed to statically compile the entire chain:

Step 1 — Building zlib

zlib is the easiest of the three. Its ./configure script doesn't play nicely with cross-compilers so I just compiled each source file by hand:

Because cosmoar handles the dual arch layout automatically you end up with both libz.a (x86-64) and .aarch64/libz.a (aarch64). Install both into a local sysroot directory and zlib is done.

Step 2 — Building OpenSSL

This one has a few landmines. I went with OpenSSL 1.1.1w (the last 1.1.1 release) instead of 3.x because the older branch has a much simpler build system that cooperates with non standard toolchains.

The configure command:

A few things worth calling out:

unset CPPFLAGS LDFLAGS is not optional on macOS. Homebrew injects /opt/homebrew/opt/openssl@3/include into CPPFLAGS automatically. If that leaks in you get a cryptic error: OPENSSL_API_COMPAT expresses an impossible API compatibility level. Unsetting those variables before configure fixes it.

no-asm is required because OpenSSL's hand written assembly uses ABI conventions that don't match what cosmocc expects.

RANLIB=true skips ranlib entirely. The cosmoranlib script has a path bug where it tries to exec x86_64-linux-cosmo-ranlib as a relative path. Since cosmoar already creates properly indexed archives you don't need ranlib at all.

Then I built only the librariesnot the apps:

The reason: OpenSSL's CLI apps define a function istext() that conflicts with Cosmopolitan's own istext(). Building just the libraries avoids the collision entirely. make build_generated has to run first because it generates opensslconf.h which the library code depends on.

After that you need to manually move the aarch64 archives into your sysroot's .aarch64/ subdirectory. OpenSSL's Makefile isn't aware of the cosmocc dual-arch convention.

Step 3 — Building libcurl

With zlib and OpenSSL in place libcurl is reasonably straightforward:

The LIBS="-lssl -lcrypto -lz" part is critical. During configure curl runs small link tests to verify that OpenSSL actually works. Without those libraries the tests fail and configure silently disables HTTPS.

The --host=x86_64-unknown-linux flag tells autoconf this is a cross-compile. Without it autoconf might try to run the compiled test binaries on the host machine which obviously doesn't work when you're generating APE binaries.

After make install there's one manual step:

Curl's Makefile only installs the x86-64 archive. The aarch64 one sits in the build tree and you have to copy it yourself.

The resulting libcurl supports file, ftp, ftps, http and https. Everything else is stripped out to keep the final binary lean.

Step 4 — Building mruby

mruby uses a rake based build system configured via a Ruby file. Here's the relevant parts of cosmopolitan_curl.rb:

The link order curl → ssl → crypto → z matters. Each library depends on the one after it so the linker needs to see them in that sequence.

The three hal-posix-* gems are required by Cosmopolitan. Without hal-posix-socket curl can't open network connections at all the socket syscalls just aren't wired up.

The Result

A single file. No installer. No dependencies. Copy it to any Linux server, macOS machine or Windows box and it runs. It includes a full Ruby interpreter with HTTP and HTTPS support baked in. I will be shortly embeding compile mruby to Rapidforge. Here is the PR

Try It Yourself

The full build is automated in github.com/rapidforge-io/mruby-cosmo. Clone the repo, make sure you have cosmocc installed and run: build.sh

That downloads the source tarballs for zlib, OpenSSL and libcurl, compiles all three with cosmocc and then builds mruby with the curl gem on top. The whole thing takes a few minutes and produces mruby binaries where you can compile and run mruby programs.

Top comments (0)