fastlane match 源码浅析与最佳实践
match 是 fastlane 的证书管理工具,提供了一种非常新颖的证书管理方式(也有可能是我太孤陋寡闻了)。相信 iOS 开发多多少少也被证书折磨过,match 将 Apple Developer 与 Git 结合起来,从管理者到使用者,整个流程非常通顺,除之前吐槽的 match 封装问题,实现的思路还是非常值得学习的。
环境信息
fastlane: 2.72.0
首先来看一下针对哪些问题:
- 证书拉取耗时过长
- 重签耗时过长
- 新增证书的解锁问题
- 团队中的账号管理问题
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
实现的:
params[:workspace] = GitHelper.clone(params[:git_url], |
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 进行解密:
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_repo
与 decrypt_repo
两个方法,这两个方法会对 repo 进行遍历,然后调用公共方法 crypt
,而这个方法中,其实是在对 openssl
的参数进行组装。
然后我们来看看,既然外部没有传入密码,那么密码从何而来。在当前类中,有一个 password
方法,可以看到这里是直接读取的环境变量 MATCH_PASSWORD
。所以,如果不想在 match 过程中输入密码,可以在 match 之前,先设置 MATCH_PASSWORD
。当然,如果输入过,fastlane 会缓存到 keychain,之后不再输入:
def password(git_url) |
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。
- 首先,判断是否已经安装
AppleWWDRCA
证书,如果没安装,则进行下载并安装。 - 接着,调用
security find-identity -v -p codesigning
命令,列举出全部可以用于签名的证书。 - 通过正则匹配命令输出,获取证书的 UUID。
- 读取即将安装的证书的 UUID,并判断是否存在。
如果不存在,则走导入钥匙串的逻辑 security import
。
在第一步判断 AppleWWDRCA
证书逻辑中,估计会有坑,从源码来看(wwdr_keychain
),判断是直接读取的第一个钥匙串,如果第一个钥匙串没有,就进行安装了。但是这个坑会有什么影响,还不清楚。除此之外,通过 security find-certificate
查找的方式,并不能排除证书已经过期的情况,目前无法验证,先立个 flag (就在这篇文章发布前一个小时,发现这个 issue 的状态变成的 bug
,flag 不能随便立啊)。
def self.wwdr_keychain |
那么重签耗时过长的原因在哪呢?当我在有问题的机器上执行 security find-identity -v -p codesigning
,也就是列举全部证书的时候,发现…:
1872 valid identities found |
而正常的机器上是:
16 valid identities found |
但是从 fastlane 的逻辑上看,已存在的证书并不会进行安装,所以为什么会出现这种情况不得而知,解决方案之后再提,接着走 match 的流程。
profile 的生成与安装
profile 的生成和两个参数有关:force_for_new_devices
与 force
,除此之外,如果 match 的 profile 不存在,且允许重新生成,也是会走生成逻辑的。
force_for_new_devices
:默认为false
,意思是如果当前的 profile 没有包含到新设备,则重新生成。force
:无论什么情况,均重新生成。
判断设备
- 读取对应 profile 的 UUID
- 到 Apple Developer Portal 上找到对应的 profile,并读取包含的设备
- 到 Apple Developer Portal 上读取全部设备
- 判断 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_directly
和 shallow_clone
缩短 git clone
的时间。
重签耗时过长
重签过长基本可以确定为证书过多的问题,但是造成证书重复安装的原因,还未找到。准备上新的策略跑一段时间再出结论:
- 每天凌晨删除证书所属的整个 keychain
- 执行证书解锁
这样,即使第二天机器打 50 次包,证书也不会太多。
对了,非常惊喜的发现这个问题同样出现在了 fastlane/issue 上,但是因为太久没人回复,被人工智障给关掉了。
除此之外,同事提供了一种可能解决方案,是删除 private key,具体可以看这篇文章。
再除此之外,在看 issue 的时候,发现另一种方案:每次操作钥匙串之前,都删除旧的,生成新的,具体可以查看这个 issue。但是考虑到新增钥匙串的各种开销,还是有点得不偿失。
2018.09.06 更新。每天执行钥匙串删除,然后证书授权这个逻辑已经执行了好几个月了,好使没毛病。相对于之前,机器在打包几周后,就会出现证书安装、签名变慢的毛病,造成整个流程多出 2~3 分钟。每天清理后,一口气上五楼不费劲。
新增证书的解锁问题
这个问题,显式感谢一下 @ppt,困扰我非常久了。不知道你是否也被证书解锁需要输入密码而困扰…。口算一下,8 个账号,16 个证书,15 台机器(包含 CI 和部分自动化机器),更新证书需要手动输入密码 240 次,手动微笑。
然后,调用以下命令可以直接解锁整个钥匙串:
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 等自动部署的机器上面去,这些机器都是统一的环境,所以操作起来很方便。
关于整个苹果账号和证书管理策略,之后也会单开文章来详谈。
2018.06.25 更新。关于苹果账号策略,可查看 苹果账号证书体系自动化策略
证书的查看
最后给大家安利一个 Quick Look 插件 provisionql
,很多人应该都知道了:
brew cask install provisionql |
查看 .ipa
,.cer
,.mobileprovision
利器,就不截图了,截图也全是马赛克。
最后
fastlane public 的有几个比较重要的模块,可以在官方文档上看到。之前为了解决 resign entitlements 的问题,阅读了 resign 模块的源码,然后是 match,下一步是已经发现问题,但是还没来得及分析的 gym。目前 fastlane 已经更新到 2.87.0 了,欢迎了解新版本特性的朋友讨论。