做为一名 iOS 工程师,CocoaPods 是咱们所不会陌生的。然而在咱们的平常开发中,编写 CocoaPods 的 Ruby 语言咱们可能不甚了解,更不要说 Bundler 以及 RVM 了。所以,当咱们遇到一些 Ruby 环境相关的问题时,可能彻底不知道发生了什么。若是刚好你对这两个工具作了什么感到好奇,那么,在这篇文章中,我会尽可能由浅入深的去说明 RVM / Bundler 的原理和做用,帮助你们对 Ruby 的环境管理有一个更加深刻的理解。html
gem install rubygems-bundler && gem regenerate_binstubs
可让你免去每次都要在 pod install
以前添加 bundle exec
的痛苦咱们都知道,macOS 是自带 Ruby 的。也就是说,当咱们拿到一台新的 MacBook Pro,进入系统,打开终端执行 whereis ruby
,咱们会获得 /usr/bin/ruby
这样的结果。git
在目前的 macOS 10.14 版本中,系统自带的 Ruby 版本为 2.3.7。github
在没有安装 RVM 或者 rbenv 这样的工具之前,你们在执行 gem install cococapods
这一行命令的时候必定会遇到这样的报错:ruby
You don't have write permissions for the /Library/Ruby/Gems/2.3.0 directory 复制代码
为何会出现这样的错误?由于 gem 做为 Ruby 默认的包管理器,会将全部下载的 gem 安装在某个特定的目录下,咱们暂且称呼这个目录为 Gem Path ,对于系统的 Ruby 来讲,这个目录就是 /Library/Ruby/Gems/2.3.0,这是一个须要启用 sudo
才能写入的目录。这也就致使咱们在每次 gem install
的时候都须要在命令以前增长 sudo
才能让命令正确执行。bash
为了解决这个问题,咱们须要让 Gem Path 指向一个咱们拥有写权限的目录。比较简单直接的办法就是咱们利用 homebrew 去安装一个新的 Ruby。服务器
彷佛很完美,但有个问题:咱们如何约束你们全部人都使用一样版本的 Ruby 呢?app
答案是使用 Ruby 的版本管理工具。以 RVM 为例,当你安装 RVM 之后,你在命令行中执行的每个 cd
命令其实都被 RVM 所替换了。RVM 会在每一次切换目录后检查当前目录中是否有 .ruby-version 文件,若是有,就检查当前使用的 Ruby 是不是文件中指定的版本。若是不是,他会给出相似 Required ruby-x.x.x is not installed
这样的警告。工具
在我司工程的早期阶段,咱们除了使用 cocoapods,还须要使用 Ruby 编写一些打包和发布的脚本,而当时系统提供的 Ruby 版本还比较低(2.0.0),开发起来不太方便,而利用 RVM ,咱们不只能够方便的安装一个新版本的 Ruby,还能够利用 .ruby-version 来保证你们可使用相同版本的 Ruby(尽管只是一个比较弱的约束)。post
相信到这里,你们已经可以理解,在咱们的项目中使用 RVM 是颇有必要的。咱们接下来看第二个问题:为何要用 Bundler?ui
为了回答这个问题,咱们须要先把目光转向 gem,回顾一下 gem 诞生时要解决的问题。
在 Ruby 中,若是你想使用另一个 Ruby 文件中的内容,你须要使用 require
关键字来加载另一个 Ruby 文件中的内容。require
会在 Ruby 预设的 $LOAD_PATH
中去查找对应的文件。你能够经过执行 ruby -e 'puts $LOAD_PATH'
来看看当前 Ruby 中的 $LOAD_PATH
都有什么内容。
例如若是你写了一个简单的 Ruby 脚本:
require 'foo'
复制代码
当执行到 require 'foo'
这一行时, Ruby 就会在 $LOAD_PATH
中出现的全部目录下去查找是否有一个叫作 foo.rb 的文件。若是有,就去加载这个文件的内容。若是在全部的 $LOAD_PATH
中都没有找到这样的一个文件,Ruby 解释器就会抛出异常。异常一般长这个样子:
LoadError - cannot load such file -- foo
复制代码
在没有 gem 之前,若是你想用别人已经写好的 Ruby 脚本,就须要手动把这些脚本下载下来,放到 $LOAD_PATH
中的某个目录下,而后你才能在你的脚本中正确的使用别人的脚本文件。这样的代码分发过程是很是原始而繁琐的。
为了解决这个问题,gem 横空出世,提供了这样的一个脚本分发解决方案:
gem install
便可前面的内容很好理解,咱们来着重看一下执行 gem install
以后发生了什么。
当你执行 gem install foo
的时候,gem 会帮你把 foo.gem 下载下来,解压缩,放到一个目录下。通常这个目录都是咱们前面提到 Gem Path 的子目录,咱们这里暂时称其为 Gems Install Path。若是 foo 的 gemspec 中声明了对其余 gem 的依赖,gem install foo
还会帮你把 foo 所依赖的 gem 下载下来。
gem install
所作的事情其实很简单。但到此时 gem 尚未彻底解决咱们的问题:gem install
所安装的那些 gem 并不存在于 $LAOD_PATH
中,咱们的 Ruby 脚本仍是没法正确的引用到他们。
为了解决这个问题,gem 在本身被安装后,就去修改了 Ruby 中 require 的实现,使得 require 在执行的时候,除了 $LOAD_PATH
,还会在 Gems Install Path 中查找文件(你能够经过执行 gem env | grep -A2 'GEM PATHS'
找到你的 gem 所安装的路径,GEMS INSTALL PATH 就在这个目录的 gems 子目录下)。 当 gem 在 GEMS INSTALL PATH 中找到对应文件后,就会把这个路径加入到 $LOAD_PATH
中,而后调用 Ruby 原本的 require。此时因为 $LOAD_PATH
中增长了新的路径,require 就能够正确的加载到你所安装的 gem 的对应文件了。
这里咱们能够作一个小实验,找一个没有 Gemfile 的目录执行 irb,而后依次输入注释之外的内容:
old_load_path = $LOAD_PATH.dup
require 'cocoapods'
new_load_path = $LOAD_PATH.dup
# 执行下面的代码能够看看 LOAD_PATH 数量的变化
"new: #{new_load_path.count} old: #{old_load_path.count}"
# 执行下面的代码能够看看 LOAD_PATH 到底变了什么。你会看到 cocoapods 以及他的依赖库所在的目录
new_load_path - old_load_path
复制代码
至此,gem 已经完美解决了分发 Ruby 脚本的问题。当你想要使用任何一个别人已经提供好的 gem 的时候,只须要简单输入 gem install
,你的脚本就能够快乐的使用这个 gem 了。
到目前为止,一切彷佛很美好,可是随着 Ruby 应用于各类大型项目之后,Ruby 的开发者们发现了新的问题:当你的项目依赖了十几个 gem 后,新接手的人的配置环境时须要输入十几回 gem install
才能正确的配置好环境。
这样的事情开发者们固然不能忍,因而他们开始使用各类脚本文件将这个过程简化,这些脚本可能叫作 setup.sh ,他们的内容通常是这样的:
gem install foo
gem install bar
复制代码
在这里咱们暂时能够称呼相似这种 setup.sh 文件为 Gem List 文件,由于他就是一个装满了全部你须要安装的 Gem 的 List 🤓🤓🤓。
当 Ruby 的开发者们解决了批量安装 gem 的问题之后,他们又发现了新的问题:多版本环境不隔离。
什么意思?咱们来举个例子说明一下这个问题。
假如你是一名 Ruby 开发者,你维护着一个你的项目 A,在这个项目中你使用了 2.0.0 版本的 foo。一段时间后,你又开始接手维护另一个项目 B,不幸的是,这个项目最开始使用的 foo 的版本是 3.0.0。因而使人头疼的事情发生了:当你配置好了项目 B 的环境之后,你的机器上就会同时存在两个版本的 foo 的 gem。同时你会发现,你的项目 A 跑不起来了,由于你在运行项目 A 时,gem 默认会去找多个版本中最新的版本,因而在项目 A 中你用到了 3.0.0 版本的 foo 而不是 2.0.0 版本。
因而各类有趣但无奈的事情发生了:你的项目在你本地可能好好的,可是在服务器上就是不对。你查了好几天,发现是由于服务器上的另一个项目装了一个高版本的 gem,致使服务器上的环境根本无法跑你的项目。你痛苦,你绝望,但你又无能为力 🤬🤬🤬。
即使你只维护一个项目,因为你的 Gem List 文件中并无指定 gem 的版本号,因此颇有可能一周前利用这个 Gem List 文件安装出来的 gem 和一周后安装出来的彻底不一样。以致于好久之前 Ruby 开发者们都会开玩笑:“你好啊新人,这是一台新的电脑,咱们但愿你能花一周把项目的依赖配置好,若是一切顺利的话”
为了解决上面使用 gem 所产生的这些问题,Bundler 横空出世,提供给开发者两个救命通常的命令:
bundle install
bundle exec
bundle install
为咱们提供了统一安装多个 gem 的便捷方式。在执行 bundle install
后,Bundler 会将他所使用的 Gem List 文件 —— Gemfile 中声明的 gem 所有安装,同时将这次决议的最终版本号保存在 Gemfile.lock 中,保证不一样时刻不一样机器执行 bundle install
可以安装一样版本的 gem。
bundle exec
则替咱们解决了多版本环境不隔离的问题。当你执行 bundle exec
的时候,Bundler 会把 $LOAD_PATH
中不相干的那些 gem 的路径全都去掉,而后读取 Gemfile.lock 中的 gem 版本(若是没有 Gemfile.lock 会决议版本后建立一个 Gemfile.lock),保证 $LOAD_PATH
中只存在 Gemfile.lock 中已经固定版本的 gem 的路径。你能够执行一下下面两行代码,看看 $LOAD_PATH
的区别:
bundle exec ruby -e 'puts $LOAD_PATH'
ruby -e 'puts $LOAD_PATH'
复制代码
至此,Bundler 已经很好的解决了 gem 安装和环境隔离的问题了,可是 Bundler 也带来了新的麻烦:每次咱们执行 Ruby 相关的命令以前都要重复的输入 bundle exec
🤦🏻♂️🤦🏻♂️🤦🏻♂️。
还好 Ruby 的开发者们都很懒,他们开发了一个新的 gem —— rubygems-Bundler 来解决这个问题。当你安装这个 gem 之后,只要执行一次 gem regenerate_binstubs
,rubygems-Bundler 就会帮你在任何 gem 安装的命令行执行以前检查一下当前目录以及父目录是否存在 Gemfile。若是存在,就自动帮你的命令行以前加上 bundle exec
再执行。完美的解决了这个问题。
小提示:1.11.0 以上版本的 RVM 在安装 Ruby 时,默认会安装 rubygems-Bundler。你能够经过
gem list rubygems-Bundler
来检查本身是否安装了这个 gem。若是你用 homebrew 安装 Ruby,则不会享受到这个隐藏的福利。
LoadError - cannot load such file -- macho
复制代码
答案:macho 这个文件不在 $LOAD_PATH 中,所以 Ruby 程序执行失败。若是有 Gemfile 应该先 bundle install。若是没有,手动 gem install macho
Could not find proper version of cocoapods (1.1.1) in any of the sources
Run `bundle install` to install missing gems.
复制代码
答案:Gemfile.lock 中已经指定了 cocoapods 这个 gem 的版本为 1.1.1,可是当前 Gem Paths 中并无安装 1.1.1 版本的 cocoapods,此时应该 bundle install
Required ruby-2.3.7 is not installed.
To install do: 'rvm install "ruby-2.3.7"'
复制代码
答案:当前目录中的 .ruby-version 指定 Ruby 版本应该为 2.3.7,可是当前机器上并无安装 2.3.7,为了之后不给本身挖坑, 仍是执行一下 rvm install 2.3.7 吧