前言
在终端中执行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。
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
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"
。
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. 解析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"}
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
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
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。
4. trigger总结
之前一节,以图1的步骤详细讲解了
trigger
命令的执行过程,图中的几个步骤完全是从使用者的角度来划分的,单看这几个步骤并不能对fastlane库有一个直观的了解,下列两个图在图一的基础上增加了一些细节。
图2中描述了trigger
命令的部分执行过程,大致可以和图1中的前三个步骤相对应。相比之前的执行步骤,图2中增加了一些细节步骤,并且将这些步骤以泳道的方式进行划分。除了Commander之外,其他步骤的执行者比如CLIToolsDistributor
、CommandsGenerator
等都是fastlane库中定义的类,而Commander则是fastlane库引用的外部库。
图3承接图2的步骤,主要描述了Fastfile中定义的lane的执行过程,大致可以和图1中的后三个步骤相对应,图3中步骤的执行者基本上都是Runner
这个类。