RVM, Passenger and symlinks

25 août 2010

Here is some kind of follow up after my previous lengthy description on how we use RVM, Passenger and Rails at work.

One of my co-workers has updated RVM this morning (to get the brand new 1.0.0) and all his local Rails web apps broke down.

We’ve spent about an hour checking with Bundler (also updated from rc5 to rc6 before the breakdown), reinstalling Passenger, completely nuking RVM and reinstalling everything from scratch (rubies, Gemsets, gems, …). Nothing good came out of this :-(

We had very few information ; nothing in the browser, no Rails logs and no Apache logs either. But when when we stopped Apache, it spit a few lines before stopping, showing a Passenger error stating that a method is missing in the RVM setup for the particular Rails app (cf. the setup_load_paths.rb file).

The same setup on my own laptop was working great.

RVM is updated with Git, so it’s quite easy to install a specific tag and see if it’s working. I’ve tracked down the changes to the point where the latest tag (1.0.0) was breaking my friend’s apps.

The wonderful RVM guys (wayneeseguin and Sutto) told me (in the IRC channel) that the major thing that has changed in 1.0.0 is the security feature about .rvmrc files. They have to be manually trusted the first time RVM meets them.

We were sure that we did this for the .rvmrc file in each of the Rails apps, but Passenger was still hanging, as if it was waiting for something

Sutto explained to me that RVM is making a md5 checksum of the path of each trusted (or not) .rvmrc file. We have verified that there was indeed a line for our .rvmrc files.

With RVM 1.0.0 you can choose to disable this security feature globally. When we did this, everything started to work fine again. As soon as we activated this again, every app stopped working. It became clear that it had to do with the .rvmrc files.

Sutto asked me to show him the VHosts for the Ruby apps and then I understood !

Usually, on Mac OS X, you put your web stuff in /Users/_my_login/Sites and point your VHosts to some subdirectories of that. It’s like /home/_my_login/www in the Linux/Unix world. I don’t know why, but my co-worker (from this point, I no longer considered him as my friend) (I’m kidding) has has created a symlink to point /Users/_his_login/www to /Users/_his_login/Sites. You’d tell me that there is nothing wrong here, and you’d be right.

In his terminal, when he wants to go to his web app directory to work, he’s doing something like cd ~/www/_project. Then, the first time, RVM asks him about the .rvmrc it finds just here. The .rvmrc file is trusted and RVM is working fine. It seems.

In his VHosts, the RootDirectory is like /Users/_his_login/Sites/_project(notice the Sites part instead of www), and Passenger executes the config script that reads the .rvmrc file to use the right things. At this very moment, the .rvmrc file’s path is /Users/_his_login/Sites/_project/.rvmrc and its md5 checksum is obviously different than the one for the trusted /Users/_his_login/www/_project/.rvmrc file.

For the moment Passenger doesn’t know what to do. It waits for RVM to give him some environment variables, RVM doesn’t give it anything because it waits for the file to be trusted (or not).

With Sutto, we agreed that the best thing to do is to document this and make Passenger aware of this, to let him show some error in the browser and/or in the web server’s log file.

That’s how we’ve lost nearly 3 hours today, because of a symlink. But once again, I learned a lot. I’d rather read this kind stuff in a blog post, in just a few minutes (that’s why I’m writing this), than spending some hours, but I don’t really regret it.

Update : Sutto has just told my that the current HEAD for RVM is showing an exception when RVM asks to trust (or not) the newly found .rvmrc file. You just have to do $ rvm update --head && rvm reload.


You are using the old router DSL !

23 août 2010

In some Rails 3 application, I get a somewhat cryptic message in my development log file :

DEPRECATION WARNING: You are using the old router DSL which will be removed in Rails 3.1. Please check how to update your routes file at: http://www.engineyard.com/blog/2010/the-lowdown-on-routes-in-rails-3/

I’ve read the designated article on the (great) EngineYard blog, but I didn’t find anything wrong in my routes file.

I searched for the message in ActionPack source code, found a call to DeprecatedMapper, then search for that and found something interesting in route_set.rb. In the “draw” method, there is a check on the block “arity“. WTF is arity ?

The arity method exists on Proc objects to check for “the number of arguments that would not be ignored” (straight from the Ruby documentation).

I understood that the route file must not have any argument in the main (draw) block.

I had to change this

MyApp::Application.routes.draw do |map|
  (…)
end

into

MyApp::Application.routes.draw do
  (…)
end

It’s as simple as that, and then I didn’t have deprecation messages for my router’s syntax anymore.

I guess it wasn’t so easy to find because in EY’s blog post, there is no example with the Proc definition (and only one in the official guide), only examples of routes themselves.

Update : I’ve just found out that there was already a comment on EY’s blog about this exact issue. I guess I’ve not search enough :-/


Rails 3 lazy-loading and the console

20 août 2010

While debugging an Rails 3 application in development, I started to wonder if the lazy-loading feature of ActiveRecord 3 was really working.

In a model, I was building a query depending on some parameters. Something like this (obviously simplified) :

class Thing < ActiveRecord::Base
  def initialize(what, options = {})
    collection = what.scoped
    collection = collection.limit(options[:limit] if options[:limit]
    [...]
  end
end

A great thing about Rails is the console. You start you application in a terminal and you can use it (except for controllers and view).

So I did a simple :

$ t = Thing.new(AnotherModel)

Then in my development.log file I saw a beautiful

SELECT * FROM things

The thing is until you really ask for the data, the database should not be queried and the object is a Relation, not a collection of your model.
In the documentation, I was reading that “asking for the data” is calling a method like “first”, “last”, “all” or any iterator. I didn’t use that kind of methods, it shouldn’t query the database.

I turned everything upside down, tried to look in ActiveRecord‘s code, then ActiveRelation‘s, then built a dummy application to see if my setup configuration was wrong, then read 2 times every article I could find about ActiveRecord Query Interface 3.0, then look in the Rails Lighthouse, then ask on several IRC channels, … nothing !
I started to question the reality of this “lazy-loading” feature.

Then I asked on Twitter and my friend Sébastien Gruhier (of the Maptimize fame) gave me the solution. It’s as simple as print, really !
The damn print IRB (the console) is using to show me the data I (in fact) asked for.

Here is the solution for the console :

$ t = Thing.new(AnotherModel); nil
nil
$ t.class
ActiveRecord::Relation < Object

I was able to verify that everything was working in the Controller/View part by clearing the whole view and still loading the page : no SELECT in the logs.

I admit that the few hours I spent on this scared me a little. Such a big bug on the Rails side could not be possible, but I used all the tricks I knew to find the issue. As often, an (great) outsider found the solution in a couple of minutes. I should have asked earlier. Even after a decade in this, I still make the mistake.


Advice on using Ruby, RVM, Passenger, Rails, Bundler, … in development

19 août 2010

This article is the english version of “Conseils pour utiliser Ruby, RVM, Passenger, Rails, Bundler, … en développement”, originally written in French.
If you have a better translation, I’d be happy to use it (full or parts) and credit you.

Last updated on March 3rd, 2011 : Rails 3 and Bundler are stable, versions of Passenger and REE are up-to-date.

Introduction : Why this advice

The dev team I’m working in is composed of 3 people who use Ruby.
Besides me, who’s is spending the most part of my time on Ruby and became “the one who knows” about this stuff, my co-workers don’t use Ruby the whole day, and use some others languages/frameworks (mostly Objective-C/iPhone and PHP/Symfony).

I’ve chosen to use some quite “bleeding edge” technologies for some reasons (I hope good ones) about performance, features, comfort, … but their beta nature is not very easy to deal with on a daily basis.
It’s especially the case when there are some bugs, frequent changes in APIs, (temporary) incompatibilities, … and when you don’t want to learn and understand every detail at every change. Sometimes, you just need it to work to “get things done”.

I’ll introduce briefly those tools and how they work together with the minimum effort and pain and how to get the most of them.

I emphasize the fact that I’m describing here a development context. I’ve not yet used RVM in production, even if I know that it seems to work really well.

The (not so) usual suspects

Ruby

“Wait, What ? Ruby is not stable yet ?”

Of course it is, but it’s also in a transition from 1.8 to 1.9 (1.9.2 released a few months ago). And there are many implementations (JRuby, Rubinius, …). It is very useful to be able to test your code in those different implementations, with RVM.

RVM – Ruby Version Manager

The main idea behind RVM is to have several different rubies on the same system, in a separate environment from the default ruby installed by the OS vendor, and being able to switch very easily between them.

For each Ruby, it allows to have different groups of gems (Gemsets) to avoid version conflicts between projects.

RVM is rich and powerful, but it introduces some concepts and tools that are not so simple to use in the beginning.
If you’re not completely in phase with it, you can easily use the wrong Ruby and/or Gemset and quickly make quite a mess.

Happily, the documentation is really great and quite up-to-date, and the dev team (@wayneeseguin and his mentee @Sutto) is awesome and always available on IRC.

Ruby on Rails

I won’t describe Rails in details, it’s a well known web development framework written in Ruby.

Since version 3, some parts have stopped being compatible with the previous stable version (2.3), mostly about the “rails” command and the scripts installed in applications. It’ become difficult to use Rails 2 and Rails 3 on the same computer without RVM.

Rails 3 also brings a brand new dependency management tool called Bundler, as a required gem.

Bundler

Bundler is a gem, written in Ruby whose purpose is to manage dependencies in a Ruby project, for example a Rails application.
Much better than the “old” way when dealing with the dependency tree, it is especially excellent to quickly install all the gems required by a project in a restricted area, without interfering with others projects and vice versa.

Its stable version is 1.0 (1.0.10 to be accurate).

Phusion Passenger

Phusion Passenger connects Rack or Rails apps with Apache (or Nginx), allowing to host this kind of Ruby apps almost as easily as PHP scripts/apps.

It is now a stable piece of software, but using it with RVM and Bundler is not always obvious, especially if you want to use a different Gemset (and the gems inside it) for each hosted application.

Issues we had

Those past few months, we mostly had issues with following properly versions changes and dealing with incompatibilities. Although I managed to keep the pace, my co-workers had better things to do than spending hours each week to make all this stuff work. Some changes that I was committing in our repositories were leading to non-working projects on their side or at least some headaches for them. Some hair were pulled, and occasionally, all these beautiful tools were called with improper names ;-)

Recommendations

I’m going to describe the setup from a developer point of view (no production right now), with Mac OS X. It should probably be quite the same with Linux, even if some tools may not come pre-installed, or with different versions, … For Windows, it’s a whole different thing. I have no viable information about what is working or not and this part of the universe is quite hostile with web development (especially Ruby).

Apple has been shipping Mac OS X with Ruby since Leopard (at least). This pre-installed version is not useful here, but it’s good to know that it exists, at least to differentiate from other rubies installed with RVM. Right now, on Mac OS X 10.6.4, Ruby 1.8.7-p174 is installed.

If you start from a state where you’ve never done anything regarding Ruby, gems, … I advise not to instal any gem at all. Everything will be dealt with RVM.

Install RVM

The installation process for RVM is quite simple et well documented. You’d rather read it there than copy/paste from here.

Once RVM is installed, you can install other “rubies”. I suggest using Ruby Enterprise Edition instead of a regular 1.8.7 :

$ rvm install ree

I also suggest to make it the default RVM Ruby and the preferred on for Passenger.

$ rvm use ree --default --passenger

From now on, every new shell will use this version of Ruby and a wrapper script will allow Passenger to use it.

To update RVM itself you just have to

$ rvm get latest

If you want to use the latest from the Git repository instead of the latest release :

$ rvm get head

How to use RVM Gemsets

The Gemset concept

As a reminder, a Gemset is a hermetic environment, dedicated to the installation and use of gems.
Each Ruby installed with RVM has at least 2 Gemsets : default (unnamed in fact), and global. By default, we are in the unnamed Gemset. The global Gemset makes every gem installed in it available for all other Gemsets of the current Ruby. Then you can create as many Gemsets as you like, for example 1 for each development project.

Here is an attempt to represent this visually :

/--------------------------|------------------|------------------\
|           Ruby EE        |     Ruby 1.9.2   |    other Ruby    |
|--------------------------|------------------|------------------|
|           @global        |      @global     |     @global      |
|--------------------------|------------------|------------------|
| @default | @app1 | @appX | @default | @appX | @default | @appX |
\--------------------------|------------------|------------------/

A Gemset by project

For every web sites/apps I’m working on, I create a Gemset. The gems the projects can “see” are isolated from others, it’s like a specific environment for the project. For those managed with Bundler, the interest is slightly smaller, but for others (Rails 2, other frameworks or no framework at all) the interest is considerable.

One of RVM’s features is to recognize the presence of a .rvmrc file in a directory when you enter it (with cd for example). If this file refers to a Ruby and/or a Gemset, RVM makes the switch automatically. Each one of my projects has its .rvmrc file containing at least this line :

rvm --create use default@projectX > /dev/null

In short, it will use the default Ruby (defined earlier, but changeable any time) and the Gemset “projectX” if it exists. If not, it is created right away. Redirecting to /dev/null hides the RVM’s output, otherwise it’s annoying to see it each time you enter such a directory.

Since version 1.0 (released on august 22nd, exactly 1 year after the first commit), each time RVM will find a new .rvmrc file, it will ask if you trust it or not. It’s a security feature.

Gems in the global Gemset

I always install some gems in the global Gemset to have them available, whatever project Gemset I’m in (or if I’m outside any specific project) including the default one.

  • passenger
  • capistrano + capistrano-ext
  • bundler
  • git_remote_branch
  • awesome_print
  • g
  • wirble

Those gems will be available in any Gemset inside the Ruby associated with this global Gemset.

Make RVM and Phusion Passenger a happy couple

Usually, Passenger uses a defined ruby binary and the “normal” environment variables to look for gems required by hosted applications.

When you install Passenger, you have to configure the webserver (here Apache, but there is a similar configuration for Nginx) with (at least) those 3 directives :

LoadModule passenger_module /usr/lib/ruby/gems/1.8/gems/passenger-3.0.4/ext/apache2/mod_passenger.so
PassengerRoot /usr/lib/ruby/gems/1.8/gems/passenger-3.0.4
PassengerRuby /usr/bin/ruby1.8

The key point is to tell Passenger to use the right Ruby, where the modules are, … Here is my setup (adapted from RVM documentation) :

LoadModule passenger_module /Users/jlecour/.rvm/gems/ree-1.8.7-2011.03@global/gems/passenger-3.0.4/ext/apache2/mod_passenger.so
PassengerRoot /Users/jlecour/.rvm/gems/ree-1.8.7-2011.03@global/gems/passenger-3.0.4
PassengerRuby /Users/jlecour/.rvm/wrappers/ree-1.8.7-2011.03/ruby

With this, Passenger will use the module from the passenger gem, installed in the global Gemset of the default Ruby and the defined wrapper as a Ruby binary.

OK, but how Passenger will be able to use the right Gemset depending on the application ?

With Passenger >= 2.2.14 and RVM >= 0.1.42 it is possible to use (in Ruby) an API for RVM and adjust the environment variables for the hosted application. It will then know where to look for gems. It’s explained in details by Darcy Laycock (Sutto) in his blog post The Path to Better RVM & Passenger Integration.

Currently, Passenger has a limitation to use only 1 Ruby interpreter. Check out the documentation for more info.

For a Rails 3 application, with Bundler, here is my config/setup_load_paths.rb file :

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

ENV['BUNDLE_GEMFILE'] = File.expand_path('../Gemfile', File.dirname(__FILE__))
require 'bundler/setup'

For a Rails 2 application without Bundler, you just have to delete the last 2 lines.

With this, I can host several Rails 2 and Rails 3 application with Passenger, on the same webserver, without any version conflicts.

Use Bundler for dependencies management

I summarize once again ; Bundler allow to specify which gems (and their versions) required by a project, to install them (with dependency management) and load them exclusively in the project. The result is : easy installation, confidence in the used versions and no conflicts.

If you’ve been using Bundler <= 0.9.6, I strongly recommend to uninstall the previous versions, (unless you explicitly need them), install the pre-release version, then make Bundler re-install the required gems.

$ gem uninstall bundler
$ gem install bundler
$ bundle install

When you bundle install, the Gemfile is processed to build the dependency tree and a Gemfile.lock is created. It is used to know exactly which version of what gems has to be installed.
As long as this file is not modified, the project will only see those gems. It guaranties that the development environment (including other developers) will use the same gems as those for tests, staging, production, …

A developer who wants to update a gem or install a new one has to modify the Gemfile file accordingly then do a bundle install from the root directory of the project (where the Gemfile file is). Then he can commit the changes in the repository for the other developers.

A developer who doesn’t want to manage gem version changes just has to let the Gemfile and Gemfile.lock files like they are. To verify he has every gem the project needs (in the right version), after an update of the codebase, or if the application doesn’t work, he just has to do a bundle install from the root directory of the project. This way, he’s sure to be up-to-date.

Using RVM make all this even easier, faster and reliable. In normal mode, Bundler installs the gems system wide, with all the other gems, but since what he sees as the whole system is only a Gemset, everyone is stays in its yard.
In deployment mode, it installs gems in a cache directory, inside the application. It looks like a Gemset. It is possible to optimize this step with Capistrano

Bundler and Capistrano for an easy depoyment

An application managed with Bundler doesn’t use the usual require methods to load the gems. Bundler takes care of this when the application is initializing. To do this, the gems have to be accessible to Bundler on the application server. We have to check if everything is OK after each deployment and if possible share the gems between releases of the application to avoid unnecessary downloads and disk usage.

Since version 1.0.0.rc.5, Bundler has a Capistrano task (in fact since rc.4 but it had some bugs). You just have to add the line require 'bundler/capistrano' in your deployment recipe (deploy.rb). You can tweak some parameters, but by default, the gems are going into shared/bundle.

Other major packages

For the management of additional packages that Apple does not provide, I usually suggest to use Homebrew. Contrary to MacPorts, it doesn’t install every library or package systematically. If it finds what it needs in the Apple provided libraries (like OpenSSL, …) it’s going to use them instead of downloading/compiling/installing them. It’s faster, more coherent and lighter.

Homebrew also install everything it needs with the user’s permissions, without sudo. It seems a little disturbing for a sysadmin, but in a development context, it makes a lot of sense.

I’ve used Homebrew to install major packages as MySQL, Git, ImageMagick, MongoDB, … and numerous small tools like tree, wget, ack, …

Credits

First, I’d like to thank all the developers and contributors of those awesome tools.

A special thank you for Wayne E Seguin and John Mettraux for the meticulous typo hunting, Thibaut Barrère and Sébastien Gruhier for their support and additional tips.


Conseils pour utiliser Ruby, RVM, Passenger, Rails, Bundler, … en développement

13 août 2010

Cet article existe en anglais. Ma propre traduction n’est probablement pas la meilleure, donc si vous avez des améliorations à suggérer, je suis preneur.

Dernière mise à jour le 3 mars 2011 : Rails 3 et Bundler sont en version stable, et les versions de Passenger et REE sont actualisées.

Intro : Pourquoi ces conseils

L’équipe de dev dans laquelle je travaille est composée de 3 personnes qui bossent avec Ruby.
À part moi qui ne fait presque que ça et qui suis de fait un peu le “référent” sur ces questions, mes 2 très chers collègues n’utilisent pas Ruby toute la journée et utilisent donc d’autres environnements de travail (Objective-C/iPhone et PHP/Symfony principalement).

J’ai choisi d’utiliser un certain nombre de technologies un peu “bleeding edge” pour des vraies (j’espère des bonnes) raisons de performance, de fonctionnalités, de confort, … mais leur nature “beta” ne les rend pas toujours faciles à comprendre et à manipuler, …
C’est surtout le cas quand il y a des bugs, des changements fréquents de fonctionnement, des incompatibilités (temporaires), … et qu’on n’a pas envie (et c’est normal) de se prendre la tête à tout comprendre. Des fois il faut juste que ça marche.

Je vais donc vous présenter rapidement ces différents outils et comment les faire fonctionner ensemble avec le minimum de souffrance et le maximum d’intérêt.
Je précise que je présente ici un usage de ces outils en environnement de développement. Je n’ai pas utilisé RVM en production, bien que ça soit tout à fait possible, et même conseillé.

Les outils du boucher

Ruby

“Ah bon, Ruby n’est pas encore stable ?”

Si si, mais Ruby est dans une phase de transition entre la version 1.8 et la 1.9 (la 1.9.2 est sortie il y a quelques mois). Et puis il y a plusieurs implémentations (notamment JRuby et Rubinius). Il est donc intéressant de pouvoir facilement tester son code dans ces différentes implémentations, via RVM.

RVM – Ruby Version Manager

Le principe de RVM est de permettre d’installer plusieurs versions de Ruby sur une même machine, dans un environnement hermétique à une éventuelle version installée sur l’OS et de pouvoir basculer de l’une à l’autre facilement.

Il permet aussi, pour chaque Ruby, d’avoir des ensembles de gems bien séparés (Gemset) pour éviter les conflits de versions entre différents projets.

L’outil est très riche et fonctionnellement très puissant, mais il introduit des concepts et des outils pas si simples que ça.
Si on n’est pas complètement en phase avec, on a vite fait de se tromper de version de Ruby et/ou de Gemset.

Heureusement la doc est très bien faite et l’équipe de dev (@wayneeseguin et @Sutto) est super disponible sur IRC.

Ruby on Rails

Je ne présente pas plus en détail Ruby on Rails : c’est un framework de développement d’applications web, écrit en Ruby.

Depuis sa version 3, il apporte énormément de nouveautés, mais aussi certaines incompatibilités par rapport à sa version stable précédente (2.3), surtout au niveau des commandes “rails” et scripts dans les applis. Il n’est donc pas simple du tout d’utiliser Rails 2 et Rails 3 sur une même machine sans utiliser RVM.

Cette version 3 apporte aussi un nouveau système de gestion de dépendances : Bundler, sous forme d’une gem requise.

Bundler

Bundler est une Gem écrite en Ruby, permettant de gérer les dépendances d’un projet écrit en Ruby, par exemple une application Rails.
Beaucoup plus efficace que l’ancien procédé au niveau de la gestion de l’arbre des dépendances il est surtout excellent pour installer rapidement les gems nécessaires à un projet dans un espace limité à ce projet, sans que les éventuelles autres gems installées sur le système ne puisse le perturber.

Sa version stable actuelle est la 1.0 (1.0.10 exactement).

Phusion Passenger

Phusion Passenger sert à connecter des applis Rack ou Rails avec Apache (ou Nginx), permettant d’héberger ces applis presque aussi facilement que des applis/scripts en PHP.

C’est un composant logiciel maintenant bien stable, mais son usage en conjonction avec RVM et Bundler n’est pas toujours évident, surtout si on veut qu’il utilise un Gemset (et les gems qui sont dedans) différent pour chaque applis.

Les difficultés rencontrées

Ces derniers mois, on a surtout rencontré des difficultés avec le suivi des versions et les incompatibilités que ça a révélé. Alors que j’arrivais à peut près à suivre le rythme des changements, … mes collègues avaient mieux à faire. Des modifs que je mettais en commun rendaient certaines fois les projets inutilisables pour les autres, ou en tous cas difficile à remettre en ordre. Ça débouchait sur des cheveux arrachés, des insultes aux outils :-), …

Recommandations

Je vais décrire une situation de poste de développement (donc pas de production), sous Mac OS X. La situation sous Linux sera probablement très proche, à la différence près que les certains composants ne seront pas pré-installés. Pour Windows, c’est tout autre chose, mais je n’ai aucune info fiable sur ce qui marche ou pas dans cet univers assez hostile au développement web (Ruby en particulier).

Apple fourni Ruby en standard depuis Leopard (peut-être même avant). Cette version pré-installée ne nous sera pas utile, mais il est bien de savoir qu’elle existe, ne serait-ce que pour la différencier des autres qui seront installées via RVM. Actuellement, sous Mac OS X 10.6.4, c’est Ruby 1.8.7-p174 qui est installé.

Si on part d’un système “vierge” au niveau de l’utilisation de Ruby, je conseille de n’installer aucune gem particulière et de ne pas toucher à ce qui est présent. On va tout confier à RVM.

Installer RVM

La procédure d’install de RVM est assez simple et bien documentée. Suivez la, c’est plus sûr que si je la recopie ici.

Un fois cette install faite, vous pouvez commencer à installer d’autres “rubies”. Je conseille particulièrement l’utilisation de Ruby Enterprise Edition :

$ rvm install ree

Je conseille aussi d’en faire la version par défaut, et celle pour Passenger.

$ rvm use ree --default --passenger

Dorénavant, tout nouveau shell utilisera cette version de Ruby et un script “wrapper” permettra à Passenger de l’utiliser.

Pour mettre à jour RVM, il suffit de faire :

$ rvm get latest

Pour utiliser la dernière version Git de RVM plutôt que la dernière version publiée :

$ rvm get head

Utilisation des Gemsets de RVM

Concept des Gemsets

Pour rappel, un Gemset est un environnement hermétique dédié à l’installation et l’utilisation de gems.
Chaque version de Ruby connue de RVM dispose d’au moins 2 Gemsets : default (sans nom, en fait), et global. Par défaut on est dans le Gemset sans nom. Le Gemset global permet de rendre disponibles les gems qui y sont installées dans tous les autres Gemsets de la version courante de Ruby.
On peut ensuite créer autant de Gemsets que l’on souhaite, par exemple 1 pour chaque projet de dev.

Voilà une tentative de représentation visuelle :

/--------------------------|------------------|------------------\
|           Ruby EE        |     Ruby 1.9.2   |    autre Ruby    |
|--------------------------|------------------|------------------|
|           @global        |      @global     |     @global      |
|--------------------------|------------------|------------------|
| @default | @app1 | @appX | @default | @appX | @default | @appX |
\--------------------------|------------------|------------------/

Un Gemset par projet

Pour chacun des sites sur lesquels je travaille, je crée donc un Gemset. Ça permet d’isoler les gems qu’il a à disposition et ainsi de simuler un environnement spécifique à ce projet. Pour les projets qui utilisent Bundler l’intérêt est légèrement moindre, mais pour les autres (Rails 2, autres frameworks ou pas de framework) le bénéfice est immédiat.

Une des fonctionnalités de RVM est de reconnaître la présence d’un fichier .rvmrc dans un répertoire lorsqu’on s’y rend (avec cd par exemple). Si ce fichier de config indique qu’il faut choisir un Ruby et un Gemset, il bascule tout seul.
Chacun de mes projets dispose donc d’un fichier .rvmrc contenant au moins cette ligne :

rvm --create use default@projetX > /dev/null

En résumé, il va utiliser la version par défaut de Ruby (définie plus haut, mais modifiable à tout instant) et le Gemset “projetX” s’il existe. S’il n’existe pas, il est créé automatiquement. Le renvoi sur /dev/null permet que cette opération ne génère aucune sortie, ce qui est le cas sinon et c’est vite casse-pieds lorsqu’on navigue beaucoup via le terminal.

Depuis la version 1.0 (sortie le 22 août, 1 an exactement après le premier commit), RVM va demander confirmation la première fois qu’il rencontre un fichier .rvmrc dans un répertoire. C’est une précaution de sécurité.

Des gems dans le Gemset global

J’installe un certains nombre de gems dans le Gemset global pour les avoir toujours sous la main, quelque soit le projet dans lequel je travail ou si je suis simplement dans un shell hors projet.

  • passenger
  • capistrano + capistrano-ext
  • bundler
  • git_remote_branch
  • awesome_print
  • wirble
  • g

Ces gems seront donc dispo pour tous les Gemsets de la version de Ruby associée au Gemset global en cours.

Couplage harmonieux de RVM et Phusion Passenger

Dans son fonctionnement normal, Passenger utilise une version définie de Ruby et les variables d’environnement “normales” pour savoir où trouver les gems, …

Lorsqu’on install Passenger, on doit indiquer au serveur web (ici c’est Apache mais il y a une variante pour Nginx) ces 3 directives :

LoadModule passenger_module /usr/lib/ruby/gems/1.8/gems/passenger-3.0.4/ext/apache2/mod_passenger.so
PassengerRoot /usr/lib/ruby/gems/1.8/gems/passenger-3.0.4
PassengerRuby /usr/bin/ruby1.8

La clé est d’indiquer à Passenger la bonne version de Ruby et l’emplacement des modules, … Voilà ma propre config :

LoadModule passenger_module /Users/jlecour/.rvm/gems/ree-1.8.7-2011.03@global/gems/passenger-3.0.4/ext/apache2/mod_passenger.so
PassengerRoot /Users/jlecour/.rvm/gems/ree-1.8.7-2011.03@global/gems/passenger-3.0.4
PassengerRuby /Users/jlecour/.rvm/wrappers/ree-1.8.7-2011.03/ruby

En l’état, Passenger utilisera donc le module présent dans la gem installée dans le Gemset global et la version de Ruby définie par le wrapper passenge_ruby.

C’est bien, mais comment va-t-il utiliser le bon Gemset selon l’application ?

Avec un Passenger >= 2.2.14 et RVM >= 0.1.42 il est désormais possible d’utiliser (en Ruby) une API interne à RVM et forcer les variables d’environnement pour l’application lancée et qu’elle sache où chercher les gems dont elle a besoin. Le procédé est détaillé dans l’article The Path to Better RVM & Passenger Integration.
En résumé, au chargement de l’application Ruby, on demande à RVM quelques sont les variables à utiliser.

Pour le moment, Passenger est limité à un seul interprêteur Ruby. Voir la doc pour plus d’info.

Pour une appli en Rails 3, avec Bundler, voilà mon fichier config/setup_load_paths.rb :

if ENV['MY_RUBY_HOME'] &amp;&amp; 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

ENV['BUNDLE_GEMFILE'] = File.expand_path('../Gemfile', File.dirname(__FILE__))
require 'bundler/setup'

Pour une application Rails, sans Bundler, il suffit de supprimer les 2 dernières lignes.

De cette manière, j’arrive à héberger avec Passenger des applis Rails 2, Rails 3 et Rack sur le même serveur web, sans conflit de version.

Utiliser Bundler pour la gestion des dépendances

Je résume une fois de plus ; Bundler permet de spécifier les gems (et leur version) nécessaires à un projet, de les installer (avec prise en charge des dépendances) et de ne “proposer” au projet en question que ces gems là. Le résultat est donc : facilité d’installation, confiance dans les versions utilisées, absence de conflits.

Si vous avez utilisé une version de Bundler <= 0.9.6, je ne peux que vous conseiller de désinstaller les version actuelles (sauf si vous en avez explicitement besoin), installer la version 1.0.rc puis recalculer les dépendances.

$ gem uninstall bundler
$ gem install bundler
$ bundle install

La version 1.0 (même depuis sa première beta) a apporté de gros changement, dont certains sont incompatibles, mais elle simplifie la gestion des déploiements en production, …

Lorsque dans un terminal, à la racine d’un projet géré avec Bundler, on fait un bundle install, le fichier de config Gemfile est analysé pour déterminer l’arbre des dépendances et un fichier Gemfile.lock est créé. Il est ensuite utilisé pour connaître l’exacte version de chaque gem à installer/utiliser.
Tant que ce fichier n’est pas modifié, le projet d’utilisera que ces gems là, ce qui garanti une cohérence entre l’environnement de développement (y compris d’un développeur à l’autre) et ceux de test, pre-production, production, …

Un développeur qui souhaite mettre à jour une gem ou en installer une nouvelle doit modifier le fichier Gemfile en conséquence, puis faire un bundle install. Il peut enfin versionner ces changements pour les rendre disponibles aux autres développeurs.

Un développeur qui ne veut pas gérer les changements de version de gems du projet en cours ne doit juste pas toucher aux fichiers Gemfile et Gemfile.lock. Pour s’assurer qu’il dispose en local des gems nécessaires à l’exécution de l’application ou bien si après une mise à jour du code il rencontre un “plantage” indiquant une erreur de gem, le 1er réflexe doit être de faire un bundle install depuis la racine du projet. Ainsi il s’assure d’être à jour au niveau des gems et de leur version.

L’utilisation de RVM rend ces processus encore plus faciles, rapides et fiables. En effet en mode normal, Bundler va installer au niveau système les gems nécessaires, mais comme le niveau système qu’il voit est en fait un Gemset, il ne pollue pas les autres projets.
En mode déploiement, il installe les gems dans un dossier de cache au sein de l’application, ce qui revient à peut près au même qu’un Gemset. Il est possible d’optimiser cette phase avec Capistrano.

Bundler et Capistrano pour un déploiement rapide et facile

Une appli gérée avec Bundler n’utilise plus les mécanismes classiques de require pour charger ses gems, c’est Bundler qui s’en charge à l’initialisation de l’appli. Il faut donc que les gems soient installées et accessibles à Bundler sur le serveur d’application. Il faut donc vérifier que tout est OK après chaque déploiement, et si possible mutualiser les gems d’une fois sur l’autre pour éviter des téléchargements et une consommation inutiles.

Depuis la version 1.0.0.rc.5, Bundler dispose d’une tache pour Capistrano (en réalité c’est depuis la rc.4 mais elle contenait des bugs). Il suffit d’ajouter require 'bundler/capistrano' dans sa recette de déploiement (deploy.rb). On peut éventuellement spécifier l’emplacement d’installation des gems, mais par défaut, c’est dans shared/bundle.

Autres paquets majeurs

Pour la gestion courante des paquets additionnels non fournis par Apple, je conseille d’une manière générale l’utilisation de Homebrew. Contrairement à MacPorts, il ne réinstalle pas toutes les dépendances systématiquement. S’il trouve son bonheur dans les paquets et librairies installés par Apple, il va les utiliser. Ça assure une plus grande cohérence, rapidité d’install et légèreté d’ensemble.

Homebrew installe aussi tout ce qui lui est nécessaire dans l’environnement utilisateur, sans nécessité d’utiliser sudo. C’est un peu déroutant pour un sysadmin, mais dans un contexte de développement, c’est plus pratique et plus simple.

J’ai utilisé Homebrew sur mon Mac pour installer MySQL, Git, ImageMagick, MongoDB, … et nombre de petits outils tels que tree, wget, ack, …, avec beaucoup de satisfaction.

Crédits

En priorité, je souhaite remercier les développeurs de ces outils géniaux et l’armée de contributeurs qui a participé.

Merci à Wayne E Seguin et John Mettraux pour une méticuleuse relecture, Thibaut Barrère et Sébastien Gruhier pour leur soutien et des astuces additionnelles


Suivre

Get every new post delivered to your Inbox.