Ruby 1.9 + Rails 2.3If you’re like me, you’ve got a Rails 2.3.x application running on Ruby 1.8.x – or perhaps Ruby Enterprise Edition. Well, that’s all fine and dandy, but Ruby 1.9.2 is about twice as fast as even REE 1.8.7. So, you’ll probably want to upgrade to the new version of Ruby.

Problem is, you upgrade Ruby and then your Rails 2.3 app starts whining about character encoding problems. Heck, it might not even run! So you search the net in vain trying to find a simple, step-by-step explanation of what you need to do to make your app run with the speedy 1.9.x version of Ruby. And, you pretty much come up empty handed.

Until now!

Here I shall endeavor to outline all the steps necessary to install and convert your Rails 2.3.x app to work flawlessly with Ruby 1.9.x.

Just a note before we begin: I’m assuming that you’re smart, and therefore you’re using UTF-8 for your app. If you’re not, you’ll have to modify some of the code in this article to use your preferred encoding. If you want to convert your app to be fully UTF-8, see the link to my relevant post later in this article! Ruby 1.9 itself defaults to US-ASCII. Don’t ask why…

Alrighty, first step is to install the new Ruby. Thanks to a lovely post on Neurons to Bytes, it’s a piece of cake. These instructions are for Ubuntu, but for other distros you can probably figure everything out. I modified them slightly from the post on Neurons to Bytes.

First you have to install a bunch of stuff just to compile Ruby. You might have this stuff already, but try the command anyway just in case:

sudo apt-get install gcc g++ build-essential autoconf openssl libssl-dev libssl1.0-dev bison libreadline-gplv2-dev zlib1g-dev linux-headers-generic 

Then you’ll go into some directory (like /tmp or something) and run the following commands as root to download, decompress, and build Ruby. It will live in /usr/local/ruby/:

$ wget ftp://ftp.ruby-lang.org/pub/ruby/1.9/ruby-1.9.2-p180.tar.gz
$ tar -xvzf ruby-1.9.2-p180.tar.gz
$ cd ruby-1.9.2-p180/
$ ./configure --prefix=/usr/local/ruby
$ make
$ make install

Next, add your new Ruby to the path:

$ sudo pico /etc/environment

You’ll need to add the path /usr/local/ruby/bin/ at the front of the PATH variable like so:

PATH="/usr/local/ruby/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games"

Then run:

source /etc/environment

Right, that was easy. Now, the ruby and gem commands will be run from your new Ruby 1.9.2 install. Rock on.

The next step is to reinstall all the gems you need for your Rails app. Be careful – you may need to update to a newer version of some gems to make sure they are Ruby 1.9-friendly. To find out what version of a gem you need, visit IsItRuby19.com. Pretty easy, eh? I have found that all of my app’s gems worked just fine even if they weren’t the latest and greatest version. Then again, I tend not to rely on many gems since quite often, they are not coded very well. So, I tend to roll my own solutions for things since I’m a stickler for performance!

If you are using Phusion Passenger and you try to reinstall the gem and it says:

OpenSSL support for Ruby... not found

Fear not! You need to go into a particular source directory in the Ruby 1.9 extracted files (/tmp/ruby-1.9.2-p136/ext/openssl/ in our case) and do the following:

  1. ruby extconf.rb
  2. make
  3. make install

Then give the Passenger install another whirl, and you should be good to go.

Anyway, next up: If you are using the mb_chars.normalize() method in your Rails app to normalize accented chars in a text string (to make UTF-8 characters like “é” become “e”) like so:

def self.noaccents(txt)
  return txt.mb_chars.normalize(:kd).gsub(/[^\x00-\x7F]/n,'')
end

Then you’ll want to change it to this:

require "unicode_utils/nfkd"
def self.noaccents(txt)
  return UnicodeUtils.nfkd(txt).gsub(/[^\x00-\x7F]/,'')
end

And then of course you’ll have to also do: gem install unicode_utils

Be advised that requiring ALL of UnicodeUtils is a very bad idea – it’s big!

So, this brings us to the next quirk of Ruby 1.9.x: the changes in regular expressions! If you are using lots of regex in your app, you’ll need to comb through it and change certain regular expressions so that things like this:

# Ruby 1.8 regex
txt.gsub!(/(\xe2\x80(\x9c|\x9d)/u, '"')

become like this:

# Ruby 1.9 regex
txt.gsub!(/(\xe2\x80\x9c|\xe2\x80\x9d)/u, '"')

In other words, you have to “spell out” each sequence of hex codes. Look carefully at the above two snippets again. They both say the same thing, but the second way is the proper Ruby 1.9 way. You CANNOT use the shortcut way that says “match \xe2 followed by \x80 followed by \x9c OR \x9d”. You’ll get an error if you don’t change the regex in the first snippet.

Okay, now we get to character encodings. If you’ve tried to convert your Rails app already, you know that Ruby 1.8 doesn’t do jack squat with character encodings. In contrast, Ruby 1.9 insists that all strings have a character encoding. Of course, in Rails 3, they made it so that “it just works” with Ruby 1.9. Problem is, they didn’t do the same for Rails 2.3.x. So, you have 2 options:

  1. Go through your app and add “# encoding: utf-8” to the first line of every single file in your app
  2. Use the monkey patch below, and then only add the above encoding string to some helpers, models and maybe a few other files

So, here’s the easy way. Create the file initializers/aa_ruby_19_patch.rb and paste this code into it:

# encoding: utf-8
if Gem::Version.new(''+RUBY_VERSION) >= Gem::Version.new("1.9.0")

  # Force MySQL results to UTF-8.
  #
  # Source: http://gnuu.org/2009/11/06/ruby19-rails-mysql-utf8/
  require 'mysql'

  class Mysql::Result
    def encode(value, encoding = "utf-8")
      String === value ? value.force_encoding(encoding) : value
    end

    def each_utf8(&block)
      each_orig do |row|
        yield row.map {|col| encode(col) }
      end
    end
    alias each_orig each
    alias each each_utf8

    def each_hash_utf8(&block)
      each_hash_orig do |row|
        row.each {|k, v| row[k] = encode(v) }
        yield(row)
      end
    end
    alias each_hash_orig each_hash
    alias each_hash each_hash_utf8
  end

  #
  # Source: https://rails.lighthouseapp.com/projects/8994/tickets/
  # 2188-i18n-fails-with-multibyte-strings-in-ruby-19-similar-to-2038
  # (fix_params.rb)

  module ActionController
    class Request
      private
      # Convert nested Hashs to HashWithIndifferentAccess and replace
      # file upload hashs with UploadedFile objects
      def normalize_parameters(value)
        case value
          when Hash
            if value.has_key?(:tempfile)
              upload = value[:tempfile]
              upload.extend(UploadedFile)
              upload.original_path = value[:filename]
              upload.content_type = value[:type]
             upload
           else
             h = {}
             value.each { |k, v| h[k] = normalize_parameters(v) }
               h.with_indifferent_access
             end
          when Array
            value.map { |e| normalize_parameters(e) }
          else
            value.force_encoding(Encoding::UTF_8) if value.respond_to?(:force_encoding)
            value
          end
        end
      end
    end

   #
   # Source: https://rails.lighthouseapp.com/projects/8994/tickets/
   # 2188-i18n-fails-with-multibyte-strings-in-ruby-19-similar-to-2038
   # (fix_renderable.rb)
   #
   module ActionView
     module Renderable #:nodoc:
       private
         def compile!(render_symbol, local_assigns)
           locals_code = local_assigns.keys.map { |key| "#{key} = local_assigns[:#{key}];" }.join
           source = <<-end_src
def #{render_symbol}(local_assigns)
old_output_buffer = output_buffer;#{locals_code};#{compiled_source}
ensure
self.output_buffer = old_output_buffer
end
end_src
           source.force_encoding(Encoding::UTF_8) if source.respond_to?(:force_encoding)

           begin
             ActionView::Base::CompiledTemplates.module_eval(source, filename, 0)
             rescue Errno::ENOENT => e
             raise e # Missing template file, re-raise for Base to rescue
           rescue Exception => e # errors from template code
             if logger = defined?(ActionController) && Base.logger
               logger.debug "ERROR: compiling #{render_symbol} RAISED #{e}"
               logger.debug "Function body: #{source}"
               logger.debug "Backtrace: #{e.backtrace.join("\n")}"
             end
           raise ActionView::TemplateError.new(self, {}, e)
         end
       end
    end
  end
end

Now, the above file will be included automagically by Rails at startup. Here’s what it does (if it detects that you are using Ruby 1.9 – if not, it does nothing, and is therefore backwards-compatible with Ruby 1.8):

  1. The first chunk makes mysql return results encoded in UTF-8. Of course, you’ll have to make sure that you’re using “encoding: utf8” in your database.yml file, and that your database is set up to use UTF-8. See this post for more info: How to Make Rails and PHP Apps Fully UTF-8 Compliant with MySQL
  2. The second chunk modifies ActionController so that nested hashes and upload hashes will work properly.
  3. The third chunk is the most important: it modifies all your views so that when they are loaded, they are all automagically encoded as UTF-8. This means you don’t have to add “# encoding: utf-8” at the beginning of every single one of your view files. Obviously, this is seriously good juju!

Now, you might think you’re done, but there are a few other things to fix. If you have any helpers or models that do text processing involving strings, you’ll need to edit each one and add “# encoding: utf-8” as the very first line of the file. Save, and upload.

Finally, there is one other little gotcha I noticed: If you are using methods like this:

FileUtils.mv(oldpath, newpath, :force => true)

Then you’ll need to wrap them like so:

if File.exists?(oldpath) then
  FileUtils.mv(oldpath, newpath, :force => true)
end

The reason is that in Ruby 1.9, even if use :force => true, an exception will be thrown if oldpath doesn’t exist. The same is true of other file operations like cp. Yes, that’s kind of dumb, because it defeats the purpose of :force => true, but it’s easy to fix. File.exists? first just checks to make sure oldpath exists (whether it’s a file or a directory). If it doesn’t, it skips the FileUtils.mv command. Voila! Problem solved.

Finally, you may encounter an error involving: active_support/core_ext/object/blank.rb:68

If so, include the following code in lib/stringfix.rb, and add require "stringfix" to your environment.rb:

# encoding: utf-8
# This fixes the bug in:
# /activesupport-2.3.10/lib/active_support/core_ext/object/blank.rb:68
module StringBlankPatch
  module String
    def blank?
      self.dup.as_bytes !~ /\S/
    end
  end
end

class String
  include StringBlankPatch::String
end

OH! I almost forgot… If you have an error with a catch-all route that directs missing pages to public/404.html, and your file includes UTF-8 chars, the easy fix is to just replace the accented characters with HTML entities. In other words, replace all instances of “é” with “é” in public/404.html. You can get a list of character codes here: HTML – Special Entity Codes

That’s it!

Now, as you can see, there are quite a few little details you have to pay attention to. I had to search the net far and wide (and just experiment) to find all these little fixes. What amazes me is that no one has compiled them together in one article to make others’ lives a bit easier. I mean, let’s face it: Ruby 1.9.2 blows even REE 1.8.7 out of the water in terms of raw performance. Things like regex are twice as fast as in Ruby 1.8.x.

I also couldn’t believe the number of posts I found where people just gave up and went back to Ruby 1.8.x. Well, now you don’t have to!

I should also note that Ruby 1.9 is only 32-bit, which is actually a good thing. If you compiled REE on your 64-bit linux box, you may have noticed that instances of your app were twice as large since REE was apparently compiled as 64-bit. With 32-bit Ruby 1.9.2, you won’t have any wasted memory, which means you can run more instances of your app in, say, Passenger! Did I mention that Ruby 1.9 is also twice as fast?

Right. So, you should be good to go. If you found other fixes, please post them to the comments!

Need help? Hire me!
Get Scottie Stuff!