Apple Silicon — M1 for Ruby developers

May 15, 2021 Ruby Apple Silicon macOS

Let’s get this out of the way first: Is M1 ready to accept Ruby developers? Definitely. There are no problems besides some possible complications during the initial setup. And now that Docker is fully supported, all the tools you might need work. Engineers at Apple made a miracle of supporting existing software.

The new, bi-architecture world

Apple Silicon computers put us in a rare situation: they run two binary architectures.

The native one — Apple Silicon — is based on ARM. It’s the one that has been running in iPhones and iPads for years. ARM is not brand new; the most widespread ARM computer out there is the Raspberry Pi. So most Unix software builds on ARM. But, not every build script is compatible with ARM on MacOS.

And the old one — Intel x86, 64-bit — transparently emulated. Programs compiled for x86 run without any special treatment, and you would never even know they are x86. You don’t have to start a virtual machine or container or run them with a special flag. GUI apps and terminal apps both work.

Sidenote: to know whether a program is Intel or Apple, use the file utility or look it up in Activity Monitor in the “Kind” column.

Not only x86 is perfectly emulated, but ARM and x86 programs are fully interoperable: you can run one from another, you can connect them with pipes, and of course, all network and filesystem calls work the same. Loading dynamic libraries from one arch on another, however, does not work and never will.

Generally, once you have binaries, you don’t need to know their architecture — they “Just Work.” Intel programs compiled before M1 existed, or without any concern over M1, also work.

Ruby and Apple Silicon

Unfortunately, we Ruby developers don’t get binaries and compile our own Ruby (most of the time). And compiling does not always “just work.” Ruby compiles on Apple Silicon from version 2.7.

But besides Ruby, gems with native extensions like pg, nokogiri, or puma must also be Apple Silicon-ready. Some gems have already been updated to include Apple Silicon binaries (nokogiri@1.11.3), or to compile with no issues (puma). But other gems either were not updated or do not work (google-protobuf.

So, what if a gem fails to compile? Or, like google-protobuf, crashes once you try to launch the app / require the gem?

First, make sure the failure is because of Apple Silicon!

If all else fails, then you need to compile Ruby for x86.

Plan B: Compile Intel Ruby

(This is also the plan if your Ruby won’t compile for Apple Silicon, and you cannot upgrade to a newer version.)

Don’t worry: after compiling, you do not notice it — and neither will any of your team members, as you only change the Ruby architecture for your machine.

Step 1: almost cross-compiling with arch

Cross-compiling is when the compiled binary is of a different architecture than the compiler. As you might expect, this is not as simple as compiling for your machine.

Fortunately, you don’t have to cross-compile; instead, you run the Intel variant of the compiler to produce an Intel Ruby binary.

The Big Sur C compiler is a universal binary. It’s as “universal” as a USB/USB-C flash drive: it contains both an Apple Silicon and an Intel binary and chooses based on the environment. (All binaries that ship with macOS are universal.)

How do you switch the environment into Intel mode? With the arch command:

arch -x86_64 <your command here>

This only affects Universal binaries; single-arch binaries would still run using their respective architecture. The default arch propagates into child processes, so you can run scripts, too.

But before you can do arch -x86_64 rbenv install, there are a few prerequisites.

Step 2: prepare Brew for x86

The Intel Ruby and its gems need Intel binary dependencies. We can get them from Homebrew. I assume you already installed Homebrew, and it uses Apple Silicon by default. But now you also need a second copy of Homebrew — with Intel binaries.

The Homebrew team planned for this - you can install both versions of Homebrew side by side with no problems. Apple Homebrew installs into /opt/homebrew and Intel Homebrew installs into /usr/local.

To install the Intel flavor of Homebrew, use the arch command:

arch -x86_64 <copy the rest of the command from https://brew.sh>

Your $PATH must list /opt/homebrew/bin before /usr/local/bin so you don’t accidentally use Intel binaries if you have Apple alternatives.

Now, whenever you want to install a package into the Intel Homebrew, run

arch -x86_64 /usr/local/bin/brew install <...>

(Save that as an alias if you want, but you shouldn’t need it often.)

Step 3: identify and prepare prerequisites

Sidenote: I saw less issues installing Ruby with asdf / ruby-build, than RVM.

Installing Ruby dependencies is easy with Intel Homebrew:

arch -x86_64 /usr/local/bin/brew install readline openssl zlib

Step 4: build, test, repeat

With this done, all I needed to build Ruby is:

arch -x86_64 \
  env RUBY_CONFIGURE_OPTS="--with-readline-dir=/usr/local/opt/readline" \
  asdf install ruby 2.7.1

For RVM, more libraries needed to be specified:

arch -x86_64 rvm install 2.7.1 \
  --with-readline-dir=/usr/local/opt/readline \
  --with-zlib-dir=/usr/local/opt/zlib \
  --with-openssl-dir=/usr/local/opt/openssl

Check that you use the correct way of passing Ruby options into the script — they are different for asdf (ruby-build) and RVM.

Step 5: gems

Once Ruby is successfully installed, proceed with installing gems. If any gems need binary dependencies, you may need to provide paths when installing them as well.

For the pg gem, you can take Intel Postgres libraries from Postgres.app, while it is still Intel-only.

gem install pg -- \
  --with-pq-lib=/Applications/Postgres.app/Contents/Versions/13/lib \
  --with-pq-include=/Applications/Postgres.app/Contents/Versions/13/include

Sidenote: Node packages with native extensions have the same issues, but such packages are rare. So a recent release of Node should “Just Work” for you. If you still use the deprecated node-sass package, switching to sass is effortless.

Once you have the binary compiled

Once Ruby and all of the gems are compiled, you are good to go. Usual Ruby utilities — bundle ,rails, rspec, and so on — continue to work with the correct Ruby binary. Nothing else changes in your workflow.

Post scriptum: I’d like to mention that another “nuclear” option is to switch your entire terminal app into Intel mode and forget that you have an M1 entirely. With Apple’s level of Intel emulation, this will likely “Just Work” as well.

Post post scriptum: Docker is now available on M1. And it can run Intel containers just as well as ARM. So you could also run your entire toolchain in Intel Docker. There are some merits to using Docker instead of the native macOS environment. But switching to avoid M1 “issues” is like moving to a warmer climate instead of buying a winter coat. Which I say to mean it comes with its own world of concerns.

And let me reiterate — this whole “recompiling Ruby for Intel” applies only if you are stuck with an old Ruby or the rare incompatible gem.

If not, you don’t need to do anything special to work with Ruby on your M1. Though, then you are probably not reading this article. :)

Buy me a coffee Liked the post? Treat me to a coffee