關(guān)于 iOS 自動(dòng)化打包的一點(diǎn)看法
如果你曾經(jīng)試過做多 target 的項(xiàng)目,到了測(cè)試人員要測(cè)試包的時(shí)候,你就會(huì)明白什么叫“生不如死”。雖然 Xcode 打包很方便,但是當(dāng)你機(jī)械重復(fù)打 N 次包的時(shí)候,就會(huì)覺得這純粹是浪費(fèi)時(shí)間的工作。所以這時(shí)候自動(dòng)化打包就顯得尤為重要(其實(shí)就算只有一個(gè) target,就算使用 Xcode 打包很方便,也應(yīng)該構(gòu)建自動(dòng)化打包,因?yàn)槟憧梢怨?jié)省大量時(shí)間)。
構(gòu)建自動(dòng)化打包腳本
xcodebuild
使用 xcodebuild -h
來看看 xcodebuild 到底是干啥的
Usage: xcodebuild [-project <projectname>] [[-target <targetname>]...|-alltargets] [-configuration <configurationname>] [-arch <architecture>]... [-sdk [<sdkname>|<sdkpath>]] [-showBuildSettings] [<buildsetting>=<value>]... [<buildaction>]...
xcodebuild [-project <projectname>] -scheme <schemeName> [-destination <destinationspecifier>]... [-configuration <configurationname>] [-arch <architecture>]... [-sdk [<sdkname>|<sdkpath>]] [-showBuildSettings] [<buildsetting>=<value>]... [<buildaction>]...
xcodebuild -workspace <workspacename> -scheme <schemeName> [-destination <destinationspecifier>]... [-configuration <configurationname>] [-arch <architecture>]... [-sdk [<sdkname>|<sdkpath>]] [-showBuildSettings] [<buildsetting>=<value>]... [<buildaction>]...
xcodebuild -version [-sdk [<sdkfullpath>|<sdkname>] [<infoitem>] ]
xcodebuild -list [[-project <projectname>]|[-workspace <workspacename>]] [-json]
xcodebuild -showsdks
xcodebuild -exportArchive -archivePath <xcarchivepath> -exportPath <destinationpath> -exportOptionsPlist <plistpath>
xcodebuild -exportLocalizations -localizationPath <path> -project <projectname> [-exportLanguage <targetlanguage>...]
xcodebuild -importLocalizations -localizationPath <path> -project <projectname>
這里我只截取了 usage 部分,option 部分太多沒有截取。
這里介紹幾條畢竟常用的命令
1. xcodebuild -list ...
xcodebuild -list [[-project <projectname>]|[-workspace <workspacename>]] [-json]
usage: 輸出 project 中的 targets 和 configurations,或者 workspace 中 schemes。
-project
和 -workspace
是輸出指定內(nèi)容,不輸入默認(rèn)輸出當(dāng)前目錄下。-json
是以 json 格式輸出。
example:
$ xcodebuild -list
Information about project "XX":
Targets:
XX
XXTests
Build Configurations:
Debug
Release
If no build configuration is specified and -scheme is not passed then "Release" is used.
Schemes:
XX
2. xcodebuild -project ...
xcodebuild [-project <projectname>] [[-target <targetname>]...|-alltargets] [-configuration <configurationname>] [-arch <architecture>]... [-sdk [<sdkname>|<sdkpath>]] [-showBuildSettings] [<buildsetting>=<value>]... [<buildaction>]...
usage:
-project
: 指定 project 名字,默認(rèn)首個(gè) project。
-target
: 指定對(duì)應(yīng)的 target ,默認(rèn)首個(gè) target。
-configuration
: 選擇Debug 或 Release,默認(rèn) Release,當(dāng)然如果你有自定義的配置的,就應(yīng)該選你配置的,上面 -list
中有輸出。
-showBuildSettings
: 顯示工程的配置。
<buildsetting>=<value>
: 修改工程的配置文件。
buildaction ...
: 如下,默認(rèn)為 build
Specify a build action (or actions) to perform on the target.
Available build actions are:
build Build the target in the build root (SYMROOT). This is
the default build action.
installsrc Copy the source of the project to the source root
(SRCROOT).
install Build the target and install it into the target's
installation directory in the distribution root
(DSTROOT).
clean Remove build products and intermediate files from the
build root (SYMROOT).
example:
$ xcodebuild -project 你的項(xiàng)目名字.xcodeproj -target 你的 target 名字 -configuration release
這行命令表示編譯 xx.xcodeproj 的 xx target。在 terminal 中會(huì)看到編譯過程,如果成功最后會(huì)輸出 ** BUILD SUCCEEDED **
。最后會(huì)在當(dāng)前目錄下生成 build/Release-iphoneos/xx.app
$ xcodebuild -project 你的項(xiàng)目名字.xcodeproj -target 你的 target 名字 -configuration release -showBuildSettings
這行命令使用 -showBuildSettings
是不會(huì) build 項(xiàng)目的,只是輸出工程的配置。這里輸出的的內(nèi)容有(內(nèi)容過多,只截取部分)
Build settings for action build and target XX:
ACTION = build
AD_HOC_CODE_SIGNING_ALLOWED = NO
ALTERNATE_GROUP = staff
ALTERNATE_MODE = u+w,go-w,a+rX
ALTERNATE_OWNER = TsuiYuenHong
ALWAYS_EMBED_SWIFT_STANDARD_LIBRARIES = NO
ALWAYS_SEARCH_USER_PATHS = NO
ALWAYS_USE_SEPARATE_HEADERMAPS = NO
APPLE_INTERNAL_DEVELOPER_DIR = /AppleInternal/Developer
APPLE_INTERNAL_DIR = /AppleInternal
APPLE_INTERNAL_DOCUMENTATION_DIR = /AppleInternal/Documentation
APPLE_INTERNAL_LIBRARY_DIR = /AppleInternal/Library
APPLE_INTERNAL_TOOLS = /AppleInternal/Developer/Tools
APPLICATION_EXTENSION_API_ONLY = NO
APPLY_RULES_IN_COPY_FILES = NO
ARCHS = armv7 arm64
...
如果要修改配置文件,就直接最命令最后加上你要修改的內(nèi)容。
例如在這行命令最后加上指定證書
$ xcodebuild -project 你的項(xiàng)目名字.xcodeproj -target 你的 target 名字 -configuration release PROVISIONING_PROFILE="你證書的id"
其中的字段是上面 -showBuildSettings
顯示的字段,也可以看官網(wǎng)介紹
3. xcodebuild -workspace ...
xcodebuild -workspace <workspacename> -scheme <schemeName> [-destination <destinationspecifier>]... [-configuration <configurationname>] [-arch <architecture>]... [-sdk [<sdkname>|<sdkpath>]] [-showBuildSettings] [<buildsetting>=<value>]... [<buildaction>]...
除了 workspace 和 scheme 之外其余選項(xiàng)都和上條命令相同。
-workspace
: 指定 workspace 名字,默認(rèn)首個(gè) workspace
-scheme
: 指定對(duì)應(yīng)的 scheme ,默認(rèn)首個(gè) scheme
4 . xcodebuild -exportArchive ...
這里順便介紹一下 archive 命令,因?yàn)樵谙旅媸褂?PackageApplication 會(huì)出一個(gè)警告說推薦使用 -exportArchive
。所以我們就來嘗試一下使用 archive 來生成 app。
首先使用一下命令來生成 .xcarchive 文件
xcodebuild archive -workspace xx.xcworkspace -scheme xx -archivePath xx.xcarchive
可以看出添加上 archive 命令和最后加入 -archivePath
生成archivePath的路徑即可。
然后該路徑下會(huì)生成一個(gè) xx.archivePath,里面包括三個(gè)文件,xx.app.dsym文件(可用于bugly等監(jiān)控bug的平臺(tái)),info.plist(保存打包的一些信息),還有我們的 xx.app 文件。
其次使用 -exportArchive 生成 ipa 包
xcodebuild -exportArchive -archivePath xx.xcarchive -exportPath xx -exportFormat ipa
-archivePath
: xx.archivePath 的路徑
-exportPath
: 輸出路徑
-exportFormat
: 生成類型,這里選擇我們需要的 ipa
這樣就利用我們的 xcodebuild 命令來生成 ipa 包
xcrun
這里也使用 xcrun 來生成 ipa 包即可
xcrun -sdk iphoneos PackageApplication build/Release-iphoneos/xx.app -o ~/Desktop/xx.ipa
但是,在 macos10.12 和 Xcode8 的環(huán)境下會(huì)出現(xiàn)一個(gè)警告
warning: PackageApplication is deprecated, use xcodebuild -exportArchive instead.
說明 PackageApplication 已經(jīng)被棄用了。
不過其實(shí)這一步可以幾乎等價(jià)于將 xx.app 放入一個(gè) payload 的文件夾下然后壓縮文件夾為 xx.ipa,當(dāng)然這樣做缺失一些信息,不過并不影響程序的運(yùn)行。
初步小結(jié)
綜上,我們有兩種方法來生成我們需要的 ipa 包。
- 使用 xcodebuild 命令來編譯我們的項(xiàng)目生成 app,然后再用 xcrun 將 app 轉(zhuǎn) ipa。
- 使用 xcodebuild archive 命令來直接生成我們需要的 ipa。
雖然現(xiàn)在網(wǎng)上幾乎都是使用 xcodebuild + xcrun 來來生成 ipa 包,不過既然官方說 PackageApplication is deprecated
,那還是推薦使用第二種方法,一步到位。
自動(dòng)化打包正式開始
這里從我工作室的一個(gè)項(xiàng)目切入,這個(gè)項(xiàng)目需要最終生成 18 個(gè) ipa 包,但是他們幾乎是共用一套代碼的,不同的地方在于bundleName/bundleDisplayName/bundleid 等,以及一些資源文件的不同,例如 icon 等。所以可想而知如果選擇手動(dòng)打包的痛苦,并且當(dāng)你打包到一半發(fā)現(xiàn)某個(gè)地方錯(cuò)了要重新打包 ......
這里說一下自動(dòng)化打包1.0解決思路:
- 使用命令
defaults write
來修改項(xiàng)目中的 plist 文件,來達(dá)到修改 bundleName/bundleDisplayName/bundleid... 的目的。 - 使用命令
cp
來替換資源文件。 - 使用
xcodebuild -workspace ..
編譯出 app 包。 - 使用
xcrun ...
生成 ipa 。
這是我最開始想到的思路,最終運(yùn)行時(shí)間大概為每個(gè)包2.5m(時(shí)間主要浪費(fèi)在編譯),然后一套下來也要半個(gè)多小時(shí)。雖然比起手動(dòng)打快了不少,但還是太慢了。畢竟自動(dòng)化的目的不僅僅是自動(dòng),還要速度。
既然問題出在編譯上,那我的思路就往編譯一次多次使用這個(gè)方向上面思考。然后想到了既然只是資源文件和plist的不同,沒有涉及到代碼的更換(不過這個(gè)項(xiàng)目后期不同 app 會(huì)執(zhí)行不同一套代碼,不過也有解決辦法),這里就出現(xiàn)了自動(dòng)化打包2.0的版本。
- 使用
xcodebuild -workspace ..
編譯出 app 包。 - 使用命令
defaults write
來修改項(xiàng)目中的 plist 文件,來達(dá)到修改 bundleName/bundleDisplayName/bundleid... 的目的。 - 使用命令
cp
來替換資源文件。 - 重簽名
codesign -f -s "iPhone Distribution: xx co., LTD" --entitlements $Entitlements $ipaPath/Payload/YouXiaoYun.app
- 使用
xcrun ...
生成 ipa 。
和1.0大致相似,不過并不是每次生成 ipa 都需要編譯一次。而是編譯一次,然后直接修改 app 下內(nèi)容,不過這里會(huì)出現(xiàn)簽名錯(cuò)誤的問題,因?yàn)樵诰幾g的最后會(huì)用證書幫 app 簽名,如果你直接替換資源然后就生成 ipa 的話會(huì)導(dǎo)致 ipa 無法安裝。
那這時(shí)候神奇的重簽名技術(shù)就出來(重簽名用在正途上的真少見...hhhh,關(guān)于重簽名的文章 google 一下就會(huì)很多),使用 codesign 命令就可以幫修改過資源的 app 重簽名。
最終使用2.0的時(shí)間基本是在5-6分鐘左右。果然能機(jī)器完成的工作絕對(duì)不要手動(dòng)完成,從半天到30分鐘到最后的6分鐘,節(jié)省下來的時(shí)間可以讓你學(xué)習(xí)到更多。
上面說到如果不同 app 間會(huì)用到不同的代碼。例如 app A 里面的 title 叫 A 部門,app B 里面 title 又叫 B 部門,這樣就不會(huì)通過命令行直接修改到代碼,不過我想到的是維護(hù)一個(gè) plist 文件,plist 文件可以這樣設(shè)計(jì)的,每個(gè)不同 app 的 bundleName 都設(shè)置字典的鍵,然后字典下就可以是你自定義的內(nèi)容。然后每次啟動(dòng) app 就根據(jù) bundleName 來尋找對(duì)應(yīng)的字典,然后 title 就賦值為 plist 下 title 的值。如果不同代碼就根據(jù) code1 里面的值來 switch 不同的代碼。
最終代碼
以下是完整的腳本文件,部分信息需要自己替換。
以下腳本適用于一次打 N 個(gè)包,適用情況:
- 可以替換 bundle 信息
- 替換音頻圖片資源
- 可以執(zhí)行不同代碼
- 生成相應(yīng)的plist文件
- 上傳到蒲公英分發(fā)平臺(tái)
當(dāng)然也可以打一個(gè)包,適當(dāng)刪除某些代碼即可。
# 1.Configuration Info
# 項(xiàng)目路徑 需修改
projectDir="你的項(xiàng)目路徑"
# 打包生成路徑 需修改
ipaPath="ipa生成路徑"
# 圖標(biāo)路徑 需修改
iconPath="~/Desktop/icon"
# Provisioning Profile 需修改 查看本地配置文件
PROVISIONING_PROFILE="xxxxxxx-xxxx-4bfa-a696-0ec7391b24d8"
############# 重簽名需要文件
# 以下文件需放在 ipaPath 路徑下
Entitlements=$ipaPath/entitlements.plist
#############
# 版本號(hào)
bundleVersion="2.0.0"
# 選擇打包序號(hào) 多選則以空格隔開 如("1" "2" "3")
appPackNum=("1 2")
# 蒲公英分發(fā)參數(shù) 不分發(fā)可忽略 默認(rèn)不分發(fā) 下面的兩個(gè)KEY是默認(rèn)測(cè)試的網(wǎng)址對(duì)應(yīng)KEY
ISUPLOAD=0
USERKEY="xxx"
APIKEY="xxx"
# ---------------------------可選 如果需要替換 app 的 icon --------------------------------- #
# 配置App信息數(shù)組 格式:"AppName(和工程中appInfo.Plist對(duì)應(yīng))" "icon"
#Schemes:
# 1.app1 app1Icon
# 2.app2 app2Icon
# 3.app3 app3Icon
# --------------------------------------------------------------------------------------- #
# 打包個(gè)數(shù)
appPackNumLength=${#appPackNum[*]}
appInfos=(
"app1" "app1Icon" "xxxx"
"app2" "app2Icon" "xxxx"
"app3" "app3Icon" "xxxx"
)
appInfosLength=${#appInfos[*]}
# Scheme Name
schemeName="xx"
# Code Sign ID
CODE_SIGN_IDENTITY="xx co., LTD"
# 生成 APP 路徑
buildDir="build/Release-iphoneos"
# 開始時(shí)間
beginTime=`date +%s`
# 創(chuàng)建打包目錄
mkdir ${ipaPath}/AllPack
# 本地存放全部 IPA 的路徑
allIPAPackPath="${ipaPath}/allPack"
# 清除緩存
rm -rf $projectDir/$buildDir
# Build 生成 APP
xcodebuild -workspace ${projectDir}/xx.xcworkspace -scheme ${schemeName} -configuration Release clean -sdk iphoneos build CODE_SIGN_IDENTITY="${CODE_SIGN_IDENTITY}" PROVISIONING_PROFILE="${PROVISIONING_PROFILE}" SYMROOT="${projectDir}/build"
if [[ $? = 0 ]]; then
echo "\033[31m 編譯成功\n \033[0m"
else
echo "\033[31m 編譯失敗\n \033[0m"
fi
# 先創(chuàng)建 payload 文件夾
mkdir ~/Desktop/Payload
# 移動(dòng)編譯生成的 app 到桌面的 Payload 文件夾下
cp -Rf ${projectDir}/${buildDir}/${schemeName}.app $ipaPath/Payload
# 以下二選一
# 1.----全部打包----
#for (( i=0; i<appInfosLength; i+=3 )); do
# 2.----自定義打包----
for (( j=0; j<$appPackNumLength; j++)); do i=`expr ${appPackNum[$j]} - 1` i=`expr $i \* 3`
# App Bundle Name (CFBundleName)
appName=${appInfos[${i}]}
# App DisPlay Name
appDisplayName=${appInfos[${i}]}
# App Icon Name
appIconName=${appInfos[$i+1]}
# App Download Name
appDownloadName=${appInfos[$i+2]}
# 創(chuàng)建不同 app ipa 目錄
mkdir $allIPAPackPath/$appName
rm -rf $allIPAPackPath/$appName/*
echo "\033[31m appName:$appName appIconName:$appIconName appDownloadName:$appDownloadName\n \033[0m"
# 將對(duì)應(yīng)的 icon 復(fù)制到需要修改的 app 的目錄下
cp -Rf $iconPath/$appName/* $ipaPath/Payload/YouXiaoYun.app
if [[ $? = 0 ]]; then
echo "\033[31m 修改 icon 成功\033[0m"
else
echo "\033[31m 修改 icon 失敗\033[0m"
fi
# 修改 Plist
defaults write $ipaPath/Payload/xx.app/info.plist "CFBundleName" $appName
defaults write $ipaPath/Payload/xx.app/info.plist "CFBundleDisplayName" $appDisplayName
if [[ $? = 0 ]]; then
echo "\033[31m 修改 Plist 成功\033[0m"
else
echo "\033[31m 修改 Plist 失敗\033[0m"
fi
# 重簽名
codesign -f -s "xx co., LTD" --entitlements $Entitlements $ipaPath/Payload/xx.app
if [[ $? = 0 ]]; then
echo "\033[31m 簽名成功\n \033[0m"
else
echo "\033[31m 簽名失敗\n \033[0m"
fi
# 生成 ipa
xcrun -sdk iphoneos -v PackageApplication $ipaPath/Payload/.app -o ${ipaPath}/$appDownloadName.ipa
if [[ $? = 0 ]]; then
echo "\033[31m \n 生成 IPA 成功 \n\n\n\n\n\033[0m"
else
echo "\033[31m \n 生成 IPA 失敗 \n\n\n\n\n\033[0m"
fi
# 創(chuàng)建 Plist
plist_path=$allIPAPackPath/$appName/$appDownloadName.plist
cat << EOF > $plist_path
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>items</key>
<array>
<dict>
<key>assets</key>
<array>
<dict>
<key>kind</key>
<string>software-package</string>
<key>url</key>
<string>https://xxxxxxxxxxxx/$appDownloadName.ipa</string>
</dict>
<dict>
<key>kind</key>
<string>display-image</string>
<key>url</key>
<string>https://xxxxxxxxxxxx/${appIconName}.png</string>
</dict>
<dict>
<key>kind</key>
<string>full-size-image</string>
<key>url</key>
<string>https://xxxxxxxxxxxx/${appIconName}.png</string>
</dict>
</array>
<key>metadata</key>
<dict>
<key>bundle-identifier</key>
<string>你的bundid</string>
<key>bundle-version</key>
<string>$bundleVersion</string>
<key>kind</key>
<string>software</string>
<key>title</key>
<string>$appDownloadName</string>
</dict>
</dict>
</array>
</dict>
</plist>
EOF
# 移動(dòng)
mv ${ipaPath}/$appDownloadName.ipa ${allIPAPackPath}/$appName
# 6.上傳蒲公英分發(fā)平臺(tái)
if [[ $ISUPLOAD = 1 ]]; then
echo "正在上傳蒲公英..."
curl -F "file=@$allIPAPackPath/$appName/$appDownloadName.ipa" -F "uKey=$USERKEY" -F "_api_key=$APIKEY" http://www.pgyer.com/apiv1/app/upload
fi
done
# 清除無關(guān)文件
rm -rf $ipaPath/Payload
# 結(jié)束時(shí)間
endTime=`date +%s`
echo -e "打包時(shí)間$[ endTime - beginTime ]秒"