In a previous post I postulated virtual environments as a solution to the problem of dependency and version collision for software built using languages like Python, Ruby, Node.js, and Go.

In this post I give the details and some examples of using a Ruby virtual environment. First I walk through everything using OS X–i.e., using a Mac–and then, briefly, I describe the same procedures using Windows.

Update 2016.04.07: This post has been updated to address rbenv 1.0.0, which was very recently made available through Mac Ports. Previously version 0.4.0 was available. I also added the section “Updating the rbenv plugins”.

Ruby installation (OS X)

A version of Ruby is already installed on OS X. On my 10.9.5 (Mavericks) system, the command ruby -v tells me I have Ruby version 2.0.0p481. Apple does not typically update the Ruby version, but may apply patches to it, so I can expect that Mavericks will stay at version 2.0.0. If a project I’m working on needs a different version of Ruby, then I need to install an additional copy of Ruby on the system.

Installing rbenv

On OS X and Linux, I think the best way to install and manage multiple side-by-side installations of Ruby is to use the Ruby virtual environment manager rbenv.

Note: There is also RVM, the first popular Ruby environment manager. But rbenv has many advantages over RVM. I will only be discussing the use of rbenv here.1

To install rbenv you can either use a package manager like MacPorts or Homebrew. Or you can install it the old fashioned way, as described in the rbenv project README. I installed rbenv on my system using MacPorts2 using these commands:

sudo port selfupdate
sudo port install rbenv

Once rbenv is installed, you should be able to check it’s version and see what version of Ruby is active. Here’s how that looks on my system:

~/$ rbenv -v
rbenv 0.4.0
~/$ rbenv version
system (set by /Users/neo/.rbenv/version)
~/$ rbenv versions
* system (set by /Users/neo/.rbenv/version)
~/$ ruby -v
ruby 2.0.0p481 (2014-05-08 revision 45883) [universal.x86_64-darwin13]

Add the ruby-build plugin

Now you’ll need to add the plugin ruby-build that lets you install additional version of Ruby. Just clone the ruby-build repo into the rbenv plugins directory, like so:

git clone https://github.com/rbenv/ruby-build.git ~/.rbenv/plugins/ruby-build

Install another version of Ruby

With the ruby-build plugin installed, if I need to use a different version of Ruby, I can use rbenv install to make that available on the system. To see all the versions available for installation this way, use the rbenv install -l command.

For example, if I needed to add Ruby 1.9.3 because I’ve got a project that requires that older version, then I can list all the 1.9.3 variants available and choose one to install. That might look like this:

~/$ rbenv install -l | grep 1.9.3
  1.9.3-dev
  1.9.3-preview1
  1.9.3-rc1
  1.9.3-p0
  1.9.3-p125
  1.9.3-p194
  1.9.3-p286
  1.9.3-p327
  1.9.3-p362
  1.9.3-p374
  1.9.3-p385
  1.9.3-p392
  1.9.3-p429
  1.9.3-p448
  1.9.3-p484
  1.9.3-p545
  1.9.3-p547
  1.9.3-p550
  1.9.3-p551
~/$ rbenv install 1.9.3-p551
Downloading yaml-0.1.6.tar.gz...
-> https://dqw8nmjcqpjn7.cloudfront.net/7da6971b4bd08a986dd2a61353bc422362bd0edcc67d7ebaac68c95f74182749
Installing yaml-0.1.6...
Installed yaml-0.1.6 to /Users/neo/.rbenv/versions/1.9.3-p551

Downloading ruby-1.9.3-p551.tar.gz...
-> https://dqw8nmjcqpjn7.cloudfront.net/bb5be55cd1f49c95bb05b6f587701376b53d310eb1bb7c76fbd445a1c75b51e8
Installing ruby-1.9.3-p551...
Installed ruby-1.9.3-p551 to /Users/neo/.rbenv/versions/1.9.3-p551

Notice that Ruby is installed to a directory under the user’s home directory. The Ruby installation is not global to the system.

Now rbenv versions shows two versions of Ruby, with the system version still active:

~/$ rbenv versions
  1.9.3-p551
* system (set by /Users/neo/.rbenv/version)

Setting the Ruby version for a project

rbenv uses a set of shims to route Ruby commands (like ruby and gem) to the intended installation of Ruby. It does so by looking in the current and parent directories for files that contain the version number specification.

For a particular project, you’d set the version of Ruby to use for that project by changing to the root directory of the project and issuing a ruby local command. For example:

~$ cd trash/dummy-project
~/trash/dummy-project$ rbenv local 1.9.3-p551
~/trash/dummy-project$ ruby -v
ruby 1.9.3p551 (2014-11-13 revision 48407) [x86_64-darwin13.4.0]
~/trash/dummy-project$ rbenv versions
* 1.9.3-p551 (set by /Users/neo/trash/dummy-project/.ruby-version)
  system
~/trash/dummy-project$ cd ..
~/trash$ ruby -v
ruby 2.0.0p481 (2014-05-08 revision 45883) [universal.x86_64-darwin13]
~/trash$ rbenv versions
  1.9.3-p551
* system (set by /Users/neo/.rbenv/version)
~/trash$ cd dummy-project/
~/trash/dummy-project$ ruby -v
ruby 1.9.3p551 (2014-11-13 revision 48407) [x86_64-darwin13.4.0]

Read through that carefully, and you’ll see that any time the working directory is in ~/trash/dummy-project or below, the version of Ruby being used will be 1.9.3. If the working directory is outside of that tree–really, if the rbenv shim cannot locate a .ruby-version file in the current directory or above–then the Ruby version is controlled by the ~/.rbenv/version file.

You can remove the local version setting by setting the working directory to the project root and issuing rbenv local --unset. That should simply remove the .ruby-version file.

Mandatory rbenv plugins

To make rbenv as complete a virtual environment manager as Python’s venv, there are a few plugins that you’ll want to install. One is rbenv-gem-rehash, required for rbenv 0.4.0, which ensures that the rbenv shims are up to date with the latest gem changes. And the other is rbenv-gemset which allows gems to be installed within the project directory tree, rather than within the Ruby installation directory tree.

For rbenv 0.4.0 (or earlier), install the plugins with the following commands:

git clone https://github.com/sstephenson/rbenv-gem-rehash.git ~/.rbenv/plugins/rbenv-gem-rehash
git clone git://github.com/jf/rbenv-gemset.git ~/.rbenv/plugins/rbenv-gemset

For rbenv 1.0.0 (or later), just install rbenv-gemset with the following command:

git clone git://github.com/jf/rbenv-gemset.git ~/.rbenv/plugins/rbenv-gemset

The rbenv-gem-rehash plugin will just do it’s thing silently; no configuration is needed.

The rbenv-gemset plugin works by looking for a .rbenv-gemset text file in the directory tree. In the next section you’ll see an example of how that is used.

Updating the rbenv plugins

You keep the plugins up to date by fetching and pulling the latest versions via Git. I put a script file called fetchall.sh in the root of each plugin, including the ruby-build plugin, and execute it when I believe an update is needed. That script file contains:

#! /bin/bash
#
git fetch --all
git remote prune origin
git pull --ff-only

Managing gems

Ruby gems are software packages for the Ruby system, and the gem command is used to install and update them.

Some gems are installed with Ruby itself. After installing a fresh copy of Ruby, you can see what gems are already in place with the gem list command.

~/trash/dummy-project$ gem list

*** LOCAL GEMS ***

bigdecimal (1.1.0)
io-console (0.3)
json (1.5.5)
minitest (2.5.1)
rake (0.9.2.2)
rdoc (3.9.5)

When gems are installed, they get placed in a repository inside the Ruby installation. Continuing with my 1.9.3 example, any gems I install will get placed in ~/.rbenv/versions/1.9.3-p551/lib/ruby/gems/1.9.1/gems.

Of course, this means that all gems in 1.9.3 are shared with all projects using Ruby 1.9.3 on this system (for this user). And that’s not what we want. Each project or application should have available only the additional gems needed, and only certain versions of those gems. We can achieve that with a combination of rbenv-gemset (which you should have already installed) and Bundler (a gem that you will soon install).

Note: if you get a ‘certificate verify error’ when attempting to install gems, see the section below: Old certificates cause gem to fail.

Update 2016-04-08: gem install errors due to SSL/TLS issues are so common that I wrote a post on it. See that post for solutions and more information on the issue.

.rbenv-gemset

rbenv-gemset is an rbenv plugin that lets us control where Ruby gems get installed. Normally we want them to be installed local to the application or project directory tree. Usually this is a subdirectory called .gem located in project or application root directory.

The plugin is informed of the location by the contents of the file .rbenv-gemsets also located in the project or application root directory.

Our example project might have a ~/trash/dummy-project/.rbenv-gemsets file containing:

.gems

Any gems that get installed will get installed into the projects .gem folder.

Bundler

With rbenv-gemsets keeping gem installations within the project tree, there still needs to be a way to declare the gems required by the project and the gem versions required. The best way to manage this is to use Bundler. Bundler uses a text file called Gemfile to control which gems, and which versions, get installed for a project—much in the same way that a requirements.txt file controls Python packages with pip in a Python virtual environment. The syntax of the version constraint specifications in a Bundler Gemfile is very similar to how the dependencies of a gem are specifed in a gemspec file in an add_runtime_dependency statement.

Of course Bundler needs to be installed to use it. You can either install it in a project specific gemset, or you can install it into the Ruby installation directory. I’ve gone back and forth on this, but right now I think it is best to install it like any other gem for the project—into the gemset.

Given that you’ve already configured rbenv-gemsets for the project, here’s how that Bundler install looks:

~/trash/dummy-project$ gem install bundler
Fetching: bundler-1.11.2.gem (100%)
Successfully installed bundler-1.11.2
1 gem installed
Installing ri documentation for bundler-1.11.2...
Installing RDoc documentation for bundler-1.11.2...
~/trash/dummy-project$ gem which bundler
/Users/neo/trash/dummy-project/.gems/gems/bundler-1.11.2/lib/bundler.rb
~/trash/dummy-project$ gem list

*** LOCAL GEMS ***

bigdecimal (1.1.0)
bundler (1.11.2)
io-console (0.3)
json (1.5.5)
minitest (2.5.1)
rake (0.9.2.2)
rdoc (3.9.5)

In the previous post in this series on virtual environments, I talked about an example project that builds a website. To author the CSS used on that website I want to use Sass and it’s companion Compass—both written in Ruby and available as gems.

I’m going to constrain the versions to those circa June 2011. That means Compass 0.11.3, Sass 3.1.3, fssm 0.2.7, and chunky_png 1.2. The Gemfile that specifies the versions I want of each looks like this:

source "https://rubygems.org"

gem 'sass', '3.1.3'
gem 'chunky_png', '>= 1.2.0', '< 1.2.1'
gem 'fssm', '0.2.7'
gem 'compass', '0.11.3'

And I use bundler to install those specific gems using bundler install, like so:

~/trash/dummy-project$ bundle install
Fetching gem metadata from https://rubygems.org/.............
Fetching version metadata from https://rubygems.org/...
Fetching dependency metadata from https://rubygems.org/..
Resolving dependencies...
Rubygems 1.8.23.2 is not threadsafe, so your gems will be installed one at a time. Upgrade to Rubygems 2.1.0 or higher to enable parallel gem installation.
Installing chunky_png 1.2.0
Installing fssm 0.2.7
Installing sass 3.1.3
Using bundler 1.11.2
Installing compass 0.11.3
Bundle complete! 4 Gemfile dependencies, 5 gems now installed.
Use `bundle show [gemname]` to see where a bundled gem is installed.

And you can see they are installed in the project’s gemset (the .gems folder)

~/trash/dummy-project$ gem which sass
/Users/neo/trash/dummy-project/.gems/gems/sass-3.1.3/lib/sass.rb

A Ruby virtual environment

In the first post about Python virtual environments I enumerated the three key features of virtual environments. Using rbenv, some rbenv plugins, and Bundler, we can have a Ruby virtual environment with those same characteristics.

  • A local, mutable, deployment directory for the execution environment that is independent of the global (system) environment.

    With Ruby, this is accomplished with the rbenv utility and these rbenv plugins: rbenv-gem-rehash, rbenv-gemset, ruby-build. With that in place we can specify a local directory to contain any gems we want to install as dependencies of the project or application.

  • An activation mechanism for standing up the virtual environment—for switching an execution context to use the local deployment directory preferentially ahead of the global environment, or masking the global environment.

    With Ruby virtual environments, this is accomplished using the rbenv local command (e.g. ruby local 1.9.3-p551) to set a .ruby-version file in the project root folder, and a .ruby-gemsets file in the project root directing the location of gems to a .gems file, also in the project root.

    Note that, unlike Python virtual environment activation, this does not change the command prompt to indicate that a Ruby virtual environment is active. We’ll tackle that in the next post in this series.

  • A deactivation mechanism for standing down the virtual environment—for switching the execution context back to using the global environment and ignoring the deployment directory.

    This is accomplished using the rbenv local --unset command, which removes the .ruby-version file in the project root.

    But this doesn’t fully deactivate the virtual environment. If the working directory is within the project or application directory tree, then the .rbenv-gemsets file will still affect any gem install command. That’s not great, but I will solve that with the activation and deactivation scripts discussed in the next post.

Next steps

In the next post I’ll discuss how to create an activation and deactivation mechanism, one that will update the command prompt to indicate the active Ruby virtual environment. I’ll also show how to do upgrades of dependencies to pick up patches without picking up major or minor upgrades, how to test for the correct virtual environment and dependency versions in script (e.g. test scripts, build scripts), and how to provision the virtual environment from a list of known dependencies.

Ruby virtual environments on Windows

Unfortunately, neither rbenv nor the rbenv-gemset and ruby-build plugins are available for Windows; they only work on Linux and OS X. So, until we have an equivalent utility available for Windows system, the best alternative seems to be placing a copy of Ruby within the project or application directory.

The process I favor is to install each Ruby version in a well-known global location and then copy the install folder into a .rblcl folder in the root of the project directory tree.

Installing Ruby (Windows)

The most convenient way to get Ruby on Windows is by using the installers available from the RubyInstaller for Windows project. You can get past versions of Ruby from the project’s archives

Continuing the example I’ve been using, I’m going to install the 64-bit version of Ruby 2.0.0p481 in the general location for 64-bit application software on modern Windows systems. This will be the source for any project or application specific installations of that version of Ruby.

  1. Launch the installer using “Run as administrator”
  2. Set the install directory to “C:\Program Files\Ruby200-p481”
  3. Do not add Ruby to the path, or set any shortcuts, or any integration with the shell (no hookups with .sh files, or .rb files)

Adding the deployment directory (Windows)

Once Ruby is installed, then copy it to an .rblcl directory in the project tree. For example:

> cd \P\Git\trash\dummy-project
C:\P\Git\trash\dummy-project> mkdir .rblcl
C:\P\Git\trash\dummy-project> cd .rblcl
C:\P\Git\trash\dummy-project\.rblcl> xcopy /S /E 'C:\Program Files\Ruby200-p481' .
C:\P\Git\trash\dummy-project\.rblcl> cd ..

That provides the project with the first characteristic of a virtual environment: a local, mutable, deployment directory.

To satisfy the minimal requirements of a virtual environment, we also need an activation mechanism to use the local version of Ruby when working in the project, and a deactivation mechanism to restore the default version of Ruby (which might mean no active Ruby) when we are done.

Activation and deactivation (Windows)

I use a Powershell script called activate_rblcl.src.ps1 to do that, and invoke it with the source command (“. activate-rblcl.src.ps1”). The script looks like this:

# A script to activate the local version of Ruby
#
# Should be run with the source command: . activate-rblcl.src.ps1
#
$cwd = (Get-Item -Path ".\" -Verbose).FullName
$rblcl = $cwd + "\.rblcl"
If (-Not (Test-Path $rblcl)) {
  Write-Output "Did not find, or could not access, sub-directory '.rblcl'."
  Write-Output "Cannot activate Ruby virtual environment."
  Write-Output ""
  Return
}

# Add Ruby to the path
#
$Env:RBLCL_ = "$rblcl\bin"
$Env:Path = "$rblcl\bin" + ";" +  "$Env:Path"

# Set the deactivation script
#
$deactivateFile = $cwd + "\deactive-rblcl.src.ps1"
"If (-Not (Test-Path `$Env:RBLCL_)) {" | Out-File $deactivateFile -Width 256
"  Write-Output `"Ruby virtual environment not active.`"" |
  Out-File $deactivateFile -Append -Width 256
"  Write-Output `"`"" | Out-File $deactivateFile -Append -Width 256
"}" | Out-File $deactivateFile -Append -Width 256
"[System.Collections.ArrayList]`$parts = `$Env:Path.split(';')" |
  Out-File $deactivateFile -Append -Width 256
"`$parts.Remove(`$Env:RBLCL_)" | Out-File $deactivateFile -Append -Width 256
"`$Env:Path = `$parts -Join ';'" | Out-File $deactivateFile -Append -Width 256

When run, it generates a deactivation script deactivate-rblcl.src.ps1 that looks like this:

If (-Not (Test-Path $Env:RBLCL_)) {
  Write-Output "Ruby virtual environment not active."
  Write-Output ""
}
[System.Collections.ArrayList]$parts = $Env:Path.split(';')
$parts.Remove($Env:RBLCL_)
$Env:Path = $parts -Join ';'

You would activate the virtual environment using . activate-rblcl.src.ps1 and deactivate the virtual environment using . deactivate-rblcl.src.ps1.

Note: make sure to add deactivate-rblcl.src.ps1 to your .gitignore file, if you are using Git for source code version control.

Old certificates cause gem to fail

Update 2016-04-08: gem install errors due to SSL/TLS issues are so common that I wrote a post on it. See that post for solutions and more information on the issue.

If you are using an older version of Ruby, or not the latest patch, then you might see a gem failure like this:

> gem install 'bundler'
ERROR:  Could not find a valid gem 'bundler' (>= 0), here is why:
          Unable to download data from https://rubygems.org/ - SSL_connect returned=1 errno=0 state=SSLv3 read server certificate B: certificate verify failed (https://rubygems.org/latest_specs.4.8.gz)

Ruby ships with some certificates that are used to validate the connection with rubygems.org. Inevitably, and for a variety or reasons, those certs become invalid. If you see the error above, that is because gem is unable to validate the server certificate provided by rubygems.org.3

You can resolve that problem by downloading the latest certificate (make sure the file is saved with the extension .pem) and placing it in this relative directory in your Ruby installation: lib\ruby\x.x.x\rubygems\ssl_certs

I recommend making the same update to the installation in the general location, in the C:\Program Files directory tree. That way any future projects or applications that copy that Ruby installation will already have the updated CA certificate.

Bundler (Windows)

After activating the virtual environment, install Bundler using the gem install bundler command.

Then create a Gemfile, identical to the example above, in the project root directory.

Install the specified gems using the command bundler install. You should see output something like this:

> bundler install
DL is deprecated, please use Fiddle
Fetching gem metadata from https://rubygems.org/.............
Fetching version metadata from https://rubygems.org/...
Fetching dependency metadata from https://rubygems.org/..
Resolving dependencies...
Rubygems 2.0.14 is not threadsafe, so your gems will be installed one at a time. Upgrade to Rubygems 2.1.0 or higher to
enable parallel gem installation.
Installing chunky_png 1.2.0
Installing fssm 0.2.7
Installing sass 3.1.3
Using bundler 1.11.2
Installing compass 0.11.3
Bundle complete! 4 Gemfile dependencies, 5 gems now installed.
Use `bundle show [gemname]` to see where a bundled gem is installed.

As you can see, the gems are installed in the local Ruby installation tree, the one within the project root directory.

> gem which sass
C:/P/Git/trash/dummy-project/.rblcl/lib/ruby/gems/2.0.0/gems/sass-3.1.3/lib/sass.rb

So, now you have a working Ruby virtual environment.

Next steps (Windows)

In the next post I’ll refine the virtual environment activation mechanism to update the command prompt with an indication that a Ruby virtual environment is active. And I’ll show how to do upgrades of dependencies to pick up patches without picking up major or minor upgrades, how to test for the correct virtual environment and dependency versions in script (e.g. test scripts, build scripts), and how to provision the virtual environment from a list of known dependencies.

  1. RVM and rbenv are not compatible. Because of the significant advantages of rbenv over RVM, I recommend that current RVM users give serious consideration to switching to rbenv.

  2. There are three popular package managers for OS X. MacPorts, HomeBrew, and Fink. All three work well. But I prefer MacPorts or Fink to HomeBrew because both MacPorts and Fink install into alternative directory trees and do not replace or modify the system as originally laid down by Apple. HomeBrew, on the other hand, resets permissions on system files (dangerously giving the current user write authority into privileged locations) and installs into system folders (which can cause issues during operating system upgrades). In short, I view HomeBrew as more intrusive, less stable, and less secure than MacPorts or Fink.

  3. A detailed description of the issue can be found here: https://gist.github.com/luislavena/f064211759ee0f806c88