Episode #673

Lose the bundle exec muscle memory and get back to the good old days… with the help of binstubs!

Episode Script

Once upon a time, in the misty early days of Ruby, when we wanted to issue a project command we just typed the name of the executable.

Like rails, or rake, or rspec.

But as Ruby matured, we started having to worry about the versions of our libraries and tools. Sometimes two different projects would rely on two different versions of a tool. Sometimes we didn’t want to install project dependencies globally. And thus bundler was born.

By running bundle install, we ensure that all the required versions of tools and libraries are installed for this project.

And when we execute project-associated commands, we prefix them with bundle exec.

bundle exec rails s

bundle exec ensures that all the correct gem versions—and none of the incorrect ones—are available in the Ruby load path

and that the correct executable path for those gems is available in the shell’s search path.

While it’s not that big a deal to run bundle exec before every command, it kind of goes against the Ruby philosophy of developer ease and happiness. The Ruby Way is for the tools to labor behind the scenes to let us work the way we want to; not to put implementation details in our face all the time.

Fortunately, there’s a way we can go back to the old days and avoid having to prefix our commands! And it involves something called a binstub.

What’s a binstub?

Well, if we look in the bin directory of a modern Rails application, we can see a few of them.

Each of these is an executable script.

Let’s look at the one for the rake command.

#!/usr/bin/env ruby
  load File.expand_path('../spring', __FILE__)
rescue LoadError => e
  raise unless e.message.include?('spring')
require_relative '../config/boot'
require 'rake'

We can see that this is a short Ruby script. It first tries to load the spring library, which Rails applications use to cache code in memory to avoid long start times.

Then it loads the app’s boot.rb script. Let’s take a quick look at that.

ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __dir__)

require 'bundler/setup' # Set up gems listed in the Gemfile.
require 'bootsnap/setup' # Speed up boot time by caching expensive operations.

This script locates the project’s Gemfile, which is needed by Bundler. Then it uses bundler to make sure all the correct gem versions are available. There’s also a line setting up bootsnap, which is another Rails library for caching code to speed up commands.

Back in the bin/rake script, after the boot script is loaded it sources in the rake library. Because the Bundler setup has been invoked at this point, we can be confident that the version of Rake needed by this particular project will be the one to be loaded.

The final line is the entry point to the Rake tool.

So that’s a binstub. In a nutshell, it’s a tiny script that first arranges to have the right version of Ruby gems loadable, and then kicks off the tool it was named after. In other words, it’s a lot like running bundle exec before a command!

When we execute a binstub, we don’t need to preface it with bundle exec.

$ bin/rake -T

But of course now we’re prefacing the commands with bin/, which isn’t exactly a big improvement.

Not to mention that if we’re in a subdirectory, we have to account for that in our command invocation.

$ cd test
$ ../bin/rake -T

But now that we know we have a directory that contains the “right” executables for this project,

we can add that directory to our shell’s executable search path!

$ export PATH=`pwd`/bin:$PATH

And now we can run project commands in this shell without any prefix!

$ rake -T test
Warning: the running version of Bundler (2.1.2) is older than the version that created the lockfile (2.1.4). We suggest you to upgrade to the version that created the lockfile by running `gem install bundler:2.1.4`.
Running via Spring preloader in process 30126
rake test         # Runs all tests in test folder except system ones
rake test:db      # Run tests quickly, but also reset db
rake test:system  # Run system tests only

So that’s cool. But unfortunately it’s not comprehensive. What do I mean? Well, let’s look at our Gemfile.

There are other important command-line tools listed here. Like the rufo command for formatting Ruby code. Or the byebug debugger. We don’t have binstubs for these!

  # we want this for formatting
  gem 'rufo'
  # for intellisense in VSCode
  gem 'solargraph'
  gem 'ruby-debug-ide', require: false
  gem 'byebug', require: false

All we have are a few binstubs that Rails generated for us at the beginning.

$ ls bin
bundle  rails  rake  setup  spring  webpack  webpack-dev-server  yarn

If we’re going to execute any Ruby-based tools without a prefix, we want to be able to consistently do that with all the tools associated with our project!

Fortunately it’s easy to fill in the blanks here.

We execute the command bundle binstubs --all

$ bundle binstubs --all

When we list our bin directory again, we can see it has been considerably expanded!

$ ls bin
bundle       listen    puma     rake              ruby-parse    solargraph  tilt                yardoc
byebug       maruku    pumactl  rdebug-ide        ruby-rewrite  spring      webpack             yarn
dotenv       marutex   rackup   reverse_markdown  rufo          sprockets   webpack-dev-server  yri
gdb_wrapper  nokogiri  rails    rubocop           setup         thor        yard

Now we have binstubs for every Ruby gem executable directly or indirectly required by our Gemfile.

Which means that with the project’s bin directory still in our shell path, we can now execute commands like byebug without a prefix, secure in the knowledge that it will be found and the correct version executed.

$ byebug rails test

Of course, the next shell we start won’t have our PATH customization and we’ll have to do it over again. Fortunately there are tools out there we can use to ensure our path is correctly modified whenever we work inside our project directory. But we’ll talk about those on another day. Happy hacking!