Understanding binstubs

Austin Kabiru edited this page Jan 3, 2017 · 14 revisions

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.

RubyGems

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:

  1. <ruby-prefix>/bin/rspec (binstub generated by RubyGems)
  2. <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

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:

  1. $RBENV_ROOT/shims/rspec (rbenv shim)
  2. $RBENV_ROOT/versions/1.9.3-pXXX/bin/rspec (RubyGems binstub)
  3. $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.

Project-specific binstubs

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-generated binstubs

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.

Adding project-specific binstubs to PATH

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.

Manually created binstubs

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.