作为架构师的我们常常要面临的一个难题就是技术选型。现在无论是商业项目也好,开源项目也好,可供选择的方案实在是太多,其中优秀的方案也是层出不穷,这就要求我们在做技术选型的时候,需要从多个维度进行考量,其中良好的扩展性是我们重点考量的对象。
任何一个优秀的框架或平台都应该具有良好的扩展性,以满足不断变化的业务场景和个性化要求,而这种扩展性的其中一个方面则体现在:是否能够提供一种机制,这种机制既能满足二次开发的便捷性,又最小化甚至不会对原有的系统产生任何的侵入或破坏。
站在这个角度上,今天我们就来介绍一下 Fastlane 的两种扩展机制,Action 和 Plugin。
Fastlane 的 Action 机制
Fastlane 本身包含两大模块,一个是其内核部分,另外一个就是 Action 了。Action 是 Fastlane 自动化流程中的最小执行单元,直观上来讲就是 Fastfile 脚本中的一个个命令,比如:git_pull,deliver,pod_install 等等,而这些命令背后都对应一个用 Ruby 编写的脚本。
我猜想,Fastlane 的作者们在项目的早期甚至规划的阶段,应该就考虑到了这一点:在移动开发中,自动化的业务场景太多,每个团队都有自己的特殊要求,单靠一两个人的力量是无法满足的,所以如何将涉及到实际业务的功能开发,用优雅的方式交给开源社区中庞大工程师们来维护,成为 Fastlane 架构中需要重点考虑的内容。
于是经过不断的探索,讨论和实践,Action 这种扩展机制应运而生。我们可以理解为 Fastlane 建立了一套完整的规则,这个规则是如此的简单易行,无论是官方的工程师还是开源社区的工程师们,大家都在这个规则里进行游戏,这样不但降低了扩展的门槛,吸引工程师们来完善 Fastlane 本身;同时又增强了约束,减少不必要的沟通和代码检查成本。所以我们可以看到无论是官方贡献的,还是 Github 社区贡献的 Action 们,无一例外都隶属于 Action 扩展的一部分。
到目前为止 Fastlane 包含大约 170 多个 Action,大约分为如下几类:
- 和移动端持续交付相关的 15 个核心的工具链:如:deliver(上传 ipa,截屏和 meta 信息到 ITC),supply(上传 apk,截屏和 meta 信息到 Google Play),sigh(iOS Provisioning 文件管理)等等,详情如下: https://github.com/fastlane/fastlane#fastlane-toolchain
- 和 iOS 相关的,如:ipa,xcode_install 等等
- 和 Android 相关的,如:gradle,adb 等等
- 和版本控制相关的,如 git_pull,hg_push 等等
- 和 iOS 依赖库管理相关的,如:cocoapods,carthage 等等
- 第三方平台对接相关的,如:hipchat,jira,twitter,slack 等等
这些 Action 的详情和使用方法可以查看这个链接: https://docs.fastlane.tools/actions/Actions/ 。
应该说几乎涵盖了所有常见的场景,但是如果仍然无法完全满足你的要求的话,就得自己来动手自定义一个了。
场景分析
那么如何来自定义一个 Action 呢?按照习惯,为了便于大家理解,我们还是先从一个业务场景入手。在上一篇文章中,我曾经举过一个例子:私有 Pod 的发布,其步骤如下:
- 增加 Podspec 中的版本号
- 执行 pod lib lint 命令进行库验证
- Git Commit 代码
- Git Push 代码到远端
- 打一个 Git Tag
- 将 Tag Push 到远端
- 执行 pod repo push 命令发布库到私有仓库
然后对应以上的几个步骤,我们都可以找到现成的 Action 来实现,所以我们可以在 Fastfile 中增加如下 Lane:
desc "Release new private pod version" lane :do_release_lib do |options| target_version = options[:version] project = options[:project] path = "#{project}.podspec" git_pull ensure_git_branch # 确认 master 分支 pod_install pod_lib_lint(verbose: true, allow_warnings: true, sources: SOURCES, use_bundle_exec: true, fail_fast: true) version_bump_podspec(path: path, version_number: target_version) # 更新 podspec git_commit_all(message: "Bump version to #{target_version}") # 提交版本号修改 add_git_tag(tag: target_version) # 设置 tag push_to_git_remote # 推送到 git 仓库 pod_push(path: path, repo: "GMSpecs", allow_warnings: true, sources: SOURCES) # 提交到私有仓库 end
自定义 Action
讲到这里,大家可能会问,这不都写完了吗,哪里还需要自定义 Action 啊?别急,其实大约 3 个月前,笔者编写这个 Fastfile 的时候,Fastlane 正好缺少一个 Action 能够支持 Cocoapods 的这个命令,即:
pod lib lint
这个命令是用来验证私有的 Pod 库是否正确,所以当时无奈之下,只能自己动手写一个了。写完后发现,这个工作也并没有想象中的那么困难,Fastlane 已经为我们提供了现成的模板,即使你对 Ruby 的语法不熟悉,也没有关系,Fastlane 是开源的嘛,可以直接下载源码看看别人的 Action 是怎么写的就知道了,我们可以在这个目录下找到所有的 Action 文件:
fastlane/fastlane/lib/fastlane/actions/
自定义 Action 的流程大约如下,首先,我们在终端中执行命令:
fastlane new_action
然后根据提示,在命令行中敲入 action 的名字 pod_lib_lint,然后 Fastlane 会在当前目录的 actions 文件夹中帮我们创建了一个 pod_lib_lint.rb 的 Ruby 文件,内容大致如下(省略了非重点部分):
module Fastlane module Actions class PodLibLintAction < Action def self.run(params) UI.message "Parameter API Token: #{params[:api_token]}" end ...... def self.available_options [ FastlaneCore::ConfigItem.new(key: :api_token, env_name: "FL_POD_LIB_LINT_API_TOKEN", # The name of the environment variable description: "API Token for PodLibLintAction", # a short description of this parameter verify_block: proc do |value| UI.user_error!("No API token for PodLibLintAction given, pass using `api_token: 'token'`") unless (value and not value.empty?) end), ...... ] end end end
大家可以看到,自定义的 Action 都是隶属于 Fastlane/Actions 这个 module,并且继承自 Action 这个父类。虽然模板中的内容还挺多,不过不用担心,大部分内容都是一些简单的文本描述,对于我们来说只需要重点关注这两个方法就行:
- self.run 方法:这里放置的是实际的业务处理代码。
- self.available_options 方法:这里声明需要对外暴露出的参数,没有声明的参数在执行过程中无法使用。
在开始编写实际的业务代码之前,我们需要了解清楚这个 Action 具体包含的业务逻辑,所以我们首先来分析一下 Cocoapods 的 pod lib lint 命令,在终端执行
pod lib lint --help
终端打印出(只保留重点部分)
Usage: $ pod lib lint Validates the Pod using the files in the working directory. Options: --quick Lint skips checks that would require to download and build the spec --allow-warnings Lint validates even if warnings ......
可以看出这个命令包含了不少选项(Options),而我们需要做的是将这些选项映射到 action 中的参数,所以接下来我们根据选项的类型,在 self.available_options 中进行声明,代码如下(只保留重点部分):
def self.available_options [ FastlaneCore::ConfigItem.new(key: :use_bundle_exec, description: "Use bundle exec when there is a Gemfile presented", is_string: false, default_value: true), FastlaneCore::ConfigItem.new(key: :verbose, description: "Allow ouput detail in console", optional: true, is_string: false) ...... ] end
声明完毕后,在 self.run 方法中编写最终的业务逻辑,同时将上面的 options 通过 params 暴露出去,这样在运行 pod_lib_lint 这个 action 的时候,我们就可以传入对应的参数,从而 Fastlane 可以执行携带各种选项的完整命令,代码如下(只保留重点部分):
def self.run(params) command = [] command << "bundle exec" if File.exist?("Gemfile") && params[:use_bundle_exec] command << "pod lib lint" command << "--verbose" if params[:verbose] command << "--allow-warnings" if params[:allow_warnings] ...... result = Actions.sh(command.join(' ')) UI.success("Pod lib lint Successfully") return result end
从这段代码可以看出,关键点在于 Actions.sh() 这句话,所以我们要保证这里的 sh 方法执行的 command 和 pod lib lint 命令在终端中输出的一致,例如:
pod lib lint --quick --verbose --allow-warnings --use-libraries
最后,我们将 pod_lib_lint.rb 拷贝到 iOS 项目下的 fastlane/actions 文件夹中,然后在该项目目录下,执行如下命令:
fastlane action pod_lib_lint
如果没有错误的话,终端中会输出如下内容:
(点击放大图像)
其实,最初写这个Action,我只是打算在团队内部使用,并没有贡献到Github 上的计划,所以只实现了一部分参数。我们自己使用了一段时间,感觉比较稳定的时候,才将所有参数都补齐,然后贡献到了Fastlane 的主仓库中,地址如下:
https://github.com/fastlane/fastlane/blob/master/fastlane/lib/fastlane/actions/pod_lib_lint.rb
这里说一个题外话:
对于开源项目的代码提交,整个过程会比较严格,除了功能无 Bug,单元测试需要完全覆盖之外,对于语法格式等软指标也有一定的要求。当提交 pull request 的时候,Github 会先使用自动化工具(HoundCI 和 CircleCI)进行全面的检查,通过后才会交给 Code Review 团队人工 Check,所以平常代码习惯不好的同学需要多加注意。
Fastlane 的 Plugin 机制
我们在使用 Fastlane 的时候常常会遇到这样的场景:
- 我的自定义 Action 需要在多个内部项目中使用
- 我觉得这个自定义 Action 很不错,想共享给其他的团队使用
此时,拷贝粘贴虽然可以解决问题,但并不是一个聪明的方案。将 Action 发布到 Fastlane 的官方仓库倒是一个不错的选择,但是官方仓库本身对 Action 的要求比较高,并不会接收非通用性的 Action,即使接收了,整个发布周期也会比较长,而且以后无论是升级还是 Bug 修复,都依赖 Fastlane 本身的发版,大大降低了灵活性。
所以从 1.93 开始,Fastlane 提供了一种 Plugin 的机制来解决这种问题。大家可以理解为:Plugin 就是在 Action 的基础上做了一层包装,这个包装巧妙的利用了 RubyGems 这个相当成熟的 Ruby 库管理系统,所以其可以独立于 Fastlane 主仓库进行查找,安装,发布和删除。
我们甚至可以简单的认为:Plugin 就是 RubyGem 封装的 Action,我们可以像管理 RubyGems 一样来管理 Fastlane 的 Plugin。
安装 Plugin
到目前为止,大约有 30 个 Plugin 发布到了 RubyGems 下,我们可以通过如下命令来查找:
fastlane search_plugins [query]
详情可以看这里
AvailablePlugins
假设我们的项目中需要使用一个名叫 version_from_last_tag,用于获取 git 的最近一个 tag,那么我们在终端的项目目录下执行:
fastlane add_plugin version_from_last_tag
添加完成后,项目中会多出一个 Gemfile,Gemfile.lock,fastlane/Pluginfile 三个文件,其中这个 Pluginfile 实际上就是一个 Gemfile,里面包含对于 Plugin 的引用,格式如下:
# Autogenerated by fastlane # # Ensure this file is checked in to source control! gem 'fastlane-plugin-version_from_last_tag'
而 Pluginfile 本身又被 Gemfile 引用,所以又印证上上文中的那句话:对 Plugin 的管理其实就是对 RubyGem 的管理。
此后的 Plugin 是实际用法和使用 Action 是一致的,所以就不在此赘述了。
发布 Plugin
如果你想发布一个 Plugin,可以选择直接作为一个 Gem 发布到 RubyGems 上,这样大家就可以通过 search_plugins 命令搜索到了;也可以选择只提交代码到 Github 上,然后提供一个 github 的地址给其它项目或团队使用,这时需要在 Pluginfile 中这样声明:
gem "fastlane-plugin-version_from_last_tag", git: "https://github.com/jeeftor/fastlane-plugin-version_from_last_tag"
发布之前,为了本地调试方便,可以将 gem 指向本地,在 Pluginfile 这样声明:
gem "fastlane-plugin-version_from_last_tag", path: "../fastlane-plugin-version_from_last_tag"
有了 Plugin 之后,Fastlane 的更新频率大大降低,主仓库上 Action 的数量将维持在目前的水平上,取而代之的是 Plugin 的不断增多。企业和团队可以选择适合自己的 Plugin,也可以随时随地发布 Plugin 给别的团队使用。
结语
有了 Action,Fastlane 的可扩展性大大的增强,我们可以非常方便的编写适合自己业务场景的工具;Plugin 的出现,又在扩展性的基础之上大大增强了其灵活性,两者结合在一起使用可以将 Fastlane 的优势充分的的发挥出来。
通常情况下,如果一个工具只打算在一个项目中使用,那么建议直接用 Action,毕竟一个 Ruby 脚本就能解决,成本比较低;如果打算在多个项目中甚至跨团队使用,那么则建议使用 Plugin。
关于 Action 和 Plugin 更为详细的描述可以查看官方提供的文档:
Action: https://docs.fastlane.tools/actions/Actions/
Plugin: https://docs.fastlane.tools/plugins/CreatePlugin/
目前的两篇文章中的内容和场景都和 iOS 相关,接下来的一篇文章中,我将详细讲解一下如何将 Fastlane 应用于 Andriod 平台。
感谢徐川对本文的审校。
给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ , @丁晓昀),微信(微信号: InfoQChina )关注我们。
评论