一、背景
七魚SDK在今年經(jīng)過出海業(yè)務(wù)、視頻客服業(yè)務(wù)的迭代后,發(fā)布流程已經(jīng)變的繁瑣,這些流程不僅費(fèi)時(shí)費(fèi)力,還特別容易出現(xiàn)人為的錯(cuò)誤。于是,我從優(yōu)化打包腳本出發(fā),實(shí)現(xiàn)一鍵自動(dòng)發(fā)布版本。
二、發(fā)版流程
七魚現(xiàn)有的發(fā)版流程如下圖:

每次發(fā)版,都需要發(fā)上圖中的國內(nèi)版、國際版、cocpoads版。由于這三個(gè)版本所對應(yīng)的資源和代碼分支是有一定差別的,所以每次發(fā)完一種版本后,都需要手動(dòng)切換對應(yīng)的資源和配置,切換完之后,分別打蒲公英包和SDK的包。整個(gè)流程不僅繁瑣耗時(shí),而且非常容易出錯(cuò)。如果這個(gè)流程某個(gè)步驟出錯(cuò)了,很難人為的發(fā)現(xiàn),更多是依賴客戶或者技術(shù)支持的同事在真正使用場景時(shí)才發(fā)現(xiàn)包有問題。
三、打包腳本--Fastlane
基于上述打包流程的問題,如果能夠通過腳本自動(dòng)去執(zhí)行,那么不僅可以簡化發(fā)版流程,降低錯(cuò)誤率,而且大量節(jié)省人力。
于是,我們打算使用Fastlane來優(yōu)化流程。
1、Fastlane簡介
Fastlane 是一整套的客戶端 CI 工具集合,替代開發(fā)者處理構(gòu)建和發(fā)布 App 中繁瑣的任務(wù),可以非??焖俸唵蔚拇罱ㄒ粋€(gè)自動(dòng)化發(fā)布服務(wù),并且支持Android,iOS,MacOS。Fastlane本身沒有一套特殊語法,使用的 Ruby 語言。Fastlane的安裝以及環(huán)境的搭建,網(wǎng)上有很多的資料可以翻閱,本文將不贅述,本文將著重介紹如何實(shí)現(xiàn)上述復(fù)雜流程的一鍵發(fā)布。
2、Fastlane的使用
搭建好Fastlane的環(huán)境以后,在工程目錄下執(zhí)行 fastlane init,打開Fastlane文件,就可以開始編寫腳本了。
platform :ios do
desc "Description of what the lane does"
lane :custom_lane do
# add actions here: https://docs.fastlane.tools/actions
end
end
這是每一個(gè)fastlane腳本的最簡單形式。最外面的"ios"層是腳本入口,類似于main函數(shù)。在腳本中可以自定義action,其中fastlane是關(guān)鍵字,相當(dāng)于函數(shù)的function,當(dāng)然,也可以寫成有參的形式:
lane :custom_lane do |options|
end
fastlane提供了一些內(nèi)置的action能力,可以通過查閱官方文檔來獲取。
fastlane中的action既可以被內(nèi)部調(diào)用,也可以被外部命令行調(diào)用。
例如內(nèi)部調(diào)用:
platform :ios do
desc "Description of what the lane does"
lane :custom_lane do
# add actions here: https://docs.fastlane.tools/actions
end
lane :custom_lane_options do |options|
custom_lane
end
end
例如命令行調(diào)用:
bundle exec fastlane ios custom_lane
內(nèi)部調(diào)用時(shí),就和調(diào)用函數(shù)用法相同,命令行調(diào)用時(shí),由于fastlane是依賴于bundle環(huán)境的,需要用bundle來驅(qū)動(dòng),同時(shí)需要申明執(zhí)行的fastlane腳本名稱“ios”。
四、一鍵發(fā)版
從上述發(fā)版流程我們可以知道,一次發(fā)版,總共需要產(chǎn)出以下幾項(xiàng):
1、國內(nèi)版:蒲公英版本A、zip壓縮包A
2、國際版:蒲公英版本B、zip壓縮包B
3、cocoapods版
這三項(xiàng)的內(nèi)容有一部分是重疊的,有一部分是不同的。
在寫腳本之前,我們先對各版本內(nèi)部組成有一個(gè)簡單的了解:
蒲公英版本A、zip壓縮包A:在線模塊+視頻模塊+國內(nèi)版NIM+國內(nèi)版配置文件
蒲公英版本B、zip壓縮包B:在線模塊+國際版NIM+國際版配置文件
cocoapods版:在線模塊+視頻模塊+國內(nèi)版NIM(或國際版NIM)+國際版配置文件
其中,蒲公英版本是ipa包傳到蒲公英平臺;zip壓縮包是各模塊SDK組合而成。
1、各分支模塊拆解
基于上述內(nèi)容我們可以知道,“在線模塊”是公共模塊,剩余的模塊是選配模塊,并且各模塊是需要單獨(dú)打包的.其中,在線和視頻模塊需要使用xcframework形式的包,而NIM需要使用framework的包(保持與云信同步)。所以,我們可以先打在線模塊、視頻模塊的包:
lane :sdk do |options|
#清理文件夾
clear_derived_data(derived_data_path: "./DerivedData")
clear_derived_data(derived_data_path: "./QYProduct")
#打SDK包
customSchemes = ["QYSDK", "QYVideoService"]
for customScheme in customSchemes
xcbuild(
scheme: customScheme,
configuration: "Release",
destination: "generic/platform=iOS Simulator",
sdk: "iphonesimulator",
xcargs: "-quiet ARCHS='x86_64' BITCODE_GENERATION_MODE=marker ",
derivedDataPath: "./DerivedData"
)
xcbuild(
scheme: customScheme,
destination: "generic/platform=iOS",
configuration: "Release",
sdk: "iphoneos",
xcargs: "-quiet ARCHS='arm64' BITCODE_GENERATION_MODE=bitcode ",
derivedDataPath: "./DerivedData"
)
create_xcframework(
frameworks_with_dsyms: {
"DerivedData/Build/Products/Release-iphonesimulator/#{customScheme}.framework" => { dsyms: Dir.pwd + "/../DerivedData/Build/Products/Release-iphonesimulator/#{customScheme}.framework.dSYM" },
"DerivedData/Build/Products/Release-iphoneos/#{customScheme}.framework" => { dsyms: Dir.pwd + "/../DerivedData/Build/Products/Release-iphoneos/#{customScheme}.framework.dSYM" } },
output: "QYProduct/QY_iOS_SDK/SDK/#{customScheme}.xcframework")
end
end
解析: fastlane中支持使用Xcode的工具來打包,官方已經(jīng)提供xcbuild了,各入?yún)⒖梢愿鶕?jù)項(xiàng)目需要自行配置,分別打真機(jī)和模擬器的包即可。打完包后,需要將兩種包合并成xcframework,使用create_xcframework即可。
接下來我們需要打NIM的SDK包。由于國內(nèi)版和國際版使用了不同的NIM分支,因此,我們在打包之前需要先切換分支,然后再打包。然而,我們的NIM模塊是以submodule的方式存在的,而fastlane本身能使用submodule的功能很有限,于是我們想到,是否可以在fastlane內(nèi)部去執(zhí)行g(shù)it指令,這樣就可以間接的完成分支切換了,于是就有如下的代碼:
cmd = "git submodule foreach git checkout . && git submodule foreach git checkout 9.2.8"
`#{cmd}`
fastlane支持在控制臺間接執(zhí)行g(shù)it指令,如果有需要連續(xù)執(zhí)行多條指令,指令直接可以使用“&&”來連接。上述指令則實(shí)現(xiàn)了清空子模塊的變更,同時(shí)將分支切換到9.2.8。仔細(xì)觀察,我們會(huì)發(fā)現(xiàn),指令中有一個(gè)foreach,這是循環(huán)遍歷執(zhí)行子模塊的指令。那么問題就出現(xiàn)了,我的工程是有多個(gè)子模塊的,而我僅僅需要切換一個(gè)模塊,恰好我需要執(zhí)行的子模塊處于第一個(gè),當(dāng)foreach執(zhí)行出錯(cuò)后,會(huì)停止繼續(xù)執(zhí)行,這樣也算勉強(qiáng)能夠?qū)崿F(xiàn)功能。除了foreach,是否有別的命令可以實(shí)現(xiàn)切換單個(gè)子模塊分支呢?經(jīng)過一番尋找和思考后,最終還是失敗了,但也有想到兩個(gè)替代方案。
方案一: git是支持命令擴(kuò)展的,我們可以通過擴(kuò)展的方式去自定義一個(gè)切換指定子模塊分支的命令,然后再通過fastlane去執(zhí)行這個(gè)命令。
方案二: 用shell腳本或note.js腳本來實(shí)現(xiàn)切換分支,然后通過fastlane去執(zhí)行腳本。
這兩種方案還待后續(xù)研究,此處先粗糙使用foreach過度一下。
我們但NIM包是需要根據(jù)不同分支打不同包的,因此我們就需要使用到帶參數(shù)的fastlane action,參考如下:
lane :nim_package do |options|
case options[:type]
when 'abroad'
#國際版
else
#國內(nèi)版
end
我們可以根據(jù)入?yún)磉x擇需要切換的分支,這樣,我們NIM打包action的完整內(nèi)容如下:
lane :nim_package do |options|
code_path = File.expand_path("..", File.dirname(__FILE__)).to_s
case options[:type]
when 'abroad'
cmd = "git submodule foreach git checkout . && git submodule foreach git checkout 9.2.8"
clear_derived_data(derived_data_path: "./QYProduct/abroad")
out_path = "#{code_path}/QYProduct/abroad/QY_iOS_SDK/NIMSDK"
else
cmd = "git submodule foreach git checkout . && git submodule foreach git checkout feature_8.9.2_noopenssl_compress"
clear_derived_data(derived_data_path: "./QYProduct/enterprise")
out_path = "#{code_path}/QYProduct/enterprise/QY_iOS_SDK/NIMSDK"
end
`#{cmd}`
xcbuild(
scheme: "NIMSDK",
configuration: "Release",
destination: "generic/platform=iOS Simulator",
sdk: "iphonesimulator",
xcargs: "-quiet ARCHS='x86_64' BITCODE_GENERATION_MODE=marker ",
derivedDataPath: "./DerivedData"
)
xcbuild(
scheme: "NIMSDK",
destination: "generic/platform=iOS",
configuration: "Release",
sdk: "iphoneos",
xcargs: "-quiet ARCHS='arm64' BITCODE_GENERATION_MODE=bitcode ",
derivedDataPath: "./DerivedData"
)
iphonesimulator_path = "#{code_path}/DerivedData/Build/Products/Release-iphonesimulator/NIMSDK.framework"
iphoneos_path = "#{code_path}/DerivedData/Build/Products/Release-iphoneos/NIMSDK.framework"
command = "mkdir -p #{out_path} && cp -rf #{iphoneos_path} #{out_path}/NIMSDK.framework && lipo -create #{iphoneos_path}/NIMSDK #{iphonesimulator_path}/NIMSDK -output #{out_path}/NIMSDK.framework/NIMSDK"
`#{command}`
end
打完NIM的包后,我們得到了兩個(gè)文件夾,分別存放了國內(nèi)版和國際版的SDK,這樣,我們后續(xù)發(fā)版的時(shí)候也不再需要手動(dòng)調(diào)整文件夾的內(nèi)容。
到目前為止,SDK的包都已經(jīng)打好了,接下來需要打ipa包,并上傳到蒲公英。
由于蒲公英的包也需要打國內(nèi)和國外的包,同時(shí)需要分配不同的配置文件,因此,我們?nèi)匀恍枰玫綆?shù)的action,并輸出到指定的路徑。
lane :qy_package do |options|
case options[:type]
when 'abroad'
copy_artifacts(
target_path: 'QYDemo/Resources',
artifacts: ['Abroad/QYConfigResource.bundle']
)
abroad
copy_artifacts(
target_path: 'QYProduct/abroad/QY_iOS_SDK/SDK',
artifacts: ['QYProduct/QY_iOS_SDK/SDK/QYSDK.xcframework']
)
else
copy_artifacts(
target_path: 'QYDemo/Resources',
artifacts: ['Default/QYConfigResource.bundle']
)
enterprise
copy_artifacts(
target_path: 'QYProduct/enterprise/QY_iOS_SDK/SDK',
artifacts: ['QYProduct/QY_iOS_SDK/SDK/QYSDK.xcframework','QYProduct/QY_iOS_SDK/SDK/QYVideoService.xcframework']
)
end
end
這里涉及到打包上傳到蒲公英的環(huán)節(jié),這里面涉及到證書的管理,可以參考另一篇文章fastlane通過match管理證書。我們先看一下打包并上傳蒲公英的action:
platform :ios do
desc "企業(yè)包"
lane :enterprise do |options|
sync_code_signing(
type: "enterprise",
app_identifier: ["bundleId"],
readonly: true,
username: "用戶名",
keychain_password: options[:keychain_password])
build_app(
scheme: "QYDemo",
include_bitcode: false,
destination: "generic/platform=iOS",
)
pgyer(api_key: "api_key",
password: "蒲公英包下載密碼",
install_type: "2",
update_description: "update by fastlane")
end
涉及的action內(nèi)容,在fastlane通過match管理證書有詳細(xì)講解,此文不重復(fù)贅述。
2、完整打包及優(yōu)化風(fēng)險(xiǎn)項(xiàng)
至此,我們已經(jīng)將所有需要打的包都打好了,接下來需要對一下額外的資源文件進(jìn)一步分配處理,同時(shí)將調(diào)用上述所有的action:
lane :sdk_package do |options|
clear_derived_data(derived_data_path: "./DerivedData")
clear_derived_data(derived_data_path: "./QYProduct")
#打SDK包
sdk
#國內(nèi)
nim_package(type:'enterprise')
qy_package(type:'enterprise')
#國外
nim_package(type:'abroad')
qy_package(type:'abroad')
version = get_version_number(
xcodeproj: "QYDemo.xcodeproj",
target: "QYDemo"
)
version_bump_podspec(
path: "QY_iOS_SDK.podspec",
version_number: "#{version}"
)
copy_artifacts(
target_path: 'QYProduct/QY_iOS_SDK',
artifacts: ['QY_iOS_SDK.podspec']
)
targets = ["abroad/QY_iOS_SDK","enterprise/QY_iOS_SDK","QY_iOS_SDK/SDK"]
for target in targets
copy_artifacts(
target_path: "QYProduct/#{target}",
artifacts: ['開發(fā)指南.md','Abroad/資源文件說明.md']
)
copy_artifacts(
target_path: "QYProduct/#{target}/Resources",
artifacts: ['QYDemo/Resources/QYResource.bundle', 'QYDemo/Resources/QYCustomResource.bundle', 'QYDemo/Resources/QYLanguage.bundle']
)
if target != "enterprise/QY_iOS_SDK"
copy_artifacts(
target_path: "QYProduct/#{target}/Resources",
artifacts: ['Abroad/QYConfigResource.bundle']
)
end
if target != "abroad/QY_iOS_SDK"
copy_artifacts(
target_path: "QYProduct/#{target}/Resources",
artifacts: ['QYDemo/Resources/QYVideoResource.bundle']
)
end
if target == "abroad/QY_iOS_SDK"
zip(
path: "QYProduct/#{target}",
output_path: "QYProduct/QY_iOS_SDK_v#{version}_Abroad.zip"
)
end
if target == "enterprise/QY_iOS_SDK"
zip(
path: "QYProduct/#{target}",
output_path: "QYProduct/QY_iOS_SDK_v#{version}.zip"
)
end
end
#打包完,恢復(fù)對應(yīng)分支
cmd = "git submodule foreach git checkout . && git submodule foreach git checkout feature_8.9.2_noopenssl_compress"
`#{cmd}`
copy_artifacts(
target_path: 'QYDemo/Resources',
artifacts: ['Default/QYConfigResource.bundle']
)
sentry_upload_dif(
path: Dir.pwd + '/../QYProduct/'
)
end
這是一個(gè)主action,通過嵌套調(diào)用子action實(shí)現(xiàn)打包,在打完包后,將不同的資源和文件分配到每個(gè)包的路徑中,同時(shí),讀取好版本號后寫入podspec文件。
至此,我們完整的打包腳本已經(jīng)完成,我們重新梳理一下這個(gè)腳本它做了哪些事情:
1、打在線模塊、視頻模塊的xcframework
2、切換分支,并打不同分支的NIM的framework
3、根據(jù)不同的分支,打不同的ipa包,并使用match來自動(dòng)管理證書,同時(shí)將包上傳蒲公英
4、更新podspec、readme、資源包等內(nèi)容,并分發(fā)到各版本對應(yīng)文件夾路徑
5、打zip壓縮包,將分支恢復(fù)初始狀態(tài)。
經(jīng)過上述5個(gè)環(huán)節(jié)后,我們最終會(huì)得到如下的產(chǎn)物:


接下來,我們僅需將對應(yīng)的內(nèi)容,發(fā)布到官網(wǎng)、cocoapods即可。
繼續(xù)優(yōu)化方向:
1、優(yōu)化foreach遍歷所有子模塊的方式,實(shí)現(xiàn)精準(zhǔn)切換子模塊
風(fēng)險(xiǎn)考慮:
由于發(fā)版時(shí)需要將打包產(chǎn)物上傳運(yùn)維官網(wǎng)后臺和cocoapods倉庫,因此,可以讓服務(wù)端提供自動(dòng)上傳的接口,通過腳本自動(dòng)傳包。
方向2實(shí)現(xiàn)后,整個(gè)發(fā)版過程僅需要一行命令即可完成,但由于每次打完包都是自動(dòng)上傳發(fā)布,沒辦法人為檢驗(yàn)正確性,如果打包出了問題,或者打錯(cuò)包了,也會(huì)直接發(fā)出去,而cocoapods的版本管理是需要升版本號的,一旦出錯(cuò),舊的版本號就廢棄不能用了,存在一定的風(fēng)險(xiǎn)。