Fastlane实战(二):Action和Plugin机制

478 阅读10分钟

作为架构师的我们常常要面临的一个难题就是技术选型。现在无论是商业项目也好,开源项目也好,可供选择的方案实在是太多,其中优秀的方案也是层出不穷,这就要求我们在做技术选型的时候,需要从多个维度进行考量,其中良好的扩展性是我们重点考量的对象。

任何一个优秀的框架或平台都应该具有良好的扩展性,以满足不断变化的业务场景和个性化要求,而这种扩展性的其中一个方面则体现在:是否能够提供一种机制,这种机制既能满足二次开发的便捷性,又最小化甚至不会对原有的系统产生任何的侵入或破坏。

站在这个角度上,今天我们就来介绍一下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,大约分为如下几类:

  1. 和移动端持续交付相关的15个核心的工具链:如:deliver(上传ipa,截屏和meta信息到ITC),supply(上传apk,截屏和meta信息到Google Play),sigh(iOS Provisioning文件管理)等等,详情如下:github.com/fastlane/fa…
  2. 和iOS相关的,如:ipa,xcode_install等等
  3. 和Android相关的,如:gradle,adb等等
  4. 和版本控制相关的,如git_pull,hg_push等等
  5. 和iOS依赖库管理相关的,如:cocoapods,carthage等等
  6. 第三方平台对接相关的,如:hipchat,jira,twitter,slack等等

这些Action的详情和使用方法可以查看这个链接:docs.fastlane.tools/actions/Act…

应该说几乎涵盖了所有常见的场景,但是如果仍然无法完全满足你的要求的话,就得自己来动手自定义一个了。

场景分析

那么如何来自定义一个Action呢?按照习惯,为了便于大家理解,我们还是先从一个业务场景入手。在上一篇文章中,我曾经举过一个例子:私有Pod的发布,其步骤如下:

  1. 增加Podspec中的版本号
  2. 执行pod lib lint命令进行库验证
  3. Git Commit代码
  4. Git Push代码到远端
  5. 打一个Git Tag
  6. 将Tag Push到远端
  7. 执行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这个父类。虽然模板中的内容还挺多,不过不用担心,大部分内容都是一些简单的文本描述,对于我们来说只需要重点关注这两个方法就行:

  1. self.run方法:这里放置的是实际的业务处理代码。
  2. 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

如果没有错误的话,终端中会输出如下内容: image

其实,最初写这个Action,我只是打算在团队内部使用,并没有贡献到Github上的计划,所以只实现了一部分参数。我们自己使用了一段时间,感觉比较稳定的时候,才将所有参数都补齐,然后贡献到了Fastlane的主仓库中,地址如下:

github.com/fastlane/fa…

这里说一个题外话
对于开源项目的代码提交,整个过程会比较严格,除了功能无Bug,单元测试需要完全覆盖之外,对于语法格式等软指标也有一定的要求。当提交pull request的时候,Github会先使用自动化工具(HoundCI和CircleCI)进行全面的检查,通过后才会交给Code Review团队人工Check,所以平常代码习惯不好的同学需要多加注意。

Fastlane的Plugin机制

我们在使用Fastlane的时候常常会遇到这样的场景:

  1. 我的自定义Action需要在多个内部项目中使用
  2. 我觉得这个自定义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:docs.fastlane.tools/actions/Act…
Plugin:docs.fastlane.tools/plugins/Cre…

最后,附上一个我们团队正在使用到自定义Actions:github.com/thierryxing…

目前的两篇文章中的内容和场景都和iOS相关,接下来的一篇文章中,我将详细讲解一下如何将Fastlane应用于Andriod平台。