懒到极致之怒撸一键打包发布系统
一切得從上個(gè)版本的打包發(fā)布說起。
開發(fā)中本人負(fù)責(zé)了iOS包的版本發(fā)布工作。iOS打包:不就是選一下證書,再在Xcode上點(diǎn)幾下按鈕,IDE全都給你設(shè)置好流程了,有必要這么麻煩嗎?
誠然,如果只是打包,在不考慮團(tuán)隊(duì)協(xié)同合作、打包效率、重復(fù)工作量的前提下,使用Xcode自帶的打包方式當(dāng)然是沒問題的。但實(shí)際開發(fā)中,每次打包大概包含以下流程: 拉取最新代碼(SVN或Git) → 編譯通過 → 設(shè)置打包環(huán)境(開發(fā)、測試、生成等) → 導(dǎo)出IPA包 → 上傳IPA包(App Store或者企業(yè)包上傳至指定服務(wù)器)
可以看出,其中的很多步驟都是機(jī)械重復(fù)的,特別當(dāng)進(jìn)入測試驗(yàn)收階段,有時(shí)每修復(fù)幾個(gè)bug就要重新打包發(fā)布測試,比如上個(gè)版本的時(shí)候新功能主要是隊(duì)友在開發(fā),測試到最后頻繁地讓我打包發(fā)布,不停地打斷我的工作去重復(fù)機(jī)械的事情,這簡直就是在浪費(fèi)人生啊!!!???
雖然之前為了打包方便,我已經(jīng)整理了一份腳本打包的流程(傳送門:Shell腳本——Xcode腳本打包),但還是不夠方便快捷,乘著新版發(fā)布后的空檔期,擼出 一鍵打包發(fā)布系統(tǒng) ,功能包括: 自動(dòng)拉取Git最新代碼 → 自動(dòng)選擇簽名證書并打包導(dǎo)出ipa文件 → Git自動(dòng)同步代碼→ 自動(dòng)上傳ipa包(我們是企業(yè)包,上傳至自己的服務(wù)器,這一步是可選的)
一鍵打包
項(xiàng)目鏈接地址
一鍵打包資源(ArchiveSource)簡介:
- Code文件夾下是打包應(yīng)用程序源代碼
- Source文件夾下是打包資源
Source文件夾:
一鍵打包步驟:
啟動(dòng)Xcode,手動(dòng)選擇簽名證書(如果默認(rèn)簽名失敗的話)
打包生成的IPA包路徑說明:
../項(xiàng)目所在路徑/Archiving/(以AppId命名的文件夾)/(App名+版本號(hào)命名的文件夾)/(以打包時(shí)間命名的文件夾)
示例: ../Archiving/(AppID)/(App名)_3_1_29/2018_04_10_11:18:51
一鍵打包原理
一鍵打包發(fā)布系統(tǒng)其實(shí)很簡單:開發(fā)一款Mac應(yīng)用,應(yīng)用啟動(dòng)時(shí)讀取本機(jī)已有的Certificates和Provisioning Profiles信息,再在應(yīng)用內(nèi)調(diào)用Shell腳本,主要通過腳本來實(shí)現(xiàn)Git同步以及打包的相關(guān)操作。
-
Shell腳本調(diào)用
Objective-C中調(diào)用Shell腳本可以使用 NSTask 。通過NSTask,我們可以在應(yīng)用中調(diào)用另一個(gè)程序或運(yùn)行一段腳本并獲得其執(zhí)行狀態(tài)和最終結(jié)果,NSTask最為常用的一個(gè)場景是為命令行操作提供圖形化的界面。
//創(chuàng)建一個(gè)新的TaskNSTask *optTask = [[NSTask alloc] init];//設(shè)置調(diào)用路徑optTask.launchPath = shellPath;//設(shè)置調(diào)用參數(shù)(被調(diào)用程序命令)optTask.arguments = @[@"-ls"];//創(chuàng)建輸出PipeNSPipe *outputPipe = [NSPipe pipe];[optTask setStandardOutput:outputPipe];//創(chuàng)建錯(cuò)誤輸出PipeNSPipe *errorPipe = [NSPipe pipe];[optTask setStandardError:errorPipe];//執(zhí)行完成Block 通知optTask.terminationHandler = ^(NSTask *theTask) {[theTask.standardOutput fileHandleForReading].readabilityHandler = nil;[theTask.standardError fileHandleForReading].readabilityHandler = nil;};//錯(cuò)誤輸出[[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]; 復(fù)制代碼-
自動(dòng)選擇簽名證書
獲取電腦中以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); } 復(fù)制代碼獲取電腦中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)換為對(duì)應(yīng)實(shí)體 YAProvisioningProfile *profile = [[YAProvisioningProfile alloc] initWithPath:path]; 復(fù)制代碼根據(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;} } 復(fù)制代碼-
讀寫Xcode工程配置
右鍵 XXX.xcodeproj 文件,顯示包內(nèi)容,可以看到project.pbxproj。project.pbxproj存儲(chǔ)著 Xcode 工程的各項(xiàng)配置參數(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 ] thenBundle_Identifier=${Array[3]} fi echo "Bundle_Identifier = ${Bundle_Identifier}" 復(fù)制代碼修改配置信息: 修改 project.pbxproj 配置可以通過 sed 命令實(shí)現(xiàn),比如將簽名類型指定為Manual
key="CODE_SIGN_STYLE" value="Manual;" # 修改 project.pbxproj 配置,指定簽名類型為手動(dòng)選擇 sed -i "" "s/$key =.*$/$key = $value/g" $Pbxproj_Path 復(fù)制代碼如果程序成功讀取到對(duì)應(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};" 復(fù)制代碼修改項(xiàng)目版本號(hào):
Info.plist文件中CFBundleShortVersionString對(duì)應(yīng)Version號(hào),CFBundleVersion對(duì)應(yīng)Build號(hào),使用腳本可以直接指定版本號(hào)
# info.plist文件路徑 InfoPlist_Path="${Project_Path}/${App_Name}/Info.plist" # 如果是舊的項(xiàng)目,info.plist文件對(duì)應(yīng)的名字為 (App_Name)-Info.plist if [ ! -e "${InfoPlist_Path}" ];thenInfoPlist_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} 復(fù)制代碼-
打包項(xiàng)目
# 指定打包模式為ReleaseConfiguration="Release"# 判斷編譯的項(xiàng)目類型是workspace還是projectif [[ ${Is_Workspace} == "YES" ]]; then# 編譯前清理工程,>> ${Log_path} 表示將日志輸出寫入到Log_path文件xcodebuild clean -configuration "${Configuration}" -alltargets >> ${Log_path}# 自定義簽名,指定簽名證書if [[ ${Custom_Sign} == "YES" ]]; thenxcodebuild archive -workspace "${Workspace_Path}" \-scheme "${Scheme_Name}" \-archivePath "${Xcarchive_path}" \-configuration "${Configuration}" \PROVISIONING_PROFILE="${Profile_Name}" \CODE_SIGN_IDENTITY="${Sign_Identity}" \>> ${Log_path}elsexcodebuild archive -workspace "${Workspace_Path}" \-scheme "${Scheme_Name}" \-archivePath "${Xcarchive_path}" \-configuration "${Configuration}" \>> ${Log_path}fielse# 編譯前清理工程xcodebuild clean -configuration "${Configuration}" -alltargets >> ${Log_path}if [[ ${Custom_Sign} == "YES" ]]; thenxcodebuild archive -project "${Xcodeproj_Path}" \-scheme "${Scheme_Name}" \-archivePath "${Xcarchive_path}" \-configuration "${Configuration}" \PROVISIONING_PROFILE="${Profile_Name}" \CODE_SIGN_IDENTITY="${Sign_Identity}"\>> ${Log_path}elsexcodebuild archive -project "${Xcodeproj_Path}" \-scheme "${Scheme_Name}" \-archivePath "${Xcarchive_path}" \-configuration "${Configuration}" \>> ${Log_path}fifi 復(fù)制代碼 -
導(dǎo)出IPA包
xcodebuild -exportArchive -archivePath "${Xcarchive_path}" -exportPath "${IPA_Archiving_Path}" -exportOptionsPlist "${ExportOptionsPlistPath}" >> ${Log_path} 復(fù)制代碼
Xcarchive_path指向上一步打包生成的*.xcarchive文件路徑;IPA_Archiving_Path指向?qū)С龅?.ipa文件所在路徑;ExportOptionsPlistPath對(duì)應(yīng)ExportOptions.plist文件路徑,ExportOptions.plist是使用xcodebuild -exportArchive指令導(dǎo)出ipa包時(shí)需要指定的配置文件
- compileBitcode:不上架App Store,Xcode是否啟用Bitcode重新編譯,默認(rèn)為YES。
- method:歸檔類型,包括app-store、ad-hoc、 package、enterprise、development以及developer-id。
- provisioningProfiles:打包證書信息,包含的字典信息格式:<key為App ID>:<value為對(duì)應(yīng)的profile文件名>。
- uploadBitcode:上線App Store是否開啟Bitcode,默認(rèn)為YES。
- uploadSymbols:上線App Store,是否開啟符號(hào)序列化,這是與查crash相關(guān)的,默認(rèn)為YES。
關(guān)于更多的xcodebuild指令,可以通過xcodebuild -help查看。
-
其他
其他部分還包括Git代碼管理、IPA包上傳、無效資源刪除等,都可以直接通過NSTask執(zhí)行腳本命令實(shí)現(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è)包,以供他人下載)。一開始想著將自動(dòng)上傳也集成到打包程序內(nèi),但最后發(fā)現(xiàn)這涉及到內(nèi)網(wǎng)間不同服務(wù)器以及賬號(hào)驗(yàn)證過程,太過復(fù)雜暫時(shí)將這一部分閹割了……
寫在最后
以上便是一鍵打包系統(tǒng)的功能講解,想了解更多可以查看源碼
CJShellDemo 說明:
ArchiveSource 一鍵打包程序資源 CrashScript 線上crash查找腳本 ReleaseDir Xcode打包腳本
歡迎點(diǎn)贊以及GitHub Star?
轉(zhuǎn)載于:https://juejin.im/post/5ad9aeb66fb9a07aa6315431
總結(jié)
以上是生活随笔為你收集整理的懒到极致之怒撸一键打包发布系统的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 【C++多线程系列】【四】将类的成员函数
- 下一篇: android ------- TCP/