There’s a fantastic probability that your Ruby app sometimes explodes throughout bundle set up due to native extensions. There’s an excellent better probability that it occurs with nokogiri
, ffi
or another infamous gem with C extensions. The issue will get worse if you’re working throughout completely different working methods or upgrading Ruby variations. Let’s repair this as soon as and for all.
My drawback with native gems
As we carry out a ton of Ruby and Rails upgrades throughout completely different initiatives at arkency, we have been struck by these points many instances.
My fundamental concern with native gems is that they create pointless friction in your improvement and deployment workflow:
- Each single
bundle set up
takes ages if compilation has to happen - Ruby model upgrades? Put together to recompile every little thing
- Totally different OS than your teammates? Get pleasure from your distinctive set of errors
- CI pipeline working sluggish? Blame these C extensions
- YJIT efficiency features are restricted with C extensions
This final level is commonly missed. Ruby’s YJIT (But One other Simply–In–Time compiler) can considerably velocity up your software, but it surely works finest with pure Ruby code. C extensions bypass the Ruby VM, which suggests YJIT can’t optimize them. The extra your app depends on native extensions, the less advantages you’ll see from YJIT. With Ruby 3.3, YJIT may be enabled with the --yjit
flag, so that you’re probably lacking out on free efficiency features, however Rails will do it for you.
Btw. right here’s wonderful article on rushing up Ruby by rewriting C… in Ruby.
It’s significantly irritating on deployment. You’ve constructed a fantastic containerized setup, however nonetheless want to put in construct dependencies simply to compile the identical gems time and again. Your Docker photographs are bloated with compilers and dev headers that serve no objective in manufacturing.
It’s not a brand new drawback
What impressed me to share this answer with you is a latest chat with my pal, particularly this half:
Lately I needed to implement a tiny backend app. I dusted off Rails and every little thing was the identical. Similar instructions, identical gems, even nokogiri crashed the identical means throughout bundle set up like 10 years in the past…
It doesn’t need to be that means, I believed.
Bundler, the hero we’d like
Right here it comes: bundle lock --add-platform
.
This command tells Bundler to resolve dependencies for platforms apart from your present one and retailer that data in your Gemfile.lock
. When these platforms present precompiled variations, Bundler will use them as an alternative of attempting to compile from supply.
The --add-platform
possibility has been out there since Bundler 2.2.0
, so be sure you’re working a latest model . This may be simply checked with bundle -v
.
If for some cause your Gemfile.lock
is missing PLATFORMS
part, e.g. you’re upgrading good’ol app, it’s best to comply with subsequent steps.
In case your Gemfile.lock
has PLATFORMS
current, but it surely’s missing the particular platform you run your app on, it’s best to comply with my article.
What are the PLATFORMS?
The reply is straightforward and lives in your machine:
➜ gem assist platform
RubyGems platforms are composed of three components, a CPU, an OS, and a
model. These values are taken from values in rbconfig.rb. You'll be able to view
your present platform by working `gem surroundings`.
RubyGems matches platforms as follows:
* The CPU should match precisely except one of many platforms has
"common" because the CPU or the native CPU begins with "arm" and the gem's
CPU is precisely "arm" (for gems that assist generic ARM structure).
* The OS should match precisely.
* The variations should match precisely except one of many variations is nil.
For instructions that set up, uninstall and checklist gems, you'll be able to override what
RubyGems thinks your platform is with the --platform possibility. The platform
you move should match "#{cpu}-#{os}" or "#{cpu}-#{os}-#{model}". On mswin
platforms, the model is the compiler model, not the OS model. (Ruby
compiled with VC6 makes use of "60" because the compiler model, VC8 makes use of "80".)
For the ARM structure, gems with a platform of "arm-linux" ought to run on a
cheap set of ARM CPUs and never rely on directions current on a restricted
subset of the structure. For instance, the binary ought to run on platforms
armv5, armv6hf, armv6l, armv7, and many others. When you use the "arm-linux" platform
please take a look at your gem on quite a lot of ARM {hardware} earlier than launch to make sure it
capabilities appropriately.
Instance platforms:
x86-freebsd # Any FreeBSD model on an x86 CPU
universal-darwin-8 # Darwin 8 solely gems that run on any CPU
x86-mswin32-80 # Home windows gems compiled with VC8
armv7-linux # Gem complied for an ARMv7 CPU working linux
arm-linux # Gem compiled for any ARM CPU working linux
When constructing platform gems, set the platform within the gem specification to
Gem::Platform::CURRENT. This can appropriately mark the gem together with your ruby's
platform.
Stipulations
Fashionable tooling
Ensure you have latest Bundler and Rubygems, simply to keep away from hiccups and profit from enhancements:
gem set up bundler
gem replace --system
If new model of bundler has been put in, be certain to let your Gemfile.lock
to concentrate on it:
bundle replace --bundler
git add Gemfile.lock
git commit -m "Up to date bundler"
Precompiled gem variations out there to your platform
I like to recommend going to Rubygems web page and checking variations web page of a desired gem, let’s use nokogiri for instance.
For the day of scripting this submit, 1.18.7
is the latest model. You’ll see uncooked model (compiled in your machine):
1.18.7 March 31, 2025 (4.16 MB)
together with precompiled ones:
1.18.7 March 31, 2025 x86_64-linux-gnu (3.88 MB)
1.18.7 March 31, 2025 arm-linux-gnu (3.25 MB)
1.18.7 March 31, 2025 aarch64-linux-gnu (3.8 MB)
1.18.7 March 31, 2025 arm-linux-musl (3.44 MB)
1.18.7 March 31, 2025 x86_64-linux-musl (3.87 MB)
1.18.7 March 31, 2025 arm64-darwin (6.23 MB)
1.18.7 March 31, 2025 x86_64-darwin (6.4 MB)
1.18.7 March 31, 2025 aarch64-linux-musl (3.77 MB)
1.18.7 March 31, 2025 java (9.88 MB)
1.18.7 March 31, 2025 x64-mingw-ucrt (6.02 MB)
Up to date Gemfile.lock
with platform–particular dependencies
The rule of thumb for me is including platforms beneath:
bundle lock --add-platform arm64-darwin
Writing lockfile to /Customers/fidel/code/Gemfile.lock
bundle lock --add-platform x86_64-darwin
Writing lockfile to /Customers/fidel/code/Gemfile.lock
bundle lock --add-platform x86_64-linux
Writing lockfile to /Customers/fidel/code/Gemfile.lock
bundle set up
git add Gemfile.lock
git commit -m "Use precompiled gems for all of the platforms"
The instance above covers the most typical platforms for Rails improvement:
- Intel/AMD Linux (most servers)
- Apple Silicon (M1/M2/M3/M4 and counting Macs)
- Intel Macs — as not everyone seems to be working slicing–edge {hardware}
Clearly, you’ll be able to add every other platform that you simply want.
Get pleasure from no surprises throughout deployment or subsequent Ruby improve.
Overlook every little thing I’ve informed you to date
When you’re working a contemporary Bundler, it is going to do the platform job for you:
➜ cat Gemfile
supply "http://rubygems.org"
gem "nokogiri"
➜ bundle
Fetching gem metadata from http://rubygems.org/.......
Resolving dependencies...
Fetching nokogiri 1.18.7 (arm64-darwin)
Putting in nokogiri 1.18.7 (arm64-darwin)
Bundle full! 1 Gemfile dependency, 3 gems now put in.
Use `bundle data [gemname]` to see the place a bundled gem is put in.
➜ cat Gemfile.lock
GEM
distant: http://rubygems.org/
specs:
nokogiri (1.18.7-aarch64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.7-aarch64-linux-musl)
racc (~> 1.4)
nokogiri (1.18.7-arm-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.7-arm-linux-musl)
racc (~> 1.4)
nokogiri (1.18.7-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.7-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.18.7-x86_64-linux-gnu)
racc (~> 1.4)
nokogiri (1.18.7-x86_64-linux-musl)
racc (~> 1.4)
racc (1.8.1)
PLATFORMS
aarch64-linux-gnu
aarch64-linux-musl
arm-linux-gnu
arm-linux-musl
arm64-darwin
x86_64-darwin
x86_64-linux-gnu
x86_64-linux-musl
DEPENDENCIES
nokogiri
BUNDLED WITH
2.6.5
See, I didn’t even must lock platforms manually. Nonetheless, it added all of the platforms this specific gem is accessible for. Besides Home windows and Java ones — coincidence? 😉
However I believe that much less is extra and I want maintaining Gemfile.lock
as minimal as attainable. And sure, there’s a command for that:
bundle lock --remove-platform x86_64-linux-musl
Writing lockfile to /Customers/fidel/code/Gemfile.lock
bundle lock --remove-platform aarch64-linux-gnu
Writing lockfile to /Customers/fidel/code/Gemfile.lock
bundle lock --remove-platform aarch64-linux-musl
Writing lockfile to /Customers/fidel/code/Gemfile.lock
bundle lock --remove-platform arm-linux-musl
Writing lockfile to /Customers/fidel/code/Gemfile.lock
bundle lock --remove-platform arm-linux-gnu
Writing lockfile to /Customers/fidel/code/Gemfile.lock
➜ cat Gemfile.lock
GEM
distant: http://rubygems.org/
specs:
nokogiri (1.18.7-arm64-darwin)
racc (~> 1.4)
nokogiri (1.18.7-x86_64-darwin)
racc (~> 1.4)
nokogiri (1.18.7-x86_64-linux-gnu)
racc (~> 1.4)
racc (1.8.1)
PLATFORMS
arm64-darwin
x86_64-darwin
x86_64-linux-gnu
DEPENDENCIES
nokogiri
BUNDLED WITH
2.6.5
It eliminated PLATFORMS
entries and enough gems specs we aren’t planning on utilizing.
Okay, however why ought to I try this? — you may ask. And right here’s the reply:
- you gained’t be downloading out of date gem variations in your CI or manufacturing deployment
- you save your bandwidth
- you make RubyGems.org glad by not pulling pointless stuff from their CDN
Get pleasure from your quick and predictable builds 🖖