前言
在終端中執行fastlane lane_name
之後,fastlane會去執行Fastfile中定義的同名lane,這個是如何實現的。
本文按照解析引數這一主線,嘗試解釋fastlane的執行邏輯和內部結構。
在開始正文之前,有一些概念和名稱需要解釋一下,在之前的文章中,已經提到過一些fastlane的領域專用名稱,比如platform、lane、action等,除了這些以外,還有兩個重要的名稱需要了解一下,Command和Tool。
1. Tool和Command
fastlane是一個龐大的工具集,為了更好的使用和管理這些工具,將功能相似的工具劃分在一起組成一個Tool,每一種Tool都代表fastlane的一個大的功能點。
fastlane中的Tool列表:
TOOLS = [
:fastlane,
:pilot,
:spaceship,
:produce,
:deliver,
:frameit,
:pem,
:snapshot,
:screengrab,
:supply,
:cert,
:sigh,
:match,
:scan,
:gym,
:precheck
]
複製程式碼
每一個Tool都有其特定的應用領域,比如cert
用於證書相關,sigh
用於簽名相關,gym
用於打包相關,等等。
其中,fastlane
是預設的Tool,比如fastlane lane_name
、fastlane init
、fastlane action action_name
、fastlane add_plugin plugin_name
等,因為這些命令都沒有顯式的指定Tool,所以使用的都是fastlane
這個Tool,它是fastlane庫中最重要的Tool。
每一種Tool下都有多個Command,如果把Tool看做是某個領域的專用工具,Command則是其中的一個操作,比如cert
就是專門用於簽名證書相關的Tool,當需要建立新的簽名證書時,可以使用cert
下的create
這個Command,其具體的執行命令是fastlane cert creat
,因為create
是預設命令,所以也可以使用fastlane cert
;當需要移除過期證書時,則可以使用revoke_expired
這個Command,其具體的命令是fastlane cert revoke_expired
。
上文中提到的幾條命令,fastlane init
中的init
,fastlane action action_name
中的acton
,fastlane add_plugin plugin_name
中的add_plugin
等,這些都是fastlane
這個預設Tool的Command。而fastlane lane_name
使用的是預設Tool的預設Command:trigger
。
Command必須和Tool結合起來才有意義,因為不同Tool下的Command可能會出現同名的情況,fastlane允許這種情況出現。只有確定了Tool之後,才能確定真正的Command。
2. lane、action
之前在Fastlane用法中有講到lane和action的簡單使用,這裡再結合Tool和Command,談一談它們的聯絡和區別。
default_platform :ios
lane :build do
match(git_url: your_git_url)
gym(export_method: 'enterprise')
end
複製程式碼
上述程式碼中的build
是一個lane,match
和gym
都是action。
想一想如何執行build
這個lane
fastlane build
複製程式碼
只要在終端執行上述命令列就可以了
那麼,執行了上述命令之後,fastlane庫最終會呼叫哪一個Tool和Command呢
之前的文章中已經說過了,當沒有顯式指定Tool和Command時,使用預設的Tool:fastlane
和預設Tool的預設Command:trigger
。
fastlane build
的完整命令
fastlane fastlane trigger build
複製程式碼
當使用在Fastfile中定義的lane進行打包、測試和釋出時,最終呼叫的都是trigger
這個Command。
lane和action是trigger
這個Command內部定義的領域名稱,它們只能在trigger
中使用,它們和Command不是同一個層次的。只要說起lane和action,那麼就預設了Tool是fastlane
,Command是trigger
。
當執行build
這個lane之後,最終目的是去執行它包含的action,build
內部包含了兩個action,分別是match
和gym
,而這兩個action最終會去呼叫它們同名的Tool。
除了fastlane
這個預設的Tool,其他所有的Tool都有其同名的action,通過在lane中新增action,可以呼叫其他所有的Tool。
除了這些與Tool同名的action,fastlane還內建了其他很多action,比如關於git和pod的。
3. fastlane執行流程
fastlane中所有命令的執行都可以簡單的分為兩步:
- 解析Command
- 執行Command
比如常用的fastlane lane_name
,這條命令沒有顯式的指定Tool和Command,所以,fastlane會使用預設Tool:fastlane
和預設Tool的預設Command:trigger
,然後執行trigger
。
3.1. 解析Command
fastlane庫中幾乎所有命令都可以寫成下列格式:(如果把fastlane-credentials
也當做是一種Tool的話,那這個幾乎就可以去掉了。)
fastlane [tool] [command] [args][--key value]
複製程式碼
tool和command指定使用的Tool和其Command;args通常是一個或多個字串組成的陣列;類似--key value
或-k value
格式的組合會被當做option。args和option會被當做引數傳給Command。
其中tool、command、args和option用[]包含起來,表示它們可以被省略。如果省略了command和tool,則會使用預設的tool和預設tool的預設command。
下圖中展示的是解析Command的簡易流程
下列以兩個例子來說明
-
獲取ARGV 例一:終端輸入
fastlane lane_name
,則ARGV = ["lane_name"];
例二:終端輸入fastlane cert --username "your_usernmae" --development false
,則ARGV = ["cert", "--username", "your_username", "--development", "false"]
-
解析Tool 不同Tool包含的Command不同,確定了Tool,才能真正確定Command。如果ARGV.first是一個Tool的名字,比如:fastlane、cert等,則載入這個Tool,
require 'tool_name/commands_generator'
;如果ARGV.first等於 "fastlane-credentials",則載入require 'credentials_manager'
;如果都不是,則載入fastlane
這個預設的Tool,require "fastlane/commands_generator"
。 如果匹配上了Tool之後,刪除ARGV.first。 例一:使用預設Tool:fastlane
,ARGV = [ "lane_name"]
例二:使用Tool:cert
,ARGV = ["--username", "your_username", "--development", "false"]
-
解析Command 將ARGV複製給一個新陣列,在新陣列中去掉所有以
-
開頭的字串物件,然後使用陣列的第一個物件去匹配此Tool下的command列表,如果能匹配上,則使用匹配到的Command;如果不能,則使用預設Command。 如果匹配上,則將匹配上的字串物件從ARGV中刪除。 例一:使用fastlane
這個Tool的預設Command:trigger
,ARGV = [ "lane_name"]
例二:使用cert
這個Tool的預設Command:create
,ARGV = ["--username", "your_username", "--development", "false"]
這裡有個問題需要注意一下,當在終端輸入fastlane match --type enterprise
時,這條命令的初衷是想使用match
這個Tool的預設Command:run
,但按照本步驟的方法,最終使用的是enterprise
這個Command。所以在這裡最好顯示指定要使用的Command,fastlane match run --type enterprise
。 -
解析command對應的option 遍歷ARGV,如果字串是以
--
或-
開頭,則將此字串物件和其後的字串物件作為一對key-value值,並從ARGV中刪除這兩個物件。遍歷完畢之後,將ARGV中剩餘的的引數賦值給args。 例一:option等於nil,args等於lane_name
例二:option等於{"username":"your_username", "development": false}
,args等於nil -
執行command 每個command都會設定一個對應的block,匹配到這個command並解析完option之後,則執行其對應的block,並將[步驟4]中獲取的option和args傳給這個block。 從這個地方開始,業務程式碼才會真正開始執行。
上述解析過程描述的非常粗糙,如果想了解詳細的解析過程,可以參考**commander**,fastlane內部通過這個庫來解析這些引數的。
把這個過程再豐富一下,就變成了下圖
(由於篇幅原因,圖中只畫出了cert
、sigh
和fastlane
這三個Tool)
3.2. 執行Command
到了這一步,就開始深入到各個Tool的核心內容了,在fastlane這個庫中,Tool共有16個,在這裡並不會對所有的Tool展開討論,這裡只討論預設Command:trigger
。
4. trigger
trigger
是fastlane這個Tool的預設命令,其作用是執行一個指定的lane,而fastlane
這個Tool又是fastlane庫的預設Tool,所以一般在執行lane的時候,可以省略掉Tool和Command,只需要執行命令fastlane [platform_name] lane_name
,如果設定了default_platform,platform_name也可以省略。
trigger
的目的是去執行一個指定的lane,而執行lane的目的是去執行其中的action,根據這一需求,作圖如下
下面以例子的方式來了解這一過程,本文準備了兩個自定義action,分別是example_action
和example_action_second
,fastlane會將它們載入作為外部action。
4.1. 前提條件
相關檔案的目錄結構
-fastlane
-Fastfile
-actions
-example_action.rb
-example_action_second.rb
複製程式碼
fastfile
default_platform :ios
platform :ios do
lane :test do |options|
puts "lane options #{options}"
example_action(foo:"ruby", bar:"ios")
example_action_second(foo:"ruby", bar:"ios")
end
end
lane :test_without_platform do
puts "lane whithout platform"
end
複製程式碼
example_action.rb
module Fastlane
module Actions
class ExampleActionAction < Action
def self.run(options)
binding.pry
puts "this is example_action action"
puts options
end
def self.is_supported?(platform)
true
end
def self.available_options
[]
end
end
end
end
複製程式碼
example_action_second.rb
module Fastlane
module Actions
class ExampleActionSecondAction < Action
def self.run(options)
puts "this is example action second action, options:"
puts "foo:#{options[:foo]}"
puts "bar:#{options[:bar]}"
end
def self.is_supported?(platform)
true
end
def self.available_options
[
FastlaneCore::ConfigItem.new(key: :foo,
short_option: "-f",
description: "this is foo"),
FastlaneCore::ConfigItem.new(key: :bar,
short_option: "-b",
description: "this is bar")
]
end
end
end
end
複製程式碼
4.2. 執行trigger
在終端執行fastlane test key1:value1 key2:value2 --env local1,local2
,按照上文所說的,第一步解析command後,fastlane庫找到需要執行的目標command:trigger
,然後執行此command對應的block。
fastlane庫中trigger
命令的定義
command :trigger do |c|
c.syntax = 'fastlane [lane]'
c.description = 'Run a specific lane. Pass the lane name and optionally the platform first.'
c.option('--env STRING[,STRING2]', String, 'Add environment(s) to use with `dotenv`')
c.option('--disable_runner_upgrades', 'Prevents fastlane from attempting to update FastlaneRunner swift project')
c.action do |args, options|
if ensure_fastfile
Fastlane::CommandLineHandler.handle(args, options)
end
end
end
複製程式碼
trigger
支援兩種option,分別是--env STRING[,STRING2]
和disable_runner_upgrades
,其中第一個option的作用是指定檔名,這些檔案會被dotenv載入,用來配置環境變數。在當前這個例子中,設定了--env local1,local2
,如果.env.local1
和.env.local2
這兩個檔案存在於Fastfile所在的資料夾或其上級資料夾,則dotenv
會去載入它們來設定環境變數。(不管--env
有沒有設定,dotenv都預設載入.env
和.env.default
)
執行trigger
就是執行下列程式碼
c.action do |args, options|
if ensure_fastfile
Fastlane::CommandLineHandler.handle(args, options)
end
end
複製程式碼
當fastlane庫執行這個block時,傳入了兩個引數,args
和options
,通過解析命令字串可知,其中args
的值為["test", "key1:value1", "key2:value2"]
,options
的值是一個Options
型別的物件,且options.env 的值為 "local1,local2"
。
4.3. 解析lane
解析lane的目的就是獲取Fastfile中定義的Lane
型別的物件
在這個階段,fastlane庫會載入Fastfile,並將其中定義的lane轉換成Fastlane::Lane
型別的物件,並將這些物件儲存在一個Hash型別的物件lanes
中。
類Fastlane::Lane
中定義的變數
module Fastlane
# Represents a lane
class Lane
attr_accessor :platform
attr_accessor :name
# @return [Array]
attr_accessor :description
attr_accessor :block
# @return [Boolean] Is that a private lane that can't be called from the CLI?
attr_accessor :is_private
end
end
複製程式碼
Fastlane::Lane
型別的物件中儲存了一個lane的所有資訊,:platform
指定lane使用的平臺,:name
指定lane的名字,:block
儲存了lane對應的執行程式碼。
在本節例子中,lanes
儲存了所有Fastlane::Lane
型別的物件,它的具體結構如下:
{
ios: {
test: Lane.new
},
nil: {
test_without_platform: lane.new
}
}
複製程式碼
fastlane庫使用lanes
這個Hash物件結合之前得到的args
來獲取對應Lane
型別物件
其虛擬碼如下:
#使用platform_lane_info儲存platform名稱和lane名稱
platform_lane_info = []
#過濾掉帶有冒號":"的字串物件
args.each do |current|
unless current.include?(":")
platform_lane_info << current
end
end
#獲取platform名稱和lane名稱
platform_name = nil
lane_name = nil
if platform_lane_info.size >= 2
platform_name = platform_lane_info[0]
lane_name = platform_lane_info[1]
else
if platform_lane_info.first 是一個平臺名字 || platform_lane_info是空陣列
platform_name = platform_lane_info.first
lane_name = 在終端列印一個lane列表供使用者選擇
else
lane_name = platform_lane_info.first
if platform==nil && lanes[nil][lane_name]==nil
platform = default_platform
end
end
end
#返回lane物件
return lanes[platform][lane_name]
複製程式碼
args
的值為["test", "key1:value1", "key2:value2"]
,把args
和lanes
帶入到上述虛擬碼中,可以得到相應的Lane
型別物件。
4.4. 解析lane的options
回顧一下,之前在Fastfile檔案中定義test
這個lane的程式碼
platform :ios do
lane :test do |options|
puts "lane options #{options}"
example_action(foo:"ruby", bar:"ios")
example_action_second(foo:"ruby", bar:"ios")
end
end
複製程式碼
本步驟的目的就是要獲取傳給test
的options
,它是一個Hash型別的物件。
這個options
引數的值是如何得到的,其實,也是通過解析args
獲取的。
其實現邏輯如下
options = {}
args.each do |current|
if current.include?(":")
key, value = current.split(":", 2)
if key.empty?
報錯
end
value = true if value == 'true' || value == 'yes'
value = false if value == 'false' || value == 'no'
options[key.to_sym] = value
end
end
複製程式碼
上述程式碼是在fastlane庫原始碼的基礎上作了一些修改
將args
帶入到上述程式碼中,可以得出lane:test
的options的值為{key1:value1, key2:value2}
fastlane test key1:value1 key2:value2 --env local1,local2
,在終端執行後,一部分輸出如下
[16:37:43]: ------------------------------
[16:37:43]: --- Step: default_platform ---
[16:37:43]: ------------------------------
[16:37:43]: Driving the lane 'ios test' ?
[16:37:43]: lane options {:key1=>"value1", :key2=>"value2"}
複製程式碼
4.5. 解析action
解析action的目的是找到action_name對應的類,本例中,需要執行兩個action,其action_name分別是example_action
和example_action_second
,其對應類分別是ExampleActionAction
和ExampleActionSecondAction
其實現邏輯如下
tmp = action_name.delete("?")
class_name = tmp.split("_").collect!(&:capitalize).join + "Action"
class_ref = Fastlane::Actions.const_get(class_name)
unless class_ref
class_ref = 嘗試把action_name當做別名,重新載入
end
if action_name 是一個lane的名字
執行這個lane
elsif class_ref && class_ref.respond_to?(:run)
解析action的options
執行action
else
報錯
end
複製程式碼
4.6. 解析action的options
action的options指的是傳給action的引數,比如example_action_second
這個action的options是{foo:"ruby", bar:"ios"}
,準確的來說應該是[{foo:"ruby", bar:"ios"}]
,不過一般都只是用這個陣列的第一個物件,所以接下來會去掉外面的一層陣列。
本步驟的目的是將傳給action的options轉換成Configuration
型別的物件,並且在轉換過程中,驗證options中key
和value
的合法性。
action和Configuration
型別的物件是一一對應的,Configuration
類的作用主要是儲存:availabel_options
和:values
,在執行action的時候,也就是在執行action響應類的run
方法時,把Configuration
型別的物件當做引數傳入,然後action響應類使用它來獲取key對應的value。
Configuration
中定義的例項變數
module FastlaneCore
class Configuration
attr_accessor :available_options
attr_accessor :values
# @return [Array]
attr_reader :all_keys
# @return [String]
attr_accessor :config_file_name
# @return [Hash]
attr_accessor :config_file_options
end
end
複製程式碼
:availabel_options
表示action響應類中定義的available_options
,比如example_action_second
這個action,它的響應類是ExampleActionSecondAction
,ExampleActionSecondAction
中類方法available_options
的定義
def self.available_options
[
FastlaneCore::ConfigItem.new(key: :foo,
short_option: "-f",
description: "this is foo"),
FastlaneCore::ConfigItem.new(key: :bar,
short_option: "-b",
description: "this is bar")
]
end
複製程式碼
:values
表示傳給action的options,給:values
賦值之後還需要驗證它的key、value
是否合法,如果不合法,程式中止。比如example_action_second
這個action的options是{foo:"ruby", bar:"ios"}
。
:all_key
表示:available_options
中的key
的陣列,具體程式碼:@available_options.collect(&:key)
。
:config_file_name
和:config_file_options
:在action的響應類中,可以使用Configuration.load_configuration_file(config_file_name)
來載入這個action專有的配置檔案,然後把檔案中的資料以key:value
的方式儲存在:cofnig_file_options
變數中。
其實現程式碼如下
values = 傳給action的options
action_responder = action響應類
first_element = (action_responder.available_options || []).first
if (first_element && first_element kind_of?(FastlaneCore::ConfigItem)) || first_element == nil
values = {} if first_element==nil
return FastlaneCore::Configuration.create(action_responder.available_options, values)
else
#action響應類中定義了available_options類方法,且其返回物件的第一個元素的型別不是FastlaneCore::ConfigItem,則不對values做任何處理,直接返回。
return values
end
複製程式碼
建立FastlaneCore::Configuration
時,內部的驗證邏輯
values = 傳給action的options
action_responder = action響應類
available_options = action_responder.available_options
#available_options必須是一個Array,且其內部的元素都必須是FastlaneCore::ConfigItem的型別
verify_input_types
#values中的每一個key都必須在available_options中定義過,如果在建立FastlaneCore::ConfigItem型別的物件時,設定了type和verify_block,則values中對應的value都必須滿足。
verify_value_exists
#不能再available_options中重複定義同一個key
verify_no_duplicates
#在定義FastlaneCore::ConfigItem型別的物件時,可以設定與自己衝突的key,在values中,不能同時存在衝突的兩個key。
verify_conflicts
#在定義FastlaneCore::ConfigItem型別的物件時,同時設定了default_value和verify_block,且values中沒有設定這個key,則需要呼叫verify_block驗證default_value的合法性。
verify_default_value_matches_verify_block
複製程式碼
4.7. 執行action
執行action就是執行action響應類的類方法run
,同時將[步驟6]的解析結果傳給run
作為引數。類方法run
中包含了這個action的所有業務程式碼,fastlane庫中所有的內建action都遵循這一設定,同樣,在定義外部action時,也應該這樣做。
例子中actionexample_action_second
的響應類ExampleActionSecondAction
中的run
的定義
def self.run(options)
puts "this is example action second action, options:"
puts "foo:#{options[:foo]}"
puts "bar:#{options[:bar]}"
end
複製程式碼
其中引數options是一個FastlaneCore::Configuration
的物件,可以通過options[key]
或options.fetch(key)
的方式來獲取key對應的value。
5. trigger總結
之前一節,以圖1的步驟詳細講解了trigger
命令的執行過程,圖中的幾個步驟完全是從使用者的角度來劃分的,單看這幾個步驟並不能對fastlane庫有一個直觀的瞭解,下列兩個圖在圖一的基礎上增加了一些細節。
圖2中描述了trigger
命令的部分執行過程,大致可以和圖1中的前三個步驟相對應。相比之前的執行步驟,圖2中增加了一些細節步驟,並且將這些步驟以泳道的方式進行劃分。除了Commander之外,其他步驟的執行者比如CLIToolsDistributor
、CommandsGenerator
等都是fastlane庫中定義的類,而Commander則是fastlane庫引用的外部庫。
圖3承接圖2的步驟,主要描述了Fastfile中定義的lane的執行過程,大致可以和圖1中的後三個步驟相對應,圖3中步驟的執行者基本上都是Runner
這個類。