blog

The Importance of Locking Gem Versions in Ruby Projects

In the smoky haze of 19th-century Britain, announced by the roar of steam engines and clanking of factory wheels, a revolution began. In time, it would transform the world.

It’s aftershocks are still effecting us today.

Some of its effects are so commonplace we forget their existence; not everything is quite so picturesque as a steam engine… but that does not mean such things are not important.

Such things include the standardization of parts, just the kind of thing we take for granted; the kind of thing governed by boring monolithic entities named with boring initialisms like ANSI and ISO.

It’s quite important, though.

Imagine if you needed new tires for your car, and instead of simply purchasing a new one at a local store, you had to have them custom made by a local tire artisan. That might seem ridiculous - but that was how the world worked before the Industrial Revolution. Indeed, clothes weren’t made in ready-to-wear sizes until the 1820s - for most of human history, you’d instead have someone make your clothes by hand.

For finished goods, we’ve well learned this lesson - we bemoan when a company uses a nonstandard charger for a phone, for example. Consumers prefer standardization.

For software, though, we’re still learning some of those lessons.

We might scoff at the idea of a custom tire handmade by an expert - but how often do we need to rely on software experts to install, much less create, some piece of software?

I don’t have all the answers, of course; software packaging is a mess because it’s an as-yet unsolved problems. Some pieces to a future with better software dependency management are out there, but they aren’t assembled or fully realized. Nix is a great example: it solves some problems better than any other system out there right now, but it creates problems of its own.

I plan to write quite a bit more about dependency management - in fact, I’m working on the second edition of my book “Practical Ruby Gems.”

For the rest of this post, though, I plan to drill down on just one aspect of dependency management: tightening version requirements. Specifically, tightening version requirements for Ruby applications.

Let’s take, for example, this sample Gemfile:

source 'https://rubygems.org'

gem 'rails', '~> 7.2.1'
gem 'pg'
gem 'puma'
gem 'sass-rails'
gem 'webpacker'
gem 'turbolinks'
gem 'jbuilder'
gem 'bootsnap', '>= 1.4.4'

group :development, :test do
gem 'byebug', 
     platforms: [:mri, :mingw, :x64_mingw]
end

group :development do
gem 'web-console'
gem 'rack-mini-profiler'
gem 'listen'
gem 'spring'
end

gem 'tzinfo-data', 
   platforms: [:mingw, :mswin, :x64_mingw, :jruby]

Let’s next install it:

$ bundle install
... snip ..
Bundle complete! 14 Gemfile dependencies, 85 gems now installed.
Use `bundle info [gemname]` to see where a bundled gem is installed.

The above Gemfile looks reasonable - but only two of the gems have their versions explicitly specified. Is that a problem? It could be - RubyGems will use an installed gem if it matches our requirements, which means that anyone who has to use this code may end up using a random version of quite a few different gems.

To find out a measure of how many different gems, let’s use the lock-gemfile tool:

$ gem install lock-gemfile
$ lock-gemfile --report
Total gems: 14
Matching gems locally available: 22 (57.14% extra)
Matching gems remotely available: 1346 (9514.29% extra)

Yes, there’s a total of 1346 gems available that match those requirements. True, Rails is locked down using the ~>' operator - and indeed, Rails is definitely something whose version you want to track closely - but quite a few others aren’t.

Let’s lock down our gems a bit more using lock-gemfile:

$ lock-gemfile rewrite
Gemfile updated with locked versions

$ cat Gemfile
source 'https://rubygems.org'

gem 'rails', '~> 7.2.1'
gem 'pg', '~> 1.5.8'
gem 'puma', '~> 6.4.3'
gem 'sass-rails', '~> 6.0.0'
gem 'webpacker', '~> 5.4.4'
gem 'turbolinks', '~> 5.2.1'
gem 'jbuilder', '~> 2.13.0'
gem 'bootsnap', '>= 1.4.4'

group :development, :test do
gem 'byebug', '~> 11.1.3', 
     platforms: [:mri, :mingw, :x64_mingw]
end

group :development do
gem 'web-console', '~> 4.2.1'
gem 'rack-mini-profiler', '~> 3.3.1'
gem 'listen', '~> 3.9.0'
gem 'spring', '~> 4.2.1'
end

gem 'tzinfo-data', 
     platforms: [:mingw, :mswin, :x64_mingw, :jruby]

Hurrah! As we can see, our Gemfile has been updated to use the ~> operator. Notice that the bootsnap requirement - ‘>= 1.4.4’ - was not changed, since it was pre-specified. Let’s take a look and see how much that has decreased our possible set of installable gems:

$ lock-gemfile report 
Matching gems locally available: 14 (0.0% extra)
Matching gems remotely available: 140 (900.0% extra)

1346 down to 140 is quite a decrease! In most cases, this is more than sufficient; after all, the ‘~>’ operator should, in theory, prevent any breaking changes from happening. (Theory is, of course, not practice.)

Of course, you could’ve used the -e operator to lock down with the ‘=’ operator and got it even further… but I’ll leave that as an exercise for the reader.

 durableprogramming/lock-gemfile