OC底层原理43.1:自动化打包(一)Xcode + Shell脚本

张建 lol

前言

Apple提供的常规打包方式主要是由 Xcode 支持的,下面展开来聊聊

Xcode打包

Xcode的打包主要分为两步:

  • Archive:对 target 进行 编译、归档,生成 .xcarchive 文件

  • Export:对生成的 .xcarchive 文件进行进一步的处理,生成 不同渠道ipa 包,进行分发

Archive编译归档

Archive 主要是对 target 进行 编译、归档,生成 .xcarchive 文件。可在 Xcode -> Window -> Organizer -> Archives 中查看

这里所说的 归档,主要就 对项目源码进行编译后,再将编译生成的各种文件、资源、记录统一封装到一个文件中,便于管理和回溯。

随机选择一个 .xcarchive 文件,点击 show in Finder,可以看到一个 .xcarchive 后缀的文件

这个 .xcarchive 文件包含了 应用、符号表信息以及其他的资源,可以右键 -> 显示包内容进行查看,主要包含以下文件:

  • BCSymbolMaps:Xcode对BitCode符号表进行混淆(Symbol Hiding)后生成的对照表,和dSYM文件会一一对应

  • dSYMs:存储此次编译的符号表(debug symbols),用来符号化解析崩溃堆栈

  • info.plist:项目target配置文件

  • Products:存储此次编译生成的的 App 包(.app)。虽然这个文件包含了 App 运行需要的可执行文件以及其它资源,但是和最终用户下载的版本会有所不同。后续的 export 操作会对其进行进一步处理。

  • SCMBlueprint:如果 Xcode 打开了版本管理(Preferences -> Source Control -> Enable Source Control),SCMBlueprint 文件夹会存储此次编译的版本控制信息,包括使用的 git 版本、仓库、分支等。如果需要回溯此次编译的源码版本,可以在这个文件中找到对应的信息。

  • SwiftSupport:在 TargetBuild Settings 中打开了ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES,此次编译使用的 Swift 版本对应的标准库文件(.dylib)会被放到这个文件夹中。发布App时,也会被复制到ipa bundle中。在iOS 12.2及以上,swift的ABI趋于稳定,已经不用再自带链接库了,因此ipa包节省了一定的体积

Export打包分发

Export 主要是的对生成的 .xcarchive 文件进行进一步的处理,生成不同渠道的ipa包,进行分发,Organize -> Archive文件 -> Distribute App

这里对应4种分发渠道:App Store、Ad Hoc、Enterprise和Development,然后一步步往下操作即可。

以上4中渠道对应的打包 method 分别是 app-store、ad-hoc、enterprise、development,可以在导出的文件夹中 ExportOptions.plist 文件看到对应参数,如下所示

最终Export导出的文件夹中主要包含以下4种文件:

  • DistributionSummary.plist:包含ipa所支持的架构、bitcode、证书、embeddedBinaries(非系统的动态库相关)、entitlements(apns环境、application-identifier等)、profile等相关信息

  • ExportOptions.plist:ipa包导出的 配置文件,主要包含包导出方式、签名方式、App Developer team的id等

  • 项目名.ipa:应用的 ipa 包,包含了App所需的签名、二进制包、资源等

  • Packaging.log:打包相关的日志

如果想要查看ipa中的内容,可以将 .ipa 改成 .zip,然后解压,也可使用命令解压
zip -0 -y -r myAppName.ipa Payload/,其中是 .app 的文件,查看其包内容,主要包含以下几部分:

  • MachO可执行文件:具体的介绍可查看 iOS逆向 12:Mach-O文件(上)iOS逆向 12:Mach-O文件(下)

  • 签名文件App 的签名信息会被放到 _CodeSignature 文件夹中。

  • info.plist:存储 App 主要信息的 plist 文件也会被一并打包到 ipa 中。

  • entitlements:App包含的相关权限,在这个文件中通过 xml 的格式将这些授权记录下来,例如Push notifications、App Group等

  • App Plugins:如果App实现了 App Extension扩展,扩展的包会以 .appex 的后缀存储在 PlugIns 文件夹中,随着App的安装一起安装到用户手机上

  • 链接库:App 运行所需要的各种链接库会被放入 Frameworks 文件夹。

  • 资源文件:App 运行需要的各种资源文件也是 ipa 体积的大头。常见的有各种多媒体资源:图片、音视频

  • xib 文件:.nib .storyboard

  • 各种打包的资源: .bundle

  • 其它类型的资源:字体、数据库、证书等等

除了 Xcode 自带的工具 application loader 上传ipa,还可以通过 Transporter 上传。Transporter 可以以简单轻松的方式将内容交付到 Apple,提供如下功能:

  • 只需要将ipa包拖放到 Transporter 中即可开始使用
  • 同时验证和上传多个文件以快速交付
  • 查看交付进度(警告、错误和交付日志),以便快速修复交付问题
  • 查看历史交付记录(包含日期、时间)

Transporter 应用现已上架 Mac App Store,你需要下载并安装,如下图:

登录你的Apple ID,将导出的 .ipa 包拖进来即可:

所以,综上所述,通过 Xcode 提供的打包方式 比较繁琐,需要人为操作许多步骤,那么我们是否可以通过其他的方式进行简化呢?答案是当然可以呀。

简化打包操作的方式的主要有两种:

  • Shell打包脚本
  • Jenkins部署自动打包

这里我们主要介绍 Shell打包脚本 的方式,主要分为以下几步:

  • 配置打包相关的名称:.xcworkspace的名字、scheme名称、编译方式、打包方式、profile文件、bundleID等

  • 配置打包的相关路径:导出路径、archive文件路径、ipa文件路径、ExportOptions.plist文件路径

  • 清理工程:通过 xcodebuild clean 清理

  • 编译:通过 xcodebuild archive编译、归档工程

  • 导出ipa:通过 xcodebuild 命令将 .xcarchive 导出为 ipa 文件

  • 校验并上传AppStore:如果是AppStore的包,通过 xcrun altool 校验并上传 AppStore

使用方式:进入 podfile 所在目录,在终端执行 .sh 文件,例如:sh xxxx.sh

具体的shell脚本如下(注:使用时,需要自行填充其中的xxxxx所在的脚本代码)

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
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
#!/bin/bash

# 前提:enterprise打包时,需要切换到 企业账户 及 BundleID

# 使用方法:
# step1: 将该脚本放在工程的根目录下(跟.xcworkspace文件or .xcodeproj文件同目录)
# step2: 根据情况修改下面的参数
# step3: 打开终端,执行脚本。(输入sh,然后将脚本文件拉到终端,会生成文件路径,然后enter就可)

echo "-----------开始执行脚本-----------"

# =============项目自定义部分(自定义好下列参数后再执行该脚本)=================== #
echo "请选择打包方式 ? [1:enterprise_debug 2:enterprise_release 3:ad_hoc 4:app_store]"

read number
while([[ $number != 1 ]] && [[ $number != 2 ]] && [[ $number != 3 ]] && [[ $number != 4 ]])
do
echo "Error! Should enter 1 or 2 or 3 or 4"
echo "请选择打包方式 ? [ 1:enterprise_debug 2:enterprise_release 3:ad_hoc 4:app_store]"
read number
done

#-----------脚本配置信息-----------
# .xcworkspace的名字,必填
workspace_name="xxxxx"

# 指定项目的scheme名称(也就是工程的target名称),必填
scheme_name="xxxxx"

# 指定要打包编译的方式 : Release,Debug。一般用Release。必填
build_configuration="Release"

# method,打包的方式。方式分别为 development, ad-hoc 。必填
method="enterprise"

# 下面两个参数只是在手动指定Pofile文件的时候用到,如果使用Xcode自动管理Profile,直接留空就好
# (跟method对应的)mobileprovision文件名,需要先双击安装.mobileprovision文件.手动管理Profile时必填
# mobileprovision_name="9d8c7290-4345-4ebf-82d4-a74cab2ea40b"
mobileprovision_name=""

# 项目的bundleID,手动管理Profile时必填
bundle_identifier=""
# if [[ $number == 1 ]]; then
# bundle_identifier="com.mi.global.sho"
# else
# bundle_identifier="com.mi.global.shop"
# fi

# 每次编译后是否Build自动加1,
# 可以修改该常量的值,以决定编译后还是打包后Build自动加1
# # 0: 每次打包后Build自动加1
# # 1: 每次编译后Build自动加1
DEBUG_ENVIRONMENT_SYMBOL=0


# 根据选项配置不同的包
if [ $number == 1 ];then
build_configuration="Debug"
method="enterprise"
DEBUG_ENVIRONMENT_SYMBOL=1
elif [[ $number == 2 ]]; then
build_configuration="Release"
method="enterprise"
DEBUG_ENVIRONMENT_SYMBOL=1
elif [[ $number == 3 ]]; then
build_configuration="Release"
method="ad-hoc"
DEBUG_ENVIRONMENT_SYMBOL=1
else
build_configuration="Release"
method="app-store"
DEBUG_ENVIRONMENT_SYMBOL=1
fi

echo "--------------------脚本配置参数检查--------------------"
echo "\033[33;1mworkspace_name = ${workspace_name}"
echo "scheme_name = ${scheme_name}"
echo "build_configuration = ${build_configuration}"
echo "bundle_identifier = ${bundle_identifier}"
echo "method = ${method}"
echo "mobileprovision_name = ${mobileprovision_name} \033[0m"

# =======================脚本的一些固定参数定义(无特殊情况不用修改)====================== #
# 获取当前脚本所在目录
script_dir="$( cd "$( dirname "$0" )" && pwd )"
# 工程根目录
project_dir=$script_dir

# 指定输出导出文件夹路径
export_path="$project_dir/Build"
# 指定输出归档文件路径
export_archive_path="$export_path/$scheme_name.xcarchive"
# 指定输出ipa文件夹路径
export_ipa_path="$export_path/"
# 指定导出ipa包需要用到的plist配置文件的路径
export_options_plist_path="$project_dir/ExportOptions.plist"

echo "--------------------脚本固定参数检查--------------------"
echo "\033[33;1mproject_dir = ${project_dir}"
echo "export_path = ${export_path}"
echo "export_archive_path = ${export_archive_path}"
echo "export_ipa_path = ${export_ipa_path}"
echo "export_options_plist_path = ${export_options_plist_path}\033[0m"

# =======================自动打包部分(无特殊情况不用修改)====================== #
echo "------------------------------------------------------"
echo "\033[32m开始构建项目 \033[0m"
# 进入项目工程目录
cd ${project_dir}

# 指定输出文件目录不存在则创建
if [ -d "$export_path" ];
then rm -rf "$export_path"
fi

/usr/bin/xcrun xcodebuild -UseNewBuildSystem=YES -xcconfig InnerXcconfig/innerInner/tt.xcconfig

# 编译前清理工程
xcodebuild clean -workspace ${workspace_name}.xcworkspace \
-scheme ${scheme_name} \
-configuration ${build_configuration}

xcodebuild archive -workspace ${workspace_name}.xcworkspace \
-scheme ${scheme_name} \
-configuration ${build_configuration} \
-archivePath ${export_archive_path}

# 检查是否构建成功
# xcarchive 实际是一个文件夹不是一个文件所以使用 -d 判断
if [ -d "$export_archive_path" ] ; then
echo "\033[32;1m项目构建成功 🚀 🚀 🚀 \033[0m"
else
echo "\033[31;1m项目构建失败 😢 😢 😢 \033[0m"
exit 1
fi
echo "------------------------------------------------------"

echo "\033[32m开始导出ipa文件 \033[0m"

# 先删除export_options_plist文件
if [ -f "$export_options_plist_path" ] ; then
#echo "${export_options_plist_path}文件存在,进行删除"
rm -f $export_options_plist_path
fi

# 根据参数生成export_options_plist文件
/usr/libexec/PlistBuddy -c "Add :method String ${method}" $export_options_plist_path
/usr/libexec/PlistBuddy -c "Add :provisioningProfiles:" $export_options_plist_path
/usr/libexec/PlistBuddy -c "Add :provisioningProfiles:${bundle_identifier} String ${mobileprovision_name}" $export_options_plist_path
/usr/libexec/PlistBuddy -c "Add :compileBitcode bool NO" $export_options_plist_path


xcodebuild -exportArchive \
-archivePath ${export_archive_path} \
-exportPath ${export_ipa_path} \
-exportOptionsPlist ${export_options_plist_path} \
-allowProvisioningUpdates

# 检查文件是否存在
if [ -f "$export_ipa_path/$scheme_name.ipa" ] ; then
echo "\033[32;1m导出 ${scheme_name}.ipa 包成功 🎉 🎉 🎉 \033[0m"
open $export_path
else
echo "\033[31;1m导出 ${scheme_name}.ipa 包失败 😢 😢 😢 \033[0m"
exit 1
fi

# 删除export_options_plist文件(中间文件)
if [ -f "$export_options_plist_path" ] ; then
#echo "${export_options_plist_path}文件存在,准备删除"
rm -f $export_options_plist_path
fi

# 输出打包总用时
echo "\033[36;1m使用AutoPackageScript打包总用时: ${SECONDS}s \033[0m"
echo "------------------------------------------------------"

# AppStore上传到xxx
if [ $number == 4 ];then
# 将包上传AppStore
ipa_path="$export_ipa_path/$scheme_name.ipa"
# 上传AppStore的密钥ID、Issuer ID
api_key="xxxxx"
issuer_id="xxxxx"

echo "--------------------AppStore上传固定参数检查--------------------"
echo "ipa_path = ${ipa_path}"
echo "api_key = ${api_key}"
echo "issuer_id = ${issuer_id}"

# 校验 + 上传 方式1
# # 校验指令
# cnt0=`xcrun altool --validate-app -f ${ipa_path} -t ios --apiKey ${api_key} --apiIssuer ${issuer_id} --verbose`
# echo $cnt0
# cnt=`echo $cnt0 | grep “No errors validating archive” | wc -l`

# if [ $cnt = 1 ] ; then
# echo "\033[32;1m校验IPA成功🎉 🎉 🎉 \033[0m"
# echo "------------------------------------------------------"
# cnt0=`xcrun altool --upload-app -f ${ipa_path} -t ios --apiKey ${api_key} --apiIssuer ${issuer_id} --verbose"`
# echo $cnt0
# cnt=`echo $cnt0 | grep “No errors uploading” | wc -l`
# if [ $cnt = 1 ] ; then
# echo "\033[32;1m上传IPA成功🎉 🎉 🎉 \033[0m"
# echo "------------------------------------------------------"

# else
# echo "\033[32;1m上传IPA失败😢 😢 😢 \033[0m"
# echo "------------------------------------------------------"
# fi
# else
# echo "\033[32;1m校验IPA失败😢 😢 😢 \033[0m"
# echo "------------------------------------------------------"
# fi

# 校验 + 上传 方式2
# 验证
validate="xcrun altool --validate-app -f ${ipa_path} -t ios --apiKey ${api_key} --apiIssuer ${issuer_id} --verbose"
echo "running validate cmd" $validate
validateApp="$($validate)"
if [ -z "$validateApp" ]; then
echo "\033[32m校验IPA失败😢 😢 😢 \033[0m"
echo "------------------------------------------------------"
else
echo "\033[32m校验IPA成功🎉 🎉 🎉 \033[0m"
echo "------------------------------------------------------"

# 上传
upload="xcrun altool --upload-app -f ${ipa_path} -t ios --apiKey ${api_key} --apiIssuer ${issuer_id} --verbose"
echo "running upload cmd" $upload
uploadApp="$($upload)"
echo uploadApp
if [ -z "$uploadApp" ]; then
echo "\033[32m传IPA失败😢 😢 😢 \033[0m"
echo "------------------------------------------------------"
else
echo "\033[32m上传IPA成功🎉 🎉 🎉 \033[0m"
echo "------------------------------------------------------"
fi

fi

fi

exit 0

这里额外补充下 xcodebuild、xcrun 命令的知识:

  1. xcodebuild

用于编译xcode中的projects和workspaces,可通过man xcodebuild查看文档

常见的命令格式如下:

  • 清理工程
1
2
3
xcodebuild clean -workspace [xcworkspace路径] \
-scheme [scheme名称] \
-configuration [编译方式Release或Debug]
  • 编译工程
1
2
3
4
xcodebuild archive -workspace [xcworkspace路径] \
-scheme [scheme名称]\
-configuration [编译方式Release或Debug] \
-archivePath [.xcarchive文件路径]
  • 导出ipa
1
2
3
4
5
xcodebuild  -exportArchive \
-archivePath [.xcarchive文件路径] \
-exportPath [.ipa文件路径] \
-exportOptionsPlist [ExportOptions.plist文件路径] \
-allowProvisioningUpdates

xcrun

用于 运行或定位开发工具以及属性,也可以通过 man xcrun 查看具体文档

常见的命令格式如下:

  • 校验ipa
1
xcrun altool --validate-app -f [.ipa的路径] -t ios --apiKey [api_key] --apiIssuer [issuer id] --verbose
  • 上传ipa到AppStore
1
xcrun altool --upload-app -f [.ipa的路径] -t ios --apiKey [api_key] --apiIssuer [issuer id] --verbose

其中上传所需的 api keyissuer id 需要在 AppStore Connect 中配置和获取。使用具有账户管理权限的账号登陆 App Store Connect 选择 用户与访问 >密钥 查询信息: issuser和apiKey(密钥 ID),如下所示,

并将 公钥 下载到本地,并将下载好的 p8文件 保存到需要放到一个固定目录下:

1
2
3
4
5
// P8文件放入以下文件中
./private_keys
~/private_keys
~/.private_keys
~/.appstoreconnect/private_keys

除了使用 xcrun altool 上传ipa,我们还可以使用 Transpoter,但相对来说,Shell脚本更方便,实现了本地自动化打包上传。

综上所述,shell脚本打包整体流程如下:

  • Post title:OC底层原理43.1:自动化打包(一)Xcode + Shell脚本
  • Post author:张建
  • Create time:2023-05-09 16:05:26
  • Post link:https://redefine.ohevan.com/2023/05/09/OC底层原理/OC底层原理43.1:自动化打包(一)Xcode-Shell脚本/
  • Copyright Notice:All articles in this blog are licensed under BY-NC-SA unless stating additionally.