Fastlane(二):結構

wangzzzzz發表於2018-06-12

前言

在終端中執行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的簡易流程

Fastlane(二):結構

下列以兩個例子來說明

  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內部通過這個庫來解析這些引數的。

把這個過程再豐富一下,就變成了下圖

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,根據這一需求,作圖如下

Fastlane(二):結構

下面以例子的方式來了解這一過程,本文準備了兩個自定義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這個類。

相關文章