fastlane match 源码浅析与最佳实践

match 是 fastlane 的证书管理工具,提供了一种非常新颖的证书管理方式(也有可能是我太孤陋寡闻了)。相信 iOS 开发多多少少也被证书折磨过,match 将 Apple Developer 与 Git 结合起来,从管理者到使用者,整个流程非常通顺,除之前吐槽的 match 封装问题,实现的思路还是非常值得学习的。

环境信息

fastlane: 2.72.0


首先来看一下针对哪些问题:

  1. 证书拉取耗时过长
  2. 重签耗时过长
  3. 新增证书的解锁问题
  4. 团队中的账号管理问题

match 源码

match 是通过 git 来对证书进行管理的,其实不看源码,跟着文档配置一次 match,就能知道 match 的流程:

这里的管理员是指有 push 权限的 user,当然,要给每个 user 都开权限,也是可以的。

run

在调用 fastlane match 之后,经过一些转发,就会进入到 match 模块的入口:run 函数。该函数位于 fastlane/match/lib/match/runner.rb,整个调用流程和上图大同小异。

git clone

每一次 fastlane match 都会 clone 整个 repo,然后再对 repo 进行解密。源码是通过 GitHelper.clone 实现的:

1
2
3
4
5
6
7
params[:workspace] = GitHelper.clone(params[:git_url],
params[:shallow_clone],
skip_docs: params[:skip_docs],
branch: params[:git_branch],
git_full_name: params[:git_full_name],
git_user_email: params[:git_user_email],
clone_branch_directly: params[:clone_branch_directly])

GitHelper 类位于 run.rb 同级目录下的 git_helper.rb,大部分的代码都是在组装 git 命令的参数,我们遇到的证书拉取耗时过长问题可以在这里得到解决。

  • shallow_clone: 对应 git--depth 1 --no-single-branch,表示 clone 深度为 1。
  • clone_branch_directly:对应 git-b x_branch --single-branch,表示仅拉取指定的分支。

两个参数默认值均为 false,但其实针对用户来说,可以直接指定 true,缩短拉取时间。

decrypt repo

在执行完 git clone 之后,就会调用 Encrypt 类的方法,对整个 repo 进行解密:

1
Encrypt.new.decrypt_repo(path: @dir, git_url: git_url, manual_password: manual_password)

这里有一个很诡异的 manual_password 变量,方法定义中,这个变量由外部传入,但是可以看到上面的 GitHelper.clone 调用,发现并没有传入过这个参数,所以在这里,这个值为 nil

Encrypt 类位于 run.rb 同级目录下的 encrypt.rb,其中包含 encrypt_repodecrypt_repo 两个方法,这两个方法会对 repo 进行遍历,然后调用公共方法 crypt,而这个方法中,其实是在对 openssl 的参数进行组装。

然后我们来看看,既然外部没有传入密码,那么密码从何而来。在当前类中,有一个 password 方法,可以看到这里是直接读取的环境变量 MATCH_PASSWORD。所以,如果不想在 match 过程中输入密码,可以在 match 之前,先设置 MATCH_PASSWORD。当然,如果输入过,fastlane 会缓存到 keychain,之后不再输入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
def password(git_url)
password = ENV["MATCH_PASSWORD"]

# 如果未指定,则从 keychain 中进行读取
unless password
item = Security::InternetPassword.find(server: server_name(git_url))
password = item.password if item
end

# 如果 keychain 中没有,则提示输入,并存储
unless password
password = ChangePassword.ask_password(confirm: true)
store_password(git_url, password)
end

return password
end

spaceship

git clone 之后,如果不是 readonly,那么会生成一个 SpaceshipEnsure 的对象。这个类很有意思,从名字可以看出它算得上是 fastlane 的一个通用工具类。实际上,这个工具对 Apple Developer Portal 和 iTunes Connect 的接口进行了封装,并且用 headless web browser 替代了传统 HTTP 请求,使请求时间缩短了 90%。spaceship 是一个独立的模块,与 match 模块同级。如果有访问 Apple 接口的需求,可以直接进行调用。

这里暂时不深入 spaceship 的源码,大概知道它用来给 Apple 发送请求的就可以了。关于 spaceship 的介绍,我单独写了一篇文章 fastlane spaceship 源码浅析,可作参考。

certificate 的生成与安装

如果 repo 里面没有证书,并且不是 readonly,则生成新的证书。

生成

通过调用 Generator.generate_certificate(params, cert_type) 方法,而最终的调用,是 spaceship 模块的 create_certificate_signing_request 方法。向 Apple Developer Portal 发起请求、下载证书和 p12、存储并导入钥匙串。

安装

如果能在 repo 里面找到证书,则会直接走证书的安装逻辑。

安装的方法已经封装到了 security 命令中,security 是系统提供的钥匙串相关命令,这里不详细介绍,主要来看下如何判断证书已经安装,重签耗时过长 的原因能在这一步找到。这部分代码位于 fastlane_core/lib/fastlane_core/cert_checker.rb

  1. 首先,判断是否已经安装 AppleWWDRCA 证书,如果没安装,则进行下载并安装。
  2. 接着,调用 security find-identity -v -p codesigning 命令,列举出全部可以用于签名的证书。
  3. 通过正则匹配命令输出,获取证书的 UUID。
  4. 读取即将安装的证书的 UUID,并判断是否存在。

如果不存在,则走导入钥匙串的逻辑 security import

在第一步判断 AppleWWDRCA 证书逻辑中,估计会有坑,从源码来看(wwdr_keychain),判断是直接读取的第一个钥匙串,如果第一个钥匙串没有,就进行安装了。但是这个坑会有什么影响,还不清楚。除此之外,通过 security find-certificate 查找的方式,并不能排除证书已经过期的情况,目前无法验证,先立个 flag (就在这篇文章发布前一个小时,发现这个 issue 的状态变成的 bug,flag 不能随便立啊)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def self.wwdr_keychain
priority = [
"security list-keychains -d user",
"security default-keychain -d user"
]
priority.each do |command|
# 执行 command,获取 keychain list
unless keychains.empty?
# 直接选中第一个 keychain 进行分析
return keychains[0].strip.tr('"', '')
end
end
return ""
end

那么重签耗时过长的原因在哪呢?当我在有问题的机器上执行 security find-identity -v -p codesigning ,也就是列举全部证书的时候,发现…:

1
1872 valid identities found

而正常的机器上是:

1
16 valid identities found

但是从 fastlane 的逻辑上看,已存在的证书并不会进行安装,所以为什么会出现这种情况不得而知,解决方案之后再提,接着走 match 的流程。

profile 的生成与安装

profile 的生成和两个参数有关:force_for_new_devicesforce,除此之外,如果 match 的 profile 不存在,且允许重新生成,也是会走生成逻辑的。

  • force_for_new_devices:默认为 false,意思是如果当前的 profile 没有包含到新设备,则重新生成。
  • force:无论什么情况,均重新生成。

判断设备

  1. 读取对应 profile 的 UUID
  2. 到 Apple Developer Portal 上找到对应的 profile,并读取包含的设备
  3. 到 Apple Developer Portal 上读取全部设备
  4. 判断 Apple 上的 profile 是否与读取到的全部设备数量一致

不知道各位怎么看,反正我是觉得这里又有一个坑。源码有点舍近求远的感觉,git repo 里面的 profile 仅仅是提供了一个 UUID,然后拿着这个 UUID 到 Apple 上重新下载了一份,直接用本地的不是更方便吗?而且如果有操作绕过了 fastlane 的 push git repo,那么 repo 中的 profile 就无法得到更新了(不过一般也没人这么干,因为 push 之前还要进行加密)。但是依然不太理解为什么 fastlane 没有直接对比 repo 里面的 profile,讲道理,它应该是和 Apple 上保持一致的。

生成

生成的逻辑和证书生成类似,最终调用的是 Spaceship::Portal::ProvisioningProfile.new.create! 方法。

安装

安装即是将 profile 拷贝到 ~/Library/MobileDevice/Provisioning Profiles/ 路径下。

安装完成后,将 path、uuid 等信息写入环境变量,也就是我们看到的:

这里就是我无数次吐槽 fastlane 的原因。安装完之后竟然是写入环境变量,返回一个 model 给我也好啊,为什么要用环境变量。不过 fetch_provisioning_profile 会返回一个重要的 uuid 回来,可以根据这个 id 自己到目录下面去找 😑。

push

最后一步,如果有生成新的证书或者 profile,则 push 到远程。到此,整个 match 流程就结束了。

实践

接下来,逐一解决之前提出的问题。

证书拉取耗时过长

根据 fastlane action match 提供的参数,可以通过指定 clone_branch_directlyshallow_clone 缩短 git clone 的时间。

重签耗时过长

重签过长基本可以确定为证书过多的问题,但是造成证书重复安装的原因,还未找到。准备上新的策略跑一段时间再出结论:

  1. 每天凌晨删除证书所属的整个 keychain
  2. 执行证书解锁

这样,即使第二天机器打 50 次包,证书也不会太多。

对了,非常惊喜的发现这个问题同样出现在了 fastlane/issue 上,但是因为太久没人回复,被人工智障给关掉了。

除此之外,同事提供了一种可能解决方案,是删除 private key,具体可以看这篇文章

再除此之外,在看 issue 的时候,发现另一种方案:每次操作钥匙串之前,都删除旧的,生成新的,具体可以查看这个 issue。但是考虑到新增钥匙串的各种开销,还是有点得不偿失。

新增证书的解锁问题

这个问题,显式感谢一下 @ppt,困扰我非常久了。不知道你是否也被证书解锁需要输入密码而困扰…。口算一下,8 个账号,16 个证书,15 台机器(包含 CI 和部分自动化机器),更新证书需要手动输入密码 240 次,手动微笑。

然后,调用以下命令可以直接解锁整个钥匙串:

1
security set-key-partition-list -S apple-tool:,apple: -s -k $PASS $KEYCHAIN_PATH

关于 security 这个蜜汁命令,之后深入了解了再说,而且听说很多 undocumented 的参数。

2018.06.08 更新。这个命令的详细参数介绍,已经包含在了新文章中 如果你也被 security 坑过 - 授权弹窗

团队中的账号管理问题

为了解决一个 Apple 账号只能添加 100 台 iPhone 的问题,公司一共有 8 个账号,不同的设备加到了不同的账号下面,新人入职在各个账号上就要理解半天,而且 fastlane 的使用成本还是比较高的,比如 id_rsa 不能有密码,match 过程中不能有任何输入,更不要说千奇百怪的 terminal 环境了。

目前的解决方案是,针对用户,写一个客户端,直接由服务器下发项目对应的证书和描述文件,这样,本地只需要调用自带的 security 命令就可以了,将用户的环境成本降到最低。而 fastlane 的 match 流程,放到服务器和 CI 等自动部署的机器上面去,这些机器都是统一的环境,所以操作起来很方便。

关于整个苹果账号和证书管理策略,之后也会单开文章来详谈。

证书的查看

最后给大家安利一个 Quick Look 插件 provisionql,很多人应该都知道了:

1
brew cask install provisionql

查看 .ipa.cer.mobileprovision 利器,就不截图了,截图也全是马赛克。

最后

fastlane public 的有几个比较重要的模块,可以在官方文档上看到。之前为了解决 resign entitlements 的问题,阅读了 resign 模块的源码,然后是 match,下一步是已经发现问题,但是还没来得及分析的 gym。目前 fastlane 已经更新到 2.87.0 了,欢迎了解新版本特性的朋友讨论。