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:
A patch release for a critical fix to a service daemon, or a single shared gem used by many daemons, does not expand into the need to release everything all at once. (The implication here is, yes, we are using semantic versioning and allowing controlled patch updates in production.)
Testing updates is similarly much less invasive to arrange in your development environment.
Efficiency of upgrade over the network: Deploying an update does not involve pushing monolithic 10’s of megabytes of statically linked software from your development hosts to production. Instead rubygems figures our what incremental gem updates are actually required, dynamically.
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.
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.
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.
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