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"
fiWhen 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
endWe 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:
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:
- 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.
- It extracts the
GEM_HOME,GEM_PATH,BUNDLE_PATHenvironment variables and sets them in the current process. - 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.