1. 程式人生 > >詳解Shell指令碼實現iOS自動化編譯打包提交

詳解Shell指令碼實現iOS自動化編譯打包提交

目錄

  • 前言
  • Shell指令碼涉及的工具
    • xcodebuild和xcrun
    • altool
    • fir-cli
    • PlistBuddy
  • 一些概念的區別
  • 具體實現
    • xcodebuild和xcrun
    • 準備Plist檔案
    • 獲取命令列引數
    • 清理構建目錄
    • 編譯打包成Archive
    • 將Archive匯出
    • 上傳到Fir
    • 驗證並上傳到App Store
    • 郵件通知相關同事
    • 上傳符號表到Bugly
  • 簡單例子
  • 對比實驗
    • 三種方式的對比
    • xcodebuild+xcrun和僅xcodebuild的比較
    • 命令到底做了什麼
  • 總結

前言

現在涉及到編譯打包的工作主要是以下兩個:

  1. 提交測試版本給測試同事
  2. 提交App Store稽核

兩個流程分別是:

  • 修改證書和配置檔案,然後「Product -> Archive」編譯打包,之後在自動彈出的 「Organizer」 中進行選擇,根據需要匯出 ad hoc enterprise 型別的 ipa 包。等待匯出之後再提交到Fir上,等Fir提交完成就需要告知測試同事。整個流程下來一般都要半個多小時,而且需要人工監守操作。
  • 第二個也是差不多,打包完之後需要操作幾個步驟然後上傳到App Store,上傳時間較長,而且中間可能會有錯誤需要處理。上傳後等待蘋果處理二進位制包,蘋果處理後上去選擇構建包,點選提交稽核。

所以研究下自動化編譯打包,提高下效率,減少人工操作成本。

主要有兩種實現途徑,AppleScript和Shell指令碼,AppleScript沒怎麼研究,網上說是很強大的指令碼語言。

下面主要講Shell指令碼的實現,網上也有人實現了並託管在github上,可以參考下。

Shell指令碼涉及的工具

主要是以下幾個工具:

  1. xcodebuild
  2. xcrun
  3. altool(提交到App Store使用)
  4. fir-cli(上傳到fir時使用)
  5. Python的smtplib(之前已經寫過python的發郵件了,所以就直接用沒有用Shell寫。)
  6. PlistBuddy
  7. BuglySymboliOS(Bugly的符號表工具包)

xcodebuild和xcrun

xcodebuildxcrun都是來自Command Line Tools,Xcode自帶,如果沒有可以通過以下命令安裝:

xcode-select --install

或者在下面的連結下載安裝:

安裝完可在以下路徑看到這兩個工具:

/Applications/Xcode.app/Contents/Developer/usr/bin/

  • xcodebuild
    主要是用來編譯,打包成Archive和匯出ipa包。

可以執行 xcodebuild -help 檢視,主要展示了幾種用法、一些可選項,最後是比較重要的exportOptionsPlist檔案的一些可選key,這個檔案在後面匯出ipa包會用到。

主要下面三個檢視的命令比較重要:

-showsdks                           display a compact list of the installed SDKs
-showBuildSettings                  display a list of build settings and values
-list                               lists the targets and configurations in a project, or the schemes in a workspace

後面兩個需要在Xcode的project或者workspace目錄下才能用。

  • xcrun
    xcrun -h

主要是打包,看網上比較多是用這個工具打包各種渠道包。

altool

這個工具在網上搜索幾乎沒有什麼結果,大概國內直接用命令列工具提交App Store的比較少。後來在StackOverflow上才找到相關的文件:

在上面的文件第38頁講述瞭如何使用altool上傳二進位制檔案。

這個工具實際上是ApplicationLoader,開啟Xcode-左上角Xcode-Open Developer Tool-Application Loader 可看到。有個“交付您的應用”操作,網上看到有人是直接用這個工具上傳的。

altool的路徑是:

/Applications/Xcode.app/Contents/Applications/Application\ Loader.app/Contents/Frameworks/ITunesSoftwareService.framework/Support/altool

使用時會提示下面的錯誤:

altool[] *** Error: Exception while launching iTunesTransporter: 
Transporter not found at path: /usr/local/itms/bin/iTMSTransporter. 
You should reinstall the application.

建立個軟連結可解決(類似於Windows的快捷方式):

ln -s /Applications/Xcode.app/Contents/Applications/Application\ Loader.app/Contents/itms /usr/local/itms

fir-cli

安裝時會提示各種許可權不允許,可以執行下面命令:

echo 'gem: --bindir /usr/local/bin' >> ~/.gemrc
sudo 'gem install fir-cli

fir有提供Android Studio、Eclipse、gradle外掛,可以看下。

這是它的github地址,其中講到有對?xcodebuild?原生指令進行了封裝。

PlistBuddy

Plist在Mac OSX系統中起著舉足輕重的作用,系統和程式使用Plist檔案來儲存自己的安裝/配置/屬性等資訊。而PlistBuddy是Mac裡一個用於命令列下讀寫plist檔案的工具,在/usr/libexec/下。可以通過它讀取或修改plist檔案的內容。

這裡我僅通過它來獲取內部版本號、外部版本號。在一些文章中見過用來修改plist檔案的資訊來匯出出不同需要的包。

一些概念的區別

Workspace、Project、Scheme、Target的區別。

下面是官方文件:

下面從上往下大概說下,具體看文件比較好:

  • Workspace
    Workspace是最大的集合,可以包含多個Project,可以管理不同的Project之間的關係。Workspace是以xcworkspace的檔案形式存在的。(這點和Project一致)。Workspace的存在是為了解決原來僅有Project的時候不同的Project之間的引用和呼叫困難的問題。同時,一個WorkspaceProject共用一個編譯路徑。比如使用CocoaPod、或者使用其他開發庫/框架。

  • Project
    Project是一個倉庫,包含編譯一個或多個product所需的檔案、資源和資訊,保持和聚合這些元素間的關係。(每個Target能指定自己的Build Settings來覆蓋Project的)

  • Source code, including header files and implementation files
  • Libraries and frameworks, internal and external
  • Resource files
  • Image files
  • Interface Builder (nib) files
  • Scheme
    Scheme包含了一些要構建的Scheme,一些構建時用到的設定,一些要執行的測試。同時只能有一個Scheme是有效的。

  • Target
    Target是對應了具體一個想要構建的Product,包含了一些構建這個Product所需的配置和檔案(build settingsbuild phases)。一個Project可以包含多個Target

具體實現

看起來有兩種實現方法:

  • 網上可以查到的文章,大多數都是用xcodebuildxcrun實現的,比如:
    xcodebuild -workspace XXX -scheme XXX -configuration Release
    xcrun -sdk iphoneos PackageApplication -v "/XXX/XXX.app" -o "/XXX/XXX"

這些文章都是相對比早期的,大多數用於打包不同渠道包。

  • 另一種是xcodebuildarchive-exportArchive,只有一兩篇文章是用這個,而且也過時了,因為現在最新是需要用-exportOptionsPlist這個選項。

我用的是第二種,並用上-exportOptionsPlist選項,後面我會簡單給下這兩種的結果比較。指令碼流程是:

  1. 準備兩個Plist檔案,用於匯出不同ipa包時使用。
  2. 獲取命令列引數,區分上傳到Fir還是App Store
  3. 清理構建目錄
  4. 編譯打包
  5. 匯出包
  6. 上傳到Fir或者驗證並上傳到App Store
  7. 發郵件通知

準備Plist檔案

根據xcodebuild -help提供的可選key可以知道,compileBitcodeembedOnDemandResourcesAssetPacksInBundleiCloudContainerEnvironmentmanifestonDemandResourcesAssetPacksBaseURLthinning這幾個key用於非App Store匯出的;uploadBitcodeuploadSymbols用於App Store匯出;methodteamID共用。

method的可選值為:

app-store, package, ad-hoc, enterprise, development, and developer-id

所以我建了兩個檔案:AppStoreExportOptions.plistAdHocExportOptions.plist

AppStoreExportOptions.plist:method=app-store,uploadBitcode=YES,uploadSymbols=YES

AdHocExportOptions.plist:method=ad-hoc,compileBitcode=NO

獲取命令列引數

Shell內建的getopts命令,這屬於Shell的範疇就不多講了:

if [ $# -lt 1 ];then
    echo "Error! Should enter the archive type (AdHoc or AppStore)."
    echo ""
    exit 2
fi
while getopts 't:' optname
do
    case "$optname" in
    t)
        if [ ${OPTARG} != "AdHoc" ] && [ ${OPTARG} != "AppStore" ];then
            echo "invalid parameter of $OPTARG"
            echo ""
            exit 1
        fi
        type=${OPTARG}
        ;;
    *)
        echo "Error! Unknown error while processing options"
        echo ""
        exit 2
        ;;
    esac
done

清理構建目錄

就如在Xcode操作「Product -> Clean」。

log_path="/XXX/XXX"
configuration="Release"
xcodebuild clean -configuration "$configuration" -alltargets >> $log_path

log_path是一個文件路徑,只是用來記錄命令的輸出,因為都打在終端會很多,另外也方便後面分析。後面的命令也是如此。這裡面帶的選項可以根據需要參考xcodebuild -help的資訊。

編譯打包成Archive

就如在Xcode操作「Product -> Archive」

workspaceName="XXX.xcworkspace"
scheme="XXX"
configurationBuildDir="XXX/build"
codeSignIdentity="iPhone Distribution: XXX, Ltd. (xxxxxxxxxx)"
adHocProvisioningProfile="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
appStoreProvisioningProfile="xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
configuration="Release"
archivePath="/xxx/XXX.xcarchive"

xcodebuild archive -workspace "$workspaceName" -scheme "$scheme" -configuration "$configuration" -archivePath "$archivePath" CONFIGURATION_BUILD_DIR="$configurationBuildDir" CODE_SIGN_IDENTITY="$codeSignIdentity" PROVISIONING_PROFILE="$provisioningProfile" >> $log_path

這裡的CONFIGURATION_BUILD_DIR是中間檔案生成的路徑,可以不指定;CODE_SIGN_IDENTITY是證書名(在對應TARGETSBuild Settings中選擇完Code Sinning,再點選選擇Other...,就可以得到這串東西);PROVISIONING_PROFILE是配置檔案(獲取方法同CODE_SIGN_IDENTITY,格式一般是xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx)。還可以新增其他引數,不設定的都是預設使用專案Build Settings裡面的配置,包括CODE_SIGN_IDENTITYPROVISIONING_PROFILE

如果是workspace就用-workspace,就像編譯帶有CocoaPods的專案,如果是普通專案則用-project

執行完會生成一個.xcarchive檔案和build資料夾如下:

.xcarchive
build資料夾
    |------.a
    |------.app
    |------.app.dSYM
    |------.swiftmodule資料夾
        |------arm.swiftdoc
        |------arm.swiftmodule
        |------arm64.swiftdoc
        |------arm64.swiftmodule

將Archive匯出

xcodebuild -exportArchive -archivePath "$archivePath" -exportOptionsPlist "$exportOptionsPlist" -exportPath "/XXX/XXX" >> $log_path

其中$exportOptionsPlist是對應使用的Plist的完整路徑(包括檔名)。

然後就會在指定的exportPath路徑下生成.ipa檔案。

上傳到Fir

firApiToken="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
ipaPath="/xxx/xxx.ipa"
fir publish "$ipaPath" -T "$firApiToken" >> $log_path

firApiToken在登入Fir後,右上角-API token看到。

驗證並上傳到App Store

altoolPath="/Applications/Xcode.app/Contents/Applications/Application\ Loader.app/Contents/Frameworks/ITunesSoftwareService.framework/Versions/A/Support/altool"
${altoolPath} --validate-app -f ${ipaPath} -u xxxxxx -p xxxxxx -t ios --output-format xml >>
${altoolPath} --upload-app -f ${ipaPath} -u xxxxxx -p xxxxxx -t ios --output-format xml

在上面的PDF文件第38頁講明瞭用法和各個可選項,具體可以看下PDF。需要說明的是,生成的結果是xml列印在終端,可以儲存到文件再解析出key來判斷是否成功,目前這步還沒做。

這是成功的結果:

<?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>os-version</key>
    <string>10.11.2</string>
    <key>success-message</key>
    <string>No errors validating archive at /XXX/XXX.ipa</string>
    <key>tool-version</key>
    <string>1.1.902</string>
    <key>xcode-versions</key>
    <array>
        <dict>
            <key>path</key>
            <string>/Applications/Xcode.app</string>
            <key>version.plist</key>
            <dict>
                <key>BuildVersion</key>
                <string>7</string>
                <key>CFBundleShortVersionString</key>
                <string>7.2</string>
                <key>CFBundleVersion</key>
                <string>9548</string>
                <key>ProductBuildVersion</key>
                <string>7C68</string>
                <key>ProjectName</key>
                <string>IDEFrameworks</string>
                <key>SourceVersion</key>
                <string>9548000000000000</string>
            </dict>
        </dict>
    </array>
</dict>
</plist>

這是失敗的結果(找不到iTMSTransporter的情況,用前面說的ln -s解決):

<?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>os-version</key>
    <string>10.11.2</string>
    <key>product-errors</key>
    <array>
        <dict>
            <key>code</key>
            <integer>-10001</integer>
            <key>message</key>
            <string>Transporter not found at path: /usr/local/itms/bin/iTMSTransporter.  You should reinstall the application.</string>
            <key>userInfo</key>
            <dict>
                <key>MZUnderlyingException</key>
                <string>Transporter not found at path: /usr/local/itms/bin/iTMSTransporter.  You should reinstall the application.</string>
                <key>NSLocalizedDescription</key>
                <string>Transporter not found at path: /usr/local/itms/bin/iTMSTransporter.  You should reinstall the application.</string>
                <key>NSLocalizedFailureReason</key>
                <string>Transporter not found at path: /usr/local/itms/bin/iTMSTransporter.  You should reinstall the application.</string>
            </dict>
        </dict>
    </array>
    <key>tool-version</key>
    <string>1.1.902</string>
    <key>xcode-versions</key>
    <array>
        <dict>
            <key>path</key>
            <string>/Applications/Xcode.app</string>
            <key>version.plist</key>
            <dict>
                <key>BuildVersion</key>
                <string>7</string>
                <key>CFBundleShortVersionString</key>
                <string>7.2</string>
                <key>CFBundleVersion</key>
                <string>9548</string>
                <key>ProductBuildVersion</key>
                <string>7C68</string>
                <key>ProjectName</key>
                <string>IDEFrameworks</string>
                <key>SourceVersion</key>
                <string>9548000000000000</string>
            </dict>
        </dict>
    </array>
</dict>
</plist>

可見,成功會有個success-message的key,而失敗會有product-errors的key。

郵件通知相關同事

發郵件時可能會想帶上當前版本的一些資訊,如版本號、內部版本號等,可以用PlistBuddy實現讀取甚至修改Plist檔案。

appInfoPlistPath="`pwd`/xxx/xxx-Info.plist"
bundleShortVersion=$(/usr/libexec/PlistBuddy -c "print CFBundleShortVersionString" ${appInfoPlistPath})
bundleVersion=$(/usr/libexec/PlistBuddy -c "print CFBundleVersion" ${appInfoPlistPath})

之後便是發郵件:

python sendEmail.py