阅读 771

Fastlane(二):结构

前言

在终端中执行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_namefastlane initfastlane action action_namefastlane 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中的initfastlane action action_name中的actonfastlane 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,matchgym都是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,分别是matchgym,而这两个action最终会去调用它们同名的Tool。 除了fastlane这个默认的Tool,其他所有的Tool都有其同名的action,通过在lane中添加action,可以调用其他所有的Tool。

除了这些与Tool同名的action,fastlane还内置了其他很多action,比如关于git和pod的。

3. fastlane执行流程

fastlane中所有命令的执行都可以简单的分为两步:

  1. 解析Command
  2. 执行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的简易流程

下列以两个例子来说明

  1. 获取ARGV 例一:终端输入fastlane lane_name,则ARGV = ["lane_name"]; 例二:终端输入fastlane cert --username "your_usernmae" --development false,则ARGV = ["cert", "--username", "your_username", "--development", "false"]

  2. 解析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:fastlaneARGV = [ "lane_name"] 例二:使用Tool:certARGV = ["--username", "your_username", "--development", "false"]

  3. 解析Command 将ARGV复制给一个新数组,在新数组中去掉所有以-开头的字符串对象,然后使用数组的第一个对象去匹配此Tool下的command列表,如果能匹配上,则使用匹配到的Command;如果不能,则使用默认Command。 如果匹配上,则将匹配上的字符串对象从ARGV中删除。 例一:使用fastlane这个Tool的默认Command:triggerARGV = [ "lane_name"] 例二:使用cert这个Tool的默认Command:createARGV = ["--username", "your_username", "--development", "false"] 这里有个问题需要注意一下,当在终端输入fastlane match --type enterprise时,这条命令的初衷是想使用match这个Tool的默认Command:run,但按照本步骤的方法,最终使用的是enterprise这个Command。所以在这里最好显示指定要使用的Command,fastlane match run --type enterprise

  4. 解析command对应的option 遍历ARGV,如果字符串是以---开头,则将此字符串对象和其后的字符串对象作为一对key-value值,并从ARGV中删除这两个对象。遍历完毕之后,将ARGV中剩余的的参数赋值给args。 例一:option等于nil,args等于lane_name 例二:option等于{"username":"your_username", "development": false},args等于nil

  5. 执行command 每个command都会设置一个对应的block,匹配到这个command并解析完option之后,则执行其对应的block,并将[步骤4]中获取的option和args传给这个block。 从这个地方开始,业务代码才会真正开始执行。

上述解析过程描述的非常粗糙,如果想了解详细的解析过程,可以参考**commander**,fastlane内部通过这个库来解析这些参数的。

把这个过程再丰富一下,就变成了下图

(由于篇幅原因,图中只画出了certsighfastlane这三个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_actionexample_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时,传入了两个参数,argsoptions,通过解析命令字符串可知,其中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"],把argslanes带入到上述伪代码中,可以得到相应的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
复制代码

本步骤的目的就是要获取传给testoptions,它是一个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_actionexample_action_second,其对应类分别是ExampleActionActionExampleActionSecondAction

其实现逻辑如下

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中keyvalue的合法性。 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,它的响应类是ExampleActionSecondActionExampleActionSecondAction中类方法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
之前一节,以图1的步骤详细讲解了trigger命令的执行过程,图中的几个步骤完全是从使用者的角度来划分的,单看这几个步骤并不能对fastlane库有一个直观的了解,下列两个图在图一的基础上增加了一些细节。

2
3

图2中描述了trigger命令的部分执行过程,大致可以和图1中的前三个步骤相对应。相比之前的执行步骤,图2中增加了一些细节步骤,并且将这些步骤以泳道的方式进行划分。除了Commander之外,其他步骤的执行者比如CLIToolsDistributorCommandsGenerator等都是fastlane库中定义的类,而Commander则是fastlane库引用的外部库。

图3承接图2的步骤,主要描述了Fastfile中定义的lane的执行过程,大致可以和图1中的后三个步骤相对应,图3中步骤的执行者基本上都是Runner这个类。

文章分类
iOS