bundle exec muscle memory and get back to the good old days… with the help of binstubs!
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.
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.
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 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
#!/usr/bin/env ruby begin load File.expand_path('../spring', __FILE__) rescue LoadError => e raise unless e.message.include?('spring') end require_relative '../config/boot' require 'rake' Rake.application.run
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
$ 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
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
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!