Skip to content
Lucas Caton

Have a Rails 2 app? You can run it on the newest Ruby!

Lucas Caton

Lucas Caton

@lucascaton
Do you have a legacy app still running on Rails 2?
There are several reasons to migrate your application to a newer version of Rails: improving security, using a better syntax, taking advantage of new features, and ensuring compatibility with most current gems, which typically require Rails 3 or higher. However, this process can be challenging, especially for large projects. It's worth noting that many projects are still running on Rails 2 today (in 2014).
But there's one important thing you can (and should) do! I'm talking about using the newest Ruby version. Yes, I'm serious. When I wrote this post, the latest Ruby version was 2.1.1, and it's not too difficult to get it working smoothly with Rails 2.
Obviously, it's better if you have good test coverage.
That said, let's break it down into a few steps:

Replacements

  • Gemfile
Rails 2 apps don't use Bundler by default. If you're not already using Bundler to manage your gems, you should check this guide to set it up.
ruby
## There's no way to know for sure if future Ruby versions will work,
## but so far the current one does:
ruby "2.1.1"

## The same for the `rake` gem:
rake "10.1.1"

## You might need the `iconv` gem:
gem "iconv"
  • Rakefile
ruby
## Replace:
## require "rake/rdoctask"
## with:
require "rake/task"
  • config.ru
ruby
## Replace:
## require "config/environment"
## with:
require File.dirname(__FILE__) + "/config/environment"
  • FasterCSV âž™ CSV
Replace all FasterCSV constants with CSV. Also, include require "csv" to relevant files (or include the require to your config/environment.rb file).

Inclusions

  • config/environment.rb
ruby
## Include this before the `Rails::Initializer.run` line:
if RUBY_VERSION >= "2.0.0"
  module Gem
    def self.source_index
      sources
    end
    def self.cache
      sources
    end
    SourceIndex = Specification
    class SourceList
      # If you want vendor gems, this is where to start writing code.
      def search(*args); []; end
      def each(&block); end
      include Enumerable
    end
  end
end
  • config/initializers/paperclip.rb
ruby
## The patches below are needed when using an old version of PaperClip + Ruby 2.x
## https://github.com/thoughtbot/paperclip/issues/262
## https://github.com/thoughtbot/paperclip/commit/1bcfc14388d0651c5fc70ab9ca3511144c698903

module Paperclip
  class Tempfile < ::Tempfile
    def make_tmpname(basename, n)
      extension = File.extname(basename)
      sprintf("%s,%d,%d%s", File.basename(basename, extension), $$, n.to_i, extension)
    end
  end
end

module IOStream
  def to_tempfile
    name = respond_to?(:original_filename) ? original_filename : (respond_to?(:path) ? path : "stream")
    tempfile = Tempfile.new(["stream", File.extname(name)])
    tempfile.binmode
    self.stream_to(tempfile)
  end
end

New files

  • config/initializers/ruby2.rb
ruby
## This is a very important monkey patch to make Rails 2.3.18 to work with Ruby 2+
## If you're thinking to remove it, really, don't, unless you know what you're doing.

if Rails::VERSION::MAJOR == 2 && RUBY_VERSION >= "2.0.0"
  module ActiveRecord
    module Associations
      class AssociationProxy
        def send(method, *args)
          if proxy_respond_to?(method, true)
            super
          else
            load_target
            @target.send(method, *args)
          end
        end
      end
    end
  end
end
  • config/initializers/rails_generators.rb
It'll prevent Rails migration generator from stop working, otherwise you'll receive the following error message:
ruby
undefined local variable or method `vars' for # Rails::Generator::Commands::Create
(Thanks to Mr. S and jnwheeler44 for helping me to fix this one)
ruby
## This is a very important monkey patch to make Rails 2.3.18 to work with Ruby 2+
## If you're thinking to remove it, really, don't, unless you know what you're doing.

if Rails::VERSION::MAJOR == 2 && RUBY_VERSION >= "2.0.0"
  require "rails_generator"
  require "rails_generator/scripts/generate"

  Rails::Generator::Commands::Create.class_eval do
    def template(relative_source, relative_destination, template_options = {})
      file(relative_source, relative_destination, template_options) do |file|
        # Evaluate any assignments in a temporary, throwaway binding
        vars = template_options[:assigns] || {}
        b = template_options[:binding] || binding

        # this no longer works, eval throws "undefined local variable or method `vars'"
        # vars.each { |k, v| eval "#{k} = vars[:#{k}] || vars['#{k}']", b }
        vars.each { |k, v| b.local_variable_set(:"#{k}", v) }

        # Render the source file with the temporary binding
        ERB.new(file.read, nil, "-").result(b)
      end
    end
  end
end

RSpec

  • Make sure you're using the last compatible version with Rails 2.3.18:
ruby
gem "rspec", "1.3.2"
gem "rspec-rails", "1.3.4"
  • Remove the file script/spec
  • Remove the following lines from the lib/tasks/rspec.rake file:
ruby
gem "test-unit", "1.2.3" if RUBY_VERSION.to_f >= 1.9

rspec_gem_dir = nil

Dir["#{Rails.root}/vendor/gems/*"].each do |subdir|
  rspec_gem_dir = subdir if subdir.gsub("#{Rails.root}/vendor/gems/","") =~ /^(\w+-)?rspec-(\d+)/ && File.exist?("#{subdir}/lib/spec/rake/spectask.rb")
end

rspec_plugin_dir = File.expand_path(File.dirname(__FILE__) + "/../../vendor/plugins/rspec")

if rspec_gem_dir && (test ?d, rspec_plugin_dir)
  raise "\n#{'*'*50}\nYou have rspec installed in both vendor/gems and vendor/plugins\nPlease pick one and dispose of the other.\n#{'*'*50}\n\n"
end

if rspec_gem_dir
  $LOAD_PATH.unshift("#{rspec_gem_dir}/lib")
elsif File.exist?(rspec_plugin_dir)
  $LOAD_PATH.unshift("#{rspec_plugin_dir}/lib")
end

Ruby syntax

  • Ruby syntax has changed a bit, especially from 1.8.x to 1.9.x.
ruby
# Replace:
when "foo": bar

# with:
when "foo" then bar

The behaviour for protected methods in new Ruby versions is a little bit different. See more in this post.
ruby
# In some cases, you might need to replace:
respond_to?(:foobar)

# with:
respond_to?(:foobar, true)

ruby
# Replace:
order: [:day, :month, :year]

# with:
order:
  - :year
  - :month
  - :day

Ruby changes

  • The default encoding for Ruby 2.0 (or higher) is UTF-8
Remove all the code similar to:
ruby
# encoding: utf-8
Or:
ruby
$KCODE = "UTF-8"

Update on July 27th, 2014

Check out the insightful comments below by Gabriel Sobrinho, Kyle Ries, and Greg. They offer valuable insights!

Conclusion

Different project might have different issues, but I hope this little guide helps you to use new Ruby versions on your legacy Rails applications!

Post updated at 13/01/2025, 07:00:00