Monday, July 5, 2010

Install a Rails App's dependencies where 'rake gems:install' fails.

Using rake gems:install to install the gem dependencies of your Rails 2 app just doesn't seem to work. There are a number of problems you may encounter:

Problem 1: It depends on Rails.
You have to manually install Rails before you can use rake gems:install. Ok, so this might not be that big of a deal, but consider this scenario:

  • Your app is running in production on rails 2.3.4.
  • You upgrade your app to rails 2.3.5, including the RAILS_GEM_VERSION.
  • You deploy your app.
  • Your deployment script calls rake gems:install to keep the dependencies up to date with each new release.
  • That fails with something that looks like:

Problem 2: Sometimes, for some reason, it just doesn't work.
Sometimes (most times) you add a new config.gem command to environment.rb. The next deployment runs rake gems:install and it fails with the complaint that you are missing the very gem you are hoping it will install for you. For example, in a working Rails 2.3.5 project, add this line to environment.rb:

config.gem 'sanitize', :version => '1.2.1'

Then, run rake gems:install, and it fails with a "Missing these required gems:" message, e.g.:

WTF? It's telling me to run the command I just ran, to install the missing gem that's preventing that command from running.

Problem 3: It depends on your Rails environment.
This can create a circular dependency where some file (particularly vendored plugins/gems) can require a gem to be loaded before the task to install it runs. For example, consider that some file in your vendor directory does this:

require 'gdata'

Then naturally, you add this to environment.rb:

config.gem 'gdata'

The next time you run rake gems:install, it will fail with a "no such file to load" error, e.g.:

The Solution: rails_gem_install
I have created a new tool called rails_gem_install to help alleviate this problem. Use this instead of rake gems:install, and you should be much more successful at getting all your Rails app's dependencies installed without manual intervention.

To install:

gem install rails_gem_install

To use:

cd my_rails_app
RAILS_ENV=production rails_gem_install

This will install all the gems required to run your app in the production environment, including Rails. Replace rake gems:install in your deployment scripts with rails_gem_install, and as you change config.gem requirements, those gems will be properly installed.

See the github project page for more details.

How It Works
The main principle behind this tool is that it does not depend on Rails. It creates its own module named Rails that provides some of the functionality from the real Rails that it needs, up until the point that Rails is installed. Then, it only loads some very specific parts of Rails that implement the gem dependency and installation mechanisms, without actually running the app's Rails::Initializer and loading all of its environment, plugins, etc.

First, it runs a simple rake -T to see if it complains about Rails missing. It parses the output of this, and if necessary, installs the indicated version of Rails.

Next, it parses out the config.gem statements and uses those to ensure all the listed gems requirements are met, and installs those that aren't. It does this without actually loading Rails or the app environment. This carries us most of the way.

Finally, it runs the rake gems command and parses its output to detect all the kinds of errors described above and install the corresponding gems. This step is repeated until rake gems does not complain anymore.

Thursday, July 1, 2010

Hudson CI behind an Nginx Reverse Proxy with SSL

Here is a quick example nginx configuration to reverse proxy on an HTTPS virtual host to a Hudson CI server running on localhost. When I first tried to do this, the management page displays an error about the configuration being wrong. There are instructions for Running Hudson behind Apache that were helpful, and this email thread that seems to suggest terminating SSL at Hudson, not at the reverse proxy. Well, after a bit of tinkering, I worked out this configuration for nginx that worked out great: