前言
在終端中執(zhí)行fastlane lane_name
之后,fastlane會(huì)去執(zhí)行Fastfile中定義的同名lane,這個(gè)是如何實(shí)現(xiàn)的。
本文按照解析參數(shù)這一主線,嘗試解釋fastlane的執(zhí)行邏輯和內(nèi)部結(jié)構(gòu)。
在開(kāi)始正文之前,有一些概念和名稱需要解釋一下,在之前的文章中,已經(jīng)提到過(guò)一些fastlane的領(lǐng)域?qū)S妹Q,比如platform、lane、action等,除了這些以外,還有兩個(gè)重要的名稱需要了解一下,Command和Tool。
1. Tool和Command
fastlane是一個(gè)龐大的工具集,為了更好的使用和管理這些工具,將功能相似的工具劃分在一起組成一個(gè)Tool,每一種Tool都代表fastlane的一個(gè)大的功能點(diǎn)。
fastlane中的Tool列表:
TOOLS = [
:fastlane,
:pilot,
:spaceship,
:produce,
:deliver,
:frameit,
:pem,
:snapshot,
:screengrab,
:supply,
:cert,
:sigh,
:match,
:scan,
:gym,
:precheck
]
每一個(gè)Tool都有其特定的應(yīng)用領(lǐng)域,比如cert
用于證書(shū)相關(guān),sigh
用于簽名相關(guān),gym
用于打包相關(guān),等等。
其中,fastlane
是默認(rèn)的Tool,比如fastlane lane_name
、fastlane init
、fastlane action action_name
、fastlane add_plugin plugin_name
等,因?yàn)檫@些命令都沒(méi)有顯式的指定Tool,所以使用的都是fastlane
這個(gè)Tool,它是fastlane庫(kù)中最重要的Tool。
每一種Tool下都有多個(gè)Command,如果把Tool看做是某個(gè)領(lǐng)域的專用工具,Command則是其中的一個(gè)操作,比如cert
就是專門用于簽名證書(shū)相關(guān)的Tool,當(dāng)需要?jiǎng)?chuàng)建新的簽名證書(shū)時(shí),可以使用cert
下的create
這個(gè)Command,其具體的執(zhí)行命令是fastlane cert creat
,因?yàn)?code>create是默認(rèn)命令,所以也可以使用fastlane cert
;當(dāng)需要移除過(guò)期證書(shū)時(shí),則可以使用revoke_expired
這個(gè)Command,其具體的命令是fastlane cert revoke_expired
。
上文中提到的幾條命令,fastlane init
中的init
,fastlane action action_name
中的acton
,fastlane add_plugin plugin_name
中的add_plugin
等,這些都是fastlane
這個(gè)默認(rèn)Tool的Command。而fastlane lane_name
使用的是默認(rèn)Tool的默認(rèn)Command:trigger
。
Command必須和Tool結(jié)合起來(lái)才有意義,因?yàn)椴煌琓ool下的Command可能會(huì)出現(xiàn)同名的情況,fastlane允許這種情況出現(xiàn)。只有確定了Tool之后,才能確定真正的Command。
2. lane、action
之前在Fastlane用法中有講到lane和action的簡(jiǎn)單使用,這里再結(jié)合Tool和Command,談一談它們的聯(lián)系和區(qū)別。
default_platform :ios
lane :build do
match(git_url: your_git_url)
gym(export_method: 'enterprise')
end
上述代碼中的build
是一個(gè)lane,match
和gym
都是action。
想一想如何執(zhí)行build
這個(gè)lane
fastlane build
只要在終端執(zhí)行上述命令行就可以了
那么,執(zhí)行了上述命令之后,fastlane庫(kù)最終會(huì)調(diào)用哪一個(gè)Tool和Command呢
之前的文章中已經(jīng)說(shuō)過(guò)了,當(dāng)沒(méi)有顯式指定Tool和Command時(shí),使用默認(rèn)的Tool:fastlane
和默認(rèn)Tool的默認(rèn)Command:trigger
。
fastlane build
的完整命令
fastlane fastlane trigger build
當(dāng)使用在Fastfile中定義的lane進(jìn)行打包、測(cè)試和發(fā)布時(shí),最終調(diào)用的都是trigger
這個(gè)Command。
lane和action是trigger
這個(gè)Command內(nèi)部定義的領(lǐng)域名稱,它們只能在trigger
中使用,它們和Command不是同一個(gè)層次的。只要說(shuō)起lane和action,那么就默認(rèn)了Tool是fastlane
,Command是trigger
。
當(dāng)執(zhí)行build
這個(gè)lane之后,最終目的是去執(zhí)行它包含的action,build
內(nèi)部包含了兩個(gè)action,分別是match
和gym
,而這兩個(gè)action最終會(huì)去調(diào)用它們同名的Tool。
除了fastlane
這個(gè)默認(rèn)的Tool,其他所有的Tool都有其同名的action,通過(guò)在lane中添加action,可以調(diào)用其他所有的Tool。
除了這些與Tool同名的action,fastlane還內(nèi)置了其他很多action,比如關(guān)于git和pod的。
3. fastlane執(zhí)行流程
fastlane中所有命令的執(zhí)行都可以簡(jiǎn)單的分為兩步:
- 解析Command
- 執(zhí)行Command
比如常用的fastlane lane_name
,這條命令沒(méi)有顯式的指定Tool和Command,所以,fastlane會(huì)使用默認(rèn)Tool:fastlane
和默認(rèn)Tool的默認(rèn)Command:trigger
,然后執(zhí)行trigger
。
3.1. 解析Command
fastlane庫(kù)中幾乎所有命令都可以寫(xiě)成下列格式:(如果把fastlane-credentials
也當(dāng)做是一種Tool的話,那這個(gè)幾乎就可以去掉了。)
fastlane [tool] [command] [args][--key value]
tool和command指定使用的Tool和其Command;args通常是一個(gè)或多個(gè)字符串組成的數(shù)組;類似--key value
或-k value
格式的組合會(huì)被當(dāng)做option。args和option會(huì)被當(dāng)做參數(shù)傳給Command。
其中tool、command、args和option用[]包含起來(lái),表示它們可以被省略。如果省略了command和tool,則會(huì)使用默認(rèn)的tool和默認(rèn)tool的默認(rèn)command。
下圖中展示的是解析Command的簡(jiǎn)易流程
下列以兩個(gè)例子來(lái)說(shuō)明
獲取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是一個(gè)Tool的名字,比如:fastlane、cert等,則加載這個(gè)Tool,require 'tool_name/commands_generator'
;如果ARGV.first等于 "fastlane-credentials",則加載require 'credentials_manager'
;如果都不是,則加載fastlane
這個(gè)默認(rèn)的Tool,require "fastlane/commands_generator"
。
如果匹配上了Tool之后,刪除ARGV.first。
例一:使用默認(rèn)Tool:fastlane
,ARGV = [ "lane_name"]
例二:使用Tool:cert
,ARGV = ["--username", "your_username", "--development", "false"]
解析Command
將ARGV復(fù)制給一個(gè)新數(shù)組,在新數(shù)組中去掉所有以-
開(kāi)頭的字符串對(duì)象,然后使用數(shù)組的第一個(gè)對(duì)象去匹配此Tool下的command列表,如果能匹配上,則使用匹配到的Command;如果不能,則使用默認(rèn)Command。
如果匹配上,則將匹配上的字符串對(duì)象從ARGV中刪除。
例一:使用fastlane
這個(gè)Tool的默認(rèn)Command:trigger
,ARGV = [ "lane_name"]
例二:使用cert
這個(gè)Tool的默認(rèn)Command:create
,ARGV = ["--username", "your_username", "--development", "false"]
這里有個(gè)問(wèn)題需要注意一下,當(dāng)在終端輸入fastlane match --type enterprise
時(shí),這條命令的初衷是想使用match
這個(gè)Tool的默認(rèn)Command:run
,但按照本步驟的方法,最終使用的是enterprise
這個(gè)Command。所以在這里最好顯示指定要使用的Command,fastlane match run --type enterprise
。解析command對(duì)應(yīng)的option
遍歷ARGV,如果字符串是以--
或-
開(kāi)頭,則將此字符串對(duì)象和其后的字符串對(duì)象作為一對(duì)key-value值,并從ARGV中刪除這兩個(gè)對(duì)象。遍歷完畢之后,將ARGV中剩余的的參數(shù)賦值給args。
例一:option等于nil,args等于lane_name
例二:option等于{"username":"your_username", "development": false}
,args等于nil
- 執(zhí)行command
每個(gè)command都會(huì)設(shè)置一個(gè)對(duì)應(yīng)的block,匹配到這個(gè)command并解析完option之后,則執(zhí)行其對(duì)應(yīng)的block,并將[步驟4]中獲取的option和args傳給這個(gè)block。
從這個(gè)地方開(kāi)始,業(yè)務(wù)代碼才會(huì)真正開(kāi)始執(zhí)行。
上述解析過(guò)程描述的非常粗糙,如果想了解詳細(xì)的解析過(guò)程,可以參考commander,fastlane內(nèi)部通過(guò)這個(gè)庫(kù)來(lái)解析這些參數(shù)的。
把這個(gè)過(guò)程再豐富一下,就變成了下圖
(由于篇幅原因,圖中只畫(huà)出了
cert
、sigh
和fastlane
這三個(gè)Tool)
3.2. 執(zhí)行Command
到了這一步,就開(kāi)始深入到各個(gè)Tool的核心內(nèi)容了,在fastlane這個(gè)庫(kù)中,Tool共有16個(gè),在這里并不會(huì)對(duì)所有的Tool展開(kāi)討論,這里只討論默認(rèn)Command:trigger
。
4. trigger
trigger
是fastlane這個(gè)Tool的默認(rèn)命令,其作用是運(yùn)行一個(gè)指定的lane,而fastlane
這個(gè)Tool又是fastlane庫(kù)的默認(rèn)Tool,所以一般在運(yùn)行l(wèi)ane的時(shí)候,可以省略掉Tool和Command,只需要執(zhí)行命令fastlane [platform_name] lane_name
,如果設(shè)置了default_platform,platform_name也可以省略。
trigger
的目的是去運(yùn)行一個(gè)指定的lane,而運(yùn)行l(wèi)ane的目的是去執(zhí)行其中的action,根據(jù)這一需求,作圖如下
下面以例子的方式來(lái)了解這一過(guò)程,本文準(zhǔn)備了兩個(gè)自定義action,分別是example_action
和example_action_second
,fastlane會(huì)將它們加載作為外部action。
1. 前提條件
相關(guān)文件的目錄結(jié)構(gòu)
-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
2. 執(zhí)行trigger
在終端執(zhí)行fastlane test key1:value1 key2:value2 --env local1,local2
,按照上文所說(shuō)的,第一步解析command后,fastlane庫(kù)找到需要執(zhí)行的目標(biāo)command:trigger
,然后執(zhí)行此command對(duì)應(yīng)的block。
fastlane庫(kù)中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
,其中第一個(gè)option的作用是指定文件名,這些文件會(huì)被dotenv加載,用來(lái)配置環(huán)境變量。在當(dāng)前這個(gè)例子中,設(shè)置了--env local1,local2
,如果.env.local1
和.env.local2
這兩個(gè)文件存在于Fastfile所在的文件夾或其上級(jí)文件夾,則dotenv
會(huì)去加載它們來(lái)設(shè)置環(huán)境變量。(不管--env
有沒(méi)有設(shè)置,dotenv都默認(rèn)加載.env
和.env.default
)
執(zhí)行trigger
就是執(zhí)行下列代碼
c.action do |args, options|
if ensure_fastfile
Fastlane::CommandLineHandler.handle(args, options)
end
end
當(dāng)fastlane庫(kù)執(zhí)行這個(gè)block時(shí),傳入了兩個(gè)參數(shù),args
和options
,通過(guò)解析命令字符串可知,其中args
的值為["test", "key1:value1", "key2:value2"]
,options
的值是一個(gè)Options
類型的對(duì)象,且options.env 的值為 "local1,local2"
。
3. 解析lane
解析lane的目的就是獲取Fastfile中定義的Lane
類型的對(duì)象
在這個(gè)階段,fastlane庫(kù)會(huì)加載Fastfile,并將其中定義的lane轉(zhuǎn)換成Fastlane::Lane
類型的對(duì)象,并將這些對(duì)象保存在一個(gè)Hash類型的對(duì)象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
類型的對(duì)象中保存了一個(gè)lane的所有信息,:platform
指定lane使用的平臺(tái),:name
指定lane的名字,:block
保存了lane對(duì)應(yīng)的執(zhí)行代碼。
在本節(jié)例子中,lanes
保存了所有Fastlane::Lane
類型的對(duì)象,它的具體結(jié)構(gòu)如下:
{
ios: {
test: Lane.new
},
nil: {
test_without_platform: lane.new
}
}
fastlane庫(kù)使用lanes
這個(gè)Hash對(duì)象結(jié)合之前得到的args
來(lái)獲取對(duì)應(yīng)Lane
類型對(duì)象
其偽代碼如下:
#使用platform_lane_info保存platform名稱和lane名稱
platform_lane_info = []
#過(guò)濾掉帶有冒號(hào)":"的字符串對(duì)象
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 是一個(gè)平臺(tái)名字 || platform_lane_info是空數(shù)組
platform_name = platform_lane_info.first
lane_name = 在終端打印一個(gè)lane列表供用戶選擇
else
lane_name = platform_lane_info.first
if platform==nil && lanes[nil][lane_name]==nil
platform = default_platform
end
end
end
#返回lane對(duì)象
return lanes[platform][lane_name]
args
的值為["test", "key1:value1", "key2:value2"]
,把args
和lanes
帶入到上述偽代碼中,可以得到相應(yīng)的Lane
類型對(duì)象。
4. 解析lane的options
回顧一下,之前在Fastfile文件中定義test
這個(gè)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
,它是一個(gè)Hash類型的對(duì)象。
這個(gè)options
參數(shù)的值是如何得到的,其實(shí),也是通過(guò)解析args
獲取的。
其實(shí)現(xiàn)邏輯如下
options = {}
args.each do |current|
if current.include?(":")
key, value = current.split(":", 2)
if key.empty?
報(bào)錯(cuò)
end
value = true if value == 'true' || value == 'yes'
value = false if value == 'false' || value == 'no'
options[key.to_sym] = value
end
end
上述代碼是在fastlane庫(kù)源代碼的基礎(chǔ)上作了一些修改
將args
帶入到上述代碼中,可以得出lane:test
的options的值為{key1:value1, key2:value2}
fastlane test key1:value1 key2:value2 --env local1,local2
,在終端執(zhí)行后,一部分輸出如下
[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"}
5. 解析action
解析action的目的是找到action_name對(duì)應(yīng)的類,本例中,需要執(zhí)行兩個(gè)action,其action_name分別是example_action
和example_action_second
,其對(duì)應(yīng)類分別是ExampleActionAction
和ExampleActionSecondAction
其實(shí)現(xiàn)邏輯如下
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 = 嘗試把a(bǔ)ction_name當(dāng)做別名,重新加載
end
if action_name 是一個(gè)lane的名字
執(zhí)行這個(gè)lane
elsif class_ref && class_ref.respond_to?(:run)
解析action的options
執(zhí)行action
else
報(bào)錯(cuò)
end
6. 解析action的options
action的options指的是傳給action的參數(shù),比如example_action_second
這個(gè)action的options是{foo:"ruby", bar:"ios"}
,準(zhǔn)確的來(lái)說(shuō)應(yīng)該是[{foo:"ruby", bar:"ios"}]
,不過(guò)一般都只是用這個(gè)數(shù)組的第一個(gè)對(duì)象,所以接下來(lái)會(huì)去掉外面的一層數(shù)組。
本步驟的目的是將傳給action的options轉(zhuǎn)換成Configuration
類型的對(duì)象,并且在轉(zhuǎn)換過(guò)程中,驗(yàn)證options中key
和value
的合法性。
action和Configuration
類型的對(duì)象是一一對(duì)應(yīng)的,Configuration
類的作用主要是存儲(chǔ):availabel_options
和:values
,在執(zhí)行action的時(shí)候,也就是在執(zhí)行action響應(yīng)類的run
方法時(shí),把Configuration
類型的對(duì)象當(dāng)做參數(shù)傳入,然后action響應(yīng)類使用它來(lái)獲取key對(duì)應(yīng)的value。
Configuration
中定義的實(shí)例變量
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響應(yīng)類中定義的available_options
,比如example_action_second
這個(gè)action,它的響應(yīng)類是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
賦值之后還需要驗(yàn)證它的key、value
是否合法,如果不合法,程序中止。比如example_action_second
這個(gè)action的options是{foo:"ruby", bar:"ios"}
。
:all_key
表示:available_options
中的key
的數(shù)組,具體代碼:@available_options.collect(&:key)
。
:config_file_name
和:config_file_options
:在action的響應(yīng)類中,可以使用Configuration.load_configuration_file(config_file_name)
來(lái)加載這個(gè)action專有的配置文件,然后把文件中的數(shù)據(jù)以key:value
的方式存儲(chǔ)在:cofnig_file_options
變量中。
其實(shí)現(xiàn)代碼如下
values = 傳給action的options
action_responder = action響應(yīng)類
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響應(yīng)類中定義了available_options類方法,且其返回對(duì)象的第一個(gè)元素的類型不是FastlaneCore::ConfigItem,則不對(duì)values做任何處理,直接返回。
return values
end
創(chuàng)建FastlaneCore::Configuration
時(shí),內(nèi)部的驗(yàn)證邏輯
values = 傳給action的options
action_responder = action響應(yīng)類
available_options = action_responder.available_options
#available_options必須是一個(gè)Array,且其內(nèi)部的元素都必須是FastlaneCore::ConfigItem的類型
verify_input_types
#values中的每一個(gè)key都必須在available_options中定義過(guò),如果在創(chuàng)建FastlaneCore::ConfigItem類型的對(duì)象時(shí),設(shè)置了type和verify_block,則values中對(duì)應(yīng)的value都必須滿足。
verify_value_exists
#不能再available_options中重復(fù)定義同一個(gè)key
verify_no_duplicates
#在定義FastlaneCore::ConfigItem類型的對(duì)象時(shí),可以設(shè)置與自己沖突的key,在values中,不能同時(shí)存在沖突的兩個(gè)key。
verify_conflicts
#在定義FastlaneCore::ConfigItem類型的對(duì)象時(shí),同時(shí)設(shè)置了default_value和verify_block,且values中沒(méi)有設(shè)置這個(gè)key,則需要調(diào)用verify_block驗(yàn)證default_value的合法性。
verify_default_value_matches_verify_block
7. 執(zhí)行action
執(zhí)行action就是執(zhí)行action響應(yīng)類的類方法run
,同時(shí)將[步驟6]的解析結(jié)果傳給run
作為參數(shù)。類方法run
中包含了這個(gè)action的所有業(yè)務(wù)代碼,fastlane庫(kù)中所有的內(nèi)置action都遵循這一設(shè)定,同樣,在定義外部action時(shí),也應(yīng)該這樣做。
例子中actionexample_action_second
的響應(yīng)類ExampleActionSecondAction
中的run
的定義
def self.run(options)
puts "this is example action second action, options:"
puts "foo:#{options[:foo]}"
puts "bar:#{options[:bar]}"
end
其中參數(shù)options是一個(gè)FastlaneCore::Configuration
的對(duì)象,可以通過(guò)options[key]
或options.fetch(key)
的方式來(lái)獲取key對(duì)應(yīng)的value。
4. trigger總結(jié)
之前一節(jié),以圖1的步驟詳細(xì)講解了
trigger
命令的執(zhí)行過(guò)程,圖中的幾個(gè)步驟完全是從使用者的角度來(lái)劃分的,單看這幾個(gè)步驟并不能對(duì)fastlane庫(kù)有一個(gè)直觀的了解,下列兩個(gè)圖在圖一的基礎(chǔ)上增加了一些細(xì)節(jié)。
圖2中描述了trigger
命令的部分執(zhí)行過(guò)程,大致可以和圖1中的前三個(gè)步驟相對(duì)應(yīng)。相比之前的執(zhí)行步驟,圖2中增加了一些細(xì)節(jié)步驟,并且將這些步驟以泳道的方式進(jìn)行劃分。除了Commander之外,其他步驟的執(zhí)行者比如CLIToolsDistributor
、CommandsGenerator
等都是fastlane庫(kù)中定義的類,而Commander則是fastlane庫(kù)引用的外部庫(kù)。
圖3承接圖2的步驟,主要描述了Fastfile中定義的lane的執(zhí)行過(guò)程,大致可以和圖1中的后三個(gè)步驟相對(duì)應(yīng),圖3中步驟的執(zhí)行者基本上都是Runner
這個(gè)類。