-
Notifications
You must be signed in to change notification settings - Fork 1.4k
Understanding binstubs
Binstubs are wrapper scripts around executables (sometimes referred to as "binaries", although they don't have to be compiled) whose purpose is to prepare the environment before dispatching the call to the original executable.
In the Ruby world, the most common binstubs are the ones that RubyGems generates after installing a gem that contains executables. But binstubs can be written in any language, and it often makes sense to create them manually.
Let's see what happens when we gem install rspec-core
. RSpec ships with an
executable located at ./exe/rspec
inside of the gem. After the
installation, RubyGems will provide us with the following executables:
-
<ruby-prefix>/bin/rspec
(binstub generated by RubyGems) -
<ruby-prefix>/lib/ruby/gems/1.9.1/gems/rspec-core-XX.YY/exe/rspec
(original)
The first file is a binstub created to wrap the second. RubyGems puts it in
<ruby-prefix>/bin
because that directory is considered to already be in our
$PATH
. (That's the job of Ruby version managers.)
The directory where RubyGems installed the second file (the original) isn't in
our $PATH
, but even if it was, it wouldn't be safe to run it directly because
executables in Ruby projects often aren't meant to be called directly without
any setup. At minimum, they require $RUBYOPT
to be set so that they can
require the source files of the project they belong to.
The generated binstub <ruby-prefix>/bin/rspec
is a short Ruby script,
presented in a slightly simplified form here:
#!/usr/bin/env ruby
require 'rubygems'
# Prepares the $LOAD_PATH by adding to it lib directories of the gem and
# its dependencies:
gem 'rspec-core'
# Loads the original executable
load Gem.bin_path('rspec-core', 'rspec')
The purpose of every RubyGems binstub is to use RubyGems to prepare the
$LOAD_PATH
before calling the original executable.
rbenv adds its own "shims" directory to $PATH
which contains binstubs for
every executable related to Ruby. There are binstubs for ruby
, gem
, and for
all RubyGems binstubs across each installed Ruby version.
When you call rspec
on the command-line, it results in this call chain:
-
$RBENV_ROOT/shims/rspec
(rbenv shim) -
$RBENV_ROOT/versions/1.9.3-pXXX/bin/rspec
(RubyGems binstub) -
$RBENV_ROOT/versions/1.9.3-pXXX/lib/ruby/gems/1.9.1/gems/rspec-core-XX.YY/exe/rspec
(original)
An rbenv shim, presented here in a slightly simplified form, is a short shell script:
#!/usr/bin/env bash
export RBENV_ROOT="$HOME/.rbenv"
exec rbenv exec "$(basename "$0")" "$@"
The purpose of rbenv's shims is to route every call to a ruby executable through
rbenv exec
, which ensures it gets executed with the right Ruby version.
When you run rspec
within your project's directory, rbenv can ensure that it
gets executed with the selected Ruby version configured for that project. However,
nothing will ensure that the right version of RSpec gets activated; in fact,
RubyGems will simply activate the latest RSpec version even if your project
depends on an older version. In the context of a project, this is unwanted
behavior.
This is why bundle exec <command>
is so essential. It ensures the right
versions of dependencies get activated, ensuring a consistent ruby runtime
environment. However, it's a pain to always have to write bundle exec
.
Bundler can install binstubs for executables contained in your project's bundle:
# generates binstubs for ALL gems in the bundle
bundle install --binstubs
# ...OR, generate binstubs for a SINGLE gem (recommended)
bundle binstubs rake
bundle binstubs rspec-core
You are encouraged to check these binstubs in the project's version control so your colleagues might benefit from them.
This creates, for example, ./bin/rspec
(simplified version shown):
#!/usr/bin/env ruby
require 'rubygems'
# Prepares the $LOAD_PATH by adding to it lib directories of all gems in the
# project's bundle:
require 'bundler/setup'
load Gem.bin_path('rspec-core', 'rspec')
RSpec can now be easily run with just bin/rspec
.
Projects that are themselves gems should use a directory other than bin/
, via a command like bundle install --binstubs exe
. If you check in bin/rspec
to your gem repo, installing your gem will break the rspec
command.
Assuming the binstubs for a project are in the local bin/
directory, you can
even go a step further to add the directory to shell $PATH
so that rspec
can
be invoked without the bin/
prefix:
export PATH="./bin:$PATH"
However, doing so on a system that other people have write access to (such as a
shared host) is a security risk.
For extra security, you can make a script/shell function to add only the current
project's bin/
directory to $PATH
:
export PATH="$PWD/bin:$PATH"
hash -r 2>/dev/null || true
The downside of the more secure approach is that you have to execute it per-project instead of setting it once globally.
See also: direnv.
Now that you know that binstubs are simple scripts written in any language and understand their purpose, you should consider creating some binstubs for your project or your local development environment.
For instance, in the context of a Rails application, a manually generated
binstub to run Unicorn could be in ./bin/unicorn
:
#!/usr/bin/env ruby
require_relative '../config/boot'
load Gem.bin_path('unicorn', 'unicorn')
Using bin/unicorn
now ensures that Unicorn will run in the exact same
environment as the application: same Ruby version, same Gemfile dependencies.
This is true even if the binstub was called from outside the app, for instance
as /path/to/app/current/bin/unicorn
.