本文简单的介绍 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 变量为一个数组,里面存放的路径字符串。
打印出来的有三个重要的目录分类。
可以进入对应的目录查看一下,目录下有什么文件。
演示代码:
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 主要通过 ruby 的 monkey patch 特性,重写了 require 函数的实现。
gem 一般安装到和 site_ruby 平级的 gems 目录下面,我们主要关心 gems(代码) 目录和 specifications(gemspec) 目录。
rubygems require 解析
此部分基于 2.3.4 的 ruby 源码分析。
文件跳转有点晕,觉得麻烦的朋友,可以略过。结论是把 对应的 gem 的 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 方法加载。
个人现在的使用习惯是 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 这个文件的代码。
rbenv 的原理和 bundler 差不多,主要是先修改环境变量,再调用 exec 替换当前进程。
在 rbenv 环境我们调用 which ruby 命令可以看到,ruby 执行文件总是在 ~/.rbenv/shims 目录下面。shims 目录下的 ruby 脚本会根据 .ruby-version 文件,找到对应 ruby 的执行文件路径,修改好环境变量后,再执行 exec 命令。
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 技巧,但注意别让自己掉到坑里去了。