翻译 Bundler 的作用及原理

yesmeck · 2015年05月12日 · 最后由 teacafe2000 回复于 2015年06月15日 · 25541 次阅读
本帖已被管理员设置为精华贴

原文:http://bundler.io/rationale.html

首先,你要在你应用根目录下一个叫Gemfile文件里声明这些依赖,它看起来是这个样子的:

source 'https://rubygems.org'

gem 'rails', '4.1.0.rc2'
gem 'rack-cache'
gem 'nokogiri', '~> 1.6.1'

这个Gemfile说明了这些事情:

首先,他告诉 bundler 默认是在Gemfile里指定的https://rubygems.org上来找 gem。如果你的一些 gem 需要从一个私有的 gem 服务器上获取,那么你可以为这些 gem 覆盖掉这个默认的源设置。

接着,你声明了一些依赖:

  • 版本是4.1.0.rc2rails
  • 任意版本的rack-cache
  • 版本是>= 1.6.1但是< 1.7.0nokogiri

在你第一次声明完依赖后,你要告诉 bundler 去获取它们:

$ bundle install    # 也可以直接运行 'bundle',相当于 'bundle install'

Bundler 会连接rubygems.org(或者其他你声明的源),然后列出所有你指定的符合你需要的 gem。因为所有你在Gemfile里的依赖有它们自己的依赖,所以基于上面的Gemfile运行bundle install会安装相当多的的 gem。

$ bundle install
Fetching gem metadata from https://rubygems.org/.........
Fetching additional metadata from https://rubygems.org/..
Resolving dependencies...
Using rake 10.3.1
Using json 1.8.1
Installing minitest 5.3.3
Installing i18n 0.6.9
Installing thread_safe 0.3.3
Installing builder 3.2.2
Installing rack 1.5.2
Installing erubis 2.7.0
Installing mime-types 1.25.1
Using bundler 1.6.2
Installing polyglot 0.3.4
Installing arel 5.0.1.20140414130214
Installing hike 1.2.3
Installing mini_portile 0.5.3
Installing multi_json 1.9.3
Installing thor 0.19.1
Installing tilt 1.4.1
Installing tzinfo 1.1.0
Installing rack-test 0.6.2
Installing rack-cache 1.2
Installing treetop 1.4.15
Installing sprockets 2.12.1
Installing activesupport 4.1.0.rc2
Installing mail 2.5.4
Installing actionview 4.1.0.rc2
Installing activemodel 4.1.0.rc2
Installing actionpack 4.1.0.rc2
Installing activerecord 4.1.0.rc2
Installing actionmailer 4.1.0.rc2
Installing sprockets-rails 2.0.1
Installing railties 4.1.0.rc2
Installing rails 4.1.0.rc2
Installing nokogiri 1.6.1
Your bundle is complete!
Use `bundle show [gemname]` to see where a bundled gem is installed.

如果任何需要的 gem 已经被安装了,bundler 会直接使用它们。在你的系统上安装完所有的 gem 后,bundler 会写一个所有这些 gem 和它们的版本号的快照到 Gemfile.lock 里。

配置你的应用使用 Bundler

Bundler 保证 Ruby 能找到Gemfile里的所有 gem 和这些 gem 自己的依赖。如果你的应用是个 Rails 3 以上的应用的话,你的应用默认已经有运行 bundler 的代码了。如果是 Rails 2.3 的应用,可以看在 Rails 2.3 中设置 Bundler

对于另外的应用来说(比如说基于 Sinatra 的应用),你需要在你引用任何 gem 之前配置一下 bundler。在你应用加载的第一个文件的第一行(对于 Sinatra, 就是写着require 'sinatra'的那个文件)加入以下下代码:

require 'rubygems' # Ruby 1.8 以后的版本不再需要这句(楼主注)
require 'bundle/setup'

这样 bundler 就能自动找到你的Gemfile,并且让你Gemfile里的所有 gem 是可用的(从技术上讲,就是把这些 gem 放到$LOAD_PATH里)。

现在你的代码就可以运行了,你可以引用你需要的 gem。比如说你可以require 'sinatra'。如果你有很多依赖,你可能希望“引用所有我Gemfile的 gem”。如果要这样做的话,可以把下面这行代码放到require 'bundler/setup'的下一行:

Bundler.require(:default)

对于我们刚才的Gemfile来说,这行代码相当于:

require 'rails'
require 'rack-cache'
require 'nokogiri'

精明的读者会发现正确引用rack-cache的方式是rake/cache,而不是require 'rack-cache'。为了告诉 bundler 使用require 'rack/cache',只要更新你的Gemfie:

source 'https://rubygems.org'

gem 'rails', '4.1.0.rc2'
gem 'rack-cache', require: 'rack/cache'
gem 'nokogiri', '~> 1.6.1'

对于这么小的一个Gemfile来说,我们建议你跳过Bundler.require而是手动引用这些 gem(特别是你还需要在Gemfile里写一个:require配置)。对于很大的Gemfile来说,使用Bundler.require让你省略了大量重复的依赖引用。

把你的代码放进版本库

在你开发你的应用一段时间后,把应用跟GemfileGemfile.lock一起放到版本库里。这样,你的版本库里就有了你的应用最后一次你确定能正常工作时所有的 gem 以及版本号的记录。要记住,尽管你的Gemfile里只有三个 gem,但是当你去考虑你依赖的 gem 也依赖其他 gem 时,你的应用实际上依赖了大量的 gem,

这个非常重要:Gemfile.lock把你的应用变成一个你的代码跟第三方代码最后一次你确定能正常工作的包。Gemfile里确切指定你依赖的第三方代码的版本并不能提供同样的保证,因为 gem 通常给它们自己的依赖声明一个版本号的范围。

你在同一台机器上再次运行bundle install的时候,bundler 会发现系统上已经有了你需要的依赖,然后就会跳过安装的过程。

不要把.bundle目录放入版本库,以及所有它里面的文件。这些文件在不同的机器上是不同的,主要是用来保存运行bundle install时的参数。

如果你运行了bundle pack,你需要的 gem(除了来源是 git 仓库的 gem 以外)都会被下载到vendor/cache目录。如果所有你需要的 gem 都在那个目录里而且你把它放进了版本库里,bundler 运行的时候就不需要联网了。这是一个可选的步骤,因为这样做你的版本库就会变得很大。

与其他开发者共享你的应用

当你的同事(或者你在另外一台机器上)获取你的代码的时候,它会包含你最近开发时使用的所有第三方代码的确切版本。当他们运行bundle install,bundler 会找到Gemfile.lock并跳过解决依赖的步骤,改为安装所有你原来机器上一样的 gem。

换句话说,你不需要去猜你需要安装什么版本的依赖。在我们刚才用过的栗子里,尽管rake-cache声明了依赖rack >= 0.4,但是我们确定他能正常工作在rack 1.5.2下。即使 Rack 的团队发布了rack 1.5.3,bundler 还是会安装1.5.2这个我们已经知道的确切的版本。这为开发者减轻了大量的维护负担,因为所有机器上都运行着同样的第三方代码。

更新依赖

当然,有时候你可能要更新你的应用依赖的部分 gem。比如说,你想要把rails升级到4.1.0。重点是,你只想要升级一个依赖,而不是要重新解决你的所有依赖并且使用所有 gem 最新的版本。在我们栗子里,你只有 3 个依赖,但是即使在这个栗子里,更新任何一个东西都会变得复杂。

比如说,rails 4.1.0.rc2依赖actionpack 4.1.0.rc2,而actionpack又依赖rack ~> 1.5.2(意思是>= 1.5.2< 1.6.0)。rack-cache又依赖rack >= 0.4。我们假设rails 4.1.0也依赖rack ~> 1.5.2,并且在rails 4.1.0发布后 Rack 团队发布了rack 1.5.3

如果我们为了更新 Rails,天真地更新了所有它依赖的 gem,我们得到了rack 1.5.3,这刚好满足rails 4.1.0rack-cache的要求。然而我们并没有特别说要更新rack-cache,它就可能跟rack 1.5.3不兼容(不管什么原因)。虽然把rack 1.5.2升级到rack 1.5.3不会搞坏什么东西,但是类似这种导致更大版本跨度的更新场景也会发生(见下面 [1] 更多讨论)。

为了避免这个问题,当你更新一个 gem 时,如果有其他 gem 有与它相同的依赖,bundler 就不会更新那个相同的依赖。在上面的栗子里,由于rack-cache依然依赖rack,bundler 不会更新rack。这样保证了更新rails不会不小心搞坏rack-cache。由于rails 4.1.0的依赖actionpack 4.1.0保留了rack 1.5.2的兼容,bundler 就不会管它,rack-cache就会继续工作,尽管它可能面临跟rack 1.5.3的不兼容。

由于你一开始声明了依赖rails 4.1.0.rc2,如果你想要更新到rails 4.1.0,只要简单地在Gemfile里更新成gem 'rails', '4.1.0'并且运行:

$ bundle install

根据上面地描述,bundle install总是执行保守地升级,不会更新你没有在Gemfile里显式更改的 gem(或者它们的依赖)。也就是说你不修改Gemfile里的rack-cache,bundler 就会把它 和它的依赖(rack) 当成一个不可修改的整体。如果rails 3.0.0rack-cache不兼容,bundler 就会显示你的依赖快照(Gemfile.lock)跟你更新后的Gemfile之间的冲突。

如果你更新了你的Gemfile,并且你的系统上已经有你所有需要的依赖了,当你启动应用的时候 bundler 会透明地更新Gemfile.lock。举个栗子,如果你把mysql加到你的Gemfile里,并且已经在你的系统上安装了,你可以不需要运行bundle install就能启动你的应用,并且 bundler 会把最近一次正确的配置写到Gemfile.lock

这个功能在你添加或更新依赖很少的 gem 时就会比较方便。在你更新一些比较重要的 gem(比如 rails)或者有被很多 gem 依赖的 gem(比如rack)它就可能失败。如果透明更新失败了,你的应用就会启动失败,bundler 会显示错误引导你运行bundle install

不修改 Gemfile 来更新 Gem

有时候,你想要不修改 Gemfile 来更新一个依赖。比如说,你想要更新到最新版本的rack-cache。而你又没有在Gemfie里指定rack-cache的版本,你可能想要周期性地获取rakc-cache地最新版。那么你可以使用bundle update命令:

$ bundle update rack-cache

这个命令会更新rack-cache和它地依赖更新到Gemfile里允许地最新版本(在这个栗子里就是更新到最新版本)。它不会修改其地依赖。

但是它会在需要地时候更新其他 gem 的依赖。举个栗子,如果最新版的rack-cache指定了依赖rack >= 1.5.2,bundler 会更新rack1.5.2尽管你没有要求 bundler 更新 rack。如果 bundler 需要更新一个其他的 gem 依赖的 gem,那么它会在更新完成后告诉你这件事。

如果你要更新所有 Gemfile 里的 gem 到最新的能用的版本,运行:

$ bundle update

这个命令会从头开始解决依赖并忽略掉Gemfile.lock。如果你这么做了,你要准备好git reset --hard和测试用例。从头解决依赖会有意想不到的结果,特别是一部分你依赖的第三方库在你上一次更新的时候发布了新的版本。

总结

一个简单的 Bundler 流程

  • 当你第一次创建 Rails 应用的时候,它已经包含了Gemfile。其他的应用可以运行:
$ bundle init

bundle init命令会创建一个简单的Gemfile让你编辑。

  • 下面,添加你的应用需要的 gem。如果你关心部分你需要的 gem 的版本,可以加一个合适的版本约束:
source 'https://rubygems.org'

gem 'sinatra', '~> 1.3.6'
gem 'rack-cache'
gem 'rack-bug'
  • 如果你有 gem 没在你的系统上安装,运行:
$ bundle install
  • 更新一个 gem 的版本,首先修改 Gemfile:
source 'https://rubygems.org'

gem 'sinatra', '~> 1.4.5'
gem 'rack-cache'
gem 'rack-bug'

然后运行:

$ bundle install
  • 如果bundle install说你的GemfileGemfie.lock之间有冲突,运行:
$ bundle update sinatra

这个会升级 Sinatra 这个 gem,以及它所有的依赖。

  • 更新所有你Gemfile里的 gem 到最新可用的版本,运行:
$ bundle update
  • 每当你的Gemfile.lock变化的时候,把它放入你的版本库。它保存了你的应用能成功运行所依赖的所有第三方代码的确切版本的历史。

  • 当部署你的代码到测试或者生产服务器的时候,首先运行你的测试(或启动你的本地开发服务器),确定你把Gemfile.lock放到了版本库里。在远程服务器上,运行:

$ bundle install --deployment

备注

[1] 举个栗子,如果rails 4.1.0依赖rack 2.0,这个rack 2.0满足rack-cache的依赖,因为它声明了>= 0.4的依赖。当然你能指责说rack-cache不指定依赖的最高版本很愚蠢,但是这种情况确实是普遍存在的,而且很多项目声明依赖的时候会发现它们处在一个很尴尬的场面。依赖限制太严(rack = 1.5.1)就会让你的项目很难兼容其他项目。依赖限制太宽(rack >= 1.0)会在 Rack 发布新版本的时候可能搞坏你的代码。使用这样的依赖声明rack ~> 1.5.2和 SemVer 兼容的版本号基本上能解决这个问题,但是这也只是一个普遍能接受的方案。由于 RubyGems 有超过十万个库,这个假设在实际应用中可能并不成立。

半夜翻的,有几处自己看的也不是很明白,大家多多指正。

直接 bundle 和 bundle install 有啥不同?

ruby 应用服务器启动以后,是把所有的类都加载到内存,还是运行时用到的时候才去加载?

#2 楼 @est 是一样的,bundle 不加子命令默认执行 install。

#4 楼 @yakczh 如果是 Rails 应用的话,这和启动的环境有关。development 环境肯定不是,production 的话应该是几乎全加载了,极少数不在启动时加载。不过这个问题和 Bundler 无关,不同的应用不同的场景会使用不同的加载策略。

#6 楼 @yuhaidonghd #4 楼 @yakczh config/intializers里面都会在启动时加载,但是只是在model/controller/helper里面require的话,还是在第一次用的时候加载

补充几点:

  1. bundle install 可以将 gem 安装在另外的目录,参数是--path=
  2. 如果让 bundle 找到 gem,可以设置 ENV['GEM_PATH'] = "the_path_bundle_install_gem". 3.如果自己设置 ruby 代码载入 Gemfile 定义的环境,需要下列代码 ruby # Set up gems listed in the Gemfile. ENV['BUNDLE_GEMFILE'] ||= File.expand_path('../Gemfile', __FILE__) require 'bundler/setup' if File.exists?(ENV['BUNDLE_GEMFILE']) Bundler.require(:default, ENV['RACK_ENV'])

赞!!!

ruby 也是动态脚本,也有 require 'xxxx' 为什么不能象php那样修改了立即生效,是因为 web 容器启动的时候,已经把所有类加载的内存的原因吗?

这样的帖子每天来一个,生活该多美好..:)

#10 楼 @yakczh 对于你说的这种情况,应该把 require 改为 load

官方文档讲的更清楚些。

#10 楼 @yakczh 是 rails 在 prod 环境这么干的,启动时全部加载,

#14 楼 @jun1st developer 环境下,是不是跟 php 一样,新改了代码会重新解析一遍?

说好的原理呢?标题党?

虽然这篇原文和 the rails 4 way 第一章第一节的内容差不多,但支持下,翻译得很好哦~

需要 登录 后方可回复, 如果你还没有账号请 注册新账号