Ninjas on a Penny Farthing

  posted  
Use Twitter? Follow on Twitter.

The Path to Better RVM & Passenger Integration


As part of Ruby Summer of Code, I’ve been working on rvm. Namely, whilst Wayne was away for a week, I wrote a ruby wrapper around the command line api. As of a few days ago, this api got merged back into master (meaning it’s available right now if you do rvm update --head and should be in the next rvm release, 0.1.42).

Not only does this api let you do general things (e.g. modifying aliases, installing rubies and the like) it also lets you script inside ‘environments’ – e.g. you can get details about what the ruby string ree@rails3 represents or execute a script inside rbx (among other things). One of the cooler things to come out of this is the ability to switch gemsets for the current ruby process (Unfortunately, we can’t switch the interpreter out completely).

Today I’m going to talk one little thing made possible by this – the ability for rvm to reuse it’s built in rvmrc functionality to automatically switch gemsets on a per-application basis in Phusion Passenger.

The General Idea

The idea for changing a gemset is simple – We convert a ruby string (e.g. “ree@rails3”) to a set of three environment variables (as is done with rvm in general):

  • GEM_HOME – the directory rubygems installs gems into.
  • GEM_PATH – set of directories which rubygems will search for gems.
  • BUNDLE_PATH – the default place for bundler to install / search for gems.

When you use rvm from the command line, as part of the process of using a gemset rvm will manage these environment variables for you. In fact, it even has specific values for each that add all sorts of cool tricks you can learn by reading the site.

When working on projects with rvm, it is common to also setup a .rvmrc in the root of your project. rvmrc’s are essentially shell scripts that get evaluated sourced when entering a directory. Thus, you can add them to the root of your projects and when you change to the projects directory from the command line to work on it, rvm will automatically use the correct ruby / gemset combination / run everything inside of the file.

Today we’re going to show how to exploit this functionality to let passenger use different sets of gems for different applications.

Implementing it

To try it out, we’ll use an example of running rvm use ree@my-app-name --rvmrc --create which will then created a .rvmrc file containing (currently) the following as a base example:

if [[ -s "/Users/sutto/.rvm/environments/ree-1.8.7-2010.02@my-app-name" ]] ; then
  . "/Users/sutto/.rvm/environments/ree-1.8.7-2010.02@my-app-name"
else
  rvm --create use  "ree-1.8.7-2010.02@my-app-name"
fi

When you’re setting these up in projects on your server, it’s worth noting that you want to ensure that you use the same ruby (in this case, ree) as you do when you set up passenger.

If you require the ruby api inside a process, it makes available a nice little helper method to switch out the gemset. As an example,

require 'rvm' # Assuming released gem version
RVM.gemset_use! 'rails3'

Would switch the current process to use rails3.

By editing config/setup_load_paths.rb to add this at the top:

if ENV['MY_RUBY_HOME'] && ENV['MY_RUBY_HOME'].include?('rvm')
  begin
    rvm_path     = File.dirname(File.dirname(ENV['MY_RUBY_HOME']))
    rvm_lib_path = File.join(rvm_path, 'lib')
    $LOAD_PATH.unshift rvm_lib_path
    require 'rvm'
    RVM.use_from_path! File.dirname(File.dirname(__FILE__))
  rescue LoadError
    # RVM is unavailable at this point.
    raise "RVM ruby lib is currently unavailable."
  end
end

# Select the correct item for which you use below.
# If you're not using bundler, remove it completely.

# If we're using a Bundler 1.0 beta
ENV['BUNDLE_GEMFILE'] = File.expand_path('../Gemfile', File.dirname(__FILE__))
require 'bundler/setup'

# Or Bundler 0.9...
if File.exist?(".bundle/environment.rb")
  require '.bundle/environment'
else
  require 'rubygems'
  require 'bundler'
  Bundler.setup
end

We can use the ruby api to automatically attempt to use the ruby and gemset combination specified inside a projects .rvmrc file. As I mentioned earlier, the same ruby must be used in the .rvmrc and for passenger, otherwise you’d see an error similar to:

An error using the ruby

Notes

For passenger to respect config/setup_load_paths.rb, you’ll need to be using a relatively recent version. Lastly, the ruby api here is only available in head and rvm versions equal to or newer than 0.1.42 (as of publishing this article, this version is currently unreleased so I suggest updating to head via rvm update --head.

Lastly, Please make sure in the ruby code above, you edit the value of rvm_lib_path. For most people, the following should work for user-based installs:

rvm_lib_path = File.expand_path("~/.rvm/lib")

But for system wide installs, you’d instead use:

rvm_lib_path = "/usr/local/rvm/lib"

Explanation

What this method in essence does:

  1. The rvm ruby api will change to the given directory (in this case, the project root) and extract the ruby string it uses – e.g. if you have an rvmrc, what it uses. Note that it will default to the current processes ruby otherwise.
  2. It extracts the GEM_HOME, GEM_PATH, BUNDLE_PATH environment variables and sets them in the current process.
  3. If rubygems is loaded, it calls Gem.clear_paths – this forces rubygems to refresh it sources.

When all is said and done, assuming your the ruby passenger is running as and the ruby used by the given projects rvmrc is compatible, it will replace the gemset of the current process.

The biggest advantage of this approach is that it makes it simpler to deploy multiple projects with conflicting versions of gems when they’re each segregated to project-specific gemsets.