iOS 使用 fastlane 打包

在一款 App 从开发到上架都会经历 编译 -> 打包 -> 签名 -> 推送 App Store Connect -> 提审 -> 上架 的过程。其中编译打包这种重复且繁琐的工作公司内都会有一个 CI 平台去负责,我们今天就来了解一下 CI 的编译打包流程是怎样的。

fastlane 简介

fastlane 是自动为 iOS 和 Android 应用程序进行测试版部署和发布的最简单方式。它会为你处理所有繁琐的任务,如生成屏幕截图、处理代码签名和发布应用程序。

下面是一个简单的编译发布的例子:

1
2
3
4
5
6
7
8
9
10
11
12
lane :beta do
increment_build_number
build_app
upload_to_testflight
end

lane :release do
capture_screenshots
build_app
upload_to_app_store # 将屏幕截图和二进制文件上传到 iTunes
slack # 让你的队友知道新版本上线了
end

上面的代码定义了两个不同的打包流程,如果你想在 App Store 发布你的应用,你只需要执行如下操作:

1
$ fastlane release

使用 fastlane 执行打包流程

Ruby 安装环境

首先我们需要安装一套开发用的 Ruby 环境,macOS 中不推荐直接使用系统的 Ruby。我们可以用多种方式来管理我们的 Ruby 环境,例如 rvmrebnvhomebrew。由于本人是通过 homebrew 安装的 Ruby 环境,所以本篇只介绍 homebrew 的管理方式,其他方式请自行查阅资料。

对于使用 homebrew 安装 Ruby 可以看我之前的这片文章,这里有详细的说明:[使用 Homebrew 管理 Ruby](https://regulusleow.github.io/2023/03/27/20230327-使用 Homebrew 管理 Ruby/)

fastlane 的安装

Ruby 环境设置完毕后我们就可以开始安装 fastlane,同样使用 homebrew 的方式:

1
$ brew install fastlane

fastlane 的配置

fastlane 安装完成后我们 cd 到我们的工程目录,执行如下命令进行初始化:

1
$ fastlane init

在此期间,fastlane 将自动检测项目,并询问任何丢失的信息。例如:开发者账号,密码等。

安装完成之后,在我们的工程中会生成一个 fastlane 文件夹,里面有两个重要的文件:AppfileFastfile

Appfile 记录了一些开发账号信息,例如:Apple IDBundle Identifier等。

Fastfile 则是你打包流程的具体实现,一个简单的例子如下所示:

1
2
3
lane :my_lane do
# Whatever actions you like go in here.
end

这时我们在终端中,cd 到我们的工程,然后执行 fastlane my_lane 即会执行我们定义的打包流程。你可以定义很多个 lane 用于执行不同的流程。

fastlane 相关功能简介

非常有用的 block

fastlane 还提供了两个非常有用的 blockbefore_allafter_all

before_all 此块将在运行 lane 流程之前执行。它支持与 lane 相同的操作。在这个 block 中我们就可以执行一些打包之前需要进行的操作,比如执行 cocoapods

1
2
3
before_all do |lane|
cocoapods
end

after_all 故名思义,就是在执行完打包流程之后进行的操作,例如我们可以在这里发送通知,执行打包完成后的脚本等,如下所示:

1
2
3
4
5
6
7
after_all do |lane|
say("Successfully finished deployment (#{lane})!")
slack(
message: "Successfully submitted new App Update"
)
sh("./send_screenshots_to_team.sh") # Example
end

fastlane 中的错误处理由 error block 处理,当任何块(before_alllane 本身或 after_all)中发生错误时将执行该 block。我们可以使用 error_info 属性获取有关错误的更多信息。:

1
2
3
4
5
6
7
error do |lane, exception|
slack(
message: "Something went wrong with the deployment.",
success: false,
payload: { "Error Info" => exception.error_info.to_s }
)
end

导入其他 Fastfile 文件

如果我们的 Fastfile 比较复杂我们可以拆分成多个文件然后进行 import 操作,就像我们开发时候的头文件导入一般。 对于 import 功能 fastlane 提供了两种方式。

import

导入本地文件路径

1
2
3
4
5
import "../GeneralFastfile"

override_lane :from_general do
# ...
end
import_from_git

从另一个 git 资源库导入,我们可以使用该资源库为所有项目创建一个带有默认 Fastfile 的 git 资源库:

1
2
3
4
5
6
7
8
import_from_git(url: 'https://github.com/fastlane/fastlane')
# or
import_from_git(url: 'git@github.com:MyAwesomeRepo/MyAwesomeFastlaneStandardSetup.git',
path: 'fastlane/Fastfile')

lane :new_main_lane do
# ...
end

这还将自动的从 repo 中导入所有的 action

如果你需要覆盖现有的 lane 声明,可以使用 override_lane 关键字

想更多的了解 Fastfile 请参阅文档: Fastfile

使用环境变量

如果我们的打包环境比较复杂,比如需要适应多个账户打包需求,此时我们可以定义环境变量来管理相关信息。我们可以在与 Fastfile 相同的目录中创建 .env.env.default 文件,然后定义环境变量。使用 dotenv 加载环境变量。下面是一个例子:

1
2
WORKSPACE=YourApp.xcworkspace
HOCKEYAPP_API_TOKEN=your-hockey-api-token

Fastfile 中我们就可以使用如下方式获取相关环境变量:

1
bundle_identifier = ENV['APP_IDENTIFIER']

更多详细的讲解请看官方文档:Environment Variables

众多的 actions

fastlane 最终要的功能就是提供了众多的 actions 供我们使用,最常用的有:gym (build_app)cocoapodsmatchupload_to_testflightupload_to_testflight等。这里不再做过多的讲解,官方文档已经说的非常清楚,详情请查阅 fastlane actions

最后附上一个完整的流程示例:

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
88
89
90
91
92
93
94
95
require './xcode_build_settings'

default_platform(:ios)

platform :ios do

bundle_identifier = ENV['APP_IDENTIFIER']
scheme = ENV['SCHEME']
xcworkspace = ENV['XCWORKSPACE']
xcodeproj = ENV['XCODEPROJ']
apple_id = ENV['APPLE_ID']
adhoc_output_path = "./fastlane/adhoc"
product_output_path = "./fastlane/release"
# branch_name = "xxxxx"

desc "Push a new beta build to TestFlight"

api_key = app_store_connect_api_key(
key_id: "xxxx",
issuer_id: "xxx-xxx-xxx",
key_filepath: './fastlane/AuthKey_xxxx.p8',
duration: 1200,
in_house: false
)

before_all do |lane|
BuildSettings.configTarget
end

lane :beta do
increment_build_number(xcodeproj: xcodeproj)
match(
api_key: api_key,
app_identifier: bundle_identifier,
type: "adhoc"
# git_branch: branch_name
)
cocoapods(
repo_update: true,
clean_install: true,
use_bundle_exec: false,
podfile: "./Podfile"
)
build_app(
clean: true,
output_directory: adhoc_output_path,
output_name: "xxxx",
workspace: xcworkspace,
scheme: scheme,
xcargs: "-allowProvisioningUpdates",
export_options: {
manifest: {
appURL: "https://server.local/apps/xxx.ipa",
displayImageURL: "https://server.local/apps/xxx.png",
fullSizeImageURL: "https://server.local/apps/xxx.png"
},
thinning: "<none>"
}
)
verify_build(
provisioning_type: "distribution",
bundle_identifier: bundle_identifier
)
upload_to_testflight(
api_key: api_key,
app_identifier: bundle_identifier,
ipa: "./fastlane/release/xxxx.ipa"
)
end

lane :product do
increment_build_number(xcodeproj: xcodeproj)
match(
api_key: api_key,
app_identifier: bundle_identifier,
type: "appstore"
# git_branch: branch_name
)
build_app(
clean: true,
output_directory: product_output_path,
output_name: "xxxx",
workspace: xcworkspace,
scheme: scheme,
xcargs: "-allowProvisioningUpdates"
)
verify_build(
provisioning_type: "distribution",
bundle_identifier: bundle_identifier
)
end

error do |lane, exception|
end
end