Keeping Up With The Joneses: Keeping Rails and its extensions up to date


This has nothing to do with anything. I just like Kermit.

Sometimes, I write these things so that six months from now, when I've completely forgotten a crucial part of this post, at least I can find it from the inevitable Google search.

This post is the practical companion to my post about a month ago about keeping up with Edge Rails and new versions. There are many wonderful things about Rails and the Rails ecosystem. A clean, well-lighted path for keeping all your extensions up to date is not one of them.

I should say up front that there are a number of mechanisms for automating or semi automating the update process by linking directly to the external source repository. These tools would include git submodules, subversion externals, a separate program like Braid, and so on. I haven't found a tool in this space that I would recommend. I've found them all kind of finicky to set up and/or run. Plus, I like the idea of having all the extensions in my source tree in sync with my actual code -- I feel like that gives me a better chance of dialing back to a specific point in the code and having all the right pieces in place.

Plugins and Gems

Rails has two separate, but equally important, extension mechanisms, gems and plugins. The plugin system is Rails-specific, while gems use the pre-existing Ruby packaging system, but with the option of inserting the gem inside your Rails application. Explicit support for gems was added in Rails 2.1, replacing speculation about why Rails needed it's own unique package mechanism with the reality of making Rails developers manage two parallel packaging tools with different functionality. For myself, I'm not completely convinced it was a good trade.

Anyway, plugins live in the vendor/plugins directory, and have the following advantages over gems.

  1. The plugin structure is slightly simpler then the gem structure.
  2. Plugins have a dedicated update command within Rails.
  3. Plugins have a better-defined set of hooks into the Rails application life cycle (install, init, etc.). My understanding, which I'm not prepared to defend with my life, is that Rails automatically looks for an init.rb for gems on startup, but I'm not sure that's an explicitly defined API -- if I'm wrong, let me know.

Plugins are installed using the Rails commandscript/plugin install REPOSITORY, where the repository is (in the most common case) the URL of the plugin's public code repository.

Keeping plugins up to date is simple -- script/plugin update will check each plugin repository and grab the current version. Plugins aren't versioned or published the same way that gems are, so you are perhaps a little more at the mercy of the update tendencies of the plugin maintainer.

Gems, on the other hand, live in the vendor/gems directory if they are part of the Rails application source tree, otherwise they live somewhere in Ruby's gem path. Gems have the following advantages over plugins:

  1. Gems have a much more robust versioning and dependency mechanism.
  2. Rails allows you to specify the list of gems that must be available in order for the application to start.
  3. Gems can be more easily placed in a common location and shared among multiple applications.
  4. Gems are not limited to Rails, the same gem could be used in a different program, or to support a different framework

If there's a principled reason to distribute a Rails extension as a gem or a plugin, I don't know what it is, and right now, many extensions are offered both ways.

Gems are traditionally installed from the command line using gem install GEMNAME. The gems application maintains a list of sources, of which the most important are RubyForge and GitHub, but the source can be overridden if you want to, say, install a pre-release version of the Rails gem directly from the Ruby on Rails server.

Getting gems into your Rails project has a couple of steps. The first is to declare the gems in your environment.rb file. Here's an example:

  config.gem "rubyist-aasm", :lib => "aasm",
      :source => ""
  config.gem "flexmock"
  config.gem 'thoughtbot-shoulda', :lib => 'shoulda/rails',
      :source => ""
  config.gem "quietbacktrace"
  config.gem "chronic"
  config.gem "andand"
  config.gem "timecop"
  config.gem 'jeremymcanally-matchy', :lib => 'matchy',
      :source => ""
  config.gem 'jeremymcanally-context', :lib => 'matchy',
      :source => ""
  config.gem 'jscruggs-metric_fu', :version => '0.9.0',
      :lib => 'metric_fu', :source => ''

Note that the GitHub gems have library names that are different from the gem names -- this is to accommodate multiple forked versions of the same gem.

Once the gems are declared, you can force installation of them with the Rake task rake gems:install. It seems to me, though, that the typical installation is more robust, especially with gems that have native extensions. Once installed, the gems can be moved into your Rails application using one of the commands rake gems:unpack or rake gems:unpack:dependencies. The latter command includes any other gem that is listed as a dependency of the gems you have listed. I recommend doing this, although it will gunk up your vendor/rails directory -- some gems have dependencies on tools for their own development or testing (like hoe) that aren't strictly needed at run time. Still, having all the dependencies in your application will insulate you from whatever crazy stuff is or is not installed on the deploy server.

There doesn't seem to be a clean way to update the vendor/gems items. As far as I can tell, the recommended way is to update the :version option of the config.gem, then reinstall the gem and rerun one of the unpack commands. This might leave you with both the old and new gems in your vendor/gems directory. On the plus side, at least gems have a nice version numbering system for tracking updates.

Rails Itself

There are a few distinct issues involved in managing Rails versions.

First off, is a problem that has really bugged me for years. I want to create a new Edge Rails project, but I don't have Edge Rails gems, and therefore don't have the Edge rails command. This blog post has a way of doing it by installing Rails into the empty directory first, but it assumes you want Rails as a submodule. Normally, I don't, so I'd modify the instructions as follows:

[~]$ mkdir example
[example]$ mkdir vendor
[example]$ mkdir vendor/rails
[example]$ git clone git:// vendor/rails
[example]$ rm -rf vendor/rails/.git
[example]$ ruby vendor/rails/railties/bin/rails .
[example]$ git init
## set up your .gitignore here
[example (master)]$ git add .
[example (master)]$ git commit -am "getting started"

This gets Rails from GitHub, removes the .git source control so that Rails can be included in your source control (again, that's a personal preference), runs the rails command to get the skeleton app, then saves the whole thing to a new git repository.

Even without maintaining the entire Rails .git repository, it's still pretty easy to keep up to date. To update your Rails application to the latest edge, run the Rake command rake rails:freeze:edge, which will download a tarball of the Rails edge as of the end of the previous day. In addition, the command will perform a rake rails:update, which updates the javascripts, command scripts, and boot.rb file. But note that you can still get in trouble if changes are made to the expected environment.rb file, which is not part of this update.

If you want to move your Rails to a different version, that's easy as well. To pick an example at random, let's say that Rails 2.3 RC 2 was released, and you'd like to update your site to that release, rather than Edge. The Rake command changes slightly: rake rails:freeze:edge RELEASE=2.3.1, where 2.3.1 is the GitHub tag for the release. This will also run the rails update command.

I hope this helps you. And to the me of six months later that's forgotten this already: Hi. You have a dentist appointment on Tuesday.