三味猪屋

iOS的自动化打包

虽说网上关于iOS的持续集成的文章数不胜数,还是那句话:不经风雨是见不到彩虹的,不亲身经历,你根本体会不到踩坑的滋味是多么酸爽。酸爽的滋味其实并不是从iOS的自动化打包开始,从构建我们产品2.0版本就已经有尝不尽的酸爽味道,如:使用cocoapods打造智能化的iOS组件化方案,使用AFNetworking打造面向对象的网络组件并将AFNetworking升级到2.0、3.0,使用Realm替换SQLite的本地持久化方案,关于AOP面向切片编程实现的无埋点日志处理方案,使用Mantle打造智能化解析时与Realm如何结合,等等。以后会慢慢再总结吧。

我一直主张好钢应该用在刀刃上,如果将一个iOS研发人员一天当中有一个小时,哪怕半个小时去做打包的事情,我觉得都是极大的浪费。由于我们项目的特性或者吐槽点说是由于不规范,打一个安装包要牵扯到的资源有:项目部提供打包时APP名称调研表格、项目助理提供打包时配置信息表格(微信key、支付宝、微博、百度、友盟、不同tag的版本号、主题色)、产品UI组提供打包的图片资源、服务端提供打包时接口配置表格(接口地址、HOSID等)、服务端提供打包时开发者信息表格(公钥、私钥、签名信息等),而且由于每个部门使用不同的svn库,如果打包,单单准备这些资料恐怕就得一两个小时(无力吐槽点不是要找的配置信息多,而是有时候这些信息根本不全),这些太浪费时间了。

现在加上苹果对四大领域审核的全面收紧,导致我们之前在一个苹果账号下挂载多个APP的路走不通了,现在几乎是一个APP就需要一个苹果账号,这样的无技术含量且重复性的劳动量占据了开发人员很多的时间成本。

好了,这里我们只说面包,不谈爱情,也就是不谈理论,只谈实践。我们的自动化打包方案:Jenkins+python+shell+fastlane。

本章就只说说shell+fastlane

fastlane是一组工具套件,旨在实现iOS应用发布流程的自动化。

环境搭建:
按照之前的文章,在Mac XOS上
1、安装brew
2、安装ruby
3、通过gem安装fastlane

自动化打包的流程和思路:
1、利用python根据配置的版本号找到对应的代码tag。并将该路径传入shell build script。
2、根据入参scheme找到xcodeproj的路径和Info.plist的路径。
3、重置Info.plist,如:bundleid、version、displayName、微信key等。
4、替换资源文件,对某些资源文件进行加密处理。
5、cocoapods生成xcworkspace。
6、安装证书,并获取到证书codesign-identity。
7、添加用户名密码到keychain。
8、下载mobileprovision配置文件。
9、根据mobileprovision配置文件生成Entitlements,从而读取UUID和teamID。
10、重置exportPlist中teamID和exprotMethod项为打包做准备。
11、编译并打包。

亟待解决的问题:
1、钥匙串访问权限
2、多个证书的选择
3、账户密码添加到keychain
4、证书安装
5、证书codesign-identity的读取
6、mobileprovision文件下载问题
7、teamID获取问题
8、配置文件UUID获取问题
9、exprotPlist配置项问题
10、cocoapods中podfile文件动态修改问题

1、security命令

1
2
## unlock keychain db
$ security unlock-keychain -p "820710" $HOME/Library/Keychains/login.keychain

1
2
## 生成自定义钥匙串
$ security create-keychain -p "" $KEYCHAIN
1
2
## 删除自定义钥匙串
$ security delete-keychain $KEYCHAIN
1
2
## 导入证书到钥匙串
$ security import $CERTPATH -k $KEYCHAIN -P "xxxx" -T /usr/bin/codesign
1
2
3
## 钥匙串中查找证书名称
CERTNAME=`security find-certificate -p $KEYCHAIN|openssl x509 -noout -subject -nameopt oneline,-esc_msb|awk -F"CN \= " '{print $2'}|awk -F", OU \=" '{print $1}'|sed s@\"@@g`
printf "%s\n" "CERTNAME:$CERTNAME"
1
2
3
## 转义字符串
## 转义单引号需注意:'\''代表单引号,其他直接书写,如:&%[]{}()等。
# echo "iPhone Distribution: YUYAO PEOPLE'S HOSPITAL OF ZHEJIANG PROVINCE (xxxxx)"|sed -e 's#[]{}()&% '\''[]#\\&#g'

参考:
https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man1/security.1.html

2、PlistBuddy命令

1
2
3
## 根据mobileprovision文件生成Entitlements.plist
## 这个命令以后在签名也会提到
$ /usr/libexec/PlistBuddy -x -c "Print Entitlements" /dev/stdin <<< $(security cms -D -i xxx.mobileprovision) >Entitlements.plist

1
2
## 读取plist项
$ /usr/libexec/PlistBuddy -c "print CFBundleShortVersionString" ${infoPlistPath}
1
2
## 设置plist已有的项
$ /usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier $str" ${infoPlistPath}
1
2
## 添加新的项
$ /usr/libexec/PlistBuddy -c "Add :CFBundleURLTypes array" $infoPlistPath
1
2
## 删除字段项
$ /usr/libexec/PlistBuddy -c "Delete :CFBundleURLTypes" $infoPlistPath

参考:
https://developer.apple.com/legacy/library/documentation/Darwin/Reference/ManPages/man8/PlistBuddy.8.html

fastlane-credentias用法:

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
#!/bin/sh
#--------------------------------------------
# 功能:操作keychain
# 使用说明: 在keychain中保存或删除账户密码
# 作者:luohs
# E-mail:luohuasheng0225@gmail.com
#--------------------------------------------
## $1:username, $2:password
function func_fastlaneCredentiasAddToKeychain() {
printf "%s\n" "fastlane credentias add"
local username=$1
local password=$2
printf "%s\n" "username: $username"
printf "%s\n" "password: $password"
if [ -z "$username" ]
then
printf "%s\n" "~~~~~~~username未知!~~~~~~~~~~~"
exit -1
fi
if [ -z "$password" ]
then
printf "%s\n" "~~~~~~~password未知!~~~~~~~~~~~"
exit -1
fi
fastlane fastlane-credentials \
add \
--username "$username" \
--password "$password"
}
## $1:username
function func_fastlaneCredentiasRemoveFromKeychain() {
printf "%s\n" "fastlane credentias remove"
local username=$1
printf "%s\n" "username: $username"
if [ -z "$username" ]
then
printf "%s\n" "~~~~~~~username未知!~~~~~~~~~~~"
exit -1
fi
fastlane fastlane-credentials \
remove \
--username "$username"
}

fastlane-sigh用法:
查看官方使用帮助:

根据官方使用帮助编写代码:

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
#!/bin/sh
#--------------------------------------------
# 功能:download mobileprovision file
# 使用说明: download <input appleID> <input appID> <input mobileprovision file name> <input mobileprovision file path>
# 作者:luohs
# E-mail:luohuasheng0225@gmail.com
#--------------------------------------------
function func_fastlaneSighDownloadProvisionProfile() {
local appleID="$1"
local appID="$2"
local provisionProfileName="$3"
local provisionProfileOutput="$4"
local sighMethod="$5"
if [ -z "$3" ]
then
provisionProfileName="fastlane-sigh".mobileprovision
else
provisionProfileName="$3".mobileprovision
fi
printf "%s\n" "sighMethod: $sighMethod"
printf "%s\n" "appleID: $appleID"
printf "%s\n" "appID: $appID"
printf "%s\n" "provisionProfileName: $provisionProfileName"
printf "%s\n" "provisionProfileOutput: $provisionProfileOutput"
fastlane sigh \
${sighMethod} \
--force false \
--skip_install false \
--skip_certificate_verification \
--username "${appleID}" \
--app_identifier "${appID}" \
--filename "${provisionProfileName}" \
--output_path "${provisionProfileOutput}"
if [ -f ${provisionProfileOutput}/${provisionProfileName} ]
then
printf "%s\n" "provisionProfile已下载"
printf "%s\n" "路径:$provisionProfileOutput"
printf "%s\n" "名称:$provisionProfileName"
provisionProfilePath="${provisionProfileOutput}/${provisionProfileName}"
else
printf "%s\n" "provisionProfile下载失败!"
fi
}

踩坑:
1、${sighMethod} 此参数为””时,创建或下载production配置文件,如果需要develop配置文件时,则需要配置成”development”。
2、–force true时,无论开发者账号上是否存在可用的配置文件都会重新生成一个。
3、–skip_install true时只下载配置文件,不安装。一定要安装才能打包,也就是必须要设置为false。

fastlane-gym用法:
查看官方使用帮助:


根据官方使用帮助编写代码:

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
#!/bin/sh
#--------------------------------------------
# 功能:fastlane gym
# 使用说明: gym
# 作者:luohs
# E-mail:luohuasheng0225@gmail.com
#--------------------------------------------
## $1:xcodeprojName, $2:codesign, $3:teamid, $4:provisionProfileSpecifier, $5:buildConfigration, $6:exportMethod, $7:exportOptionsPlistPath, $8:archiveOutput,
function func_fastlaneGymBuild() {
printf "%s\n" "fastlane gym"
#生成安装包名称
local xcodeprojName=$1
local codesign=$2
local teamid=$3
local provisionProfileSpecifier=$4
local buildConfigration=$5
local exportMethod=$6
local exportOptionsPlistPath=$7
local archiveOutput=$8
local archiveName=$(func_commonGetAppName)_V$(func_commonGetAppVersion)_`date +%Y%m%d%H%M%S`
local archivePath="$(func_commonGetTempPath)/${archiveName}.xcarchive"
#local archivePath="$archiveOutput/${archiveName}.xcarchive"
# xcrun xcodebuild -list -workspace ${project_name}.xcworkspace
# 解决Couldn't find specified scheme 'xxxx'.报错,需要打开工程去 mark scheme 'xxxx' Shared
#open ${xcodeprojName}.xcworkspace
sleep 1
local codesignEscapeString=$(func_commonEscapeString "$codesign")
printf "%s\n" "xcodeprojName: $xcodeprojName"
printf "%s\n" "codesign: $codesign"
printf "%s\n" "teamid: $teamid"
printf "%s\n" "provisionProfileSpecifier: $provisionProfileSpecifier"
printf "%s\n" "buildConfigration: $buildConfigration"
printf "%s\n" "exportMethod: $exportMethod"
printf "%s\n" "exportOptionsPlistPath: $exportOptionsPlistPath"
printf "%s\n" "archiveOutput: $archiveOutput"
printf "%s\n" "codesignEscapeString: $codesignEscapeString"
# build
fastlane gym \
--workspace ${xcodeprojName}.xcworkspace \
--scheme ${xcodeprojName} \
--clean \
--include_symbols true \
--configuration ${buildConfigration} \
--xcargs "PROVISIONING_PROFILE='${provisionProfileSpecifier}' PROVISIONING_PROFILE_SPECIFIER='${provisionProfileSpecifier}' DEVELOPMENT_TEAM='${teamid}' PRODUCT_BUNDLE_IDENTIFIER='$(func_commonGetBundleIdentifier)'" \
--export_method ${exportMethod} \
--archive_path ${archivePath} \
--codesigning_identity "${codesign}" \
--export_options ${exportOptionsPlistPath} \
--output_directory ${archiveOutput} \
--output_name "${archiveName}"
}


踩坑:
1、xcode8以后--xcargs项中需要增加PROVISIONING_PROFILE_SPECIFIER='${provisionProfileSpecifier}'

1
--xcargs "PROVISIONING_PROFILE='${provisionProfileSpecifier}' PROVISIONING_PROFILE_SPECIFIER='${provisionProfileSpecifier}' DEVELOPMENT_TEAM='${teamid}' CODE_SIGNING_IDENTITY='${codesign}' PRODUCT_BUNDLE_IDENTIFIER='$(func_commonGetBundleIdentifier)'"

2、当CODE_SIGNING_IDENTITY字符串中有特殊符号时,如:iPhone Distribution: YUYAO PEOPLE'S HOSPITAL OF ZHEJIANG PROVINCE (xxxxx),无论是否对CODE_SIGNING_IDENTITY进行特殊字符转义都编译不成功。
a、使用非转义的CODE_SIGNING_IDENTITY:iPhone Distribution: YUYAO PEOPLE'S HOSPITAL OF ZHEJIANG PROVINCE (xxxxx)
编译如下:


b、使用转义的CODE_SIGNING_IDENTITY:iPhone\ Distribution:\ YUYAO\ PEOPLE\'S\ HOSPITAL\ OF\ ZHEJIANG\ PROVINCE\ \(xxxxx\)
编译如下:


c、最终解决,去掉--xcargs中的CODE_SIGNING_IDENTITY项。

1
--xcargs "PROVISIONING_PROFILE='${provisionProfileSpecifier}' PROVISIONING_PROFILE_SPECIFIER='${provisionProfileSpecifier}' DEVELOPMENT_TEAM='${teamid}' PRODUCT_BUNDLE_IDENTIFIER='$(func_commonGetBundleIdentifier)'"

编译如下:


正常编译且签名成功!

fastlane-deliver用法:

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
#!/bin/sh
#--------------------------------------------
# 功能:上传ipa到iTunesconnect
# 使用说明: upload <input appleID> <input teamid> <input ipaPath>
# 作者:luohs
# E-mail:luohuasheng0225@gmail.com
#--------------------------------------------
## upload to iTunesConnect $1:appleID, $2:teamid, $3:ipaPath
function func_fastlaneDeliverUpload() {
printf "%s\n" "fastlane pilot"
local appleID=$1
local teamid=$2
local ipaPath=$3
printf "%s\n" "appleID: $appleID"
printf "%s\n" "teamid: $teamid"
printf "%s\n" "ipaPath: $ipaPath"
if [ -z "$appleID" ]
then
printf "%s\n" "~~~~~~~appleID未知!~~~~~~~~~~~"
exit -1
fi
if [ -z "$teamid" ]
then
printf "%s\n" "~~~~~~~teamid未知!~~~~~~~~~~~"
exit -1
fi
if [ -z "$ipaPath" -o ! -f "$ipaPath" ]
then
printf "%s\n" "~~~~~~~ipaPath未知!~~~~~~~~~~~"
exit -1
fi
fastlane deliver \
--username "${appleID}" \
-f \
--force \
--skip_screenshots \
--skip_metadata \
-b "${teamid}" \
--ipa "${ipaPath}"
}

fastlane-pilot用法:

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
#!/bin/sh
#--------------------------------------------
# 功能:upload to testflight
# 使用说明: <input appleID> <input bundleIdentifier> <input ipaPath>
# 作者:luohs
# E-mail:luohuasheng0225@gmail.com
#--------------------------------------------
## upload to testflight $1:appleID, $2:bundleIdentifier, $3:ipaPath
function func_fastlanePilotUpload() {
printf "%s\n" "fastlane pilot"
local appleID=$1
local bundleIdentifier=$2
local ipaPath=$3
printf "%s\n" "appleID: $appleID"
printf "%s\n" "bundleIdentifier: $bundleIdentifier"
printf "%s\n" "ipaPath: $ipaPath"
if [ -z "$appleID" ]
then
printf "%s\n" "~~~~~~~appleID未知!~~~~~~~~~~~"
exit -1
fi
if [ -z "$bundleIdentifier" ]
then
printf "%s\n" "~~~~~~~bundleIdentifier未知!~~~~~~~~~~~"
exit -1
fi
if [ -z "$ipaPath" -o ! -f "$ipaPath" ]
then
printf "%s\n" "~~~~~~~ipaPath未知!~~~~~~~~~~~"
exit -1
fi
fastlane pilot \
upload \
--username "${appleID}" \
--app_identifier "${bundleIdentifier}" \
--changelog "Beta版本测试内容主要涵盖预约挂号、医院导航等功能。" \
--beta_app_description "构建版本为Beta版本供测试人员进行测试。" \
--beta_app_feedback_email "luohs@hsyuntai.com" \
--ipa "${ipaPath}" \
--skip_waiting_for_build_processing true
}

最后附上整个CI工程的截图: