Motivation

We build a lot of gems, both public and private. In projects involving 5 or 10 different kinds of service daemons, it is useful for both consistency and efficiency of install and updates to package each daemon in its own gem. Now anything to be reused across multiple services should also be packaged as a gem. Before you know it, you have a lot of gems in play. For example, a sub-graph of Iūdex project dependencies:

Where several of these gems are actually dependencies of other internal service gems. Some might assume this is a recipe for a speedy descent into dependency hell. I would agree that some care in dependency management is definitely required. For example, maintaining these dependency visualizations, and avoiding loops or unnecessary coupling. What is the return on this effort? Some advantages include:

The above approach is quite different from the prototypical Ruby on Rails application, deployed direct from git, and using Bundler to bring in the gem dependencies, often vendored, or effectively, statically linked. Bundler grew out of the need to overcome dependency issues, which had as root cause the failure of many gem publishers to practice semantic versioning, i.e. interface or behavioral changes in patch releases breaking your application; or lack of awareness by application developers on the need to lock down gem dependencies against potential future breaking changes, ex: the >= 0 dependency specification.

But Bundler is also particularly useful for developing gems, and managing dependencies in the development environment before release and deployment. The TarPit 2 release fully integrates with Bundler.

Multiple Gem Projects

You won’t find much written on this as a project style or Bundler use case. Rails is however one high-profile example, where several inter-dependent gems are contained in a single git-repo. Notice the the top-level Gemfile here which loads all of the internal gemspecs. In such a project, consider the common development task of making changes across multiple gems in parallel, for example: new feature and tests in gem A, with usage and more testing in gem B.

Before Bundler, you would need to gem install A after each change, for integration testing with gem B. This added a bit of tedium as well as potential for confusion on the state of your local gem repo. (Did I install that change yet or not?)

With Bundler, and in particular using a top-level common Gemfile, Bundler now arranges for gem B’s tests to load gem A directly from the git-repo itself. No install step is required, and the current state of what is being tested is as clear as the current state of your git development repo.

Warning: dependencies may not be as close as they appear

Bundler currently has a shortcoming in support for multi-gem projects: gemspec dependency omissions might not be caught when using a top-level Gemfile. See issue 1668 for full details. For this reason, in projects of this form I suggest occasionally testing with per-gem Gemfile’s, for example prior to release or when making gemspec changes. Here is one way to arrange for that without needing to maintain some sort of parallel git branch with a separate set of gemfiles:

desc "Generate per-gem Gemfiles and jbundle install each"
task :generate_gemfile_per_gem do
  gems.each do |gname|
    Dir.chdir( gname ) do
      puts "=== Gemfile: #{gname} ==="

      File.open( 'Gemfile', 'w' ) do |fout|
        fout.write <<RUBY
# -*- ruby -*-
source :rubygems
gemspec :path => '.', :name => '#{gname}'
RUBY
      end

      sh "jbundle install --path #{ENV['HOME']}/.gem --local"
    end
  end
end

Note that you likely will want to jrake install all gems to your local repo before using jrake generate_gemfile_per_gem and then jrake test to validate your gemspecs.

jbexec

Gem project bin (or init) scripts shouldn’t require Bundler, as that would make Bundler a production dependency. So for ad hoc testing in development you either need to jgem or jrake install everything as was done pre-Bundler, or get the bundle activated through some other means. The jbundle exec as provided by Bundler ends up booting the full bundle environment twice, once in an additional process. This is particularly slow on jruby.

As an alternative, the following little jbexec script placed in your bin directory is much faster and appears adequate for this use case:

#!/usr/bin/env jruby

require 'rubygems'
require 'bundler/setup'

load ARGV.shift