一切得從上個版本的打包發(fā)布說起。
開發(fā)中本人負(fù)責(zé)了iOS包的版本發(fā)布工作。iOS打包:不就是選一下證書,再在Xcode上點幾下按鈕,IDE全都給你設(shè)置好流程了,有必要這么麻煩嗎?
誠然,如果只是打包,在不考慮團(tuán)隊協(xié)同合作、打包效率、重復(fù)工作量的前提下,使用Xcode自帶的打包方式當(dāng)然是沒問題的。但實際開發(fā)中,每次打包大概包含以下流程:
拉取最新代碼(SVN或Git) → 編譯通過 → 設(shè)置打包環(huán)境(開發(fā)、測試、生產(chǎn)等) → 導(dǎo)出IPA包 → 上傳IPA包(App Store或者企業(yè)包上傳至指定服務(wù)器)
可以看出,其中的很多步驟都是機械重復(fù)的,特別當(dāng)進(jìn)入測試驗收階段,有時每修復(fù)幾個bug就要重新打包發(fā)布測試,比如上個版本的時候新功能主要是隊友在開發(fā),測試到最后頻繁地讓我打包發(fā)布,不停地打斷我的工作去重復(fù)機械的事情,這簡直就是在浪費人生?。。。?/p>
雖然之前為了打包方便,我已經(jīng)整理了一份腳本打包的流程(傳送門:Shell腳本——Xcode腳本打包),但還是不夠方便快捷,乘著新版發(fā)布后的空檔期,擼出 一鍵打包發(fā)布系統(tǒng) ,功能包括:
自動拉取Git最新代碼 → 自動選擇簽名證書并打包導(dǎo)出ipa文件 → Git自動同步代碼→ 自動上傳ipa包(我們是企業(yè)包,上傳至自己的服務(wù)器,這一步是可選的)
一鍵打包
一鍵打包資源(ArchiveSource)簡介:
- Code文件夾下是打包應(yīng)用程序源代碼
- Source文件夾下是打包資源
Source文件夾:
- Archive.app(打包應(yīng)用程序)
- ExportOptions.plist(導(dǎo)出ipa包配置文件)
一鍵打包步驟:
- 進(jìn)入Source文件夾,啟動 Archive 應(yīng)用程序
- 選擇 .xcodeproj 或 .xcworkspace 文件
- 修改打包版本號
啟動Xcode,手動選擇簽名證書(如果默認(rèn)簽名失敗的話)
- 點擊打包
打包生成的IPA包路徑說明:
../項目所在路徑/Archiving/(以AppId命名的文件夾)/(App名+版本號命名的文件夾)/(以打包時間命名的文件夾)
示例:
../Archiving/(AppID)/(App名)_3_1_29/2018_04_10_11:18:51
一鍵打包原理
一鍵打包發(fā)布系統(tǒng)其實很簡單:開發(fā)一款Mac應(yīng)用,應(yīng)用啟動時讀取本機已有的Certificates和Provisioning Profiles信息,再在應(yīng)用內(nèi)調(diào)用Shell腳本,主要通過腳本來實現(xiàn)Git同步以及打包的相關(guān)操作。
-
Shell腳本調(diào)用
Objective-C中調(diào)用Shell腳本可以使用 NSTask 。通過NSTask,我們可以在應(yīng)用中調(diào)用另一個程序或運行一段腳本并獲得其執(zhí)行狀態(tài)和最終結(jié)果,NSTask最為常用的一個場景是為命令行操作提供圖形化的界面。
//創(chuàng)建一個新的Task
NSTask *optTask = [[NSTask alloc] init];
//設(shè)置調(diào)用路徑
optTask.launchPath = shellPath;
//設(shè)置調(diào)用參數(shù)(被調(diào)用程序命令)
optTask.arguments = @[@"-ls"];
//創(chuàng)建輸出Pipe
NSPipe *outputPipe = [NSPipe pipe];
[optTask setStandardOutput:outputPipe];
//創(chuàng)建錯誤輸出Pipe
NSPipe *errorPipe = [NSPipe pipe];
[optTask setStandardError:errorPipe];
//執(zhí)行完成Block 通知
optTask.terminationHandler = ^(NSTask *theTask) {
[theTask.standardOutput fileHandleForReading].readabilityHandler = nil;
[theTask.standardError fileHandleForReading].readabilityHandler = nil;
};
//錯誤輸出
[[errorPipe fileHandleForReading] setReadabilityHandler:^(NSFileHandle *file) {
NSData *data = [file availableData];
NSString *errorMsg = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"errorMsg = %@",errorMsg);
}];
//執(zhí)行結(jié)果輸出
[[outputPipe fileHandleForReading] setReadabilityHandler:^(NSFileHandle *file) {
NSData *data = [file availableData];
NSString *msg = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
NSLog(@"msg = %@",msg);
}];
//開啟執(zhí)行
[optTask launch];
//阻塞直到執(zhí)行完畢(NSTask默認(rèn)是異步執(zhí)行,如果有同步需求,可調(diào)用waitUntilExit()方法)
[optTask waitUntilExit];
[[outputPipe fileHandleForReading] closeFile];
[[errorPipe fileHandleForReading] closeFile];
-
自動選擇簽名證書
獲取電腦中以iPhone Distribution和iPhone Developer命名開頭的Certificates:
+ (void)loadCerListBlock:(CerListBlock)listBlock {
NSDictionary *options = @{(__bridge id)kSecClass: (__bridge id)kSecClassCertificate,
(__bridge id)kSecMatchLimit: (__bridge id)kSecMatchLimitAll};
CFArrayRef certs = NULL;
__unused OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)options, (CFTypeRef *)&certs);
NSArray *certificates = CFBridgingRelease(certs);
NSMutableArray *tempArray=[NSMutableArray array];
for (int i=0;i<[certificates count];i++) {
SecCertificateRef certificate = (__bridge SecCertificateRef)([certificates objectAtIndex:i]);
NSString *name = CFBridgingRelease(SecCertificateCopySubjectSummary(certificate));
if ([name hasPrefix:@"iPhone Distribution"]||[name hasPrefix:@"iPhone Developer"]) {
[tempArray addObject:name];
}
}
listBlock(tempArray);
}
獲取電腦中Provisioning Profiles:
//獲取電腦中滿足條件的Provisioning Profiles的路徑
+ (NSArray *)getAllProvisioningProfileList{
NSString *path=[NSString stringWithFormat:@"%@/%@", NSHomeDirectory(), kMobileprovisionDirName];
NSFileManager *fileManager=[NSFileManager defaultManager];
NSArray *provisioningProfiles =[fileManager contentsOfDirectoryAtPath:path error:nil];
provisioningProfiles = [provisioningProfiles filteredArrayUsingPredicate:[NSPredicate predicateWithFormat:@"pathExtension IN %@", @[@"mobileprovision", @"provisionprofile"]]];
return provisioningProfiles;
}
//自定義 YAProvisioningProfile ,將profile轉(zhuǎn)換為對應(yīng)實體
YAProvisioningProfile *profile = [[YAProvisioningProfile alloc] initWithPath:path];
根據(jù)App ID匹配 Profile 描述文件:
//根據(jù)appBundleIdentifier匹配 profile 描述文件
YAProvisioningProfile *selectProfile = nil;
for (YAProvisioningProfile *profile in profileArray) {
// 查找與bundleIdentifier相等,描述文件不以 XC: 命名開頭,而且是最新的文件
if ([profile.bundleIdentifier isEqualToString:appBundleIdentifier] && ![profile.name hasPrefix:@"XC:"] && profile.newest) {
selectProfile = profile;
break;
}
}
-
讀寫Xcode工程配置
右鍵 XXX.xcodeproj 文件,顯示包內(nèi)容,可以看到project.pbxproj。project.pbxproj存儲著 Xcode 工程的各項配置參數(shù),我們可以通過腳本來直接讀取或修改其配置,執(zhí)行效果等同于在Xcode的General、Build Settings中的修改。
讀取App Bundler Identifier:
# Pbxproj_Path指向 project.pbxproj 文件路徑
configuration=$(grep -i "PRODUCT_BUNDLE_IDENTIFIER =" ${Pbxproj_Path})
Array=($(echo $configuration))
# project.pbxproj 中存在多行PRODUCT_BUNDLE_IDENTIFIER信息,從后往前讀取
Bundle_Identifier=${Array[5]}
if [ ! -n $Bundle_Identifier ]
then
Bundle_Identifier=${Array[3]}
fi
echo "Bundle_Identifier = ${Bundle_Identifier}"
修改配置信息:
修改 project.pbxproj 配置可以通過 sed 命令實現(xiàn),比如將簽名類型指定為Manual
key="CODE_SIGN_STYLE"
value="Manual;"
# 修改 project.pbxproj 配置,指定簽名類型為手動選擇
sed -i "" "s/$key =.*$/$key = $value/g" $Pbxproj_Path
如果程序成功讀取到對應(yīng)的證書簽名,那么打包前需要修改的配置包括:
#修改 PRODUCT_BUNDLE_IDENTIFIER
changeConfiguration "PRODUCT_BUNDLE_IDENTIFIER" "${APP_ID};"
#修改 PROVISIONING_PROFILE
changeConfiguration "PROVISIONING_PROFILE" "\"${Profile_Name}\";"
#修改 PROVISIONING_PROFILE_SPECIFIER
changeConfiguration "PROVISIONING_PROFILE_SPECIFIER" "${Profile_Specifier};"
#修改 PRODUCT_NAME
changeConfiguration "PRODUCT_NAME" "${Scheme_Name};"
#修改 "CODE_SIGN_IDENTITY[sdk=iphoneos*]"
changeConfiguration "\"CODE_SIGN_IDENTITY\[sdk=iphoneos\*\]\"" '"iPhone Distribution";'
#修改 CODE_SIGN_STYLE
changeConfiguration "CODE_SIGN_STYLE" "Manual;"
#修改 ProvisioningStyle
changeConfiguration "ProvisioningStyle" "Manual;"
#修改 DEVELOPMENT_TEAM
changeConfiguration "DEVELOPMENT_TEAM" "${Development_Team};"
#修改 DevelopmentTeam
changeConfiguration "DevelopmentTeam" "${Development_Team};"
修改項目版本號:
Info.plist文件中CFBundleShortVersionString對應(yīng)Version號,CFBundleVersion對應(yīng)Build號,使用腳本可以直接指定版本號
# info.plist文件路徑
InfoPlist_Path="${Project_Path}/${App_Name}/Info.plist"
# 如果是舊的項目,info.plist文件對應(yīng)的名字為 (App_Name)-Info.plist
if [ ! -e "${InfoPlist_Path}" ];then
InfoPlist_Path="${Project_Path}/${App_Name}/${App_Name}-Info.plist"
fi
/usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString ${Version_String}" ${InfoPlist_Path}
/usr/libexec/PlistBuddy -c "Set :CFBundleVersion ${Version_String}" ${InfoPlist_Path}
-
打包項目
# 指定打包模式為Release
Configuration="Release"
# 判斷編譯的項目類型是workspace還是project
if [[ ${Is_Workspace} == "YES" ]]; then
# 編譯前清理工程,>> ${Log_path} 表示將日志輸出寫入到Log_path文件
xcodebuild clean -configuration "${Configuration}" -alltargets >> ${Log_path}
# 自定義簽名,指定簽名證書
if [[ ${Custom_Sign} == "YES" ]]; then
xcodebuild archive -workspace "${Workspace_Path}" \
-scheme "${Scheme_Name}" \
-archivePath "${Xcarchive_path}" \
-configuration "${Configuration}" \
PROVISIONING_PROFILE="${Profile_Name}" \
CODE_SIGN_IDENTITY="${Sign_Identity}" \
>> ${Log_path}
else
xcodebuild archive -workspace "${Workspace_Path}" \
-scheme "${Scheme_Name}" \
-archivePath "${Xcarchive_path}" \
-configuration "${Configuration}" \
>> ${Log_path}
fi
else
# 編譯前清理工程
xcodebuild clean -configuration "${Configuration}" -alltargets >> ${Log_path}
if [[ ${Custom_Sign} == "YES" ]]; then
xcodebuild archive -project "${Xcodeproj_Path}" \
-scheme "${Scheme_Name}" \
-archivePath "${Xcarchive_path}" \
-configuration "${Configuration}" \
PROVISIONING_PROFILE="${Profile_Name}" \
CODE_SIGN_IDENTITY="${Sign_Identity}"\
>> ${Log_path}
else
xcodebuild archive -project "${Xcodeproj_Path}" \
-scheme "${Scheme_Name}" \
-archivePath "${Xcarchive_path}" \
-configuration "${Configuration}" \
>> ${Log_path}
fi
fi
-
導(dǎo)出IPA包
xcodebuild -exportArchive -archivePath "${Xcarchive_path}" -exportPath "${IPA_Archiving_Path}" -exportOptionsPlist "${ExportOptionsPlistPath}" >> ${Log_path}
Xcarchive_path指向上一步打包生成的*.xcarchive文件路徑;IPA_Archiving_Path指向?qū)С龅?code>*.ipa文件所在路徑;ExportOptionsPlistPath對應(yīng)ExportOptions.plist文件路徑,ExportOptions.plist是使用xcodebuild -exportArchive指令導(dǎo)出ipa包時需要指定的配置文件

- compileBitcode:不上架App Store,Xcode是否啟用Bitcode重新編譯,默認(rèn)為YES。
- method:歸檔類型,包括app-store、ad-hoc、
package、enterprise、development以及developer-id。- provisioningProfiles:打包證書信息,包含的字典信息格式:<key為App ID>:<value為對應(yīng)的profile文件名>。
- uploadBitcode:上線App Store是否開啟Bitcode,默認(rèn)為YES。
- uploadSymbols:上線App Store,是否開啟符號序列化,這是與查crash相關(guān)的,默認(rèn)為YES。
關(guān)于更多的xcodebuild指令,可以通過xcodebuild -help查看。
-
其他
其他部分還包括Git代碼管理、IPA包上傳、無效資源刪除等,都可以直接通過NSTask執(zhí)行腳本命令實現(xiàn),比如拉取Git代碼:git pull origin,當(dāng)然前提是你本地的Git已經(jīng)配置為免密操作。
如果你是使用SourceTree進(jìn)行Git管理,而且是http模式,那么可以這樣設(shè)置免密操作,第3步將遠(yuǎn)程倉庫路徑編輯為 http://用戶名:密碼@倉庫地址 這樣的方式;當(dāng)然如果你是SSH模式,那么本身就支持免密碼操作了。

上傳IPA包。
我這里打的是企業(yè)包,所以只要將ipa文件上傳到指定服務(wù)器就能夠下載了(想了解更多關(guān)于企業(yè)包的下載信息,可以參照我的另一篇文章iOS如何部署企業(yè)包,以供他人下載)。一開始想著將自動上傳也集成到打包程序內(nèi),但最后發(fā)現(xiàn)這涉及到內(nèi)網(wǎng)間不同服務(wù)器以及賬號驗證過程,太過復(fù)雜暫時將這一部分閹割了……
寫在最后
以上便是一鍵打包系統(tǒng)的功能講解,想了解更多可以查看源碼
CJShellDemo 說明:
ArchiveSource 一鍵打包程序資源
CrashScript 線上crash查找腳本
ReleaseDir Xcode打包腳本
歡迎點贊以及GitHub Star
最新補充
根據(jù)反饋發(fā)現(xiàn):不同的Xcode版本構(gòu)建的項目配置文件project.pbxproj會存在差異,另外不同用戶電腦上的Provisioning Profiles也存在各種情況(比如團(tuán)隊中不同成員擁有不同名字的有效Profile)。
此時運行一鍵打包可能會失??!提示 重命名ipa包失敗 或者 導(dǎo)出ipa包失敗 ,這種情況下請關(guān)閉“默認(rèn)簽名”選項,同時打開Xcode在工程文件中手動選擇對應(yīng)打包證書后,再返回Archive.app執(zhí)行一鍵打包。

應(yīng)用打包截圖
