做爲一名 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
才能讓命令正確執行。服務器
爲了解決這個問題,咱們須要讓 Gem Path 指向一個咱們擁有寫權限的目錄。比較簡單直接的辦法就是咱們利用 homebrew 去安裝一個新的 Ruby。app
彷佛很完美,但有個問題:咱們如何約束你們全部人都使用一樣版本的 Ruby 呢?工具
答案是使用 Ruby 的版本管理工具。以 RVM 爲例,當你安裝 RVM 之後,你在命令行中執行的每個 cd
命令其實都被 RVM 所替換了。RVM 會在每一次切換目錄後檢查當前目錄中是否有 .ruby-version 文件,若是有,就檢查當前使用的 Ruby 是不是文件中指定的版本。若是不是,他會給出相似 Required ruby-x.x.x is not installed
這樣的警告。post
在我司工程的早期階段,咱們除了使用 cocoapods,還須要使用 Ruby 編寫一些打包和發佈的腳本,而當時系統提供的 Ruby 版本還比較低(2.0.0),開發起來不太方便,而利用 RVM ,咱們不只能夠方便的安裝一個新版本的 Ruby,還能夠利用 .ruby-version 來保證你們可使用相同版本的 Ruby(儘管只是一個比較弱的約束)。ui
相信到這裏,你們已經可以理解,在咱們的項目中使用 RVM 是頗有必要的。咱們接下來看第二個問題:爲何要用 Bundler?.net
爲了回答這個問題,咱們須要先把目光轉向 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
Could not find proper version of cocoapods (1.1.1) in any of the sources Run `bundle install` to install missing gems.
Required ruby-2.3.7 is not installed. To install do: 'rvm install "ruby-2.3.7"'