Недавно вышло обновление rake с версии 0.8.7 до версии 0.9.0, которое наделало много шума в сообществе и в очередной раз выявило проблему управления версиями. Мне бы хотелось прояснить ситуацию и снова проговорить основные моменты, которые я уже упоминал во времена релиза Bundler 1.0. Вначале я расскажу о простых правилах работы, а затем слегка углублюсь в детали.
Когда вы устанавливаете гем, Rubygems создает обертки для всех исполняемых файлов, идущих в комплекте с гемом. При запуске обертки, запускается Rubygems, который подключает сам гем с помощью стандартного механизма активации. Rubygems запустит самую свежую версию гема, установленную в системе, даже если в Gemfile.lock указана другая версия. Кроме того, будут активированы последние (совместимые) версии всех зависимостей гема, даже если в Gemfile.lock указаны другие версии.
Это означает, что запуск исполняемых файлов как простых команд просто минует Bundler и все залоченые им зависимости. Иногда это не проблема, поскольку у разработчиков скорее всего стоят как раз нужные версии всех гемов. Долгое время Rake был отличным примером такого гема с «правильной» версией, поскольку большинство Gemfile.lock были залочены на версию 0.8.7, которая являлась последней доступной версией и была установлена у всех.
В результате образовалось заблуждение, что прямой запуск исполняемых файлов совместим с Bundler и использует его для разрешения зависимостей. В случае проблем обычно рекомендовалось использовать гемсеты из RVM, поскольку гемсет, собранный из Gemfile.lock, по сути подразумевал, что все исполняемые файлы окружения работали с нужными версиями зависимостей.
К сожалению из-за этого костыля люди продолжают игнорировать совет из документации всегда пользоваться
Нет никаких оснований полагать, что набирая в консоле
Чтобы хоть как-то облегчить все эти пляски с , который создает директорию bin, в которую помещает исполняемые файлы всех используемых в приложении гемов. Таким образом, запуск
Директория bin создает «переносимые» обертки для запускаемых файлов, поэтому ее смело можно добавлять в репозиторий.
Команда
Единственным исключением приведенных выше правил является команда
Содержимое файла
Короче, исполняемый файл
Такое поведение невозможно для большинства прикладных гемов и еще остается открытым вопрос, стоит ли вообще использовать этот хак и не будет ли факт того, что
Простое управление версиями
- Добавляйте Gemfile.lock в репозиторий после того, как сделали
bundle
. - После изменения Gemfile, всегда первым делом делайте
bundle install
— это «консервативно» обновит Gemfile.lock. Изменятся только те гемы, которые вы изменили в Gemfile. Все остальное останется как было. - Если «консервативный» апдейт невозможен, Bundler предложит вам сделать
bundle update [somegem]
. Эта команда обновит только указанный гем и все необходимые для него зависимости. Остальные гемы останутся нетронутыми. - Если надо заново разрешить все зависимости «с нуля», нужно сделать
bundle update
. - Когда запускаете исполняемые файлы, ВСЕГДА делайте это через
bundle exec [command]
. Цитата из документации: В некоторых случаях запуск файлов безbundle exec
может сработать, если этот файл — часть гема, который установлен в вашей системе и не грузит никаких зависимостей, которые могли бы конфликтовать с вашим бандлом. Тем не менее, этот способ крайне ненадежен и является источником множества проблем. Даже если все вроде бы работает, не факт, что оно будет работать завтра или на другом сервере. Следующий раздел «Исполняемые файлы» как раз про это. - Помните, что всегда сможете вернуть свой старый Gemfile.lock командой
git checkout Gemfile.lock
или аналогичной для других SCM.
Исполняемые файлы
Когда вы устанавливаете гем, Rubygems создает обертки для всех исполняемых файлов, идущих в комплекте с гемом. При запуске обертки, запускается Rubygems, который подключает сам гем с помощью стандартного механизма активации. Rubygems запустит самую свежую версию гема, установленную в системе, даже если в Gemfile.lock указана другая версия. Кроме того, будут активированы последние (совместимые) версии всех зависимостей гема, даже если в Gemfile.lock указаны другие версии.
Это означает, что запуск исполняемых файлов как простых команд просто минует Bundler и все залоченые им зависимости. Иногда это не проблема, поскольку у разработчиков скорее всего стоят как раз нужные версии всех гемов. Долгое время Rake был отличным примером такого гема с «правильной» версией, поскольку большинство Gemfile.lock были залочены на версию 0.8.7, которая являлась последней доступной версией и была установлена у всех.
В результате образовалось заблуждение, что прямой запуск исполняемых файлов совместим с Bundler и использует его для разрешения зависимостей. В случае проблем обычно рекомендовалось использовать гемсеты из RVM, поскольку гемсет, собранный из Gemfile.lock, по сути подразумевал, что все исполняемые файлы окружения работали с нужными версиями зависимостей.
К сожалению из-за этого костыля люди продолжают игнорировать совет из документации всегда пользоваться
bundle exec
.Нет никаких оснований полагать, что набирая в консоле
rake foo
вы запускаете код песочнице Bundler, поскольку на самом деле он никак не задействован и нигде не запускается. Bundler должен активироваться в самом начале загрузки и иметь возможность подменить загрузчик подсунув ему нужные версии гемов прописанные в Gemfile.lock. Запуская исполняемые файлы напрямую вы исполняете руби-код до того, как Bundler мог бы вмешаться в процесс подключения зависимостей. В итоге подключается совсем не тот код, на который вы расчитываете. Как только это произошло, все становится весьма непредсказуемым.bundle install --binstubs
Чтобы хоть как-то облегчить все эти пляски с
bundle exec
, Bundler 1.0 предлагает специальный флаг --binstubs
bin/cucumber
, например, эквивалентен команде bundle exec cucumber
.Директория bin создает «переносимые» обертки для запускаемых файлов, поэтому ее смело можно добавлять в репозиторий.
Команда rails
Единственным исключением приведенных выше правил является команда
rails
. Начиная с версии 3.0 эта команда первым делом пытается запустить script/rails
из текущего каталога. А script/rails
в свою очередь первым делом запускает Bundler#This command will automatically be run when you run "rails" with Rails 3 gems installed from the root of your application.
APP_PATH = File.expand_path('../../config/application', __FILE__)
require File.expand_path('../../config/boot', __FILE__)
require 'rails/commands'
Содержимое файла
boot.rb
в свою очередь весьма нетривиально:require 'rubygems'
# Set up gems listed in the Gemfile.
ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../../Gemfile', __FILE__)
require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE'])
Короче, исполняемый файл
rails
специально делает все возможное, чтобы гарантировать, что логика запуска песочницы Bundler срабатывает в самом начале и использует Kernel#exec
, чтобы перегрузить текущий процесс, если какие-то гемы все же успели загрузиться.Такое поведение невозможно для большинства прикладных гемов и еще остается открытым вопрос, стоит ли вообще использовать этот хак и не будет ли факт того, что
rails
можно запускать и без bundle exec
запутывать еще сильнее.