michael orlitzky

Decoupling Rails and Passenger from Bundler

Background

Package management is a solved problem. All modern Linux distributions come with package managers (e.g. Portage, APT, RPM) that are capable of resolving dependencies and preventing conflicts. They manage every piece of software that is installed on the system, and make sure that it all works together.

Kids with Macbooks love to reinvent this wheel, ignoring the lessons of the past. Most programming languages have their own “package manager” (written in the language) which solves the easy part of package management while ignoring the hard parts. Examples of these include,

If you're on a system with a real package manager, these are all pointless. But people who don't know any better insist on using them to distribute their software. To be fair, if you want other people with Macbooks to install your stuff, you don't really have a choice. And they do provide a standard format to specify dependencies, as long as you don't try to enforce them outside of a real package manager.

The Problem

The problem this causes is that you have dependencies specified in two different places. First, in the package manager. And then locally in whatever file the pseudo-package-manager uses to declare such things. We can take as an example my own mailshears package. Portage uses a file called an ebuild to declare a package's dependencies. In the ebuild for mailshears, I have,

1
RDEPEND=">=dev-ruby/pg-0.11.0"

Which states a dependency on ‘pg’ version 0.11.0 or greater. But I also ship a gemspec file containing,

1
s.add_runtime_dependency 'pg', '>= 0.11.0'

This is redundant for anyone with a real package manager, and can lead to conflicts when the two don't agree (and they occasionally won't). Our problem of choice is Bundler attempting to manage gems for Rails websites that happen to be running on Passenger. If you're not using Passenger, then you can stop paying attention halfway through.

How it Works out of the Box

Rails 3.x uses Bundler to make sure that all of the necessary gems for a website are installed. You declare your dependencies in a file called Gemfile in the root of your web application. For example,

user $ cat Gemfile

gem 'rails'

gem 'pg'

gem 'sqlite3'

Rails then loads up Bundler to read this junk in its config/boot.rb:

1
2
3
4
5
6
require 'rubygems'

# Set up gems listed in the Gemfile.
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)

require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])

What's more, Passenger also integrates with Bundler. See the Passenger user guide for more information.

The end result is that if there's a problem with Bundler (which is useless for people with a real package manager!), then the Rails app is going to die. We don't need Bundler, so we don't want our Rails apps to depend on it.

Removing the Integration

Separating them is fairly easy for custom applications. First, comment out or delete the Bundler code in Rails' config/boot.rb:

1
2
3
4
5
6
7
8
# Disregard Bundler nonsense.
#
#require 'rubygems'

# Set up gems listed in the Gemfile.
#ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)

#require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])

Now, without Bundler loading your gems, you're going to have to do it yourself. Look in your Gemfile, and figure out which gems you're installing. We'll reuse the example above:

user $ cat Gemfile

gem 'rails'

gem 'pg'

gem 'sqlite3'

Now, remove each line from the Gemfile, and move it into Rails' config/application.rb, right after the line where Bundler would have been invoked. Change gem to require.

1
2
3
4
5
6
# If you have a Gemfile, require the gems listed there, including any gems
# you've limited to :test, :development, or :production.
Bundler.require(:default, Rails.env) if defined?(Bundler)

require 'pg'
require 'sqlite3'

Next, create a blank file in config/setup_load_paths.rb to prevent Passenger from invoking Bundler:

user $ echo "# Ignore Bundler." > config/setup_load_paths.rb

Finally, get rid of the old Gemfiles.

user $ rm Gemfile Gemfile.lock

Now if you restart Apache, you should be unbundled.

Addendum: Off-the-shelf Apps

If you're using off-the-shelf Rails applications like Redmine, you're probably screwed here. If you attempt to deintegrate Bundler, then you're going to be stuck moving the gems from Gemfile into config/application.rb every time you update the app. It probably isn't worth the trouble.