Blog header transparent bg

Bundler template moves bins to exe

by Benjamin Fleischer on

Bundler 1.8 moves the executables directory in generated gemspecs from bin/ to exe/.

-  spec.executables   = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
+  spec.executables   = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }

This means that the Bundler-generated gems can use and commit binstubs, such as bin/rake, to the bin/ directory. Only files in the exe/ directory will be built with the gem. Prior to this change, we would need to either not commit binstubs or change the gemspec not to include all files in bin as executables.

There’s nothing that needs to be done for existing gems. To modify an existing gem to use this convention, we only need to move the executable(s), if any, into exe/, and modify the gemspec executables directory to exe/, as above.

Using the exe/ directory for gem executables frees up bin/ to be used for bundle binstubs rspec-core; bin/rspec and other libraries that have adopted this convention, such as Rails, which installs the scripts bin/rails, bin/rake, and bin/setup with all generated apps.

Background

This is a new convention. The current practice of both specifying bin/ as the executables directory and where we put binstubs and other development-only executables such as bin/rails or bin/setup, meant that Bundler-generated gems with executables were quite likely to have these development executables included in the built gem, and then installed along with the gem.

Rather than make the gemspec template more restrictive by only specifying one executable in bin/ as an executable, the Bundler team has chosen to use a different directory, exe/, as the executables directory in the template.

This change is just part of the evolving conventions in gem development. RSpec, for example, has had its executable in exe since 2011.

Here’s an example of the kind of buggy behavior you might see when binstubs are build with a gem as executables.

Demo

bundle gem new_gem
git add .; git commit -am "Initial Commit"
bundle binstub rake
git add bin/rake; git commit -am "bundle binstub rake"
bundle
which rake #=>  ~/.rvm/gems/ruby-2.1.5/bin/rake
cat `which rake`

and

#!/usr/bin/env ruby_executable_hooks
#
# This file was generated by RubyGems.
#
# The application 'new_gem' is installed as part of a gem, and
# this file is here to facilitate running it.
#

require 'rubygems'

version = ">= 0"

if ARGV.first
  str = ARGV.first
  str = str.dup.force_encoding("BINARY") if str.respond_to? :force_encoding
  if str =~ /\A_(.*)_\z/ and Gem::Version.correct?($1) then
    version = $1
    ARGV.shift
  end
end

gem 'new_gem', version
load Gem.bin_path('new_gem', 'rake', version)

The problem is found in the last two lines:

gem 'new_gem', version
load Gem.bin_path('new_gem', 'rake', version)

The rake executable no longer uses the rake gem. It now requires rake from new_gem.

Now, when I run rake elsewhere, I get the warning:

Bundler is using a binstub that was created for a different gem.
This is deprecated, in future versions you may need to `bundle binstub new_gem` to work around a system/bundle conflict.

This happened because of the line in the gemspec that installed all git-versioned files in bin/:

  spec.executables   = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }

To fix our rake install:

gem pristine rake
# (In your gem folder, you may also want to run `bundle exec gem pristine rake`)
cat `which rake`

and

#!/usr/bin/env ruby_executable_hooks
#
# This file was generated by RubyGems.
#
# The application 'rake' is installed as part of a gem, and
# this file is here to facilitate running it.
#

require 'rubygems'

version = ">= 0"

if ARGV.first
  str = ARGV.first
  str = str.dup.force_encoding("BINARY") if str.respond_to? :force_encoding
  if str =~ /\A_(.*)_\z/ and Gem::Version.correct?($1) then
    version = $1
    ARGV.shift
  end
end

gem 'rake', version
load Gem.bin_path('rake', 'rake', version)

To fix our gemspec:

  spec.executables   = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }

Or just delete the line altogether, since new_gem doesn’t have an executable.

If it did, we could continue to use bin/, create the file as bin/new_gem and specify it as the only executable.

  spec.executables   = "new_gem"

Or use the new exe/ convention, create a file such as exe/new_gem, and not worry.

Enjoy!