Ruby и .

Gem глазами потребителя

Послемыслия и работа над ошибками

  • уточнение к «активации» gem'ов при использовании Bundler (07.08.11)
  • переписан раздел RVM (07.08.11)

Наряду с основными заповедями разработчика «Не продублируй» (DRY) и «Будь проще» (KISS), есть еще одна важная: «Не изобрети велосипед». К счастью, в Ruby сообществе уже довольно давно существует стандартный формат для обмена готовыми велосипедами — gem.

Статья призвана дать представление о том, как выглядит система распространения библиотек на Ruby, и помочь понять проблему использования на машине разных версий одного и того же gem'а.

Всё, что вам нужно знать о gem'ах

Минутка лингвистики

«Gem» переводится с английского как «драгоценный камень». Дав формату такое название, авторы недвусмысленно указали на связь с рубином (Ruby — рубин). Произносится он как «джем» [ʤem], хотя в русскоговорящем сообществе нередко можно услышать «гем». Чтобы никого не смущать, я буду использовать оригинальное написание этого слова, т. е. gem.

История

С инициативой стандартного формата для распространения библиотек, написанных на Ruby или C (т. н. расширений), в 2001 году выступил Райан Ливенгуд (Ryan Leavengood), он же является автором первой версии системы управления пакетами Rubygems.

На тот момент рубисты использовали для этих целей архив приложений (RAA), который, по сути, являлся каталогом ссылок. Для того, чтобы подключить в свой проект стороннюю библиотеку, требовалось:

  1. Зайти на сайт RAA.
  2. Через поиск найти страницу интересующего проекта.
  3. Скачать по приведенной ссылке архив с файлами (при условии, что она рабочая, т. к. вела на сторонний ресурс, например, домашнюю страницу разработчика).
  4. Распаковать его куда-нибудь.
  5. Перейти на страницы проектов, которые указаны в разделе «Зависимости».
  6. Скачать оттуда конкретные версии библиотек-зависимостей.
  7. Распаковать их куда-нибудь.
  8. Подключить нужные файлы с помощью require, вспомнив, где находится «куда-нибудь».

Этот нудный процесс просто умолял «Автоматизируйте меня!» И надо сказать, Райан не стал в этих прериях первопроходцем. Системы управления пакетами программ уже имелись как в операционных системах (например, dpkg для ОС, построенных на Debian), так и для языков программирования (CPAN для Perl). Сам Ливенгуд описывал Rubygems как помесь dpkg с jar архивами в Java.

Тем не менее, проект не пошел в массы, до тех пор пока в 2003 году Рич Килмер (Rich Kilmer), Чад Фаулер (Chad Fowler), Девид Блек (David Black), Пол Бреннан (Paul Brannan) и Джим Вайрих (Jim Weirich) не написали свою версию системы управления пакетами. Название ей, с разрешения Ливенгуда, дали Rubygems, хотя от прежней библиотеки в ней не было ни строчки.

Проект продолжает развиваться (в последнее время, не без скандалов) и для официальной реализации Ruby, начиная с версии 1.9, является системной библиотекой (т. е. не требует явного подключения с помощью require).

Что такое gem?

Технически файл с расширением .gem представляет собой обычный архив, внутри которого находится файл спецификации и исходный код библиотеки в состоянии на момент релиза.

В спецификации содержится достаточно много информации, но самое главное:

  • название и версия данного gem'а;
  • названия и версии gem'ов, без которых работа будет невозможна (зависимости);
  • данные об авторе и описание gem'а.

Кроме библиотечных файлов, которые вы подключаете в коде своего приложения, в состав gem'а могут входить исполняемые файлы, которые после установки «видны» на системном уровне. На самом деле это не бинарные, а текстовые файлы — программы, написанные на Ruby. Когда вы запускаете их, ОС вызывает интерпретатор ruby, который и занимается их выполнением. Должно быть, самые известные исполняемые файлы — это rails, rake и gem.

Репозиторий

Никто не может вам запретить собрать свой gem, выложить на файлообменнике и дать пользователям ссылку на скачивание. Gem'ы действительно можно устанавливать локально из файлов, но если бы все так поступали, то — снова здравствуй, 2001 год и ненавистная рутинная работа.

К счастью, система Rubygems подразумевает наличие как минимум одного централизованного, доступного 24 часа в сутки, хранилища gem'ов. Благодаря этому утилита gem может не только выполнить установку, запросив в репозитории одно лишь название gem'а, но и тут же автоматически доустановить все отсутствующие зависимости, исходя из спецификации.

На момент написания этой статьи gem'ы хранятся на Amazon S3, дирижированием ссылок занимается репозиторий rubygems.org. Все его службы написаны на Ruby.

Как возникают gem'ы?

Любой gem появляется как следствие борьбы с проблемой, с которой пришлось столкнуться разработчику. Например, ему была поставлена задача организовать на сайте генерацию отчетов в формате Microsoft Word XP. Если готовых решений для этого не нашлось или не устроила их реализация, разработчик пишет такой генератор самостоятельно.

Поскольку данная задача является типовой, хорошим тоном считается ее gem'ификация, под которой понимают выделение из общего приложения в отдельный проект кода, отвечающего за эту задачу. Внутри проекта структуру папок и наименования файлов приводят к негласному стандарту, а также создают спецификацию. Чтобы теперь назвать это gem'ом, достаточно упаковать папку с проектом с помощью утилиты gem и отправить на rubygems.org.

Публикация gem'а влечет за собой несколько весьма положительных последствий:

  • резко упрощается повторное использование кода в своих же проектах;
  • происходит интенсивное тестирование за счет использования в чужих проектах, что позволяет рано обнаружить возможные ошибки;
  • обнаруженные ошибки могут исправить сторонние разработчики;
  • тешится собственное самолюбие.

Для того, чтобы другие разработчики могли исправить ошибки не только у себя, но и у автора, исходники gem'а обязательно размещают на Github'е. Часто страница проекта на Гитхабе является и основным источником документации.

Как их искать?

К сожалению, формат спецификации gem'а не предусматривает указания его категорий или тегов, поэтому если вы пытаетесь найти gem под конкретные нужды, следует надеятся на то, что автор упомянул ключевые слова в описании. Поиск по описанию можно выполнять как на rubygems.org, так и на Гитхабе (выбрав Ruby в списке языков).

Отдельно стоит упомянуть Ruby Toolbox, где gem'ы рассортированы по категориям и популярности. Новые gem'ы добавляет туда вручную автор сайта, поэтому не стоит рассчитывать при поиске только на этот ресурс.

Версии

Система Rubygems подразумевает, что разработчики придерживаются целесообразной политики назначения номеров версий gem'ов. Так, к сожалению, поступают не все, но понимать, в чем она заключается, нужно. Например, чтобы спать спокойно, когда происходит обновление чужого gem'а.

Номер версии должен состоять из трех чисел, разделенных точками: А.Б.В

Число В увеличивается, когда меняется внутренняя реализация библиотеки с сохранением первоначальной логики. Например, это может быть исправление текущих ошибок, могут появиться/быть убраны private методы. Алгоритм работы может быть переписан так, что библиотека заработает в десять раз быстрее! Но эта реорганизация кода абсолютно прозрачна для текущих пользователей gem'а: ничего по существу не добавилось, ничего у себя исправлять не надо.

Число Б увеличивается (и обнуляет В), когда добавляется новый функционал. Например, могут появиться новые public методы или текущие могут начать принимать дополнительные аргументы. Обновившиеся до этой версии пользователи могут внести изменения в код своих приложений, чтобы начать использовать новые возможности, но могут ничего не трогать, и всё должно работать, как и раньше.

Число А увеличивается (и обнуляет Б и В), когда происходят изменения, для которых невозможно обеспечить обратную совместимость. Если перейти на эту версию и ничего не изменить в своем приложении, gem не будет работать или будет работать с ошибками. Такой переход может потребовать переписывания приложения с нуля.

Из всего вышесказанного можно сделать вывод, что после версии 0.9.9 совсем не обязательно должна появиться 1.0.0, вполне ожидаемой будет как раз версия 0.9.10 или 0.10.0.

Установка gem'а под микроскопом

Чтобы хорошо понять проблему подключения gem'ов в приложении, нужно разобраться, в каком виде они хранятся на машине. Для этого давайте проследим, что происходит, когда вы набираете в командной строке

gem install rails
  1. Rubygems проверяет, установлен ли у вас этот gem и какой версии.
  2. Ищет в текущей директории файл rails-*.gem (где * может быть любым номером версии). Если такой файл найден, дальнейшая установка будет происходить прежде всего с его использованием.
  3. Если gem файл не найден, считывает локальный список репозиториев (по умолчанию там находится только rubygems.org).
  4. Запрашивает у них последнюю версию этого gem'а, а также названия и версии его зависимостей.
  5. Если Rails у вас не установлены или в репозитории появилась более новая версия, скачивает в папку ../cache их gem файл. Если на момент запроса актуальной версией Ruby on Rails является 3.0.9, будет загружен rails-3.0.9.gem.
  6. Параллельно проверяет, установлены ли у вас зависимости, и скачивает в ../cache то, что нужно установить или обновить (загружаются activesupport-3.0.9.gem, builder-2.1.2.gem и т. д.).
  7. Распаковывает загруженные файлы в папку ../gems, названия подпапок будут совпадать с названиями gem файлов (например, там появится /rails-3.0.9). Подпапки со старыми версиями никуда не исчезают.
  8. Откладывает (для себя) спецификации gem'ов в папку ../specifications.
  9. Запускает утилиту RDoc, которая извлекает из распакованных файлов комментарии к исходному коду, формирует на их основе html файлы с документацией и складывает их в папку ../doc.
  10. Если в состав gem'ов входят исполняемые файлы, создает в папке /usr/local/bin одноименные исполняемые файлы для их запуска (подробнее об этом ниже). И действительно, там появляются bundler, rails, thor и т. д.

Таким образом, команда gem install не выполняет никаких сверхъестественных операций: она только скачивает и распаковывает. Более того, она никоим образом не взаимодействует с уже установленными gem'ами и не трогает окружение, т. е. эта команда сама по себе безвредна. Но можно ли считать процедуру установки абсолютно безопасной? Да, если речь идет о новых gem'ах, и нет, если подразумевается обновление уже установленных, поскольку это косвенно может привести к конфликтам (об этом, как и о способах решения проблемы — ниже).

В качестве бонуса: если вы из любопытства после установки заглянете в папку /usr/local/lib/ruby/gems/1.9.1/gems/rails-3.0.9, то возможно, будете удивлены, что в ней вообще не содержится никакого кода. Так и есть, трижды героический gem Rails — всего лишь пустышка, тянущая за собой целый пучок зависимостей.

Подключение gem'ов

Магический (?) require

Итак, вы понемногу осваиваете работу с gem'ами и решили поэкспериментировать с созданием PDF документов. Для этого хорошо подходит Prawn:

you@your-comp:~$ gem install prawn
Fetching: Ascii85-1.0.1.gem (100%)
Fetching: pdf-reader-0.10.0.gem (100%)
Fetching: ttfunk-1.0.1.gem (100%)
Fetching: prawn-0.11.1.gem (100%)
Successfully installed Ascii85-1.0.1
Successfully installed pdf-reader-0.10.0
Successfully installed ttfunk-1.0.1
Successfully installed prawn-0.11.1
4 gems installed

На главной странице сайта Prawn есть пример использования этой библиотеки:

require 'prawn'
Prawn::Document.generate('hello.pdf') do |pdf|
  pdf.text("Hello Prawn!")
end

Вот так просто, оказывается, подключаются gem'ы.

Но понимаете ли вы, что делает строка require 'prawn'?

Для начала давайте вспомним, что require — метод, который загружает в память и выполняет содержимое файла (если он не был загружен ранее). Когда не указан полный путь к файлу (наш случай), Ruby всё равно пытается обнаружить файл. Поскольку физически невозможно во время каждого require просматривать все имеющиеся папки на компьютере, область его поиска ограничена только несколькими.

Список просматриваемых папок Ruby хранит в глобальной переменной-массиве $LOAD_PATH:

you@your-comp:~$ irb
irb(main):001:0> puts $LOAD_PATH
/urs/local/lib/ruby/site_ruby/1.9.1
/urs/local/lib/ruby/site_ruby/1.9.1/i686-linux
/urs/local/lib/ruby/site_ruby
/urs/local/lib/ruby/vendor_ruby/1.9.1
/urs/local/lib/ruby/vendor_ruby/1.9.1/i686-linux
/urs/local/lib/ruby/vendor_ruby
/urs/local/lib/ruby/1.9.1
/urs/local/lib/ruby/1.9.1/i686-linux
 => nil

Теперь поищите на машине папку со свежеустановленным Prawn (вспоминайте пошаговую процедуру установки). Она тут: /usr/local/lib/ruby/gems/1.9.1/gems/prawn-0.11.1, в ней еще одна папка — /lib, где и лежит искомый файл — prawn.rb. Как же так получается, пути ../prawn-0.11.1/lib/ в массиве $LOAD_PATH нет, но Ruby ухитряется найти файл?

Дело в том, что Rubygems во время загрузки (а в Ruby 1.9 это происходит автоматически) перезаписывают родной метод require своим.

Когда Ruby не находит файл в текущих папках области поиска (как в случае с prawn.rb), возбуждается стандартная ошибка. Но Rubygems перехватывают ее, и затем по названию файла пытаются определить, к какому gem'у он относится. Если это удается сделать, пути к этому gem'у и его зависимостям добавляются в $LOAD_PATH. Gem помечается как активированный, после чего снова вызывается родной require, который, конечно же, находит и загружает файл.

irb(main):002:0> require 'prawn'
 => true
irb(main):003:0> puts $LOAD_PATH
/usr/local/lib/ruby/gems/1.9.1/gems/Ascii85-1.0.1/lib
/usr/local/lib/ruby/gems/1.9.1/gems/pdf-reader-0.10.0/lib
/usr/local/lib/ruby/gems/1.9.1/gems/ttfunk-1.0.1/lib
/usr/local/lib/ruby/gems/1.9.1/gems/prawn-0.11.1/lib
/urs/local/lib/ruby/site_ruby/1.9.1
/urs/local/lib/ruby/site_ruby/1.9.1/i686-linux
/urs/local/lib/ruby/site_ruby
/urs/local/lib/ruby/vendor_ruby/1.9.1
/urs/local/lib/ruby/vendor_ruby/1.9.1/i686-linux
/urs/local/lib/ruby/vendor_ruby
/urs/local/lib/ruby/1.9.1
/urs/local/lib/ruby/1.9.1/i686-linux
 => nil

Уточнение версий

Представьте теперь, что вы написали замечательно работающее приложение, в котором используется Prawn. Через некоторое время gem обновился, и последней версией на сайте значится уже 0.12.0. Дух экспериментаторства умирает только после программиста, поэтому вы решаетесь попробовать новую версию.

После установки на вашей машине находятся уже две папки: ../prawn-0.11.1/lib/ и ../prawn-0.12.0/lib/, и когда в приложении вызывается require 'prawn', Rubygems должны принять решение, какую версию gem'а активировать.

По умолчанию активируется самая последняя версия.

irb(main):001:0> require 'prawn'
 => true
irb(main):002:0> puts Gem.loaded_specs
{"prawn"=>#<Gem::Specification name=prawn version=0.12.0>,
 "pdf-reader"=>#<Gem::Specification name=pdf-reader version=0.10.3>,
 "Ascii85"=>#<Gem::Specification name=Ascii85 version=1.0.1>,
 "ttfunk"=>#<Gem::Specification name=ttfunk version=1.0.1>}
 => nil

Одна беда: как оказалось, ваше приложение с новой версией не работает. Никто не застрахован от ошибок, и похоже, в версию 0.12.0 они проникли. Что делать? Самый простой вариант — удалить эту версию с помощью команды gem uninstall. К сожалению, это не всегда возможно. Например, вы хотите сообщить авторам Prawn о возникшей ошибке, и свежая версия вам необходима для тестирования и отладки.

Для решения таких проблем в Rubygems есть метод для принудительной активации — gem. Он принимает строковые аргументы: название gem'а и его версию.

irb(main):001:0> gem 'prawn', '0.11.1'
 => true
irb(main):002:0> require 'prawn'
 => true
irb(main):003:0> puts Gem.loaded_specs
{"prawn"=>#<Gem::Specification name=prawn version=0.11.1>,
 "pdf-reader"=>#<Gem::Specification name=pdf-reader version=0.10.0>,
 "Ascii85"=>#<Gem::Specification name=Ascii85 version=1.0.1>,
 "ttfunk"=>#<Gem::Specification name=ttfunk version=1.0.1>}
 => nil

Запись версии '0.11.1' эквивалентна '= 0.11.1', это самое радикальное соответствие: или указанная версия, или я за себя не отвечаю! Существуют и более либеральные варианты:

  • gem 'prawn', '>= 0.11.1' — вы уверены, что ваше приложение будет безошибочно работать с версией не ниже 0.11.1 (например, 2.3.5 вполне сойдет). Это очень рискованное утверждение, как вы успели убедиться с версией 0.12.0;
  • gem 'prawn', '>= 0.11.1', '< 0.12.0' — вы уверены, что ваше приложение будет нормально работать с версиями не ниже 0.11.1, но насчет 0.12.0 у вас уже сомнения (а вот 0.11.97 — сгодится). Обычно подобного ограничения вполне достаточно, чтобы застраховать себя от ошибок при обновлении gem'а;
  • gem 'prawn', '~> 0.11.1' — этот оптимистичный сперматозоид (который западные товарищи окрестили twiddlewakka)) является просто сокращенной записью предыдущего выражения. Другими словами: второе, если считать справа, число фиксируется, его менять нельзя.

Когда версия указывается без третьего числа, Rubygems опять фиксируют второе справа число, т. е. первое. Таким образом, вызов метода

gem 'prawn', '~> 0.14'

делает то же, что и

gem 'prawn', '>= 0.14.0', '< 1.0'

Если gem был активирован, попытка активации другой версии этого же gem'а приведет к ошибке:

irb(main):001:0> gem 'prawn', '0.11.1'
 => true
irb(main):002:0> gem 'prawn', '0.12.0'
Gem::LoadError: can not activate prawn (= 0.12.0) for [], already activated
prawn-0.11.1 for []

И это разумное решение: менять версии в запущенном приложении бессмысленно и опасно.

Там, где бессильны Rubygems

Было время, когда метод gem вполне справлялся со своими обязанностями. Его использовали во вторых версиях Ruby on Rails, и все были счастливы.

Но росло число публикуемых gem'ов, в том числе тех, которые из-за своей популярности использовались в качестве зависимостей других gem'ов. И нередкой стала ситуация, когда при установке двух gem'ов их зависимости пересекались.

Представьте, что на вашей машине установлен следующий набор:

actionpack (2.3.5)
rack (1.1.2, 1.0.1)
thin (1.2.11)

В зависимостях actionpack и thin фигурирует rack:

actionpack (2.3.5)
  rack ~> 1.0.0

thin (1.2.11)
  rack >= 1.0.0

Теперь подключим эти gem'ы:

irb(main):001:0> require 'actionpack'
 => true
irb(main):002:0> require 'thin'
 => true

Активируя actionpack, Rubygems спускаются к зависимостям, находят в установленных gem'ах подходящую под указанное ограничение версию rack (1.0.1) и активируют ее. Когда подходит очередь активации thin, его зависимость удовлетворяется автоматически.

Но что, если порядок подключения будет обратным?

irb(main):001:0> require 'thin'
 => true
irb(main):002:0> require 'actionpack'
 # Ошибка! Уже активирован rack 1.1.2!

Ошибка возникает из-за того, что нестрогая зависимость rack (>= 1.0.0) позволяет Rubygems задействовать его самую свежую версию — 1.1.2. Эта версия совершенно не устраивает actionpack, но rack уже активирован, назад дороги нет!

И чем больше на вашей машине установлено различных версий одного и того же gem'а, тем больше вероятность подобных накладок. Но ведь зоопарк разных версий — нормальная практика: на одном сервере могут быть запущены одновременно как приложение, написанное два года назад, так и позавчерашнее, и понятно, что они не будут работать с одинаковыми версиями одного и того же gem'а.

Кроме этой проблемы обязательно стоит упомянуть вопрос запуска исполняемых файлов.

Исполняемые файлы, идущие в комплекте с gem'ом, находятся в подпапке /bin конкретной версии gem'а. Если у вас на машине установлен rake версии 0.9.2, исполняемый файл rake будет находиться в папке /usr/local/lib/ruby/gems/1.9.1/gems/rake-0.9.2/bin/.

ОС не может (по аналогии с Rubygems) просматривать абсолютно все папки в поисках файла, когда вы даете команду на его запуск, и ограничивается определенным списком, который хранится в переменной PATH.

you@your-comp:~$ echo $PATH
/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games

Именно поэтому Rubygems помещают в одну из «видимых» системе папок (в нашем случае в /usr/local/bin) свои исполняемые файлы, единственная цель которых — запустить одноименный файл из поставки gem'а.

Но если на машине установлено несколько версий gem'а, Rubygems должны понять, какая именно вам нужна. Задать версию можно, указав в качестве первого параметра в командной строке, перед и после номера должны идти символы подчеркивания (например, _3.0.7_, это сделано, чтобы избежать возможного конфликта с числовыми параметрами).

Важно понимать, что если версия не указана, то всегда будет запущена самая последняя версия исполняемого файла из установленных на компьютере, независимо от того, в каком проекте/папке вы находитесь.

Например, если у вас установлены rake версии 0.8.7 и 0.9.2:

you@your-comp:~$ rake -V
rake, version 0.9.2
you@your-comp:~$ rake _0.8.7_ -V
rake, version 0.8.7

Да, задача запуска файлов определенных версий решена, но мягко говоря, не совсем элегантно (например, вам нужно постоянно помнить, в каком проекте какую версию rake или rails вы используете, да еще и указывать ее в команде).

Bundler

Конечно, по-хорошему всё это надо было исправлять в самих Rubygems. Но что-то у них там не срослось, и начали возникать альтернативные менеджеры gem'ов. Самым популярным на данный момент является Bundler, поскольку он входит в поставку и заведует gem'ами третьих Ruby on Rails.

Bundler — обычный gem, и перед тем, как использовать, его нужно вначале установить (если вы устанавливали Rails v. 3.x.x, это уже сделано). Кроме этого, в своем приложении вы должны подключить в самом начале

require 'bundler/setup'

Сердце Bundler'а — это файл Gemfile, который создают в корне проекта, и где прописывают версии gem'ов, которые проекту нужны. По сути это обычная программа на Ruby, выглядеть она может так:

source :rubygems

gem "nokogiri", "1.5.0"
gem "sinatra",  "~> 1.2.6"

gem "wirble", :group => :development

group :test do
  gem "rspec",  ">= 2.6"
  gem "ffaker", ">= 1.7"
end

Gem'ы можно объединять в группы, чтобы потом с ними было удобно работать скопом. Те, что не входят ни в какую группу, заносятся в :default (в нашем случае это nokogiri и sinatra). Названия группам можно давать, вообщем-то, любые, но обычно хватает стандартных :development и :test.

Первое, с чем отлично справляется Bundler — установка в одну команду. Можете забыть про gem install для каждого gem'а. Если они прописаны в Gemfile, достаточно перейти в директорию проекта и набрать там

bundle install

Версии установленных gem'ов будут соответствовать требованиям Gemfile (ужесточение версий абсолютно такое же, как у Rubygem'овского метода gem). Если номер версии не указан, будет установлена последняя стабильная версия.

Можно исключить установку gem'ов определенной группы (например, на сервере):

bundle install --without development test

Второе, что выполняет Bundler — разрешение конфликтов версий. Во время установки gem'ов он строит дерево их зависимостей, находит возможные пересечения и подбирает такие версии, при которых все-все, даже глубоко спрятанные, gem'ы будут довольны (а не как в примере с rack). Результат этой работы он сохраняет в файл Gemfile.lock.

В момент подключения require 'bundler/setup' Bundler добавляет в массив $LOAD_PATH пути к версиям gem'ов, определенным в Gemfile.lock, поэтому, когда вы начинаете подключать непосредственно gem'ы проекта, Ruby находит нужные файлы, и система активации Rubygems не задействуется.

Gemfile.lock также служит слепком gem-экосистемы вашего проекта на момент, когда «всё работало», который можно безбоязненно переносить между системами. Важно понимать, что он всегда имеет бóльшую силу, чем Gemfile. Даже если вы укажете в последнем строгие версии gem'ов, не забывайте, что у них, скорее всего, существуют собственные зависимости, версии которых просчитаны и зафиксированы в Gemfile.lock.

Третье, что дает Bundler — простой и гарантированный доступ к исполняемым файлам нужной версии. Если в Gemfile прописано gem "rails", "3.0.5" и вы находитесь в папке с проектом, то команда bundle exec запускает именно то, что нужно:

you@your-comp:~/projects/one$ bundle exec rails -v
Rails 3.0.5

Если bundle exec для вас слишком длинно, можно выполнить

bundle install --binstubs

тогда Bundler создаст в проекте папку /bin с исполняемыми файлами, которые тоже будут «привязаны» к версии в Gemfile.lock, но запуск будет чуть короче:

you@your-comp:~/projects/one$ bin/rails -v
Rails 3.0.5

Четвертое, что позволяет делать Bundler — установку, минуя репозитории Rubygems, прямо из репозитория Git. Вот так можно использовать самую свежую (на момент запуска bundle install) версию Rails:

gem "rails", :git => "https://github.com/rails/rails.git"

Пятое, что можно сделать с помощью Bundler — массовый require одной или нескольких групп gem'ов. Так поступают в Rails:

Bundler.require(:default, Rails.env)

RVM как альтернатива?

RVM создавался, прежде всего, с целью быстрого и удобного переключения между версиями и реализациями Ruby.

В RVM существует понятие gemset, его стоит расценивать как фальшивое окружение, которое RVM подсовывает Rubygems, выдавая его за системное. Создавая gemset, вы создаете новую папку, в которую можно складывать новые gem'ы, будучи абсолютно уверенным, что они изолированы от ранее установленных.

Если создавать отдельный gemset под каждый проект, изоляция gem'ов означает, что, теоретически, вам не нужно использовать ни родной метод gem Rubygems, ни Bundler для ограничения версий, потому что внутри проекта будет установлена и доступна единственная версия каждого из gem'ов. Аналогично отпадает проблема с исполняемыми файлами.

На практике же отказ от Bundler'а сразу потребует ручной установки одинаковых версий gem'ов на всех машинах: у каждого из разработчиков, на каждом сервере. Любая попытка автоматизировать этот процесс приведет к дублированию функционала Bundler'а.

А уж если использовать Bundler, применение gemset'ов RVM'а будет лишней надстройкой.