Ruby Ruby 包管理分析

cxh116 · March 27, 2016 · Last by qq792326645 replied at July 22, 2016 · 11820 hits
Topic has been selected as the excellent topic by the admin.

本文简单的介绍 Ruby 包管理的相关原理,写的比较粗浅,欢迎补充。

大纲

  • Ruby 本身的包管理
  • Rubygems
  • Bundler
  • RVM 与 rbenv

Ruby 本身的包管理

require method

Ruby 主要通过 require 函数来引入外部的库文件。函数原型如下:

# http://ruby-doc.org/core-1.8.7/Kernel.html#method-i-require
# http://ruby-doc.org/core-2.2.3/Kernel.html#method-i-require
require(string) => true or false

参数需要传一个 string , 文件名或文件路径。
返回值为 boolean 值,true 为 require 成功。

演示代码:

# shell
echo 'puts "a"' > /tmp/a.rb
cd /tmp
irb
require 'csv' # 文件名方式,在 $LOAD_PATH 全局变量定义的路径里搜索
require './a' # 相对路径方式,基于进程的工作目录, Dir.pwd 可以查看当前进程的工作路径
require '/tmp/a' # 绝对路径. 1.8.7 返回 true, 1.9 以后返回false. 1.9 以后同一文件,用不同的路径方式加载,也算同一文件,不会重复加载.

$LOAD_PATH

本部分基于 ruby 1.8.7 的原因是因为 ruby 1.8.7 默认还是用 ruby 自身的 require 函数,1.8 以后,默认用的是 Rubygems 实现的 require 函数。

大部分时候,我们使用 require 使用的是文件名,而不是相对路径或绝对路径的方式,所以 $LOAD_PATH 变量是个关键点。

ruby -e "puts $:" # shell, 用 ruby 命令的 -e 参数运行单行 ruby 代码. 以下为命令执行后的输出
/usr/local/rvm/rubies/ruby-1.8.7-head/lib/ruby/site_ruby/1.8
/usr/local/rvm/rubies/ruby-1.8.7-head/lib/ruby/site_ruby/1.8/x86_64-linux
/usr/local/rvm/rubies/ruby-1.8.7-head/lib/ruby/site_ruby
/usr/local/rvm/rubies/ruby-1.8.7-head/lib/ruby/vendor_ruby/1.8
/usr/local/rvm/rubies/ruby-1.8.7-head/lib/ruby/vendor_ruby/1.8/x86_64-linux
/usr/local/rvm/rubies/ruby-1.8.7-head/lib/ruby/vendor_ruby
/usr/local/rvm/rubies/ruby-1.8.7-head/lib/ruby/1.8
/usr/local/rvm/rubies/ruby-1.8.7-head/lib/ruby/1.8/x86_64-linux

$LOAD_PATH 变量为一个数组,里面存放的路径字符串。

打印出来的有三个重要的目录分类。

  • site_ruby 默认优先级最高,安装本机相关库。摘自<> 254 页。
  • vendor_ruby 操作系统供应商进行定制用的,一般为空。
  • 1.8 ruby 标准库目录。比如 date, csv 库。

可以进入对应的目录查看一下,目录下有什么文件。

演示代码:

echo 'puts "priority2"' > /usr/local/rvm/rubies/ruby-1.8.7-head/lib/ruby/vendor_ruby/1.8/prioritydemo.rb # vendor_ruby
ruby -e "require 'prioritydemo'" # puts priority2
echo 'puts"priority1"'> /usr/local/rvm/rubies/ruby-1.8.7-head/lib/ruby/site_ruby/1.8/prioritydemo.rb # site_ruby
ruby -e "require 'prioritydemo'" # puts priority1

通过代码演示可以看见,require 查找的顺序是基于 $LOAD_PATH 数组里面的路径的顺序来找的,找到了就不继续往下找。

上测试代码如果要强制加载 vendor_ruby 目录下的 prioritydemo 文件,可以使用绝对路径。

Rubygems

Rubygems 主要通过 ruby 的 monkey patch 特性,重写了 require 函数的实现。

gem 一般安装到和 site_ruby 平级的 gems 目录下面,我们主要关心 gems(代码) 目录和 specifications(gemspec) 目录。

rubygems require 解析

此部分基于 2.3.4 的 ruby 源码分析。

文件跳转有点晕,觉得麻烦的朋友,可以略过。结论是把 对应的 gem 的 gems 目录添加到 $LOAD_PATH 变量里面。

  • 当加载 lib/rubygems.rb 时,会调用 Gem::Specification.load_defaults 代码 # 1.9 自动加载
  • lib/rubygems/specification.rb#load_defaults 会把 specifications 目录下的所有 gemspec 文件的 files 描述的文件通过 lib/rubygems.rb#register_default_spec 方法注册到 @path_to_default_spec_map 变量。key 文件名,value 为 spec 对象
  • require 方法会调用 lib/rubygems.rb#find_unresolved_default_spec , find_unresolved_default_spec 拼上 .rb .so 在 @path_to_default_spec_map 变量里查找,如果找到,则返回对应的 spec , 再调用 lib/rubygems.rb#remove_unresolved_default_spec 方法,从 @path_to_default_spec_map 变量删除这个 spec 的相关值,防止重复加载。
  • 最后再调用 lib/rubygems/core_ext/kernel_gem.rb#gem, lib/rubygems/specification.rb#activate , lib/rubygems/specification.rb#add_self_to_load_path 再把这个 gems 添加到 $LOAD_PATH 变量。

演示代码:

puts Gem.instance_eval("@path_to_default_spec_map.keys.any?{|k| k =~ /minitest/}") # true
puts $LOAD_PATH # 没有 minitest gems
puts require 'minitest' # true
puts Gem.instance_eval("@path_to_default_spec_map.keys.any?{|k| k =~ /minitest/}") # false
puts $LOAD_PATH # 有 minitest gems

可以看到在 require 之前与之后的差别,多了 minitest gem 的 lib 路径 ( /home/outman/.rbenv/versions/2.2.4/lib/ruby/gems/2.2.0/gems/minitest-5.4.3/lib ) .

最终结论是 rubygems 所做的一切,只是为了把 gem 的 lib 目录添加到 $LOAD_PATH 变量里,再用原生的 require 方法加载。

Bundler

个人现在的使用习惯是 rbenv + bundler .而不是使用 rvm 的 gemset . 项目第一次执行 bundle install 加 --path=vendor/bundle 参数,把 gem 安装到项目的 vendor/bundle 目录下。再在 git 忽略此目录。

这样做就不会因为多个项目安装 gem 到系统目录,而导致系统里的 gem 冲突。

Bundler 和 Rubygems 一样,最终还是为了把项目的 gem 的 lib 目录添加到 $LOAD_PATH 变量里。

演示代码:

ruby -e 'puts $LOAD_PATH'
bundle exec ruby -e 'puts $LOAD_PATH' #可以看到 bundle 把项目 Gemfile 里定义的所有 gem 的 lib 目录都已经加到 $LOAD_PATH 变量里.

源码简单解析:

bundle exec 主要修改 PATH RUBYOPT RUBYLIB 变量,再用 exec 函数替换当前进程,从而继承修改后的 PATH RUBYOPT RUBYLIB 环境变量。

exec 后的新进程读取 RUBYOPT 环境变量的 -rbundler/setup 值,从而会先加载运行 bundler/setup 这个文件的代码。

  • lib/bundler/cli/exec.rb#run 方法
  • SharedHelpers.set_bundle_environment
    • 把 bundle 的 bin 目录加到了 PATH 环境变量
    • bundle exec ruby -e 'puts ENV["PATH"]'
    • 再把 -rbundler/setup 添加到 RUBYOPT 变量。
    • ruby -h, -rlibrary require the library before executing your script
    • echo "puts 123" > /tmp/s.rb; ruby -r '/tmp/s.rb' -e 'puts 456'
    • 把 bundle 的 lib 目录添加到 RUBYLIB 变量
    • RUBYLIB=/tmp ruby -e 'puts $LOAD_PATH' # 把 /tmp 添加到 $LOAD_PATH 的第一位了
  • 再执行 Kernel.exec ,用 exec 参数后面的命令替换当前进程。新进程会在修改的 ENV 执行。
  • lib/bundler/setup.rb -> Bundler.setup -> lib/bundler.rb -> load.setup -> lib/bundler/runtime.rb
  • Runtime 从 Bundler.definition 里拿到所有 specs ,再遍历 specs,调用 Bundler.rubygems.loaded_specs 方法把所有 gem 都加载到 $LOAD_PATH .
    • bundle exec ruby -e 'puts Bundler::Runtime.new(Bundler.root, Bundler.definition).requested_specs.first.inspect

rbenv

rbenv 的原理和 bundler 差不多,主要是先修改环境变量,再调用 exec 替换当前进程。

在 rbenv 环境我们调用 which ruby 命令可以看到,ruby 执行文件总是在 ~/.rbenv/shims 目录下面。shims 目录下的 ruby 脚本会根据 .ruby-version 文件,找到对应 ruby 的执行文件路径,修改好环境变量后,再执行 exec 命令。

rvm

rvm 与 rbenv 不同,rbenv 实现类似于设计模式里的委托模式,所有的 ruby 执行都交给 ~/.rbenv/shims 目录下的执行文件。

而 rvm 简单粗暴,直接把对应版本的 ruby 的 bin 目录添加到 PATH 环境变量里。

rvm gemset 解析

rvm 的 gemset 主要是通过修改环境变量 GEM_HOME 和 GEM_PATH 变量来实现的。此两变量 rubygems 根据其值在值定义的目录查找 gem .

演示代码:

rvm gemset use 1.8.7@testset --create
env | grep GEM
GEM_HOME=/usr/local/rvm/gems/ruby-1.8.7-head@testset
GEM_PATH=/usr/local/rvm/gems/ruby-1.8.7-head@testset:/usr/local/rvm/gems/ruby-1.8.7-head@global
gem install rack
cd /usr/local/rvm/gems/ruby-1.8.7-head@testset; ls
#bin  build_info  cachedoc  environment  gemsspecificationswrappers
ruby -e 'require "rubygems"; puts Gem.paths.path.inspect'
#["/usr/local/rvm/gems/ruby-1.8.7-head@testset", "/usr/local/rvm/gems/ruby-1.8.7-head@global"]

可以看到把 gemset 的目录添加到 Gem.paths 变量里面去了。而且固定有 global 目录,这样当我们把 gem 安装到 global 的 gemset 里,当在我们自己的 gemset 里找不到时,会去 global 的 gemset 目录里面找。

总结

$LOAD_PATH 很强大,利用它好,可以实现不错的 hack 技巧,但注意别让自己掉到坑里去了。

为自己的博客做外链: https://blog.mangege.com/tech/2016/03/27/1.html

现在也是 rbenv + rbenv-gemset

require(string) => true or false  #true代表成功,false是已经在环境中了,不需要require,如果require失败则会报错

有点看不懂了。

温故知新

这是翻译的吗?

#7 楼 @jun1st 不是,个人在公司内部的分享整理成文章的。

You need to Sign in before reply, if you don't have an account, please Sign up first.