1. 版本管理工具及 Ruby 工具鏈環境

ruby tool tour

CocoaPods 歷險記這個專題是 Edmond冬瓜 是對於 iOS / macOS 工程中版本管理工具 CocoaPods 的實現細節、原理、源碼、實踐與經驗的分享記錄,旨在幫助你們可以更加了解這個依賴管理工具,而不只侷限於 pod installpod update

本文知識目錄

版本管理工具及 Ruby 工具鏈

背景

CocoaPods 做爲業界標準,各位 iOS 開發同窗應該都不陌生。不過不少同窗對 CocoaPods 的使用基本停留在 pod installpod update 上。一旦項目組件化,各業務線邏輯拆分到獨立的 Pod 中後,光了解幾個簡單 Pod 命令是沒法知足需求的,同時還面臨開發環境的一致性,Pod 命令執行中的各類異常錯誤,都要求咱們對其有更深層的認知和 🤔。html

關於 CocoaPods 深刻的文章有不少,推薦 ObjC China 的這篇,深刻理解 CocoaPods,而本文但願從依賴管理工具的角度來談談 CocoaPods 的管理理念。node

Version Control System (VCS)

Version control systems are a category of software tools that help a software team manage changes to source code over time. Version control software keeps track of every modification to the code in a special kind of database.

軟件工程中,版本控制系統是敏捷開發的重要一環,爲後續的持續集成提供了保障。Source Code Manager (SCM) 源碼管理就屬於 VCS 的範圍之中,熟知的工具備如 Git 。而 CocoaPods 這種針對各類語言所提供的 Package Manger (PM)也能夠看做是 SCM 的一種。git

而像 GitSVN 是針對項目的單個文件的進行版本控制,而 PM 則是以每一個獨立的 Package 做爲最小的管理單元。包管理工具都是結合 SCM 來完成管理工做,對於被 PM 接管的依賴庫的文件,一般會在 Git.ignore 文件中選擇忽略它們。github

例如:在 Node 項目中通常會把 node_modules 目錄下的文件 ignore 掉,在 iOS / macOS 項目則是 Podsweb

Git Submodule

Git Submodule

Git submodules allow you to keep a git repository as a subdirectory of another git repository. Git submodules are simply a reference to another repository at a particular snapshot in time. Git submodules enable a Git repository to incorporate and track version history of external code.

Git Submodules 能夠算是 PM 的「青春版」,它將單獨的 git 倉庫以子目錄的形式嵌入在工做目錄中。它不具有 PM 工具所特有的語義化版本管理、沒法處理依賴共享與衝突等。只能保存每一個依賴倉庫的文件狀態。shell

Git 在提交更新時,會對全部文件製做一個快照並將其存在數據庫中。Git 管理的文件存在 3 種狀態:數據庫

  • working director: 工做目錄,即咱們肉眼可見的文件
  • stage area: 暫存區 (或稱 index area ),存在 .git/index 目錄下,保存的是執行 git add 相關命令後從工做目錄添加的文件。
  • commit history: 提交歷史,存在 .git/ 目錄下,到這個狀態的文件改動算是入庫成功,基本不會丟失了。

Git submodule 是依賴 .gitmodules 文件來記錄子模塊的。npm

[submodule "ReactNative"]
    path = ReactNative
    url = https://github.com/facebook/ReactNative.git

.gitmodules 僅記錄了 path 和 url 以及模塊名稱的基本信息, 可是咱們還須要記錄每一個 Submodule Repo 的 commit 信息,而這 commit 信息是記錄在 .git/modules 目錄下。同時被添加到 .gitmodules 中的 path 也會被 git 直接 ignore 掉。json

Package Manger

做爲 Git Submodule 的強化版,PM 基本都具有了語義化的版本檢查能力,依賴遞歸查找,依賴衝突解決,以及針對具體依賴的構建能力和二進制包等。簡單對好比下:swift

Key File Git submodule CocoaPods SPM npm
描述文件 .gitmodules Podfile Package.swift Package.json
鎖存文件 .git/modules Podfile.lock Package.resolved package-lock.json

從 👆 可見,PM 工具基本圍繞這個兩個文件來現實包管理:

  • 描述文件:聲明瞭項目中存在哪些依賴,版本限制;
  • 鎖存文件(Lock 文件):記錄了依賴包最後一次更新時的全版本列表。

除了這兩個文件以外,中心化的 PM 通常會提供依賴包的託管服務,好比 npm 提供的 npmjs.com 能夠集中查找和下載 npm 包。若是是去中心化的 PM 好比 iOSCarthageSPM 就只能經過 Git 倉庫的地址了。

CocoaPods

image.png

CocoaPods  是開發 iOS/macOS 應用程序的一個第三方庫的依賴管理工具。 利用 CocoaPods,能夠定義本身的依賴關係(簡稱 Pods),以及在整個開發環境中對第三方庫的版本管理很是方便。

下面咱們以 CocoaPods 爲例。

Podfile

Podfile 是一個文件,以 DSL(其實直接用了 Ruby 的語法)來描述依賴關係,用於定義項目所須要使用的第三方庫。該文件支持高度定製,你能夠根據我的喜愛對其作出定製。更多相關信息,請查閱 Podfile 指南

Podfile.lock

這是 CocoaPods 建立的最重要的文件之一。它記錄了須要被安裝的 Pod 的每一個已安裝的版本。若是你想知道已安裝的 Pod 是哪一個版本,能夠查看這個文件。推薦將 Podfile.lock 文件加入到版本控制中,這有助於整個團隊的一致性。

Manifest.lock

這是每次運行 pod install 命令時建立的 Podfile.lock 文件的副本。若是你碰見過這樣的錯誤 沙盒文件與 Podfile.lock 文件不一樣步 (The sandbox is not in sync with the Podfile.lock),這是由於 Manifest.lock 文件和 Podfile.lock 文件不一致所引發。因爲 Pods 所在的目錄並不總在版本控制之下,這樣能夠保證開發者運行 App 以前都能更新他們的 Pods,不然 App 可能會 crash,或者在一些不太明顯的地方編譯失敗。

Master Specs Repo

Ultimately, the goal is to improve discoverability of, and engagement in, third party open-source libraries, by creating a more centralized ecosystem.

做爲包管理工具,CocoaPods 的目標是爲咱們提供一個更加集中的生態系統,來提升依賴庫的可發現性和參與度。本質上是爲了提供更好的檢索和查詢功能,惋惜成爲了它的問題之一。由於 CocoaPods 經過官方的 Spec 倉庫來管理這些註冊的依賴庫。隨着不斷新增的依賴庫致使 Spec 的更新和維護成爲了使用者的包袱。

好在這個問題在 1.7.2 版本中已經解決了,CocoaPods 提供了 Mater Repo CDN ,能夠直接 CDN 到對應的 Pod 地址而無需在經過本地的 Spec 倉庫了。同時在 1.8 版本中,官方默認的 Spec 倉庫已替換爲 CDN,其地址爲  https://cdn.cocoapods.org

Spec 倉靜態頁

Ruby 生態及工具鏈

對於一部分僅接觸過 CocoaPods 的同窗,其 PM 可能並不熟悉。其實 CocoaPods 的思想借鑑了其餘語言的 PM 工具,例:RubyGems, Bundler, npmGradle

咱們知道 CocoaPods 是經過 Ruby 語言實現的。它自己就是一個 Gem 包。理解了 Ruby 的依賴管理有助於咱們更好的管理不一樣版本的 CocoaPods 和其餘 Gem。同時可以保證團隊中的全部同事的工具是在同一個版本,這也算是敏捷開發的保證吧。

RVM & rbenv

RVM vs rbenv

RVMrbenv 都是管理多個 Ruby 環境的工具,它們都能提供不一樣版本的 Ruby 環境管理和切換。

具體哪一個更好要看我的習慣。 固然 rbenv 官方是這麼說的 Why rbenv 。本文後續的實驗也都是是使用 rbenv 進行演示。

層級關係

RubyGems

RubyGems

The RubyGems software allows you to easily download, install, and use ruby software packages on your system. The software package is called a 「gem」 which contains a packaged Ruby application or library.

RubyGems 是 Ruby 的一個包管理工具,這裏面管理着用 Ruby 編寫的工具或依賴咱們稱之爲 Gem。

而且 RubyGems 還提供了 Ruby 組件的託管服務,能夠集中式的查找和安裝 library 和 apps。當咱們使用 gem install xxx 時,會經過 rubygems.org 來查詢對應的 Gem Package。而 iOS 平常中的不少工具都是 Gem 提供的,例:BundlerfastlanejazzyCocoaPods 等。

在默認狀況下 Gems 老是下載 library 的最新版本,這沒法確保所安裝的 library 版本符合咱們預期。所以咱們還缺一個工具。

Bundler

Bundler

Bundler 是管理 Gem 依賴的工具,能夠隔離不一樣項目中 Gem 的版本和依賴環境的差別,也是一個 Gem。

Bundler 經過讀取項目中的依賴描述文件 Gemfile ,來肯定各個 Gems 的版本號或者範圍,來提供了穩定的應用環境。當咱們使用 bundle install 它會生成 Gemfile.lock 將當前 librarys 使用的具體版本號寫入其中。以後,他人再經過 bundle install 來安裝 libaray 時則會讀取 Gemfile.lock 中的 librarys、版本信息等。

Gemfile

能夠說 CocoaPods 實際上是 iOS 版的 RubyGems + Bundler 組合。Bundler 依據項目中的 Gemfile 文件來管理 Gem,而 CocoaPods 經過 Podfile 來管理 Pod。

Gemfile 配置以下:

source 'https://gems.example.com' do
  gem 'cocoapods', '1.8.4'是管理 Gem 依賴的工具
  gem 'another_gem', :git => 'https://looseyi.github.io.git', :branch => 'master'
end

可見,Podfile 的 DSL 寫法和 Gemfile 一模一樣。那什麼狀況會用到 Gemfile 呢 ?

CocoaPods 每一年都會有一些重大版本的升級,前面聊到過 CocoaPodsinstall 過程當中會對項目的 .xcodeproj 文件進行修改,不一樣版本其有所不一樣,這些在變動均可能致使大量 conflicts,處理很差,項目就不能正常運行了。我想你必定不肯意去修改 .xcodeproj 的衝突。

若是項目是基於 fastlane 來進行持續集成的相關工做以及 App 的打包工做等,也須要其版本管理等功能。

如何安裝一套可管控的 Ruby 工具鏈?

講完了這些工具的分工,而後來講說實際的運用。咱們可使用 homebrew + rbenv + RubyGems + Bundler 這一整套工具鏈來控制一個工程中 Ruby 工具的版本依賴。

如下是我認爲比較可控的 Ruby 工具鏈分層管理圖。下面咱們逐一講述每一層的管理方式,以及實際的操做方法。

可管控工具鏈的分層結構

1. 使用 homebrew 安裝 rbenv

$ brew install rbenv

安裝成功後輸入 rbenv  就能夠看到相關提示:

$ rbenv

rbenv 1.1.2
Usage: rbenv <command> [<args>]

Some useful rbenv commands are:
   commands    List all available rbenv commands
   local       Set or show the local application-specific Ruby version
   global      Set or show the global Ruby version
   shell       Set or show the shell-specific Ruby version
   install     Install a Ruby version using ruby-build
   uninstall   Uninstall a specific Ruby version
   rehash      Rehash rbenv shims (run this after installing executables)
   version     Show the current Ruby version and its origin
   versions    List installed Ruby versions
   which       Display the full path to an executable
   whence      List all Ruby versions that contain the given executable

See `rbenv help <command>' for information on a specific command.
For full documentation, see: https://github.com/rbenv/rbenv#readme

2. 使用 rbenv 管理 Ruby 版本

使用 rbenv  來安裝一個 Ruby 版本,這裏我使用剛剛 release Ruby 2.7:

$ rbenv install 2.7.0

這個安裝過程有些長,由於要下載 openssl 和 Ruby 的解釋器,大概要 20 分鐘左右。

安裝成功後,咱們讓其在本地環境中生效:

$ rbenv shell 2.7.0
輸入上述命令後,可能會有報錯。 rbenv  提示咱們在 .zshrc  中增長一行 eval "$(rbenv init -)"  語句來對 rbenv  環境進行初始化。若是報錯,咱們增長並重啓終端便可。
$ ruby --version
ruby 2.7.0p0 (2019-12-25 revision 647ee6f091) [x86_64-darwin19]
$ which ruby
/Users/gua/.rbenv/shims/ruby

切換以後咱們發現 Ruby 已經切換到 rbenv  的管理版本,而且其啓動 PATH  也已經變成 rbenv  管理下的 Ruby。而且咱們能夠看一下 Ruby 捆綁的 Gem  的 PATH :

$ which gem
/Users/bytedance/.rbenv/shims/gem

對應的 Gem  也已經變成 rbenv  中的 PATH 。

3. 查詢系統級 Gem 依賴

如此,咱們使用 rbenv 已經對 Ruby 及其 Gem  環境在版本上進行了環境隔離。咱們能夠經過 gem list  命令來查詢當前系統環境下全部的 Gem  依賴:

$ gem list

*** LOCAL GEMS ***

activesupport (4.2.11.3)
...
claide (1.0.3)
cocoapods (1.9.3)
cocoapods-core (1.9.3)
cocoapods-deintegrate (1.0.4)
cocoapods-downloader (1.3.0)
cocoapods-plugins (1.0.0)
cocoapods-search (1.0.0)
cocoapods-stats (1.1.0)
cocoapods-trunk (1.5.0)
cocoapods-try (1.2.0)

記住這裏的 CocoaPods 版本,咱們後面項目中還會查詢。

如此咱們已經完成了所有的 Ruby、Gem 環境的配置,咱們經過一張漏斗圖再來梳理一下:

操做安裝流程

如何使用 Bundler 管理工程中的 Gem 環境

下面咱們來實踐一下,如何使用 Bundler 來鎖定項目中的 Gem 環境,從而讓整個團隊統一 Gem 環境中的全部 Ruby 工具版本。從而避免文件衝突和沒必要要的錯誤。

下面是在工程中對於 Gem 環境的層級圖,咱們能夠在項目中增長一個 Gemfile 描述,從而鎖定當前項目中的 Gem 依賴環境。

工程中獨立 Gem 環境示意圖

如下也會逐一講述每一層的管理方式,以及實際的操做方法。

1. 在 iOS 工程中初始化 Bundler 環境

首先咱們有一個 iOS Demo 工程 - GuaDemo :

$ ls -al
total 0
drwxr-xr-x   4 gua  staff   128 Jun 10 14:47 .
drwxr-xr-x@ 52 gua  staff  1664 Jun 10 14:47 ..
drwxr-xr-x   8 gua  staff   256 Jun 10 14:47 GuaDemo
drwxr-xr-x@  5 gua  staff   160 Jun 10 14:47 GuaDemo.xcodeproj

首先先來初始化一個 Bundler  環境(其實就是自動建立一個 Gemfile 文件):

$ bundle init

Writing new Gemfile to /Users/Gua/GuaDemo/Gemfile

2. 在 Gemfile  中聲明使用的 CocoaPods 版本並安裝

以後咱們編輯一下這個 Gemfile 文件,加入咱們當前環境中須要使用 CocoaPods 1.5.3 這個版本,則使用 Gemfile  的 DSL 編寫如下內容:

# frozen_string_literal: true

source "https://rubygems.org"

git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }

# gem "rails"
gem "cocoapods", "1.5.3"

編寫以後執行一下 bundle install :

$ bundle install
Fetching gem metadata from https://gems.ruby-china.com/............
Resolving dependencies...
...
Fetching cocoapods 1.5.3
Installing cocoapods 1.5.3
Bundle complete! 1 Gemfile dependency, 30 gems now installed.

發現 CocoaPods 1.5.3  這個指定版本已經安裝成功,而且還保存了一份 Gemfile.lock  文件用來鎖存此次的依賴結果。

3. 使用當前環境下的 CocoaPods 版本操做 iOS 工程

此時咱們能夠檢查一下當前 Bundler 環境下的 Gem  列表:

$ bundle exec gem list

*** LOCAL GEMS ***

activesupport (4.2.11.3)
atomos (0.1.3)
bundler (2.1.4)
CFPropertyList (3.0.2)
claide (1.0.3)
cocoapods (1.5.3)
...

發現相比於全局 Gem 列表,這個列表精簡了許多,而且也只是基礎 Gem 依賴和 CocoaPodsGem 依賴。此時咱們使用 bundle exec pod install  來執行 Install 這個操做,就可使用 CocoaPods 1.5.3  版原本執行 Pod  操做了(固然,前提是你還須要寫一個 Podfile ,你們都是 iOSer 這裏就略過了)。

$ bundle exec pod install
Analyzing dependencies
Downloading dependencies
Installing SnapKit (5.0.1)
Integrating client project
[!] Please close any current Xcode sessions and use `GuaDemo.xcworkspace` for this project from now on.
Sending stats
Pod installation complete! There is 1 dependency from the Podfile and 1 total pod installed.

能夠再來看一下 Podfile.lock  文件:

cat Podfile.lock
PODS:
  - SnapKit (5.0.1)

DEPENDENCIES:
  - SnapKit (~> 5.0.0)

SPEC REPOS:
  https://github.com/cocoapods/specs.git:
    - SnapKit

SPEC CHECKSUMS:
  SnapKit: 97b92857e3df3a0c71833cce143274bf6ef8e5eb

PODFILE CHECKSUM: 1a4b05aaf43554bc31c90f8dac5c2dc0490203e8

COCOAPODS: 1.5.3

發現使用的 CocoaPods 的版本確實是 1.5.3 。而當咱們不使用 bundle exec  執行前綴,則會使用系統環境中的 CocoaPods 版本。如此咱們也就驗證了工程中的 Gem 環境和系統中的環境能夠經過 Bundler 進行隔離。

總結

  • 經過版本管理工具演進的角度能夠看出,CocoaPods 的誕生並不是一蹴而就,也是不斷地借鑑其餘管理工具的優勢,一點點的發展起來的。VCS 工具從早期的 SVNGit,再細分出 Git Submodule,再到各個語言的 Package Manager 也是一直在發展的。
  • 雖然 CocoaPods 做爲包管理工具控制着 iOS 項目的各類依賴庫,但其自身一樣遵循着嚴格的版本控制並不斷迭代。但願你們能夠從本文中認識到版本管理的重要性。
  • 經過實操 Bundler 管理工程的全流程,學習了 Bundler 基礎,並學習瞭如何控制一個項目中的 Gem 版本信息。

後續咱們將會圍繞 CocoaPods ,從工具鏈逐漸深刻到細節,根據咱們的使用經驗,逐一講解。

知識點問題梳理

這裏羅列了四個問題用來考察你是否已經掌握了這篇文章,若是沒有建議你加入收藏再次閱讀:

  1. PM 是如何進行依賴庫的版本管理?
  2. RubyRVM/rbenv 之間的關係是什麼?
  3. GemBundlerCocaPods 之間的關係是什麼?
  4. 如何經過 Bundler 來管理工程中的 Gem 環境?如何鎖死工程內部的 CocoaPods 版本?
相關文章
相關標籤/搜索