I have been spending the past few weeks creating and refactoring our carbon model gems, with the goal of making them easy to enhance, fix, and test by climate scientists and Ruby developers. I wanted to make contributing a simple process and bundler fit the bill quite well.
A not-so-widely-known feature of the Rubygems API is the ability to declare a gem’s development dependencies, along with its runtime dependencies. If one planned on making changes to one of the emitter gems and testing it, she could run gem install <emitter_gem> --development
and have any needed testing gems installed for the emitter gem.
This is all fine and good, but I chose to use bundler to manage our dependencies, as it adds a few extras that have been a tremendous help to us. To contribute to any of our gems, a developer can follow a simple process:
1 2 3 4 5 |
|
And Bob’s your uncle!
Bundler + Gemspecs
The first goodie that bundler provides is the ability to use the gem’s own gemspec to define the dependencies needed for development. For instance, our flight gem has a gemspec with dependencies:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
|
Instead of defining these dependencies in both flight.gemspec and in Gemfile, we can instead give the following directive in our Gemfile:
1
|
|
Bundler + Paths
We have a chain of gem dependencies, where an emitter gem depends on the sniff gem for development, which in turn depends on the earth gem for data models. In the olden days (like, 4 months ago) if I made a change to sniff, I would have to rebuild the gem and reinstall it. With bundler, I can simply tell my emitter gem to use a path to my local sniff repo as the gem source:
1
|
|
Now, any changes I make to sniff instantly appear in the emitter gem!
I had to add some special logic (a hack, if you will) to my gemspec definition for this to work, because the above gem statement in my Gemfile would conflict with the dependency listed in my gemspec (remember, I’m using my gemspec to tell bundler what gems I need). To get around this, I added an if clause to my gemspec definition that checks for an environment variable. If this variable exists, the gemspec will not request the gem and bundler will instead use my custom gem requirement that uses a local path:
1 2 3 4 5 6 |
|
1 2 |
|
So now, if I want to make some changes to the sniff gem and test them out in my emitter, I do:
1 2 3 4 5 6 7 8 9 |
|
And then Bob is my uncle.
Bundler + Rakefile
This next idea has some drawbacks in terms of code cleanliness, but I think it offers a good way to point contributers in the right direction. One thing that frustrated me about Jeweler was that if I wanted to contribute to a gem, my typical work flow went like:
1 2 3 4 5 6 7 8 |
|
I attempted to simplify this process, so a new developer who doesn’t read the README should be able to just do:
1 2 3 4 5 6 7 8 |
|
I achieved this by adding the following code to the top of the Rakefile:
1 2 3 4 5 6 7 |
|
This was convenient, but it created a chicken and egg problem: in order to generate a gemspec for the first time, bundler needed to know which dependencies it needed, which meant that it needed the gemspec, which is generated by the Rakefile, which requires bundler, which requires the gemspec, etc. etc. I overcame this problem by allowing an override:
1 2 3 4 5 6 7 8 9 |
|
So, if you’re really desparate, you can run rake test NOBUNDLE=true
More on Local Gems
Now that I had a way to easily tell bundler to use an actual gem or a local repo holding the gem, I wanted a way to quickly “flip the switch.” I wrote up a quick function in my ~/.bash_profile:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 |
|
This gives me a few commands:
1 2 3 4 5 6 |
|
I now have a well-oiled gem development machine!
Overall, after a few frustrations with bundler, I’m now quite happy with it, especially the power and convenience it gives me in developing gems.
I’m really interested to hear any of your thoughts on this. Drop me a line at @dkastner.