懶到極致之怒擼一鍵打包發(fā)布系統(tǒng)

一切得從上個版本的打包發(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文件夾:

  1. Archive.app(打包應(yīng)用程序)
  2. ExportOptions.plist(導(dǎo)出ipa包配置文件)
一鍵打包步驟:
  1. 進(jìn)入Source文件夾,啟動 Archive 應(yīng)用程序
  2. 選擇 .xcodeproj 或 .xcworkspace 文件
  3. 修改打包版本號

啟動Xcode,手動選擇簽名證書(如果默認(rèn)簽名失敗的話)

  1. 點擊打包
打包生成的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 DistributioniPhone 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包時需要指定的配置文件

ExportOptions.png

  • 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模式,那么本身就支持免密碼操作了。

SourceTree.png

上傳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í)行一鍵打包。

關(guān)閉默認(rèn)簽名.png

應(yīng)用打包截圖


打包.png
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 用到的組件 1、通過CocoaPods安裝 2、第三方類庫安裝 3、第三方服務(wù) 友盟社會化分享組件 友盟用戶反饋 ...
    SunnyLeong閱讀 15,164評論 1 180
  • 取經(jīng)行動662/1001(17.7.14) 你若想被愛,就要先去愛人; 你期望被人關(guān)心,就要先去關(guān)心別人; 你要想...
    石林萍閱讀 283評論 0 4
  • 起床了沒 想我了沒 在干嘛 吃飯了嗎 吃了 吃的啥 吃的…… 晚安 晚安 么么噠 千篇一律 從沒放棄 因為愛 因為你
    李木只閱讀 589評論 5 3
  • 【讀經(jīng)】 伯33章 【金句】 開通他們的耳朵,將當(dāng)受的教訓(xùn)印在他們心上,好叫人不從自己的謀算,不行驕傲的事(原文是...
    chanor閱讀 528評論 0 0

友情鏈接更多精彩內(nèi)容