Many Rubyists find the need from time to time to run multiple versions of Ruby. If you are developing open-source code, it's a good idea to try to maintain compatibility with all three of the main versions of Ruby current in use, 1.8.6, 1.8.7 and 1.9
There have been some tools for this for a while now. A lot of you probably already know about multiruby, and many may be aware of a new gem called rvm for Ruby Version Manager.
These two are useful for different purposes. Multiruby excels for testing ruby code against different versions, while rvm is great for quickly switching rubies so that you can play with one or the other. I think of RVM as a set of hand tools, and multiruby as a power tool workshop
They complement each other, but I've had a few bumps getting them to work together, hence this article.
Multiruby
One tool which helps do this is the multiruby suite of tools which are part of Ryan Davis' ZenTest gem. There are three tools in this suite:
- multiruby_setup
- which allows you to install and maintain a collection of Ruby versions. The various versions are installed in a subdirectory of .multiruby in your home directory
- multiruby
- which runs each of the installed ruby commands with the same arguments.
- multigem
- which uses multiruby to run the gem command in order to install gems in the right place for each of the ruby versions under that .multiruby directory.
For my RiCal gem, I have some rake tasks which run the rspec suite for the gem using multiruby, so I can be sure it still works with "the big three" before I publish a new version. To enable this I submitted a patch to RSpec to let you tell an instance of RSpecRakeSpecTask where to find the 'ruby' command. David incorporated this several releases of RSpec ago. This lets me have this in a rake file:
multiruby_path = `which multiruby`.chomp
if multiruby_path.length > 0 && Spec::Rake::SpecTask.instance_methods.include?("ruby_cmd")
namespace :multi do
desc "Run all specs with multiruby and ActiveSupport"
Spec::Rake::SpecTask.new(:with_active_support) do |t|
t.spec_opts = ['--options', "spec/spec.opts"]
t.spec_files = FileList['spec/**/*_spec.rb']
t.ruby_cmd = "#{multiruby_path}"
t.verbose = true
t.ruby_opts << "-r #{File.join(File.dirname(__FILE__), *%w[gem_loader load_active_support])}"
end
desc "Run all specs multiruby and the tzinfo gem"
Spec::Rake::SpecTask.new(:with_tzinfo_gem) do |t|
t.spec_opts = ['--options', "spec/spec.opts"]
t.spec_files = FileList['spec/**/*_spec.rb']
t.ruby_cmd = "#{multiruby_path}"
t.verbose = true
t.ruby_opts << "-r #{File.join(File.dirname(__FILE__), *%w[gem_loader load_tzinfo_gem])}"
end
end
desc "run all specs under multiruby with ActiveSupport and also with the tzinfo gem"
task :multi => [:"spec:multi:with_active_support", :"spec:multi:with_tzinfo_gem"]
end
I've got three tasks here because RiCal works with either the tzinfo gem OR activesupport from Rails, and I want to test each combination of gems and ruby versions.
RVM
Like multiruby, rvm lets you set up and use multiple versions of ruby. As I said above the difference here is that while multiruby runs them all together, rvm is for when you want to pick one to use for a while.
The rvm command is used to:
- install a ruby implementation specifying one of ruby for MRI ruby, ree for Ruby Enterprise Edition a version of MRI patched for use with passenger (a/k/a mod-rails)
or jruby surprisingly enough for JRuby and optionally the specific version and even patch level.
- pick which ruby to use by using "rvm use which", where which is one of the above or default for the standard ruby installation for your system.
as well as other management functions.
The rvm gem is actually a thin ruby wrapper around some bash scripts. The way rvm works is to set up shell environment variables when you use "rvm use" so you get the right ruby executables and environment, and there lies the rub.
Who's got the Gem
Now did I tell you that I decided to add rvm to my arsenal right after I upgraded my MacBook to run Snow Leopard?
Because of this I had to rebuild a lot of my ruby development tool chain. I decided just to 'fault in' things that I found to be missing when I found that they were missing. A lot of those things were gems. So I'd run my various ruby projects, and when I found a missing gem, I'd install it.
So I ran my normal spec tasks against RiCal, and installed the missing gems. When I got those working, I ran the multiruby taks, and found that the tzinfo gem was missing. This wasn't a surprise since multiruby (like rvm) maintains a separate set of gems for each implementation. It was just a matter of "multigem install tzinfo" and move on to the next step. Wrong!
Multiruby reported that it had installed the tzinfo gem for each of the installed multiruby implementations, but when I ran the rake task again, no joy, same thing. Running "multigem list" revealed that there were no gems for any of the multiruby installs!
After a bit of head-scratching, I realized that rvm was setting GEM_HOME so that the gem command would know where to look for and install gems, and this was confusing multigem, which simply runs the gem command with ruby which ends up installing gems relative to the implementations installation directory. But GEM_HOME overrides this, so multigem was just reinstalling the gem three times in whatever directory rvm wanted them.
The Workaround
What's working for me is to use the bash command "unset GEM_HOME" before running multigem. This removes the variable entirely, and multigem goes back to working "normally." It's not ideal but it works.