Universal Ruby
This repository contains a script to aid in building Ruby for universal CPU architectures (arm64 and x86_64) for macOS.
Pre-requisites
You'll need the following software installed:
- Homebrew
- rbenv
- ruby-build (usually installed with rbenv)
- GCC-compatible compiler and common build tools (i.e.
make
)
For the compiler, simply downloading Xcode and installing the command line tools is sufficient. For the rest, you can install Homebrew first and then install rbenv and ruby-build.
Usage
To build Ruby, just run make
in this directory. That'll run the
build_ruby.sh script. When completed, you should have version 3.2.2 of Ruby
available via rbenv
. If you already have it installed, you'll be prompted
whether to replace it. You can then verify that you are using a Universal
version:
➜ git:(main) ✗ make
... wait for completion ...
➜ git:(main) ✗ rbenv local 3.2.2
➜ git:(main) ✗ ruby --version
ruby 3.2.2 (2023-03-30 revision e51014f9c0) [universal.arm64-darwin22]
Choices made to enable a universal Ruby
First things first, ruby-build is a fantastic way to build Ruby. However, it adds an indirection to the process of building Ruby. The build script includes a function to build Ruby directly from source. If you run into issues, you might try to build with it simply by uncommenting out a line.
Building the dependencies
Now, before we can build a universal Ruby, we need to have universal versions of
the dependencies. Fortunately, libyaml can be readily built by specifying some
CFLAGS during the standard configure
/make
/make install
dance.
I couldn't figure out how to build Readline through configuring flags, but found
that I could build an x86_64 version simply by configuring and making under
Rosetta. We then assemble a universal binary by using lipo
to combine
the arm64 and x86_64 builds.
OpenSSL can be configured for x86_64 builds and then stitched together similarly to Readling.
With all of our dependencies built with symbols for both arm64 and x86_64
architectures, we can specify their use in ruby-build through the
RUBY_CONFIGURE_OPTS
environment variable.
Fixing the code
There is an issue if you build the Ruby 3.2.2 source. Ruby will build miniruby
in order to run some files that finish configuring the build. However, there's
a bug in tool/mkconfig.rb
. Specifically, the regex used to match the arch
won't match multiple-arch values. That has since been fixed.
There is also a runtime issue. Specifically, builds of Ruby created with this
build script would run fine on arm64 but would throw a runtime error (unmatched platform
) when running in a Rosetta environment. That check was found. At
the time of this writing, a proper fix wasn't found so the solution was just to
comment that check out.
The only issue was figuring out how to update the source code prior to building.
Ruby-build doesn't provide a mechanism for interjecting between downloading the
code and compiling it but it does provide the means to introduce custom
tooling. In this case, make
is being hijacked to provide some
just-in-time replacements.
The fake make
just performs a quick check. If it finds a ruby.c
file,
it assumes that it's in the Ruby source directory and will rsync
the
replacement files. Then it simply forwards the command to the real make
to
perform the actual build.
Repo history
This repo original contained just a Gemfile and a Makefile with a few commands. The intent was to have some minimal test cases for evaluating working with Ruby under Rosetta emulation.
Eventually, some problem solving started and it inherited a handful of different ideas. Some ideas, like shim versions of the Ruby binaries that would de-Rosetta Ruby when run were considered. Otherwise, most of the work went into creating the Universal Ruby build.
Some of the tests remain, but this repo has been re-purposed to focus on building Ruby.