Upgrading Engine Plugins to work with Rails 3

Mar 21, 2010

I've just finished upgrading an application to run on Rails 3, and for the most part it's been a relatively pain free process. I refused to use the rails upgrade plugin just so that I'd be forced to work through the problems and experience first hand what has changed. It turns out, not a huge amount. The most significant change though was the need to refactor my Rails 2.3 Engine plugins to work with Rails 3 as a gem.

Creating a gem

There are countless ways to create a ruby gem these days but the problem is that they are all pretty bat-shit mental. Kudos to James (coincidentally the very same James Adam who created Rails Engines) for trying to make people wake up and realise that creating a gem really isn't that hard. If you've already got experience in doing it some way or another you can skip this bit, otherwise follow along and discover how simple it is to roll one by hand.

First you need to create yourself a gemspec file, place it in the root directory of your engine:

# my_rails_engine.gemspec
Gem::Specification.new do |s|
  s.version = '0.0.1'
  s.name = "my_rails_engine"
  s.files = Dir["lib/**/*", "app/**/*", "config/**/*"]
  s.summary = "A summary about my engine"
  s.description = "Some more details about what my engine will do"
  s.email = "glenn@rubypond.com"
  s.homepage = "http://rubypond.com"
  s.authors = ["Glenn Gillen"]
  s.test_files = []
  s.require_paths = [".", "lib"]
  s.has_rdoc = 'false'
  if s.respond_to? :specification_version then
    current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
    s.specification_version = 2
    if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
      s.add_runtime_dependency('another_gem_here', '>= 0.4')
    else
      s.add_dependency('another_gem_here', '>= 0.4')
    end
  else
    s.add_dependency('another_gem_here', '>= 0.4')
  end
end

Most of the fields are relatively self explanatory. The if s.respond_to? block could potentially be removed. It's there to add in dependencies in a way that will work with various versions of rubygems, but given this is a Rails 3 Engine, which requires Bundler, which is in turns dependent on rubygems 1.3.6+ then you could assume you've at least got that and only specify dependencies using add_runtime_dependency.

Next up, create a Rakefile in the same directory to help package up the gem for you:

# Rakefile
require 'rubygems'
require 'rake'
require 'rake/gempackagetask'

spec = nil
File.open('my_rails_engine.gemspec', 'r'){|f| spec = eval(f.read)}
Rake::GemPackageTask.new(spec) do |pkg|
  pkg.need_zip = false
  pkg.need_tar = false
end

Now from the command line you can type:

rake gem

And congratulations! You just created your first gem (it's in pkg/my_rails_engine-*.gem) without the need to download all sorts of generators or other packages. It wasn't that hard was it?

Migrating a Rails 2.3 Engines Plugin to a Rails 3 Engines Gem

I'll admit right now that I've not dug too deep into the internals of Rails 3 or the new engines/railtie setup yet. But to me they're already feeling a lot more like merb slices and django apps than just a plugin. They're basically a rails application in their own right, complete with routes, initializers, locales, and the models/views/controllers that you'd expect. That also means they carry many of the same expectations in terms of directory layout and the existence of certain files. There's a number of initializers that need to be available for the Railtie/Engine to load, so I simply created a new Rails 3 project elsewhere and copied the relevant files over.

rails my_new_rails3_app
cp -r my_new_rails3_app/config/initializers my_rails_engine/config

If you have any routes in your engine, it's worth updating those too. They need to live in the /config/routes.rb file within you engine and should be amended to use the new syntax for defining them (read the comments in routes.rb in the my_new_rails3_app we made before to see examples).

And finally, we need to remove the existing init.rb file as it's no longer used. Instead create /lib/my_rails_engine.rb with something like the following:

require "rails"
module MyRailsEngine
 class Engine < Rails::Engine
    engine_name :my_rails_engine
  end
end

For some reason the best documentation I can find at present on configuring your engine appears to live in this gist. To summarise the most important bit, you can override the path to various options like so:

class MyRailsEngine < Rails::Engine
  paths.app                 = "app"
  paths.app.controllers     = "app/controllers"
  paths.app.helpers         = "app/helpers"
  paths.app.models          = "app/models"
  paths.app.metals          = "app/metal"
  paths.app.views           = "app/views"
  paths.lib                 = "lib"
  paths.lib.tasks           = "lib/tasks"
  paths.config              = "config"
  paths.config.initializers = "config/initializers"
  paths.config.locales      = "config/locales"
  paths.config.routes       = "config/routes.rb"
end

Installing your Rails Engine within your Rails App

Now that we've got our Engine refactored into a gem, it's time to get it working within our app. First thing, it's no longer a plugin so if you've left it in it's previous location (/vendor/plugins) you need to move it out. You could of course run rake gem as we did early, then gem install it onto your system, and finally include it with Bundler with a line like the following in your Gemfile:

gem "my_rails_engine"

Alternatively, and probably useful at least during development, you can specify the path to the gem within your bundle like so:

gem "my_rails_engine", :path => "lib/my_rails_engine"

And you're done. Start your app up, and start debugging any other problems you might have. I know for my engine I had to update both HAML and Devise to be the latest pre-releases, so it would be worth checking if you need to do the same.

Hi, I'm Glenn! 👋 I've spent most of my career working with or at startups. I'm currently the Director of Product @ Ockam where I'm helping developers build applications and systems that are secure-by-design. It's time we started securely connecting apps, not networks.

Previously I led the Terraform product team @ HashiCorp, where we launched Terraform Cloud and set the stage for a successful IPO. Prior to that I was part of the Startup Team @ AWS, and earlier still an early employee @ Heroku. I've also invested in a couple of dozen early stage startups.