iOS话外篇:构建详解
1.背景
想必大多数iOS开发者不陌生,通过xcode 自带的 archive 打包,习惯GUI的同学,可能更倾向于archive打包,虽然archive打包有很多优点:包括可视化、学习门槛低、报错清晰等,但是在团队协作与日常构建中,他的弊端就显现出来了:
- 步骤多
打包前需要同步代码、打包过程中需要选择签名、打包后要归档、并选择上传包的地方(分发渠道orAppStore)
- 时间长
构建时间随着工程迭代越来越漫长,大型全量打包可能到数十分钟,archive会阻断当前研发进程,无法利用本机算力之外的其他计算集群资源,影响研效
- 灵活不佳
构建过程中可能需要对版本的build号进行自增、归档dsym文件、构建通知等,采用archive这些都可能需要手动执行
- 可靠性不佳
对于分支、代码、签名、构建方式等匹配均需靠人工维护,流程步骤多,容易造成错误,不可靠
2.构建设计
为了设计一个通用的构建脚本,需要将核心构建流程分层与归类,设计入参、使用构建环境等。程序入口如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
#通过外部传参 读取自定义参数 2:target_name
# 2 必须
coustomSettings $2
# 3 (可选) 蒲公英渠道号,拼接url
publishSettings $3
# 4 (可选)是否静默通知,有值 则不发通知
notificationSettings $4
#开始构建 1:configuration(0:DailyBuild|1:TestFlight|2:Release|3:TFInner)
#1 必须
build $1
2.1入参设计:
2.1.1参数1:构建方式
对应工程中的Configuration ,枚举值:0:DailyBuild | 1:TestFlight | 2:Release | 3:TFInner |
-
DailyBuild:用于日常构建,CI/CD
-
TestFlight:用于打灰度包
-
Release:用于打生产包
-
TFInner:用于打内部TF包
特别说明:由于,Apple的Enterprise证书($299/年)账号申请条件苛刻,通过此方案可以临时解决100台测试设备不足的问题
2.1.2 参数2:Target名称
工程的TargetName:
2.1.3 参数3(可选):渠道号
在没有使用支持iOS app分发的流水线(类似于腾讯蓝鲸(蓝盾流水线))情况下:本文例子使用蒲公英进行应用分发,为了兼容多渠道并行,用于拼接分发下载地址的url。
2.1.3.1 第三方分发:蒲公英
流水线通过API进行应用分发,下面说明,渠道号为空与非空的情况:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#其他构建包上传到pgy
echo '/// 上传 PGY 开始 IPA_PATH='${IPA_PATH}
echo '/// 上传 PGY 开始 渠道='${pgyChannel}
compileEnvironment="${buildName}:${git_log}"
if [ -z "$pgyChannel" ]; then
# 渠道空,则走默认 不指定渠道号的上传
curl -F 'file=@'${IPA_PATH} -F '_api_key='${pgyAPIKey} -F 'buildUpdateDescription='${compileEnvironment} https://www.pgyer.com/apiv2/app/upload
else
# 渠道非空,走下载地址关联 渠道
curl -F 'file=@'${IPA_PATH} -F '_api_key='${pgyAPIKey} -F 'buildUpdateDescription='${compileEnvironment} -F 'buildChannelShortcut='${pgyDownloadUrlSubfix} https://www.pgyer.com/apiv2/app/upload
fi
echo '/// 上传 PGY 结束'
2.1.3.2 扩展:阿里云OSS云存储分发
基于蒲公英的分发受限于蒲公英上行带宽与收费阈值,探索一种容灾分发方式,避免蒲公英未来服务出现异常时的健壮性。
自行分发需要用Ad-Hoc方式,遵循Apple协议:itms-services://?action=download-manifest&url=
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
#添加commit信息到二维码图片上
function add_log_commit_title_msg_to_qrcode ()
{
#二维码打上版本信息
#TODO: 这里需要安装下工具 ImageMgick [brew install ImageMgick]
title=$xcodeVersion3number.${channel_name}.$configurationName.${BuildNo}
if [ $1 = "1" ]; then
dst_line_cnt=0
export oss_source_log_arr=(`echo $source_log_str | tr '][' ' '`)
export oss_log_line_count=${#oss_source_log_arr[@]}
echo '/// oss 分发: 添加 二维码msg log_line_count='$oss_log_line_count
for(( i=0;i<${oss_log_line_count};i++))
do
single_len=$(echo -n "${oss_source_log_arr[i]}" | wc -m)
if [ -z "${oss_source_log_arr[i]}" ]; then
oss_singleStr=${oss_source_log_arr[i]};
echo '///--遍历'${oss_source_log_arr[i]}'为空文字长度:'$single_len
dst_line_cnt=$((dst_line_cnt+1))
else
line_max_len=30
ingle_cnt=$((single_len / line_max_len))
if ((ingle_cnt > 0)); then
for(( j=0;j<=${ingle_cnt};j++))
do
string=${oss_source_log_arr[i]}
single_line1=${string:(line_max_len * j):line_max_len}
echo '///--遍历' ${oss_source_log_arr[i]}'文字长度:'$single_len'超阈值阶段:第'$j'行:'$single_line1
oss_singleStr+="\n"${single_line1};
dst_line_cnt=$((dst_line_cnt+1))
done;
else
echo '///--遍历' ${oss_source_log_arr[i]}' 文字长度:'$single_len
dst_line_cnt=$((dst_line_cnt+1))
oss_singleStr+="\n"${oss_source_log_arr[i]};
fi
fi
done;
msg=$oss_singleStr
echo "/// oss render msg="$msg
contentsize_width=600
contentsize_height=$((contentsize_width + $dst_line_cnt * 25 + 40 + 40)) #第一个40是title的高度 第二个40是距离底部空间
echo "contentsize_height="$contentsize_height
#改变原图大小
convert $qrcode_path -resize ${contentsize_width}x${contentsize_width}^ -gravity center ${qrcode_path}_1.png
#变高,兼容msg
convert ${qrcode_path}_1.png -gravity north -extent ${contentsize_width}x${contentsize_height} ${qrcode_path}_2.png
#汉字字体
chn_tff_path=$build_ext_path/publish_res/chn.ttf
#渲染title
convert ${qrcode_path}_2.png -gravity north -pointsize 30 -font $chn_tff_path -fill black -annotate +0+${contentsize_width} $title ${qrcode_path}_3.png
#渲染msg
convert ${qrcode_path}_3.png -gravity north -pointsize 20 -font $chn_tff_path -fill black -annotate +0+$((${contentsize_width}+40)) $msg ${qrcode_path}_4.png
qrcode_path_with_msg=${qrcode_path}_4.png
else
#没有传 1 不需要msg,只给title
convert $qrcode_path -gravity south -pointsize 10 -fill gray -annotate +0+0 $qrcode_print_msg $qrcode_path_with_msg
echo '/// oss 分发: 添加 二维码title='${title}
fi
}
#发布到阿里云OSS
function upload_ipa_to_aliyunOSS ()
{
#更新plist的键值对:
manifest_path=${exportPath}/manifest.plist
echo '/// oss 分发: manifest_path='${manifest_path}
oss_bucket="cbs-apps-test"
oss_platform="iOS"
oss_bucket_path=$oss_platform/${TARGET_NAME}/$configurationName/${channel_name}/${BuildNo}/${TARGET_NAME}_${channel_name}_$configurationName_${BuildNo}
oss_path="oss://$oss_bucket/$oss_bucket_path"
oss_url="https://$oss_bucket.oss-cn-beijing.aliyuncs.com/$oss_bucket_path"
oss_ipa_upload_url=$oss_path".ipa"
oss_ipa_url=$oss_url".ipa"
#icon下载地址配置
oss_icon_url="https://cbs-apps-test.oss-cn-beijing.aliyuncs.com/iOS/${TARGET_NAME}/icon/icon.png"
oss_icon_fs_url="https://cbs-apps-test.oss-cn-beijing.aliyuncs.com/iOS/${TARGET_NAME}/icon/icon_fs.png"
#修改ipa下载地址
/usr/libexec/PlistBuddy -c "Set :items:0:assets:0:url $oss_ipa_url" $manifest_path
#修改icon地址
/usr/libexec/PlistBuddy -c "Set :items:0:assets:1:url $oss_icon_url" $manifest_path
#修改icon full size地址
/usr/libexec/PlistBuddy -c "Set :items:0:assets:2:url $oss_icon_fs_url" $manifest_path
#传ipa
ossutil cp ${IPA_PATH} $oss_ipa_upload_url -f
#传manifest
oss_manifest_extension="plist"
oss_manifest_upload_url=$oss_path"."$oss_manifest_extension
oss_manifest_url=$oss_url"."$oss_manifest_extension
ossutil cp ${manifest_path} $oss_manifest_upload_url -f
#上述做归档后,生成下载二维码
download_url="itms-services://?action=download-manifest&url="$oss_manifest_url
qrcode_path=${exportPath}/${TARGET_NAME}_${BuildNo}.png
#TODO: 这里需要安装下工具 qrencode [brew install qrencode]
qrencode -o $qrcode_path $download_url
#将二维码上传到oss
oss_qrencode_extension="png"
oss_qrencode_upload_url=$oss_path"."$oss_qrencode_extension
qrcode_path_with_msg=${qrcode_path}_with_msg
#如果需要二维码打上log信息则调用
need_msg=1
add_log_commit_title_msg_to_qrcode $need_msg
#上传到阿里云oss
ossutil cp ${qrcode_path_with_msg} $oss_qrencode_upload_url -f
#二维码的url 通知出来
export oss_app_download_qrcode_url=$oss_url"."$oss_qrencode_extension
echo '/// oss 分发: 二维码的oss_app_download_qrcode_url='${oss_app_download_qrcode_url}
}
2.1.4 参数4(可选):是否静默通知
本文例子通过钉钉API发送通知到钉钉群,默认不填发送,有值则不发通知,通知内容整合双分发方案:
下面代码为发送通知的逻辑,请将相应的变量替换成自己的参数
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
modify_msg=${git_log}
ProductName=${xcodeProductName}
downloadPrefix=${PrefixID}
channel=${pgyChannel}
title=${buildName}
#采用直接 拼 channel后缀
shortUrl=${downloadPrefix}${ProductName}${channel}
#蒲公英下载地址
pgy_app_donwnload_url="https://www.pgyer.com/${shortUrl}"
#oss下载地址
if [ -z "$oss_app_download_qrcode_url" ]; then
#为空则使用pgy
echo "通知:oss_app_download_qrcode_url 为空则使用pgy:'${app_donwnload_url}'"
app_donwnload_url=$pgy_app_donwnload_url
else
#不为空使用 pgy + oss
echo "通知:oss_app_download_qrcode_url 不为空使用 pgy + oss则使用pgy + oss:'${app_donwnload_url}' and '$oss_app_download_qrcode_url'"
app_donwnload_url=${pgy_app_donwnload_url}'\n'oss下载地址:$oss_app_download_qrcode_url
fi
# app_donwnload_url=$oss_app_download_qrcode_url
echo "通知 COME下载地址:'${app_donwnload_url}'"
curl 'https://oapi.dingtalk.com/robot/send?access_token='${access_token} \
-H 'Content-Type: application/json' \
-d '{"msgtype": "text",
"at": {
"atMobiles":[
"这"
"里"
"是"
"钉"
"钉"
"账"
"号"
],
"isAtAll": false
},
"text": {
"content": "'${ProductName}' App更新:'${title}'\n'${modify_msg}'\n下载地址:https://www.pgyer.com/'${app_donwnload_url}'" }
}'
echo '///-------------------------------------------------------'
echo '/// 发送通知 '
echo '///-------------------------------------------------------'
2.2 流程设计:
2.2.1 构建前:
- 入参设置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#入参设置
function coustomSettings()
{
#需要打包的target
if [ -z "$1" ]; then
echo "error: TARGET_NAME 不能为空 退出"
exit 1
else
export TARGET_NAME=$1
fi
#工程所在目录,如果在根目录则为${WORKSPACE}
if [ -z "$2" ]; then
export projectDir=${TARGET_NAME}
else
export projectDir=$2
fi
# export projectDir=YPCProject
echo "TARGET_NAME="$TARGET_NAME
echo "projectDir="$projectDir
}
- 发布设置:
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
#发布及归档的相关的设置
function publishSettings()
{
if [ -z "$1" ]; then
echo '///渠道号入参 为空'
export pgyChannel=Dev
else
echo '///渠道号入参 不为空'
export pgyChannel=$1
fi
echo "////渠道号"${pgyChannel}
#声明蒲公英分发调用APi的pgyAPIKey
export pgyAPIKey="pgyAPIKey"
#钉钉发送通知机器人的access_token
export access_token="access_token"
#AppStore的token
export appStore_apiIssuer="appStore_apiIssuer"
export appStore_apikey="appStore_apikey"
#bugly的token
export bugly_appid="bugly_appid"
export bugly_appkey="bugly_appkey"
}
- 通知设置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#通知相关的设置(pgy渠道及apikey)
function notificationSettings()
{
notificationSettings=$1
if [ ${notificationSettings} = "1" ]; then
echo '///静默入参 不为空'
export notificationSilent=$notificationSettings
else
echo '///静默入参 为空 或者不为1 不发通知'
fi
echo "////静默通知="${notificationSilent}
}
-
参数获取:
通过xcode工具获取工程项目自带的参数
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
#获取工程配置信息
function initInfoPlistParms()
{
#Plist先写死。后续可考虑改成搜索
cd ${CurDir}/${projectDir}
versionKey="MARKETING_VERSION"
bundleID="PRODUCT_BUNDLE_IDENTIFIER"
bundleNo="CURRENT_PROJECT_VERSION"
platformName="PLATFORM_NAME"
productName="PRODUCT_NAME"
#将要获取的key设置成数组
buildSettingsGrepKeyArray=($versionKey $bundleID $bundleNo $platformName $productName)
buildSettingsGrepKey=""
for(( i=0;i<${#buildSettingsGrepKeyArray[@]};i++))
do
echo "【获取Plist参数】遍历 idx="$i "value=" ${buildSettingsGrepKeyArray[i]}
if [ -z "$buildSettingsGrepKey" ]; then
buildSettingsGrepKey=${buildSettingsGrepKeyArray[i]};
else
buildSettingsGrepKey+="\|"${buildSettingsGrepKeyArray[i]};
fi
done;
#将数组拼接成 grep 参数
echo "【获取Plist参数】key:"$buildSettingsGrepKey
#-w 防止模糊匹配 比如 PRODUCT_NAME 和 FULL_PRODUCT_NAME
buildSettings=`xcodebuild -showBuildSettings -target ${TARGET_NAME} | grep -w $buildSettingsGrepKey`
echo "【获取Plist参数】result:"$buildSettings
#取值数组
buildSettingsGrepValueArray=[]
for(( i=0;i<${#buildSettingsGrepKeyArray[@]};i++))
do
#右截断
value=${buildSettings#*${buildSettingsGrepKeyArray[i]} = }
for(( j=0;j<${#buildSettingsGrepKeyArray[@]};j++))
do
#由于grep顺序不保证,遍历所有key进行右截断
value=${value%${buildSettingsGrepKeyArray[j]}*}
done;
value=`echo ${value} | sed 's/\ //g'`
echo "【获取Plist参数】i="$i "value="$value"。"
#填回取值数组
buildSettingsGrepValueArray[i]=${value}
done;
#定义全部变量
export xcodeVersion3number=${buildSettingsGrepValueArray[0]}
echo "【获取Plist参数】版本号:"$xcodeVersion3number"。"
export xcodeBundleID=${buildSettingsGrepValueArray[1]}
echo "【获取Plist参数】包名:"$xcodeBundleID"。"
export xcodebundleNo=${buildSettingsGrepValueArray[2]}
echo "【获取Plist参数】build号:"$xcodebundleNo"。"
export xcodePlatformName=${buildSettingsGrepValueArray[3]}
echo "【获取Plist参数】PlatformName:"$xcodePlatformName"。"
export xcodeProductName=${buildSettingsGrepValueArray[4]}
echo "【获取Plist参数】ProductName:"$xcodeProductName"。"
cd ..
}
- 构建号设置:
设置构建号,可以避免每次构建的build重复,导致在appstore后台管理构建时的紊乱,也便于追溯构建代码版本等,而如果可以通过自动化的方式进行构建号的设置就更好了,这里提供一种设置思路:获取当前日期+流水线构建号(自增),可以构建流水线内保证全局唯一:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#自动(自增)修改构建号
function modify_build_no()
{
cd ${CurDir}/${projectDir}
export BuildNo=`date +%Y%m%d`
#BuildNo,可以采用拼接日期+构建方式+流水线自增号来识别构建包的来源(构建方式:dailybuild、TestFlight、Release、TFInner)
BuildNo=${BuildNo}.$1.${SYS_PIPELINE_BUILD_NUMBER}
# plist_path=${CurDir}/${projectDir}/${projectDir}/Info.plist
echo "【获取Plist参数】路径:"$plist_path
echo "【设置Plist参数】build:"$BuildNo
# /usr/libexec/PlistBuddy -c "Set :CFBundleVersion ${BuildNo}" ${plist_path}
agvtool new-version -all ${BuildNo}
cd -
}
2.2.2 构建中:
-
执行构建:
在完成了所有必要的构建前的设置之后,可以进行构建流程:
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
#执行构建
function build()
{
if [ -z "$1" ]; then
echo "error: configurationName 不能为空 退出"
exit 1
fi
#定义项目前缀,生成文件时使用
export PrefixID=PSC_
#设置根目录
export CurDir=`pwd`
#进行打包的schemeName
export schemeName=dailybuildipa
#指定编译 配置 configuration
configurationNameArray=(DailyBuild TestFlight Release TFInner)
export configurationName=${configurationNameArray[$1]}
#指定编译脚本路径
#TODO: 这里需要匹配下当前脚本所在的目录
export buildsh_dir=${CurDir}/PSC_iOS_Build_SH
#设置plist参数
modify_build_no $1
#读取plist参数
initInfoPlistParms
#执行构建
bash $buildsh_dir/build_sh.sh
}
构建流程设计包括:
- 设置环境变量
-
设置内部变量
-
初始化环境
-
mPaas设置(可选)
-
执行pod
-
构建工具适配(可选)
- 执行构建
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
function build()
{
#设置内部环境变量
initDevopsEnv
#设置内部环境变量
createInternalEnv
#初始化环境
init_env
#执行pod之前,先配置mPaas在当前环境下的配置文件
mPaaSConfigSet
#执行pod
pod_install
#fix xcode 14.3 cocoapods bug
fiXCode143
#执行构建
buildProject
#执行构建归档操作
collection
#上传(发布)安装包,供下载安装
bash $buildsh_dir/$build_ext_fold/$publish_sh
if [ ${configurationName} = "DailyBuild" ]; then
#通知
bash $buildsh_dir/$build_ext_fold/$notification_sh
fi
}
构建具体的思路可以参考源码,这里不再赘述
2.2.3 构建后:
- 归档
为什么要归档:
归档主要是为了对构建的安装包IPA及dSYM进行一一对应,由于dailybuild都是在团队内分发,因此这边考虑只有在testflight及appstore时才进行归档。
此外;如果使用bugly进行崩溃收集,bugly已经不支持手动传dSYM,需要在构建时一并实现。
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
function upload_dSYM()
{
#这里可以根据各自情况,存在dSYM文件
}
function upload_dSYM_to_bugly()
{
export p=${dSYM_dict_PATH}
#根据不同的应用进行设置
if [ ${TARGET_NAME} = "PSCiOS" ]; then
bugly_appid=xxx
bugly_appkey=yyy
fi
echo '/// 上传 dsym bugly '${v}'pkgName='${pkgName}
#切换java 版本
java -version
jdk8
java -version
bugly_jar_path=$buildsh_dir/$build_ext_fold/collection_res/buglyqq-upload-symbol/buglyqq-upload-symbol.jar
java -jar ${bugly_jar_path} -appid ${bugly_appid} -appkey ${bugly_appkey} -bundleid ${pkgName} -version ${v} -platform IOS -inputSymbol ${p}
}
function collection()
{
export v=${xcodeVersion3number}.${BuildNo}
export pkgName=${xcodeBundleID}
#上传到bugly符号表
upload_dSYM_to_bugly
#上传dSYM
upload_dSYM
}
#归档
collection
- 发布
归档后,对安装包进行分发,分发也分dailybuild与release:
dailybuild:目前采用发布到蒲公英渠道:2.1.3 参数3(可选):渠道号
Release:发布到AppStore
1
2
3
4
5
6
7
8
if [ ${configurationName} = "Release" ] || [ ${configurationName} = "TestFlight" ] || [ ${configurationName} = "TFInner" ]; then
#TFInner 、TestFlight & appstore 上传到appstore
echo '/// 上传App Store 开始'
xcrun altool --validate-app -f $IPA_PATH --type ios --apiKey $appStore_apikey --apiIssuer $appStore_apiIssuer
xcrun altool --upload-app -f $IPA_PATH --type ios --apiKey $appStore_apikey --apiIssuer $appStore_apiIssuer
echo '/// 上传App Store 结束'
fi
- 通知
通知可以有很多种方式,由于笔者团队使用钉钉,所以这里举个钉钉机器人例子:2.1.4 参数4(可选):是否静默通知