简介

iOS 构建步骤较为繁琐,需要处理证书签名等问题。另外由于需要同时并行构建多个工程,整个构建流程需要将支持并行构建作为重点处理。

本文主要处理 Unity 输出的 iOS 工程,使用 fastlane 管理证书以及构建,简化工作流程。

上篇 fastlane 介绍 - 狂飙 到现在已经过去了两年多,fastlane 也有了不少变化:

  1. fastlane 现在已经被 Google 收购,可以用于 Android 构建了,但是 fastlane 主要支持的平台依然是 macOS。
  2. fastlane 中的 match 组件原来不支持管理企业证书,理由是安全问题,泄露的话影响巨大,但是现在居然改为支持管理企业证书了。

环境

  • Unity 5.6.6f2
  • macOS 10.14.4
  • Xcode 10.2
  • Python 3.7.3
  • fastlane 2.121.1

软件

Homebrew

使用 Homebrew 安装、管理、升级 Python 等软件。

1
/usr/bin/ruby -e "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install)"

Python

1
2
brew update
brew install python

fastlane

推荐使用 Ruby 包管理器 Gem 进行安装,因为后续可以使用 Gem 进行 fastlane 更新、依赖版本管理等等功能。

1
2
xcode-select --install
sudo gem install fastlane -NV

签名

iOS 签名容易出现以下问题:

  • 多台机器上需要同步签名
  • 多个签名同时存在时无法找到正确签名
  • 签名过期
  • 如果开发者有撤消证书的权限,证书易被误撤消,比如误操作、Xcode 自动进行的错误操作

业界最佳实践如下:

通用账号

创建账号

建议使用一个通用账号来管理证书,可以限制此账号的权限,而不是必须用公司的管理员账号。

管理员账号登录后前往 用户和访问 新用户职能 选择 App 管理开发人员资源 勾选 证书、 标识符和描述文件的访问权限。

注意:开发人员 职能无法创建 Mobile Provision,必须选择 App 管理 职能。否则 match 在初始化时会提示当前账号权限不足:

1
2
[!] Apple provided the following error info:
	You are not allowed to perform this operation.  Please check with one of your Team Admins, or, if you need further assistance, please contact Apple Developer Program Support. https://developer.apple.com/support

保存信息

在注册通用账号时,需要将所有信息记录下来,包括不限于账号、密码、出生日期、提示问题及答案、手机号等等,方便日后管理账号。

同意协议

需要前往 https://appleid.apple.com 登录并接受隐私协议。否则 match 会提示:

1
2
[!] The request could not be completed because:
	Need to acknowledge to Apple's Apple ID and Privacy statement. Please manually log into https://appleid.apple.com (or https://appstoreconnect.apple.com) to acknowledge the statement.

match

初始化

需要提前在 GitLab 之类的 Git 服务器上创建好需要的证书存储仓库,将访问权限设为私有,只对需要的人开放权限。

建议提前配置好访问 Git 服务器使用的 SSH 公钥,这样可以在拉取仓库时不用输入账号与密码。

1
2
fastlane match init
# 选择 Git 然后输入 Git 仓库地址即可

使用

1
2
bundle exec fastlane match development
# 输入需要管理的 App ID 与证书仓库的加密密码

注意:如果提示:

1
[!] Could not create another Development certificate, reached the maximum number of available Development certificates.

说明中途有步骤出错,那么需要在苹果证书管理网站上删除刚刚创建的证书,然后重新再来一次。

构建

目标

  • 支持并行构建,一台机器上可能会同时进行多个不同工程的 iOS 版本构建,要求相互之间使用的证书等不能互相影响。
  • 确定性的参数配置,要求不同时间构建出的包是一致的,也就是说不能使用任何会变化的参数。

安装依赖

1
2
Could not find os-1.0.0 in any of the sources
Run `bundle install` to install missing gems.

需要先安装 fastlane 组件,否则无法启动

1
2
bundle install
# 在需要时输入密码即可

注意:不要使用 sudo bundle install,否则会影响非 root 用户使用。可以直接使用 bundle install 进行安装,在需要密码时程序会向你询问密码。

Xcode 版本

Fastfile 文件中增加 xcversion(version: "10.2") 会提示需要额外安装 xcode-install 组件。

1
2
3
4
5
Missing gem 'xcode-install', please add the following to your local Gemfile:

▸ gem "xcode-install"

Add 'gem "xcode-install"' to your Gemfile and restart fastlane

gem "xcode-install" 添加到 Gemfile 中并重新执行 fastlane 命令。

Team ID

1
error: Signing for "Unity-iPhone" requires a development team. Select a development team in the build settings editor that matches the selected profile "match Development test.example.client". (in target 'Unity-iPhone')

需要禁用 Xcode 自动签名机制,同时设置 TeamID 信息。

1
2
3
4
disable_automatic_code_signing(
  path: "Unity-iPhone.xcodeproj",
  team_id: ENV["sigh_test.example.client_development_team-id"]
)

Mobile Provision

1
error: "Unity-iPhone" requires a provisioning profile. Select a provisioning profile for the "Release" build configuration in the project editor. (in target 'Unity-iPhone')

需要手动指定 Mobile Provision 以及 App IDMobile Provision 之间的关系。

证书

如果将证书导入到默认 login.keychain 中,如果存在同名过期证书则有可能会使用过期的错误证书,因此正确方法是创建新的 Keychain,然后将其搜索顺序放在搜索列表最前面。

codesign 请求访问 Keychain 弹窗问题:

1
▸ Command CodeSign failed with a nonzero exit code

解决方法就是执行 security 命令:

1
security set-key-partition-list -S apple-tool:,apple: -s -k keychainPass keychainName

xcodebuild 配置

1
2
3
4
5
[!] xcodebuild -showBuildSettings timed out after 4 retries with a
base timeout of 3. You can override the base timeout value with the
environment variable FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT, and the
number of retries with the environment variable
FASTLANE_XCODEBUILD_SETTINGS_RETRIES

将等待 Xcode 修改设置的超时时间加长:

1
ENV["FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT"] = "120"

结果

步骤

  1. 设置 Keychain 名字为项目名字+频道,这里可以使用任意方法组合,只要保证不同构建间唯一即可。
  2. 强制指定 Xcode 版本,防止构建旧版本时错误地使用了最新的 Xcode
  3. 创建 Keychain,默认解锁、不超时锁定、不添加到搜索列表中。
  4. 使用 match 同步签名相关证书与 Mobile Provision 信息。
  5. 设置刚刚创建的 Keychain 为搜索列表中第一项,防止搜索到默认环境中过期无效的证书信息。
  6. 允许 codesign 直接无密码访问 Keychain,否则构建在这一步失败退出。
  7. 禁用自动签名并设置 Team ID。
  8. 更新 Mobile Provision 信息。
  9. 设置 xcodebuild 超时时间。
  10. 构建 iOS 工程,指定 App IDMobile Provision 关系。
  11. 删除刚刚创建的 Keychain

文件

Fastfile

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
# This file contains the fastlane.tools configuration
# You can find the documentation at https://docs.fastlane.tools
#
# For a list of all available actions, check out
#
#     https://docs.fastlane.tools/actions
#
# For a list of all available plugins, check out
#
#     https://docs.fastlane.tools/plugins/available-plugins
#

# Uncomment the line if you want fastlane to automatically update itself
# update_fastlane

default_platform(:ios)

platform :ios do
  desc "iOS Development Build"
  lane :development do |options|
    keyname = options[:name] + "-" + options[:channel] + ".keychain"
    keypass = "your-password"
    app_identifier = "test.example.client"

    xcversion(version: "10.2")

    create_keychain(
      name: "#{keyname}",
      password: "#{keypass}",
      default_keychain: false,
      unlock: true,
      timeout: false,
      lock_when_sleeps: false,
      lock_after_timeout: false,
      add_to_search_list: false
    )

    sync_code_signing(
      type: "development",
      app_identifier: "#{app_identifier}",
      readonly: true,
      keychain_name: "#{keyname}",
      keychain_password: "#{keypass}"
    )

    # Keychain we created must be the first one in search list
    sh "security list-keychains -d user -s '#{keyname}' $(security list-keychains -d user | sed s/\\\"//g)"
    # Disable /usr/bin/codesign access request
    sh "security set-key-partition-list -S apple-tool:,apple: -s -k '#{keypass}' '#{keyname}'"

    disable_automatic_code_signing(
      path: "Unity-iPhone.xcodeproj",
      team_id: ENV["sigh_test.example.client_development_team-id"]
    )

    update_project_provisioning(
      xcodeproj: "Unity-iPhone.xcodeproj",
      target_filter: "Unity-iPhone",
      profile: ENV["sigh_test.example.client_development_profile-path"],
      build_configuration: "Release"
    )

    # Extend wait times for command to avoid timeout failure:
    # xcodebuild -showBuildSettings -scheme Unity-iPhone -project ./Unity-iPhone.xcodeproj
    ENV["FASTLANE_XCODEBUILD_SETTINGS_TIMEOUT"] = "120"

    build_ios_app(
      export_options: {
        method: "development",
        provisioningProfiles: {
          "#{app_identifier}" => ENV["sigh_test.example.client_development_profile-name"],
        }
      },
      include_bitcode: false,
      output_directory: options[:output_directory],
      output_name: options[:output_name]
    )
  end

  after_all do |lane, options|
    keyname = options[:name] + "-" + options[:channel] + ".keychain"

    if File.exists?(File.expand_path("~/Library/Keychains/#{keyname}-db"))
      delete_keychain(name: "#{keyname}")
    end
  end
end