fastlane resign 源码浅析与填坑

fastlane 作为移动端的自动发布工具,称得上是相当出名了。相信绝大多数公司的自动化流程,多少都会依赖 fastlane。我司也从 match 用到了 pilot。虽然使用范围很广,但是该坑的还是坑,特别是 resign 这部分,显得和 fastlane 的其他模块格格不入。所以一起深入源码来看看吧,如果你也遇到的类似的坑,欢迎探讨。

环境信息

fastlane: 2.72.0


在我看来,fastlane 是一个非常好的命令行工具,但是从 Ruby 库的角度来看,它称不上优秀。开发者众多,导致的实现方式各有千秋,有 Ruby 的,有 Bash 的,各种各样,这并不利于模块化集成。比如 match 模块,结果存储在环境变量里面,没有对应的 model,再比如今天谈到的 resign,外面套了一个 Ruby 的壳,内部是用 Bash 实现的。

重签流程

重签利用的是系统命令 /usr/bin/codesign。一个 ipa 中,需要重签的有:dylibso0vispvrframeworkappexapp。基本上可以归为两类:Framework 二进制和 app 二进制。

fastlane 采用了两种方式分别对 framework 和 app 进行签名:

# .framework 与 .dylib 签名
/usr/bin/codesign -f -s <证书> <framework 二进制>

# app 与 appex 签名
/usr/bin/codesign -f -s <证书> --entitlements <Entitlements 文件> <app 二进制>

相较于 framework,app 会多出 entitlements 参数,这个文件中,包含的是当前 app 有哪些权限,比如:推送、关联域名、钥匙串共享等等。

Entitlements

查看 app 的 entitlements 同样可以通过 codesign 命令做到:

/usr/bin/codesign -d --entitlements :- <二进制路径>

Entitlements 控制着程序的访问权限,这个权限和 AppID 对应,如果不匹配,会导致无法安装。来看一下常见的 entitlements 都包含哪些信息:

fastlane 的源码中,很大一部分都是在处理 Entitlements,而最坑的地方,也在这里。接下来一起看下源码。

源码浅析

Resign 部分分为 Ruby 和 Bash 两块。Ruby 部分是处理传入参数,然后 call bash,所以,源码直接看 Bash 的部分就可以了。这部分代码对应着 fastlane/sigh/lib/assets/resign.sh

前 300 行都可以略过,是在对传入参数进行赋值和校验。

参数

resign 支持的参数可以调用 fastlane action resign 进行查看。一一介绍一下。

参数名 含义
ipa 需要重签的 ipa,结果会直接覆盖传入 ipa
signing_identity 用于重签的证书,这个参数会在传入 resign.rb 中被处理成 id
entitlements 指定重签 app 的 entitlement。注意,这是 app 的,不是 appex,与 use_app_entitlements 参数互斥
provisioning_profile 重签所用的描述文件。主要是为了在处理 entitlements 的时候,读取当前 provision 的 entitlements
version 写入指定的值到 app 和 appex 的 version 和 build
display_name 写入 display name
short_version 写入 version
bundle_version 写入 build
bundle_id 写入 bundle id
use_app_entitlements 如果当前 entitlements 中不包含 provision 的 entitlements 的部分 key,会进行写入或修改。与 entitlements 参数互斥。
keychain_path codesign 的 keychain path

源码到 500 行的样子,都是在处理 info.plist 相关的参数,比如 versionbuildbundle_id 这些。除此之外,还对 provision 进行了解析,读取 TeamID 等信息。

之后的代码做了两件事:

  • 重签 Framework。
  • 根据 entitlementsuse_app_entitlements 两个选项,处理应该用于 app 签名的 entitlements 文件。

这两步都有不同程度的问题。

重签 Framework

重签 Framework 的部分从 542 行开始,也就是 FRAMEWORKS_DIR="$APP_PATH/Frameworks" 这行开始。核心代码片段如下:

# 指定遍历 Framework 的文件夹
FRAMEWORKS_DIR="$APP_PATH/Frameworks"
if [ -d "$FRAMEWORKS_DIR" ];
then
# 遍历
for framework in "$FRAMEWORKS_DIR"/*
do
# 找到 FRAMEWORKS_DIR 下的 .framework 和 .dylib 文件
if [[ "$framework" == *.framework || "$framework" == *.dylib ]]
then
# 执行签名
/usr/bin/codesign ${VERBOSE} ${KEYCHAIN_FLAG} -f -s "$CERTIFICATE" "$framework"
# 检查签名结果
checkStatus
else
fi
done
fi

可能是出于效率考虑?不太清楚原因,但是源码很清晰的表示,fastlane 只会对 APP_PATH/Frameworks 文件夹中的 framewrok 进行签名。那如果有 framework 不在这个文件夹中呢?很简单,就无法签名。这并不影响安装,只是启动闪退…而已。

通常,framework 都在 APP_PATH/Frameworks 中,但也有例外,比如西瓜视频。

处理 Entitlements

根据 entitlementsuse_app_entitlements 两个参数,分为三种不同的处理方式,分别是:

  • 传入 entitlements
  • 传入 use_app_entitlements
  • 两个都不传(因为这两个值互斥,所以不存在两个都传的情况)

指定 entitlements

其中指定 entitlements 的处理很简单:

# 如果给出 entitlements 参数
if [ "$ENTITLEMENTS" != "" ];
then
# 1. 校验提供的 entitlements APP_ID_PREFIX 是否和提供的 provision APP_ID_PREFIX 的匹配
# 2. 校验 TEAM_ID
# 3. 将 entitlements 存储到 APP_PATH/archived-expanded-entitlements.xcent
cp -f "$ENTITLEMENTS" "$APP_PATH/archived-expanded-entitlements.xcent"
# 4. 根据 entitlement,对 app 的二进制进行签名
/usr/bin/codesign ${VERBOSE} -f -s "$CERTIFICATE" --entitlements "$ENTITLEMENTS" "$APP_PATH"
# 5. 检测签名结果
checkStatus
fi

代码就这些,所以,app 和 appex 的签名都是用的同一个 entitlements,一旦 app 和 appex 的 entitlements 不一致,就 gg。

指定 use_app_entitlements

那么再来看 use_app_entitlements,如果指定为 true,就会修改以前二进制签名中相同的 key。

这部分代码很多,首先需要看的是它的合并规则,用一张图应该能看明白。主要有三个量:

  1. 当前二进制所用 entitlements:/usr/bin/codesign -d --entitlements :"导出路径" "二进制路径"
  2. 用于重签的 provision 的 entitlements:security cms -D -i "provision 路径" > "导出路径",导出的是整个 provision 信息,entitlements 对应的是其中的 <entitlements> 节点
  3. 合并之后的结果

所以,处理规则是:

  • 如果已存在相同的 key:string 对应关系,则直接覆盖。
  • 如果重签的 provision entitlements 不支持某个 key,则移除。
  • 如果存在相同的 key:array 对应关系,则遍历进行替换。

fastlane 中,列举了 entitlements 的所有 key,然后通过不同的格式来确认替换方式:

其中需要注意 TEAM_IDAPP_ID 两个字段。这两个字段在绝大多数情况下是一样的,但如果出现了 app 转入其他账号这种情况,值就不一样了。在进行 keychain-access-groups 的时候,要注意,它用的是 APP_ID,不是 TEAM_ID。

fastlane 这部分注释非常重要,建议认真读三遍!!!

接下来是删除 Xcode 中不支持的 key,如果不移除,会导致审核出问题。这部分 key 会在使用 facebook 开源的 Buck 上编译时出现。这部分代码中的注释非常清楚,可以参考 Buck 对应的提交,以及 fastlane issue

到此,整个 use_app_entitlements 的处理就结束了。还剩下最后一种情况。

entitlementsuse_app_entitlements 都不指定

如果两个值都不指定,则默认将 provision 中的 entitlements 导出,然后用于重签。这样会丢失 keychain 等信息,但是基本安装还是没问题的。iOS App Signer 也是采用的这种方式。

最后来看一下 entitlements 处理的坑吧,当然,我可没说这是个 bug。

That’s not a bug, it’s a feature request. 🙃

那么,问题出在哪呢?之前介绍的是,权限多的 entitlements 处理到权限少的 entitlements 中。所以,use_app_entitlements 很适合做这件事,因为用到的更多的是删除操作。如果从权限少的,签名到权限多的呢?

因为 fastlane 的参数中,并没有给出:如果需要自定义参数,应该怎么传入,这种情况,所以,这不是 bug,是新的需求!

填坑

所以,在用 fastlane 重签的时候,需要完善两个需求。

  • Framework 没有全部签名。
  • Entitlements 不支持自定义字段。

在 fastlane 未修复,且没有新的 pr 的情况下,这部分被遗漏的需求,可以在调用 fastlane 重签之前,手动修复。

Frameworks 签名问题

这个解决起来很简单,手动遍历一下 APP_PATH 中的 .framework.dylib,然后手动签名(当前,framework 还可能会存在于各种各样的文件夹中,可以遇到再修复,整个文件夹进行查找感觉不太必要)。

Dir[File.join(APP_PATH, "*.framework")].each do |f|
cmd = "/usr/bin/codesign -f -s '证书 ID' #{ f }"
puts "签名 Framework:#{ cmd }"
system(cmd)
end

Entitlements 合并问题

由于。。。 entitlements 参数不支持根据不同二进制指定 Entitlements 文件;use_app_entitlements 参数不支持自定义字段,所以,最终的解决方案是:根据 fastlane 的逻辑,手动合并 entitlements,然后手动签名。(其实,到这一步,我们几乎已经自己做完了签名的功能,完全用不着 fastlane 了 🤭

逻辑和 fastlane 逻辑一样,只是自己实现的时候,可以给出自定义的字段而已,这里就不赘述了,如果还是不清楚,可以再看下 fastlane 的合并规则。大致说一下流程就行:

# 合并 entitlements
binary_entitlements = extract(binary, provision)
# 将合并之后的 entitlements 写入到 APP_PATH 的 archived-expanded-entitlements.xcent 文件
entitlements_save_path = APP_PATH + "archived-expanded-entitlements.xcent"
binary_entitlements.write(entitlements_save_path)
# 指定 entitlements 进行签名
sign(binary, cert_name, entitlements_save_path)

处理完这两个问题后,再调用 fastlane 进行重签。use_app_entitlements=true 就行,因为已经手动合并过一次,所以 fastlane 的合并不会造成影响。

最后

最后,欢迎反馈问题,等着我的 pr 吧,哈哈哈哈,我还没开始写呢 😑。