fastlane DSL 源码浅析

其实应该叫【Fastlane 是如何执行起来的?】或者【FastFile 到底是什么?】会比较合适。但是读这部分源码的初衷,除了好奇以外,更多是想抽离 Fastlane 的 DSL 这部分逻辑,结合 Rake 的优势,写一个好使轻量的 DSL。

环境信息

fastlane 2.85.0


在使用 fastlane 的时候,通常会写一个 FastFile 作为配置文件,比如:

# FastFile
desc "这是一个自定义 lane"
lane :my_lane do |options|
puts "名字:#{options[:name]}"
puts "公司:#{options[:company]}"
end

入口

fastlane 命令位于源码工程目录的 bin/fastlane,在做了校验和配置之后,直接调用了

Fastlane::CLIToolsDistributor.take_off

整个命令的入口,都在 fastlane/lib/fastlane 目录中,当然,也包括上面的 cli_tools_distributor.rb

take_off 方法中,主要做了三件事:

  1. 校验依赖,版本等
  2. 根据 action 参数生成对应的 action cmd,然后执行 cmd
  3. 如果指定的 action 未找到,则准备查找自定义 lane

除此之外,cli_tools_distributor.rb 还有一个有趣的方法,叫 process_emojis

def process_emojis(tool_name)
return {
"🚀" => "fastlane",
"💪" => "gym"
}[tool_name] || tool_name
end

可以通过表情来调用 action,比如 fastlane 💪

Action

Action 是 Fastlane 定义的一些操作,比如 matchgympilot 等,这部分会优先查找。因为不是主要讨论的部分,而且代码也不难,直接看代码就懂:

# 通过指定的名称,直接引用,比如 require match
require tool_name
# 反射,获取到 action 名称对应的模块名,比如 match 对应 Match
commands_generator = Object.const_get(tool_name.fastlane_module)::CommandsGenerator
# 每个 action 都有 CommandsGenerator 模块,均实现了 start 方法
commands_generator.start

start 之后,内部就是通过 commander 来解析入参了。

Lane

如果调用 fastlane my_lane name:sai company:meitu 这样的命令,很明显 my_lane 不是 Fastlane 的 action,那么会进入到解析 FastFile,查找自定义 lane 的步骤。

在经过几个步骤之后,Lane 的查找交给了 Fastlane::FastFile 实例,步骤如下:

  1. fastlane/commands_generator.rb 中,默认 :trigger 调用 Fastlane::CommandLineHandler.handle(args, options) 传入除 fastlane 以外的全部输入。
  2. fastlane/commande_line_handler.rbhandle 方法中,分离输入中的 lane name 和参数部分,然后调用 LaneManagercruise_lane 方法,开始认认真真的处理 lane。

而在 cruise_lane 方法中,也就对 lane 进行查找。流程大致如下:

Parse

FastFile 会在当前执行目录下查找 fastlane/FastFile。初始化时,会调用 Fastlane::FastFileparse 方法,对文件进行解析。

其实就一句…

# data: FastFile 文件内容,来自 File.read(FastFile)
# parsing_binding: 当前实例作用域,保证定义的变量都在当前作用域内
# relative_path: FastFile 文件路径
eval(data, parsing_binding, relative_path)

DSL

终于到 DSL 的部分了。来看下 FastFile 中出现的 desclanebefore_allerror 都是什么。

fast_file.rbDSL 注释部分,能看到全部的 FastFile 支持的方法。也就是说 desclane 这些看似像关键字的配置项,其实都是在调方法,这也得益于 Ruby 的语法糖与元编程能力。只看 lane 的实现,就能知道整个 FastFile 的解析方式了:

def lane(lane_name, &block)
self.runner.add_lane(Lane.new(platform: self.current_platform,
block: block,
description: desc_collection,
name: lane_name,
is_private: false))
@desc_collection = nil # reset the collected description again for the next lane
end

对不起,只看 lane 方法好像不行,顺便看下 runner.rbadd_lane 吧,这下是真的能知道了:

def add_lane(lane, override = false)
lanes[lane.platform] ||= {}
lanes[lane.platform][lane.name] = lane
end

也就是说 lane 负责将 lane name 和 block 代码存储到 runner 的 lanes 字典中。结构为:

lanes = {
platform: {
lane_name: LaneObject,
lane_name: LaneObject,
lane_name: LaneObject,
}
}

如果一不小心看了 before_all 的源码,把 before_all 的结构算上,整体如下:

before_all_blocks = {
platform: block
}

before_each_blocks = {
platform: block
}

lanes = {
platform: {
lane_name: LaneObject,
lane_name: LaneObject,
lane_name: LaneObject,
}
}

到此就一目了然了,当要调用 lane 的时候,就找到 runner 的 lanes 字典中存储的 LaneObject.block,然后执行 block 就行。也就是 lane_manager.rb 解析完 FastFile 以后,执行的:

# ff 即 FastFile 对象
ff.runner.execute(lane, platform, parameters)

# 随即 runner 中调用的
# parameters 即调用时,除 fastlane、lane_name 剩下的参数
lane_obj.call(parameters)

Method Missing

Rake task vs. Fastlane lane 提到过 Fastlane 的复用比 Rake 更优雅。最主要的原因,就是因为 lane 可以当成一个方法来直接调用,比如在 FastFile 中:

lane :lane_a do |options|
puts options[:name]
end

lane :lane_b do |options|
lane_a(name: "sai", company: "meitu") # 直接调用
end

对于 lane_a 这样 FastFile 中根本没有定义的方法,当然是抛出 NoMethodFound 的异常才对。在仔细看 fast_file.rb 也就是执行 FastFile 的类中,定义了一个方法:

def method_missing(method_sym, *arguments, &_block)
self.runner.trigger_action_by_name(method_sym, nil, false, *arguments)
end

method_missing 捕获了全部未找到的方法,并调用了 trigger_action_by_name,明摆着未找到,就按照名称的方式,调用 lane 或者 action。这样,就解决了复用的问题。这比 Rake 的 Rake::Task["task_a"].invoke 不知优雅了多少倍。