Four choices for packaging Ruby binary distributions

How Tebako was selected to compile Ruby apps into a single binary file that runs on macOS and Linux

When I’m building new products, I like to keep as much of it in the “same language” as possible. This usually isn’t a problem because applications live on the server, but Terminalwire is different—it requires users to install the Terminalwire Client on their workstations to stream command-line applications from the server without needing an API.

I’m most productive in Ruby, so I built and shipped the prototype Terminalwire client as a Ruby Gem and tried to convince myself that installing a RubyGem wouldn’t be that bad, knowing that it would be a bad idea.

You already have Ruby installed, RIGHT!? 😅
gem install terminalwire

Turns out distributing apps as RubyGems is pretty bad for people who don’t have Ruby installed, which is most people. Fortunately, the Tebako project makes packaging up Ruby as a binary that runs on lots of machines relatively straightforward. Finding Tebako wasn’t that straightforward, which is the focus of this article in this series.

Asking users to install Ruby is a non-starter

When TRMNL beta tested their Terminalwire command-line interface with their users, it didn’t take long before a Linux user hit this wall… and it was ugly. To install Ruby, they needed sudo access to their system, which installed a version of Ruby that was too old to run Terminalwire. 🙈

Chat transcript of a person struggling to install Ruby and RubyGems in Linux

This confirmed what I knew all along—I needed to figure out how to get the Terminalwire Client running on users’ workstations without having to think at all about Ruby.

Four ways to package Ruby applications as binary files for distribution

There have been several projects over the past 15 years that have come and gone that solved the problem of distributing Ruby applications. I found most of them didn’t work out of the box when I kicked off my research in December 2024.

Traveling Ruby

The first thing I reached for was an “old friend” I remembered called Traveling Ruby.

Screenshot of the Traveling Ruby website

I quickly found that the GitHub repo hadn’t been updated in over 3 years, but I noticed an issue that pointed to a fork with support for the latest versions of Ruby.

I managed to get Terminalwire building with the Traveling Ruby fork, but it wasn’t without its problems:

  • Build scripts are cumbersome - The scripts that run the build processes for Windows, macOS, and Linux were set up for compiling Ruby and native gems into an S3 bucket, then pulling those down for packaging. My workflow didn’t work like this, which led to other issues like…

  • Native gems have to be configured manually - The process of adding additional gems with native extensions was cumbersome.

My takeaway was that Traveling Ruby was built for a time before GitHub Actions, and other multi-platform CI pipelines, made building cross-platform apps easy.

Ruby Packer

My next stop was Ruby Packer, which like Traveling Ruby, hadn’t been updated in a while.

Github README of Ruby Packer

The concerns were obvious just from the README:

  • The repo hadn’t been updated in years - Four years to be exact. The “no status” badges on the README were not confidence-inspiring.

  • 100+ open issues - Unsurprisingly, the repo had exactly 100 issues dating all the way back to 2017.

Fortunately, I clicked into the 100 open issues and noticed Ruby-packer replacement, maintained up top that mentioned a project called Tebako.

Never heard of it, but it looked interesting. 🤔

Tebako

I was pretty close to giving up on finding a well-maintained project—then I landed on Tebako.

A lot of really positive things immediately jumped out at me:

  • Up-to-date project website - The project has a dedicated website at www.tebako.org that’s kept up-to-date with announcements for the latest releases and a features page that shows all of the different operating systems & architectures they support. 😍

  • Documentation - The README was detailed and packed with lots of details on how to package Ruby apps for lots of different platforms.

  • Active community that’s responsive to pull requests and issues - As I found gaps or issues in the docs, I opened PRs with edits that were quickly merged. Ultimately, I hit a wall during my first spike getting Tebako set up for Terminalwire, so I opened an issue to work through it and lots of people helped reproduce the issue and work through it.

  • The project has a vision - Tebako is the most ambitious project I encountered in my search for Ruby packaging. Not only do they want to make native gems easy to package, but they also have their eye on packaging for other languages, like Python.

I started working my way through the documentation and similar to Traveling Ruby, the documentation had a few rough spots that required reading the source code to understand. I finally got a binary built, but when I ran it, it looked like it got stuck on the async IO selector, likely because the io-event gem requires a C extension that Tebako doesn’t take into account.

I set down Tebako to try what I knew would work: doing it from scratch by way of bash scripts.

ruby-install and my own bash scripts

While my issue was opened in the Tebako repo, I started researching what it would look like to build my own distributions. It all started with a project called ruby-install, which is a bash script that’s really good at compiling Ruby on a lot of platforms.

  • ruby-install is really good - The project does an incredible job at installing Ruby, and that’s it. That’s the point though, and I appreciate it for that; however, I was looking for something that could handle more of the packaging.

  • bash scripts actually aren’t that bad - I used to loathe writing bash scripts because they always felt cryptic, but thanks to the help of large language models, I managed to get pretty far writing and even enjoyed it! The scripts I wrote called out to ruby-install to compile Ruby, copied a bunch of application files into a folder, resolved dependencies via bundle install and put them in a specific directory, then I had to bootstrap it all with some more bash.

I got pretty far setting up GitHub Actions to compile Ruby for macOS and Linux, but when I realized I’d have to deal with dependencies like libssl and libreadline or ask users to install these dependencies. I paused to come back up for some air.

One thing I discovered during this attempt was that rolling my own Ruby packaging was viable with this approach. Getting the dependencies working was a matter of time and effort, but I knew I could get it working if I needed to.

I had my backstop.

GoLang, Rust, Crystal, and Zig

No evaluation of self-contained Ruby binaries is complete without doubting all the research you just did and considering rewriting your entire application in another language. I looked at GoLang, Rust, Crystal, and Zig—all of which have great support for building cross-platform binaries, more “street cred” than Ruby amongst developers, and technically run faster than Ruby.

The problem with all of these options is they would slow down my ability to rapidly iterate on Terminalwire within one language and one runtime.

When the Terminalwire protocol reaches 1.0 and I start expanding out to different server runtimes, I’ll revisit these options and see if it makes sense to port the Ruby client over to these languages. More about that in the Terminalwire roadmap.

Back to Tebako

Just as I ran out of steam for building my own distributions with ruby-install and my own bash scripts, a new version of Tebako was released that warranted a second look and the back-and-forth on the GitHub issue I opened felt productive. I gave it another go and it worked! 🎉

I opened up a few more issues along the way and appreciated how the Tebako community was responsive to issues and pitched in on recreation and fixes.

At this point, I committed to Tebako and started digging in.

How does Tebako work?

It’s actually a really interesting approach—Tebako compiles the Ruby source code and its dependencies, similar to ruby-install. Where it differs drastically is that it takes all of your application code, puts it into a DwarFS blob, and compiles that into the Ruby compilation. When a user runs the Ruby binary, it boots Ruby, then it boots everything from the embedded DwarFS blob.

I was pleasantly surprised how well it worked and that my binaries weighed in at around 15 megabytes!

Tebako binaries weighed in at 15 megabytes

I’m going to cover this in a future post because this article is already too long. If you want to dig in before my next post, the Tebako README is a good place to start.

Conclusion

If you need to distribute a Ruby application as a single binary, avoid asking users to install Ruby—it’s a non-starter. I explored multiple options and ultimately found that Tebako offers the best balance of maintainability, ease of use, and cross-platform support.

The takeaway? Always test multiple solutions, engage with the community, and give yourself time to work through issues. Finding the right tool often takes persistence, but in the end, you might make an interesting discovery.

In the next post, I’ll dive into more detail about Tebako including what I like about it, how it works, and ways I think it could be improved. Stay tuned!

Support this blog 🤗

If you like what you read and want to see more articles like this, please consider using Terminalwire for your web application’s command-line interface. In under 10 minutes you can build a command-line in your favorite language and web framework, deploy it to your server, then stream it to the Terminalwire thin-client that runs on your users desktops. Terminalwire manages the binaries, installation, and updates, so you can focus on building a great CLI experience.