使用fastlane實(shí)現(xiàn)framework一鍵發(fā)版

一、背景

七魚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ā)版流程.png

每次發(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)物:


image.png
image.png

接下來,我們僅需將對應(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)。

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

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

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