If 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:
ruby extconf.rb
make
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:
- Go through your app and add “# encoding: utf-8” to the first line of every single file in your app
- 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):
- 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 - The second chunk modifies ActionController so that nested hashes and upload hashes will work properly.
- 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!
Is there a reason that newpath and oldpath are reversed in the before/after sequences?
Oops… I fixed it.
Thanks!
Have you also tried RVM http://rvm.beginrescueend.com for installing Ruby 1.9?
Ya know, I haven’t. I read about it, but I wasn’t too sure about why it’s so great. Switching between REE and Ruby 1.9 is pretty simple since both can be installed in different dirs, and with Passenger and changing /etc/environment, it’s easy to just tell Rails (and everything else) to use one or the other. So, I didn’t bother trying RVM. But, maybe I should?
Man, really awesome post! Thanks for a link to me.
Greets!
A couple of different options: for mysql, instead of patching mysql’s Result class, I chose to install this:
# http://github.com/lsegal/mysql-ruby for ruby 1.9.1 support for utf-8
config.gem ‘sam-mysql-ruby’, :lib => ‘mysql’, :source => ‘http://gemcutter.org’ , :version => “2.8.1”
Also, set default external encoding in environment.rb:
Encoding.default_external = Encoding::UTF_8
I added a note about what to do if you encounter the “OpenSSL support for Ruby… not found” error when trying to reinstall Passenger after upgrading to Ruby 1.9.
Thank you, Thank you, Thank you. I was about to give up on an upgrade and you solved the issues.
Come to Los Angeles… I owe you a beer!
Awesome! Just what I was looking for. As far as I understood this problem should be fixed in Rails 3.x anyway, but before migrating to 3.x I want to ensure the latest 2.3.x (2.3.14) works just fine. Thanks man!
For the encoding thing you can also use this gem to prepend the encoding comment to all relevant files:
https://github.com/m-ryan/magic_encoding