Flutter 搭建 iOS 命令行服務(wù)打包發(fā)布全保姆式流程


theme: smartblue

在以前的 《 Android 和 iOS 打包提交審核指南》 里介紹了 Flutter 下打包 Android 和 iOS 的指南,不過這部分內(nèi)容主要介紹的是如何在本地打包發(fā)布流程。

但事實(shí)上一般的產(chǎn)品發(fā)布流程,都會(huì)有專門的機(jī)器用于打包服務(wù),在統(tǒng)一干凈的環(huán)境下進(jìn)行打包更有利于發(fā)布的管理,避免各種本地環(huán)境差異問題。

當(dāng)然大多數(shù)時(shí)候可以直接使用第三方的 CI 服務(wù),但是專門支持 Flutter 的第三方服務(wù)并不多,并且自己動(dòng)手還免費(fèi),所以本篇主要介紹自己搭建獨(dú)立打包服務(wù)的過程。

由于 Android 的命令打包服務(wù)比較簡(jiǎn)單,這里主要介紹配置搭建 iOS 下的 Flutter 打包和發(fā)布 CI ,其實(shí)主要也是 iOS 的 CI 。

一、參數(shù)支持

首先在 iOS 上很多的配置信息都是寫在 info.plist 文件,所以一開始需要解決打包時(shí)支持動(dòng)態(tài)修改 info.plist 的參數(shù),這樣有利于我們?cè)谳敵霾煌h(huán)境的包配置,如:QA、Release、Dev 等等。

/usr/libexec/PlistBuddy -c "Set  CFBundleVersion ${CFBundleVersion}" ./Runner/Info.plist

/usr/libexec/PlistBuddy -c "Print  CFBundleVersion " ./Runner/Info.plist

在 Mac 上其實(shí)本身就自帶了滿足需求的命令行工具:PlistBuddy, 如上命令所示

  • 通過 Set 命令可以直接動(dòng)態(tài)配置 plist 下的版本號(hào)、 code 和第三方 App Id 等相關(guān)配置;
  • 通過 Print 命令直接輸出對(duì)應(yīng)的 plist信息;

完成 plist 配置的支持, 接下來就需要在機(jī)器上配置開發(fā)者信息,最簡(jiǎn)單的做法就是打開 Xcode 然后直接登陸上開發(fā)者賬號(hào),通過賬號(hào)直接讓 Xcode 的 Automatically manage signing 幫助我們完成整個(gè)開發(fā)信息的配置過程。

image

但是我個(gè)人不推薦這種方式,打包機(jī)器本身可能會(huì)涉及多個(gè)項(xiàng)目組使用,都把自己的開發(fā)賬號(hào)登陸在一個(gè)公用機(jī)器上存在風(fēng)險(xiǎn),而且多個(gè)賬號(hào)同時(shí)登陸容易混亂,最后直接登陸也不利于證書和描述和管理。

所以要實(shí)現(xiàn)一個(gè)較為安全和通用的服務(wù),這里比較推薦:通過在機(jī)器上配置證書和 mobile provision 等文件的方式來完成打包認(rèn)證。

二、手動(dòng)配置證書

手動(dòng)配置證書和 mobile provision 會(huì)比較麻煩,但是它可以讓服務(wù)更加通用,也讓你更熟悉 iOS 打包的流程。

1、首先通過本地鑰匙串創(chuàng)建 CertificateSigningRequest.certSigningRequest 文件,如圖所示自動(dòng)生成就可以了。

image

2、在蘋果官方的 developer 上點(diǎn)擊創(chuàng)建證書,上傳步驟 1 中的 CertificateSigningRequest.certSigningRequest 文件,然后下載 .cer 證書文件。

image

3、這里需要注意不能直接把這個(gè) .cer 證書文件安裝到打包服務(wù)上,而是把這個(gè) .cer 先安裝到上面第 1 步中生成的 CertificateSigningRequest.certSigningRequest 的機(jī)器上,然后通過導(dǎo)出證書生成帶有密碼的 p12 證書文件,這個(gè)文件才是可以安裝到打包機(jī)器上的證書文件。

image

4、安裝證書,把 p12 文件放置到打包服務(wù)上,然后點(diǎn)擊證書,輸入 3 中創(chuàng)建時(shí)輸入的密碼,安裝到鑰匙串的 “登陸” ,這時(shí)候就可以看到鑰匙串證書里帶有 TeamId 的 Apple Distribution 證書。

5、需要額外注意安裝后可能會(huì)看到說“證書不受信任”的提示,這可能是因?yàn)闄C(jī)器上缺少 AppleWWDRCA (Apple Worldwide Developer Relations Certification Authority)證書,可以通過下面的地址進(jìn)行安裝解決:

三、配置描述文件

配置完證書后就是配置描述文件,在蘋果開發(fā)者網(wǎng)站的 Profiles 創(chuàng)建對(duì)應(yīng)的 mobile provision 。

1、選擇 Distribution - App Store 創(chuàng)建對(duì)應(yīng)的打包模式,如果是 QA 的話一般選擇 Ad Hoc ,也就是需要文件綁定設(shè)備 UDID ,而不需要上架 Store 的模式。

image

2、選擇需要支持的 App Id ,也就是 bundle Id 。

image

3、選擇前面生成的 Distribution 證書 ,這里主要一定要選擇同意同一個(gè)。

image

4、最后輸入 Provisioning Profile Name ,這個(gè) Name 在后面會(huì)有作用,另外如果是 Ad Hoc 的話,在這一步可以選擇已經(jīng)添加的 Devices 的 UDID 。

5、完成配置后下載這個(gè) mobile provision 文件,將它放到打包機(jī)器上的 /Users/你的賬號(hào)/Library/MobileDevice/Provisioning Profiles 目錄下,后面會(huì)需要用到它。

如果是 store 版本的就選擇 Distribution - App Store , 如果是 QA 版本的就選擇 Distribution - Ad Hoc , 因?yàn)?App Store 打出來的包只能通過 Store 或者官方 TestFight 下載,而 Ad Hoc 打包的可以通過內(nèi)部自定義分發(fā)下載(通過添加測(cè)試設(shè)備的 UDID)。

四、配置項(xiàng)目

完成了證書和描述文件的配置后,接下來就是針對(duì)項(xiàng)目的配置。

首先將需要打包的項(xiàng)目 clone 到打包機(jī)器上(只是為了做測(cè)試配置),然后打開項(xiàng)目 ios/Runner.xcworkspace 目錄,這時(shí)候可以看到項(xiàng)目因?yàn)闆]有開發(fā)者賬號(hào),是如下圖所示的狀態(tài):

image

然后我們?nèi)∠x購(gòu) Automatically manage signing , 然后選中我們前面放置的描述文件,就可以看到 Xcode 會(huì)自動(dòng)匹配到鑰匙串里的證書,然后顯示正常的證書和描述文件配置了。

image

這里有一個(gè)需要注意的點(diǎn),那就是項(xiàng)目在我們本地開發(fā)默認(rèn)使用的就是 Automatically manage signing 的方式,因?yàn)檫@樣比較方便,所以我們其實(shí)是需要在打包時(shí)讓它變成手動(dòng)簽名,并且指定 mobile provision 文件的模式

所以前面在打包機(jī)器上操作 Xcode 取消 Automatically manage signing 指定描述文件后,其實(shí)已經(jīng)修改了項(xiàng)目的 ios/Runner.xcodeproj/project.pbxproj ,所以這時(shí)候你只需要通過 git diff 命令就可以導(dǎo)出一個(gè) patch 文件,這樣在項(xiàng)目被 clone 下來后,通過 git apply 直接調(diào)整項(xiàng)目的描述文件。

 git diff >./release.patch     

如果有多種編譯模式,比如一個(gè)項(xiàng)目打包多個(gè) bundleId 和描述文件(QA 、Release), 那就可以生成多個(gè) .patch 文件。

?? 注意:第三方打包機(jī)器上每次打包都是 clone 一個(gè)新項(xiàng)目,打包后刪除該項(xiàng)目,這樣可以保證每次打包的獨(dú)立和干凈,而通過改生成不同的 .patch 文件,我們可以指向不同的 mobile provision,從而加載不同的證書,甚至是同一個(gè)項(xiàng)目打包出不同的 bundle id。

五、開始打包

1、開發(fā)打包之前,需要先執(zhí)行 security unlock-keychain -p xxxxx ,解鎖下 keychain ,這里的 xxxxx 就是你 Mac 上的密碼。

2、通過 flutter build ios --release 打包出 release 模式的 App.frameworkFlutter.framework 。

3、通過 xcodebuild 命令,如下開始編譯 iOS 代碼了,其中 $PWD 是所在工作目錄:

xcodebuild -workspace Runner.xcworkspace -scheme Runner -sdk iphoneos -configuration Release archive -archivePath $PWD/build/Runner.xcarchive

??這里有一個(gè)需要注意,那就是打包過程中如果出現(xiàn) .sh 腳本的相關(guān)報(bào)錯(cuò),比如xcode_backend.sh" embed_and_thin 或者 PhaseScriptExecution Thin\ Binary /Users/xxxxx/Library/Developer/Xcode/DerivedData/ 的錯(cuò)誤,推薦先在打包機(jī)上用 Xcode 執(zhí)行一次完整的 Archive 流程,在首次執(zhí)行過程應(yīng)該會(huì)出現(xiàn)關(guān)于某些 sh 的授權(quán)執(zhí)行彈框,輸入密碼點(diǎn)始終完成,然后再重新執(zhí)行上述腳本。

4、執(zhí)行完 Archive 之后,就可以進(jìn)入 export 階段,exportArchive 之前需要先準(zhǔn)備一個(gè) ExportOptions.plist 文件用戶指定到處的配置,模板類似:

<?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>destination</key>
    <string>export</string>
    <key>method</key>
    <string>app-store</string>
    <key>provisioningProfiles</key>
    <dict>
        <key>你的 bundleId </key>
        <string>前面 provision 定義的 name</string>
    </dict>
    <key>signingCertificate</key>
    <string>Apple Distribution</string>
    <key>signingStyle</key>
    <string>manual</string>
    <key>stripSwiftSymbols</key>
    <true/>
    <key>teamID</key>
    <string>你的開發(fā)證書的 Team Id</string>
    <key>uploadBitcode</key>
    <false/>
    <key>uploadSymbols</key>
    <false/>
</dict>
</plist>

其中

  • method 的數(shù)值如果是 store 就寫 app-store ,如果是 QA 就寫 ad-hoc ;
  • provisioningProfiles<dict>需要 bundleId 和前面 provision 定義的 name ;
  • teamID 需要的是你的開發(fā)證書的 Team Id;
  • 如果是 store 可以增加 uploadBitcodeuploadSymbols 的配置,如果是 QA 則可以不指定,然后 QA 可以也指定 thinning 模式;
image

接著通過指定命令 exportArchive ,指定 ExportOptions.plist ,如果是有不同 id 或者不同模式,一般需要配置 QA 和 Prod 兩種 ExportOptions.plist ,最終輸出到 package_path 這時(shí)候你就得到了一個(gè) ipa 文件。

xcodebuild -exportArchive -exportOptionsPlist ExportOptions.plist -archivePath $PWD/build/Runner.xcarchive -exportPath $package_path -allowProvisioningUpdates

最后如果是 store 模式的,接下來你只需要通過 Mac 的 Transporter 將 ipa 上傳到 App Store Connect,或者使用命令行工具將自己的應(yīng)用或內(nèi)容上傳至 App Store Connect 。

$ xcrun altool --validate-app -f file -t platform -u username [-p password] [--output-format xml]
$ xcrun altool --upload-app -f file -t platform -u username [-p password] [—output-format xml]

一般 altool 位于 /Applications/Xcode.app/Contents/Developer/usr/bin/altool ,更多可見 https://help.apple.com/asc/appsaltool/

如果你是 QA 模式,那么你需要先準(zhǔn)備一個(gè) html 文件,如下所示例子,通過 a 標(biāo)簽配置 itms-service 指定一個(gè) DistributionSummary.plist 文件。


<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Document</title>
</head>
<body>
<a href=itms-services://?action=download-manifest&url=https://xxxx.xxxx.cn/aaaa/bbbbb/ios/DistributionSummary.plist>install</a>
</body>
</html>

然后在 DistributionSummary.plist 文件中指定 software-package 的 ipa 下載地址,這樣就可以完成 QA 的內(nèi)部自助分發(fā)了。(只能安裝 QA provision 里已經(jīng)配置了 UDID 那些機(jī)器

<?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://xxxx.xxxxxx.cn/xxxx/Runner.ipa</string>
                </dict>
                <dict>
                    <key>kind</key>
                    <string>full-size-image</string>
                    <key>needs-shine</key>
                    <true/>
                    <key>url</key>
                    <string>http://xxxx.xxxxxx.cn/assets/applog/icon.png</string>
                </dict>
                <dict>
                    <key>kind</key>
                    <string>display-image</string>
                    <key>needs-shine</key>
                    <true/>
                    <key>url</key>
                    <string>http://xxxx.xxxxxx.cn/assets/applog/icon.png</string>
                </dict>
            </array>
            <key>metadata</key>
            <dict>
                <key>bundle-identifier</key>
                <string>com.xxxx.demo</string>
                <key>bundle-version</key>
                <string>1.0.0</string>
                <key>kind</key>
                <string>software</string>
                <key>title</key>
                <string>XXXX App download</string>
            </dict>
        </dict>
    </array>
</dict>
</plist>

六、多 Flutter 版本環(huán)境

如果需求有存在多個(gè)項(xiàng)目需要在一個(gè)機(jī)器打包,但是不同項(xiàng)目的 Flutter 等版本都不同,那么對(duì)于 Mac 可以開啟多個(gè)不同的登陸用戶,這樣就可以得到不同的打包環(huán)境,當(dāng)然這里主要注意的是 CocoaPod 的版本問題,因?yàn)楸热?:

  • Flutter 1.22 版本默認(rèn)是使用 1.8.0 之類的 Pod 版本,如果在 Flutter 1.22 上使用 1.10.0 的 Pod 版本會(huì)導(dǎo)致 logo 錯(cuò)誤等問題;

  • Flutter 2.0 需要的是 1.10.0 的 Pod 版本;

在 Mac 上默認(rèn) CocoaPod 是安裝在 usr/local/bin 目錄,這個(gè)目錄其實(shí)是多賬號(hào)共享,所以為了解決這個(gè)問題,需要在每個(gè)賬戶環(huán)境下安裝 rvm ,用于管理獨(dú)立的 CocoaPod 版本。

簡(jiǎn)單地說:

  • 1、先通過 curl 安裝 rvm;
curl -L get.rvm.io | bash -s stable && source ~/.rvm/scripts/rvm
  • 2、通過 rvm install 2.5.5 安裝對(duì)應(yīng)的 ruby 版本,具體可以通過 rvm list known 選中你想要需要的版本

這里需要注意 rvm install 可能會(huì)失敗,一般和 brew 需要 update 還有網(wǎng)絡(luò)情況有關(guān)系;

  • 3、可以安裝多個(gè) ruby 版本,然后通過 rvm use <Version> --default 或者 rvm use <Version> 來使用具體版本

不加 defalut 的話,下次啟動(dòng)命令行會(huì)變成原來的 defalut 版本;

  • 4、在當(dāng)前 ruby 版本下安裝想要的 cocoapods 版本,這樣當(dāng)使用 rvm use 切換版本時(shí),cocoapods 版本也會(huì)跟著切換。
sudo gem install cocoapods -v <Version> -n /usr/local/bin

事實(shí)上在不同用戶下安裝了 rvm 之后,彼此之間的 Pod 版本就已經(jīng)分割開了。

七、最后

說了那么多,其實(shí) Xcode 自動(dòng)打包確實(shí)舒服很多,但是通過整個(gè)配置過程,也可以幫助你了解到以前不知道的打包和認(rèn)證過程。

這里最后額外補(bǔ)充一句,通過如下命令,在打包 Android 或者 iOS 時(shí),可以通過 --dart-define 來指定不同的 dart 參數(shù).

flutter build ios --release --dart-define=CHANNEL=GSY --dart-define=LANGUAGE=Dart

在 dart 代碼里可以通過 String.fromEnvironment 獲取到對(duì)應(yīng)的自定義配置參數(shù)。

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

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

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