bundler-gemlocal: A per-developer Gemfile solution that works

Why have per-developer Gemfiles at all?

Ruby is a whole universe of 3rd party modules, known as gems, counting more than 140000 as of mid-2018. For almost 10 years as of this writing, Bundler, the Ruby gem version and dependency manager, has become ubiquitous. Today we can’t imagine a Ruby/Rails application without a file called Gemfile in the root of its source tree.

Since gem is just any piece of redistributable library code, there are gems of different kinds. Some are meant to be a vital part of core functionality (such as rails, activerecord, devise, cancancan, whereas the others are developer tools like consoles, debuggers, profilers etc. — something not directly involved in serving Web pages to the user, but giving developers the comfort they need.

As all developers are different, their habits, strengths and weaknesses also differ. That being said, “developer comfort” gems are a matter of personal preference in the first place. Some can’t imagine life without better_errors or ori, the others get mad when they’re forced to use Pry instead of IRB.

That’s where the need to have a local, custom, per-developer Gemfile, comes from.

TL;DR

You can jump to bundler-gemlocal page at GitHub now or keep reading.

My story

I saw first attempts to implement per-developer Gemfiles in mid-2010s in a few Rails projects I took part in. Although it wasn’t originally my idea, I quickly realized how valuable to team’s comfort this feature can be. However, my hopes for happiness were ruined time and time again once I saw yet another “half-working per-developer Gemfile hack” (I’ll use this formula a number of times below) fail on simple scenarios and causing more pain than comfort.

It appeared to be, that the author of the hack tuned it speficically for his own needs and looked no further… Needless to say, he was probably the only team member reaping the benefits of using it. The rest of his buddies almost invariably reaped pain and confusion. Not added comfort.

With all due respect, most of my past Rails teammates (like most Rails app guys in general), weren’t particularly strong dealing with ground level aspects of Ruby related to Bundler, so… unable to deliver a solution that works for everyone, the team usually agreed that there would be no custom per-developer Gemfiles and we’d default to adding everyone’s developer comfort tools, as a bulk, to the project’s Gemfile. Kaboom. 💣💥

A curious fact. All of the infamous “half-working Gemfile hacks” I’m talking about, used the name Gemfile.local for custom Gemfile.

Beginning of 2015 I felt urgent necessity to implement a solution that works.

Implementation

Filename

The first thing I decided to get rid of, was the idiotic filename: Gemfile.local. It became apparent once I’ve listed “everything Gemfile” in one of the projects:

$ ls -1 Gemfile* | nl -s ' ' -w 1
1 Gemfile
2 Gemfile.local
3 Gemfile.local.example
4 Gemfile.local.lock
5 Gemfile.lock

Friends don’t let friends make such structural mess unless there’s a really really strong reason to. Consider it yourself: files 1 and 5 are vital for production. Files 1, 3 and 5 are included in the repo, whereas 2 and 4 are not. The variety of .-separated filename sections ranges from 1 to 3, causing tears of blood on my face.

The second reason to change the filename was the fact that none of Gemfile.local-based hacks fully worked. I wanted a working solution to stand out, at least filename-wise. After a couple of prototyping rounds I’ve decided to stick with Gemlocal as a fair starting point.

Just in case you quit reading this after this section, bundler-gemlocal lets you configure everything, including the custom Gemfile name.

Bundler boot in application

No surprise, config/boot.rb (or similar file initializing Bundler) required a bit of documented tuning:

ENV["BUNDLE_GEMFILE"] ||= File.exists?(fn = File.expand_path("../../Gemlocal", __FILE__)) ? fn : File.expand_path("../../Gemfile", __FILE__)

Stackability

I’ve initially expected Gemlocal to be stacked on top of Gemfile rather than be its full extended copy. And it works, see the Gemlocal.example.

Bundler command invocation

Implementing an “upgraded” bundle command required full control over its execution. Which meant, that it must be an alias, a script or a shell function. Aliases didn’t give enough power. Script would be tricikier to set up. Outcome: shell function.

How would I name it? I could have named it bundle and “overshadow” the real executable. But that would definitely be trickier to implement, cause much greater confusion and, possibly, compatibility issues. Why not make it b? I’ve defaulted to that, it’s configurable after all. Yes, and bundle remained what it was, untouched.

Shorthand for bundle exec

Every Rails developer knows that 9/10 of everyday bundle invocations are bundle exec. Many of us are using a shorter alias to bundle exec merely to save a few hundred keystrokes a day. I’ve decided to include a b exec alias into bundler-gemlocal. Without much mulling I’ve named it bx (bundle exec).

Real optionality

My priority was to let teammates use their own custom Gemfile, not make them use it. Thus, smart environment discovery is baked into bundler-gemlocal to ensure smoothest possible operation. It’s okay that part of the team (say, frontend engineers) are completely unaware of all this Gemlocal gobbledygook and rely on default Bundler operation only.

Independence of current directory (pwd)

None of the half-working Gemfile solutions I’ve tested were able to properly run Bundler in pwd other than project root (the usual symptom was a blunt Could not locate Gemfile). I considered it very important to let developer navigate project source tree and run b and bx from any of its subdirectories. bundler-gemlocal supports it.

Keeping Gemfile.lock up-to-date

Gemfile.lock is a vital part of the project, critical for each deployment round. Again, none of the half-working Gemfile solutions I’ve seen were able to update it in a timely manner. The usual story was — Gemfile.local.lock is up-to-date, whereas Gemfile.lock lags behind and requires special manual commands to update it.

This is no longer an issue with bundler-gemlocal. It keeps Gemfile.lock auto-updated, requiring exactly zero special care from the developer side.

Intelligent shell features support

Many Bash scripts use set -x (set -o xtrace) to print the commands they run. bundler-gemlocal hides the details of b from xtrace output. Bash strict mode is supported, too.

Example Gemlocal guidelines for a Rails project team

This section is copied from bundler-gemlocal README.

  1. Team-wide Gemfile should contain no “developer comfort” gems (consoles, debuggers, profilers, …) or contain a bare, time-proven, approved minimum of 1-2 items. Adding more of such gems to Gemfile should be prohibited.
  2. Rails initializers (config/initializers/*) for “developer comfort” gems should gracefully handle the case when specific gem(s) are missing.
    1. A simple yet efficient hack is using “example files”. Say, subsys.rb is ignored by Git, whereas subsys.rb.example is in Git repository.
  3. Git should ignore Gemlocal and Gemlocal.lock in project root.
  4. A basic Gemlocal.example should be in Git repository, with comments on how to enable Gemlocal.
  5. Gemlocal.example should list, in a commented form, all “developer comfort” gems which are known to be used across the team.
  6. Project documentation should list Bundler commands in their canonical form: bundle …. Documentation about developer tools should mention that the project is compatible with b and bx.

Conclusion

The use of custom per-developer Gemfiles with bundler-gemlocal gives Rails teams a great deal of freedom. Every teammate is free to choose her own development tools without compromising project stability or causing any discomfort to fellow developers.