Shell脚本——Xcode脚本打包

最近有好一段时间没有整理博客了,也不是因为忙,就是太懒。或者说程序员都自带懒属性😅,表现在对于一些机械性重复性的工作,宁愿一次性多花些精力去封装、去写出各种各样的工具来替自己干活,也不愿意多次重复劳动(就算那是很简单的工作)。这不我结合最近的工作,研究着写了两份Shell脚本,一是关于Xcode脚本打包,还有一个是关于crash log解析(这个将在另一篇文章说明)。


Shell打包脚本实现了如下功能

  1. 指定打包项目的Build号,Version版本号——Version号可选择自增或自定义(在测试阶段打包时,每次手动改变Version号很是麻烦,而且不方便管理;而如果能打包时通过脚本参数控制,so easy~)
  2. 导出xcarchive文件,通过xcarchive文件得到dSYM文件,这是很重要的。查找线上crash时,缺它不可!crash log解析请看这里。
  3. 打包生成ipa文件,这个就是最终需要的渠道包了。

另外打包脚本可以根据项目配置,每一次成功打包后,都会生成对应目录,并生成记录本次打包的log日志文件。


打包脚本

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
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
#!/bin/bash

#--------------------------------------------------------------------------------
# 脚本说明:
# 1、实现功能:
# 1)、指定打包项目的Build号,Version版本号(Version号可选择自增或自定义)
# 2)、导出xcarchive文件
# 3)、打包生成ipa文件
# 2、使用方式:
# 1)、将ReleaseDir文件夹,放到跟所要打包的项目的根目录(TestDemo)同级别的目录下
# 2)、cd至ReleaseDir,运行脚本./Release.sh TestDemo,并选择输入相关参数,开始打包
# 3)、完成打包后,生成的目标文件在如下目录:
# /ReleaseDir/ArchivePath/TestDemo/1.0.0/2017_03_03_10:37:34
# (/ReleaseDir/ArchivePath/打包项目名称/打包版本号/打包时间)
#--------------------------------------------------------------------------------
#
#--------------------------------------------------------------------------------
# 打包project
# ./Release.sh <Project directory name> [-s <Name>] [-e] [-d] [-a] [-b <Build number>]
# 打包workspace
# ./Release.sh <Project directory name> [-w] [-s <Name>] [-e] [-d] [-b <Build number>] [-v <Version number>]
# 参数说明:
# <Project directory name> 第一个参数:所要打包的项目的根目录文件夹名称
# -w workspace打包,不传默认为project打包
# -s <Name> 对应workspace下需要编译的scheme(不传默认取xcodeproj根目录文件名)
# -e 打包前是否先编译工程(不传默认不编译)
# -d 工程的configuration为 Debug 模式,不传默认为Release
# -a 打包,Version版本号自动+1(针对多次打测试包时的版本号修改)
# -b <Build Num> Build版本号,指定项目Build号
# -v <Version Num> Version版本号,指定项目Version号
# 注意,参数-a 与 -v 互斥,只能选择传其中一种参数!!
#--------------------------------------------------------------------------------
#
#--------------------------------------------------------------------------------
# ReleaseDir文件目录说明:
#
# |___TestDemo 所要打包的项目的根目录
# |___ReleaseDir 打包相关资源的根目录
# |___Release.sh Release.sh打包脚本
# |___ExportOptions.plist -exportOptionsPlist 配置文件
# |___ArchivePath 打包文件输出路径
# |___TestDemo 打包项目名称
# |___1.0.0 打包版本号
# |___2017_03_02_16/23/28 打包时间(格式:年_月_日_时/分/秒)
# |___TestDemo_1.0.0.xcarchive 导出的.xcarchive文件
# |___TestDemo.ipa 导出的.ipa文件
# |___LogPath 打包日志
#
#--------------------------------------------------------------------------------

#计时
SECONDS=0
#--------------------------------------------
# 参数判断
#--------------------------------------------
# 如果参数个数少于1
if [ $# -lt 1 ];then
echo -e "\033[31m第一个参数请输入 Xcode project 文件所在的根目录文件夹名称!!\033[0m"
exit 2
fi

# 脚本文件所在根目录
Release_path=$(pwd)
Project_dir=$1
Project_path=""

if [ "`pwd`" != "/" ]; then
cd ..
Root_path=$(pwd)
Project_path="${Root_path}/${Project_dir}"
else
echo -e "\033[31m脚本路径错误\033[0m"
exit 1;
fi

if [ ! -d "${Project_path}" ];then
echo -e "\033[31m首参数必须是有效的文件夹名称!!\033[0m"
exit 2
fi

#Xcode project 文件路径
cd ${Project_path}
Valid_dic=false
for i in `ls`;
do
#获取文件后缀名
extension=${i##*.}
if [[ ${extension} == "xcodeproj" ]]; then
Valid_dic=true
elif [[ ${extension} == "xcworkspace" ]]; then
Valid_dic=true
fi
done

if [[ ${Valid_dic} == false ]]; then
echo -e "\033[31m请输入包含xcodeproj或xcworkspace的文件夹名称!!\033[0m"
exit 2
fi

Archive_type="project"
Scheme_name="default"
Edit="NO"
Configuration="Release"
Auto_increment_version_num="NO"
Build_str="custom"
Version_str="custom"

# 参数处理
param_pattern=":ws:edab:v:"
OPTIND=2
while getopts $param_pattern optname
do
case "$optname" in
"w")
Archive_type="workspace"
;;
"s")
tmp_optind=$OPTIND
tmp_optname=$optname
tmp_optarg=$OPTARG

OPTIND=$OPTIND-1
if getopts $param_pattern optname ;then
echo -e "\033[31m选项参数错误 $tmp_optname\033[0m"
exit 2
fi
OPTIND=$tmp_optind
Scheme_name=$tmp_optarg
;;
"e")
Edit="YES"
;;
"d")
Configuration="Debug"
;;
"a")
Auto_increment_version_num="YES"
;;
"b")
tmp_optind=$OPTIND
tmp_optname=$optname
tmp_optarg=$OPTARG

OPTIND=$OPTIND-1
if getopts $param_pattern optname ;then
echo -e "\033[31m选项参数错误 $tmp_optname\033[0m"
exit 2
fi
OPTIND=$tmp_optind
Build_str=$tmp_optarg
;;
"v")
tmp_optind=$OPTIND
tmp_optname=$optname
tmp_optarg=$OPTARG

OPTIND=$OPTIND-1
if getopts $param_pattern optname ;then
echo -e "\033[31m选项参数错误 $tmp_optname\033[0m"
exit 2
fi
OPTIND=$tmp_optind
Version_str=$tmp_optarg
;;
"?")
echo -e "\033[31m选项错误: $OPTARG\033[0m"
exit 2
;;
":")
echo -e "\033[31m选项 $OPTARG 必须带参数\033[0m"
exit 2
;;
*)
echo -e "\033[31m参数错误\033[0m"
exit 2
;;
esac
done

# echo "Project_path = ${Project_path}"
#app文件名称
appname=$(basename ./*.xcodeproj)
#通过app文件名获得工程target名字
target_name=$(echo $appname | awk -F. '{print $1}')
#app文件中Info.plist文件路径
App_infoplist_path=${Project_path}/${target_name}/Info.plist
# 获取version值
Version_initial=$(/usr/libexec/PlistBuddy -c "print CFBundleShortVersionString" ${App_infoplist_path})
# 获取build值
Build_initial=$(/usr/libexec/PlistBuddy -c "print CFBundleVersion" ${App_infoplist_path})

if [[ ${Scheme_name} == "default" ]]; then
Scheme_name=${target_name}
fi

echo "*************************************************"
echo -e "\033[36m* 打包项目: ${target_name}\033[0m"
echo "*************************************************"
echo ''

# 将数字123转换为1.2.3的格式输出
Num_str=""
caculate(){
Parameter_num=$1;
if [[ ${Parameter_num} < 10 ]]; then
if [[ ${Num_str} == "" ]]; then
Num_str="${Parameter_num}"
else
Num_str="${Parameter_num}.${Num_str}"
fi
else
#商
quotient="$[${Parameter_num}/10]"
#余数
remainder="$[${Parameter_num}%10]"
if [[ ${Num_str} == "" ]]; then
Num_str="${remainder}"
else
Num_str="${remainder}.${Num_str}"
fi
if [[ ${quotient} > 0 ]]; then
caculate ${quotient}
fi
fi
}

#--------------------------------------------
# 处理Version版本号
#--------------------------------------------
# Version版本号自增
if [[ ${Auto_increment_version_num} == "YES" ]]; then
if [[ ${Version_str} != "custom" ]]; then
echo -e "\033[31m选项-a 与 -v 不能同时存在\033[0m"
exit 2
else
#取Version版本号
Version_str=$(/usr/libexec/PlistBuddy -c "print CFBundleShortVersionString" ${App_infoplist_path})
#把Version_str字符串根据.分割为数组
Array=($(echo $Version_str | tr '.' ' ' | tr -s ' '))
#数组长度
Array_length=$(echo ${#Array[@]})
# 获取版本号数字
Version_num=0
for (( i = 0; i < ${Array_length}; i++ )); do
index_num=${Array[$i]}
sum_num="$[10**(${Array_length}-${i}-1)]"
index_num="$[${index_num}*${sum_num}]"
Version_num="$[${Version_num}+${index_num}]"
done

Version_num="$[$Version_num+1]"
Num_str=""
caculate ${Version_num}
Version_str=${Num_str}
fi
fi

#--------------------------------------------
# 修改Version版本号以及Build号
#--------------------------------------------
if [[ ${Version_str} != "custom" ]]; then
echo "初始Version = ${Version_initial}"
/usr/libexec/Plistbuddy -c "Set CFBundleShortVersionString $Version_str" "${App_infoplist_path}"
Version_result=$(/usr/libexec/PlistBuddy -c "print CFBundleShortVersionString" ${App_infoplist_path})
echo "Version改变后 = ${Version_result}"
fi
if [[ ${Build_str} != "custom" ]]; then
echo "初始Build = ${Build_initial}"
/usr/libexec/Plistbuddy -c "Set CFBundleVersion $Build_str" "${App_infoplist_path}"
Build_result=$(/usr/libexec/PlistBuddy -c "print CFBundleVersion" ${App_infoplist_path})
echo "Build改变后 = ${Build_result}"
fi

if [ $? != 0 ]; then
/usr/libexec/Plistbuddy -c "Set CFBundleShortVersionString $Version_initial" "${App_infoplist_path}"
/usr/libexec/Plistbuddy -c "Set CFBundleVersion $Build_initial" "${App_infoplist_path}"
echo -e "\033[31m************* 修改版本号出错 **************\033[0m"
exit 2
fi

#--------------------------------------------
# 准备打包
#--------------------------------------------
ExportOptionsPlist="${Release_path}/ExportOptions.plist"
#取当前时间字符串
Now=$(date +"%Y_%m_%d_%H:%M:%S")
Project_version=$(/usr/libexec/PlistBuddy -c "print CFBundleShortVersionString" ${App_infoplist_path})
Version_output_path="${Release_path}/ArchivePath/${target_name}/${Project_version}"
IPA_path="${Version_output_path}/${Now}"
Archive_file_name="${target_name}_${Project_version}"
Archive_path="${IPA_path}/${Archive_file_name}.xcarchive"

mkdir -p "${IPA_path}"
Log_path="${IPA_path}/LogPath"

echo "************* xcodebuild clean 项目 **************"

# 清除项目
# xcodebuild clean -configuration ${Configuration} &>/dev/null
xcodebuild clean -configuration ${Configuration} >> $Log_path
# if [[ ${Edit} == "YES" ]]; then
# cd "~/Library/Developer/Xcode"
# user=$USER
# Xcode_path="/Users/${user}/Library/Developer/Xcode"
# cd ${Xcode_path}
# for i in `ls`;
# do
# if [[ ${i} == "DerivedData" ]]; then
# if [ -d "${i}" ];then
# # 删除~/Library/Developer/Xcode/DerivedData文件夹
# rm -rf ${i}
# fi
# fi
# done
# fi

revertVersionNum(){
/usr/libexec/Plistbuddy -c "Set CFBundleShortVersionString $Version_initial" "${App_infoplist_path}"
/usr/libexec/Plistbuddy -c "Set CFBundleVersion $Build_initial" "${App_infoplist_path}"
rm -rf "${IPA_path}"
files=`ls ${Version_output_path}`
if [ -z "$files" ]; then
# 该版本号下没打过包,把该版本号文件夹删除
rm -rf "${Version_output_path}"
fi
}

if [ $? = 0 ]; then
echo -e "\033[32m************* xcodebuild clean 完成 **************\033[0m"
echo ''
cd ${Project_path}
else
revertVersionNum;
echo -e "\033[31m************* xcodebuild clean 失败 **************\033[0m"
echo ''
exit 1;
fi

Workspace_name="${Project_path}/${target_name}.xcworkspace"
Project_name="${Project_path}/${target_name}.xcodeproj"
if [[ ${Edit} == "YES" ]]; then
if [[ ${Archive_type} == "workspace" ]];then
#编译workspace
xcodebuild -workspace "${Workspace_name}" -scheme "${Scheme_name}" -configuration "${Configuration}" >> $Log_path
else
#编译project
xcodebuild -configuration "${Configuration}" >> $Log_path
fi

if [ $? = 0 ]; then
echo -e "\033[32m************* xcodebuild 编译 完成 **************\033[0m"
echo ''
else
revertVersionNum;
echo -e "\033[31m************* xcodebuild 编译 失败 **************\033[0m"
echo ''
exit 1;
fi
fi

echo -e "\033[36m************* 打包 版本号 ${Project_version} **************\033[0m"
echo "************* 开始导出xcarchive文件 *************"
if [[ ${Archive_type} == "workspace" ]];then
#打包workspace
xcodebuild archive -workspace "${Workspace_name}" -scheme "${Scheme_name}" -configuration "${Configuration}" -archivePath "${Archive_path}" >> $Log_path
else
#打包project
xcodebuild archive -project "${Project_name}" -scheme "${Scheme_name}" -configuration "${Configuration}" -archivePath "${Archive_path}" >> $Log_path
fi
if [ $? = 0 ]; then
echo -e "\033[32m************* 导出xcarchive文件 完成 **************\033[0m"
echo ''
else
revertVersionNum;
echo -e "\033[31m************* 导出xcarchive文件 失败 **************\033[0m"
echo ''
exit 1;
fi

echo "************* 开始导出 ipa 文件 **************"
# xcodebuild -exportArchive -archivePath "${Archive_path}" -exportPath "${IPA_path}" -exportOptionsPlist "${ExportOptionsPlist}" &>/dev/null
xcodebuild -exportArchive -archivePath "${Archive_path}" -exportPath "${IPA_path}" -exportOptionsPlist "${ExportOptionsPlist}" >> $Log_path
if [ $? = 0 ]; then
echo -e "\033[32m************* 导出 ipa 包完成 **************\033[0m"
echo ''
else
revertVersionNum;
echo -e "\033[31m************* 导出 ipa 包失败 **************\033[0m"
echo ''
exit 1;
fi
#输出总用时
echo -e "\033[32m************* 打包完成. 耗时: ${SECONDS}s **************\033[0m"
echo ''

脚本详解

  • 开始部分,对输入参数进行了判断,脚本命令: []表示可选参数,<>表示必填参数

    1
    ./Release.sh  <Project directory name> [-w] [-s <Name>] [-e] [-d] [-a] [-b <Build number>] [-v <Version number>]
  • 第二步,判断并修改Build号以及Version号, -b <Build number> -v <Version number> ,分别取对应的值;如果是-a则Version号自增,在旧版本号的基础上+1,其中的主版本号、副版本号、发布号默认采用的是10进制规则,举例

    1.0.0 执行-a后为 1.0.1

    1.1.9 执行-a后为 1.2.0

  • 导出xcarchive文件,这里要区分是project项目,还是workspace项目,如果你使用了CocoPads管理项目,那么打包的就是workspace项目

    1
    2
    3
    4
    5
    6
    7
    if [[ ${Archive_type} == "workspace" ]];then
    #打包workspace
    xcodebuild archive -workspace "${Workspace_name}" -scheme "${Scheme_name}" -configuration "${Configuration}" -archivePath "${Archive_path}" >> $Log_path
    else
    #打包project
    xcodebuild archive -project "${Project_name}" -scheme "${Scheme_name}" -configuration "${Configuration}" -archivePath "${Archive_path}" >> $Log_path
    fi

    ${Workspace_name}是.xcworkspace文件的完整路径(${Project_name} 一样),${Scheme_name}表示项目Scheme的名称,${Configuration}有两个值:Debug和Release,${Archive_path}是生成的xcarchive文件的导出路径,>> $Log_path表示将log日志输出到Log_path文件

  • 生成ipa文件

    1
    xcodebuild -exportArchive -archivePath "${Archive_path}" -exportPath "${IPA_path}" -exportOptionsPlist "${ExportOptionsPlist}" >> $Log_path

    ${ExportOptionsPlist}指向ReleaseDir文件夹下的ExportOptions.plist文件,可以在文件内填写跟打包相关的配置
    QQ20170303-150057@2x.png

  • compileBitcode:不上架App Store,Xcode是否启用Bitcode重新编译,默认为YES。

  • method:归档类型,包括app-store、ad-hoc、
    package、enterprise、development以及developer-id。

  • uploadBitcode:上线App Store是否开启Bitcode,默认为YES。

  • uploadSymbols:上线App Store,是否开启符号序列化,这是与查crash相关的,默认为YES。

关于更多的xcodebuild指令,可以通过xcodebuild -help查看。


最后附上两张打包成功的图片

QQ20170303-151520@2x.png

QQ20170303-151638@2x.png
脚本资源👆看这里。另外补充一点,脚本打包前请先在Xcode,General—Targets—Signing中选择好对应的证书。