Flutter iOS CI腳本製作

我們必須全力以赴,同時又不抱持任何希望。不管做什麼事,都要當它是全世界最重要的一件事,但同時又知道這件事根本無關緊要。

前言

分析了一堆的內容,其實主要就是爲了做Flutter的CI的事情。讓我們在本文中探討如何製作Flutter CI for iOS。

踩坑的本機環境

注意對比下我的環境和你的環境是否一樣,有些問題在Flutter的新版本中已經被修復了。

➜  app git:(master) ✗ flutter --version
Flutter 1.9.1+hotfix.6 • channel stable • https://github.com/flutter/flutter.git
Framework • revision cc949a8e8b (3 weeks ago) • 2019-09-27 15:04:59 -0700
Engine • revision b863200c37
Tools • Dart 2.5.0

鋪墊

本文基於我的前幾篇文章的分析所得,如果你不清楚Flutter的編譯產物和編譯流程,建議閱讀我的前幾篇文章:

文章 說明
Flutter build ios產物分析 介紹Flutter的編譯產物以及官方接入方案的產物的編譯、組織流程。
Flutter xcode_backend分析 承接上文,上文中使用了xcode_backend.sh腳本生成編譯產物,本文基於上文做深層次的Flutter混合編譯分析和介紹。

初始環境

我配置好了一個臨時的分析工程(Native接入Flutter的工程目錄結構)。

➜  DevelopProjects tree -L 2 temp
temp
├── Android												# Android工程
├── flutter_module										# Flutter工程
│   ├── README.md
│   ├── build
│   ├── flutterApp.podspec
│   ├── flutter_01.log
│   ├── flutter_module.iml
│   ├── flutter_module_android.iml
│   ├── flutter_podhelper.rb
│   ├── lib
│   ├── pubspec.lock
│   ├── pubspec.yaml
│   └── test
├── iOS													# iOS工程
│   ├── Podfile
│   ├── Podfile.lock
│   ├── Pods
│   ├── iOS
│   ├── iOS.xcodeproj
│   └── iOS.xcworkspace
└── startBuild.sh										# iOS產物CI編譯腳本

10 directories, 11 files

製作過程

先看一眼編譯後的東西:

➜  app ✗ tree build -L 3				# 省略了部分內容
build									# 編譯目錄
├── aot
│   ├── App.framework					# Flutter業務層代碼
│   ├── App.framework.dSYM.noindex
│   ├── app.dill
│   ├── arm64
│   ├── armv7
│   ├── frontend_server.d
│   └── kernel_compile.d
├── dSYMs.noindex
│   └── App.framework.dSYM
├── ios
│   ├── Debug-iphonesimulator 			# x86_64架構,Debug
│   ├── Release # 合併Debug-iphonesimulator和Release-iphoneos的產物
│   ├── Release
│   │   ├── libFlutterPluginRegistrant.a
│   │   ├── xxx.a
│   │   └── xxx.a
│   └── Release-iphoneos 				# arm64 armv7,Release
└── libFlutterPlugins.a 				# 所有的插件及FlutterPluginRegistrant

11 directories, 4 files

啓動流程

本腳本可以應用於Jenkints,也可以做成其他的自動化的部分。

根據Flutter build ios產物分析Flutter xcode_backend分析的分析結果,可以簡單定下以下的啓動流程,分別爲如下流程:

  1. 獲取輸入的變量
  2. 初始化環境變量
  3. 編譯App.framework(Flutter業務層代碼的framework,包含了靜態資源)
  4. 生成dSYM(iOS編譯後的符號表,用戶還原符號)
  5. Strip dSYM(剝離符號表)
  6. 編譯Flutter資源文件
  7. 編譯Flutter插件(將Flutter的插件連同註冊制都編譯爲一個framework,對外僅僅提供註冊制的header)
  8. unset環境變量
# 主要流程
main() {
  # Flutter工程目錄
  cd ./flutter_module

  InitVariable       # 獲取輸入的變量
  InitEnv            # 初始化環境變量
  BuildAppFramework  # 編譯App.framework
  GeneratedSYM       # 生成dSYM
  StripdSYM          # Strip dSYM
  BuildFlutterAssets # 編譯Flutter資源
  BuildFlutterPlugin # 編譯Flutter插件
  UnsetVariable      # unset環境變量

  # 退出到原來目錄
  cd -
}

# 啓動腳本
main

根據上述腳本,逐漸填充函數實現即可。

初始化變量

  • 如果使用Jenkints的話,可以將下述參數配置:
# 用於命令行的輸出
EchoDone() {
  echo ""
  echo " └─$1"
  echo ""
}

# 初始化從外部輸入的變量
InitVariable() {
  echo " ├──Input variable..."
  export FLT_PROJ_BUILD_MODE='release' # profile/release
  Echo " ├────FLT_PROJ_BUILD_MODE:${FLT_PROJ_BUILD_MODE}"

  export FLT_ARCH='armv7+arm64'
  echo " ├────FLT_ARCH:${FLT_ARCH}"
}

##初始化編譯環境

InitEnv() {
  # 設置Flutter Pub地址
  export PUB_HOSTED_URL='https://pub.flutter-io.cn'
  echo " ├────PUB_HOSTED_URL=${PUB_HOSTED_URL}"
  echo ""
  echo " ├────Flutter version:"
  echo ""
  flutter --version
  echo ""
  echo " ├────Clean flutter building artifacts"
  echo ""
  flutter clean
  EchoDone "Clean flutter building artifacts done"

  echo " ├────Fetch flutter project dependences"
  # 更新依賴
  flutter pub get
  flutter packages get
  EchoDone "Fetch flutter project dependences done"
	
	# 工程根目錄
  export FLT_PROJ="$(pwd)"
  # 編譯輸出目錄
  export FLT_PROJ_BUILD="${FLT_PROJ}/build"
  # Flutter編譯的ios工程或者.ios的地址
  export FLT_PROJ_iOS="${FLT_PROJ}/ios"

  # 如果.ios存在則是Module工程
  if [[ -e "${FLT_PROJ}/.ios" ]]; then
    unset FLT_PROJ_iOS
    export FLT_PROJ_iOS="${FLT_PROJ}/.ios"
  fi

  # 輸出目錄
  export FLT_PROJ_iOS_FLT="${FLT_PROJ_iOS}/Flutter"
	
	# 輸出環境變量,([]:中括號表示可選)
  echo " ├────./ -> ${FLT_PROJ}"
  echo " ├────./build -> ${FLT_PROJ_BUILD}"
  echo " ├────./[.]ios -> ${FLT_PROJ_iOS}"
  echo " ├────./[.]ios/Flutter -> ${FLT_PROJ_iOS_FLT}"

  # 需要創建文件夾用於存放生成的一些文件
  # 不然部分文件因爲目錄不存在而創建失敗
  mkdir -p "${FLT_PROJ_iOS_FLT}"
  # 清空緩存
  rm -rf "${FLT_PROJ_iOS_FLT}/App.framework"
}

編譯業務代碼

編譯Flutter業務層的代碼:

BuildAppFramework() {
  echo " ├──Building App.framework..."
  echo ""

  flutter --suppress-analytics build aot --${FLT_PROJ_BUILD_MODE} \
    --target-platform=ios \
    --target="lib/main.dart" \
    --ios-arch="${FLT_ARCH/+/,}" \
    --output-dir="${FLT_PROJ_BUILD}/aot"

  export FLT_PROJ_BUILD_AOT_APP="${FLT_PROJ_BUILD}/aot/App.framework"

  cp -r "${FLT_PROJ_BUILD_AOT_APP}" "${FLT_PROJ_iOS_FLT}"
  echo " ├────FLT_PROJ_BUILD_AOT_APP:${FLT_PROJ_BUILD_AOT_APP}"
  EchoDone "Building App.framework done"
}

生成dSYM

GeneratedSYM() {
  echo " ├─Generating dSYM file..."
  echo ""

  mkdir -p -- "${FLT_PROJ_BUILD}/dSYMs.noindex"
  xcrun dsymutil -o "${FLT_PROJ_BUILD}/dSYMs.noindex/App.framework.dSYM" "${FLT_PROJ_BUILD_AOT_APP}/App"

  if [[ $? -ne 0 ]]; then
    echo "Failed to generate debug symbols (dSYM) file for ${FLT_PROJ_BUILD_AOT_APP}/App."
    exit -1
  fi

  EchoDone "Generating dSYM file done"
}

剝離dSYM並嵌入Info.plist

StripdSYM() {
  echo " ├─Stripping debug symbols..."
  echo ""

  xcrun strip -x -S "${FLT_PROJ_iOS_FLT}/App.framework/App"
  if [[ $? -ne 0 ]]; then
    echo "Failed to strip ${FLT_PROJ_iOS_FLT}/App.framework/App."
    exit -1
  fi

  cp "${FLT_PROJ_iOS_FLT}/AppFrameworkInfo.plist" ${FLT_PROJ_iOS_FLT}/App.framework/Info.plist

  EchoDone "Stripping debug symbols done"
}

編譯Flutter assets

BuildFlutterAssets() {
  echo " ├─Building flutter_assets..."
  echo " ├─Assembling Flutter resources..."
  echo ""

  flutter --suppress-analytics build bundle --${FLT_PROJ_BUILD_MODE} \
    --target-platform=ios \
    --target="lib/main.dart" \
    --depfile="${FLT_PROJ_BUILD}/snapshot_blob.bin.d" \
    --asset-dir="${FLT_PROJ_iOS_FLT}/App.framework/flutter_assets" \
    --precompiled

  EchoDone "Assembling Flutter resources done"
}
  • --precompiled只有Release版本纔會有這個參數。

插件靜態庫的構建

插件靜態庫的構建需要用到一個flutter pub get;flutter packages get;flutter build ios的一個產物.flutter-plugins。如果對於.flutter-plugins不理解的,可以查看Flutter build ios產物分析的分析。

插件靜態庫的編譯需要編譯兩份,一份是arm64 armv7(真機),一份是x86_64(模擬器)

  • arm64 armv7:真機Release
/usr/bin/env xcrun xcodebuild BUILD_DIR							\
	-configuration Release ARCHS='arm64 armv7'				\
	-target ${plugin_name} BUILD_DIR=../../build/ios	\
	-sdk iphoneos																			\
	-quiet
  • x86_64:用於虛擬機調試用
/usr/bin/env xcrun xcodebuild build									\
	-configuration Debug ARCHS='x86_64' 							\
	-target ${plugin_name} BUILD_DIR=../../build/ios	\
	-sdk iphonesimulator -quiet
  • 合併產物
lipo -create "../../build/ios/Debug-iphonesimulator/${plugin_name}/lib${plugin_name}.a" "../../build/ios/Release-iphoneos/${plugin_name}/lib${plugin_name}.a" -o "${plugin_lib_path}"

生成插件庫

將源碼編譯爲靜態庫,輸出在./build/ios/Debug-iphonesimulator./build/ios/Release-iphoneos

# 生成靜態庫
GenerateStaticFramewrok() {
  local pluginName=$1

  /usr/bin/env xcrun xcodebuild build \
    -configuration Release ARCHS='arm64 armv7' \
    -target "${pluginName}" BUILD_DIR="${FLT_PROJ_BUILD}/ios" \
    -sdk iphoneos \
    -quiet >/dev/null

  /usr/bin/env xcrun xcodebuild build \
    -configuration Debug ARCHS='x86_64' \
    -target "${pluginName}" BUILD_DIR="${FLT_PROJ_BUILD}/ios" \
    -sdk iphonesimulator \
    -quiet >/dev/null
}

合併插件庫

# 合併靜態庫
MergeStaticFramework() {
  local pluginName=$1
  local pluginLibPath=$2

  local debugFramework="${FLT_PROJ_BUILD}/ios/Debug-iphonesimulator/${pluginName}/lib${pluginName}.a"
  local releaseFramework="${FLT_PROJ_BUILD}/ios/Release-iphoneos/${pluginName}/lib${pluginName}.a"

  lipo -create "${debugFramework}" "${releaseFramework}" -o "${pluginLibPath}"
}

編譯靜態庫併合並

# 編譯靜態庫
BuildStaticFramework() {
  local pluginName=$1
  local pluginLibPath=$2

  echo " ├───Generating lib${plugin_name}.a"
  GenerateStaticFramewrok "${plugin_name}"

  echo " ├───Merging lib${plugin_name}.a"
  echo " ├───Plugin Lib Path: ${pluginLibPath}"
  MergeStaticFramework "${plugin_name}" "${pluginLibPath}"
  echo " └───Merging lib${plugin_name}.a done"
  echo ""
}

合併所有插件和註冊制

這裏將Flutter插件都編譯成了靜態庫,並將所有的靜態庫合併爲一個,方便業務方接入。

  • 註冊制指的是:FlutterPluginRegistrant,Flutter用於主動註冊插件用的庫。
# 編譯Flutter的插件和FlutterPluginRegistrant
BuildFlutterPlugin() {
  echo " ├─Building Flutter Plugin..."
  echo ""

  cd ${FLT_PROJ_iOS}
  # 更新插件依賴,可能會遇到依賴衝突的問題╮(╯▽╰)╭,這個就需要自己解決了
  pod install --no-repo-update
  cd -

  # 進入Flutter/Pods編譯插件
  cd "${FLT_PROJ_iOS}/Pods"

  if [ -f "${FLT_PROJ}/.flutter-plugins" ]; then
    # 聲明數組保存Plugin的路徑
    declare -a plugin_lib_path_arr
    # 獲取所有的插件名稱
    plugin_arr="$(cat "${FLT_PROJ}/.flutter-plugins" | awk -F "=" '{print $1}') FlutterPluginRegistrant"

    echo ""

    # ./build/ios/Release
    local pluginOutput="${FLT_PROJ_BUILD}/ios/Release"
    mkdir -p ${pluginOutput}

    for plugin_name in ${plugin_arr}; do
      if [ "${plugin_name}" = "FlutterPluginRegistrant" ]; then
        echo " └─Building Flutter Plugin done"
        echo ""
        echo " ├─Building FlutterPluginRegistrant..."
      fi

      local pluginLibPath="${pluginOutput}/lib${plugin_name}.a"
      BuildStaticFramework "${plugin_name}" "${pluginLibPath}"
      plugin_lib_path_arr=("${plugin_lib_path_arr[@]}" "${pluginLibPath}")
    done
  fi

  EchoDone "Building FlutterPluginRegistrant done"
  echo " ├─Merging Flutter Plugin and FlutterPluginRegistrant..."
  
  # 生成在build目錄下
  libtool -static -o "${FLT_PROJ_BUILD}/libFlutterPlugins.a" "${plugin_lib_path_arr[@]}" >/dev/null

  EchoDone "Merging Flutter Plugin and FlutterPluginRegistrant done"

  cd -
}
  • libtool -static -o 合併後的文件路徑 aaa.a bbb.a ccc.a...
    • 合併後的文件路徑後的所有*.a合併爲一個,且鏈接的類型-static爲靜態庫。
    • 如果不希望不輸出has no symbols的警告,可以加上-no_warning_for_no_symbols

完結

  • CI腳本製作完畢!_

完整腳本如下:

EchoDone() {
  echo ""
  echo " └─$1"
  echo ""
}

InitVariable() {
  echo " ├──Input variable..."
  export FLT_PROJ_BUILD_MODE='release' # profile/release
  Echo " ├────FLT_PROJ_BUILD_MODE:${FLT_PROJ_BUILD_MODE}"

  export FLT_ARCH='armv7+arm64'
  echo " ├────FLT_ARCH:${FLT_ARCH}"
}

InitEnv() {
  # 設置Flutter Pub地址
  export PUB_HOSTED_URL='https://pub.flutter-io.cn'
  echo " ├────PUB_HOSTED_URL=${PUB_HOSTED_URL}"
  echo ""
  echo " ├────Flutter version:"
  echo ""
  flutter --version
  echo ""
  echo " ├────Clean flutter building artifacts"
  echo ""
  flutter clean
  EchoDone "Clean flutter building artifacts done"

  echo " ├────Fetch flutter project dependences"
  echo ""
  # 更新依賴
  flutter pub get
  flutter packages get
  EchoDone "Fetch flutter project dependences done"

  # 如果.ios存在則是Module工程
  export FLT_PROJ="$(pwd)"
  # 編譯輸出目錄
  export FLT_PROJ_BUILD="${FLT_PROJ}/build"
  # Flutter編譯的ios工程或者.ios的地址
  export FLT_PROJ_iOS="${FLT_PROJ}/ios"

  # 如果.ios存在則是Module工程
  if [[ -e "${FLT_PROJ}/.ios" ]]; then
    unset FLT_PROJ_iOS
    export FLT_PROJ_iOS="${FLT_PROJ}/.ios"
  fi

  # 輸出目錄
  export FLT_PROJ_iOS_FLT="${FLT_PROJ_iOS}/Flutter"

  echo " ├────./ -> ${FLT_PROJ}"
  echo " ├────./build -> ${FLT_PROJ_BUILD}"
  echo " ├────./[.]ios -> ${FLT_PROJ_iOS}"
  echo " ├────./[.]ios/Flutter -> ${FLT_PROJ_iOS_FLT}"

  # 需要創建文件夾用於存放生成的一些文件
  # 不然部分文件因爲目錄不存在而創建失敗
  mkdir -p "${FLT_PROJ_iOS_FLT}"
  # 清空緩存
  rm -rf "${FLT_PROJ_iOS_FLT}/App.framework"
}

BuildAppFramework() {
  echo " ├──Building App.framework..."
  echo ""

  flutter --suppress-analytics build aot --${FLT_PROJ_BUILD_MODE} \
    --target-platform=ios \
    --target="lib/main.dart" \
    --ios-arch="${FLT_ARCH/+/,}" \
    --output-dir="${FLT_PROJ_BUILD}/aot"

  export FLT_PROJ_BUILD_AOT_APP="${FLT_PROJ_BUILD}/aot/App.framework"

  cp -r "${FLT_PROJ_BUILD_AOT_APP}" "${FLT_PROJ_iOS_FLT}"
  echo " ├────FLT_PROJ_BUILD_AOT_APP:${FLT_PROJ_BUILD_AOT_APP}"
  EchoDone "Building App.framework done"
}

GeneratedSYM() {
  echo " ├─Generating dSYM file..."
  echo ""

  mkdir -p -- "${FLT_PROJ_BUILD}/dSYMs.noindex"
  xcrun dsymutil -o "${FLT_PROJ_BUILD}/dSYMs.noindex/App.framework.dSYM" "${FLT_PROJ_BUILD_AOT_APP}/App"

  if [[ $? -ne 0 ]]; then
    echo "Failed to generate debug symbols (dSYM) file for ${FLT_PROJ_BUILD_AOT_APP}/App."
    exit -1
  fi

  EchoDone "Generating dSYM file done"
}

StripdSYM() {
  echo " ├─Stripping debug symbols..."
  echo ""

  xcrun strip -x -S "${FLT_PROJ_iOS_FLT}/App.framework/App"
  if [[ $? -ne 0 ]]; then
    echo "Failed to strip ${FLT_PROJ_iOS_FLT}/App.framework/App."
    exit -1
  fi

  cp "${FLT_PROJ_iOS_FLT}/AppFrameworkInfo.plist" ${FLT_PROJ_iOS_FLT}/App.framework/Info.plist

  EchoDone "Stripping debug symbols done"
}

BuildFlutterAssets() {
  echo " ├─Building flutter_assets..."
  echo " ├─Assembling Flutter resources..."
  echo ""

  flutter --suppress-analytics build bundle --${FLT_PROJ_BUILD_MODE} \
    --target-platform=ios \
    --target="lib/main.dart" \
    --depfile="${FLT_PROJ_BUILD}/snapshot_blob.bin.d" \
    --asset-dir="${FLT_PROJ_iOS_FLT}/App.framework/flutter_assets" \
    --precompiled

  EchoDone "Assembling Flutter resources done"
}

# 生成靜態庫
GenerateStaticFramewrok() {
  local pluginName=$1

  /usr/bin/env xcrun xcodebuild build \
    -configuration Release ARCHS='arm64 armv7' \
    -target "${pluginName}" BUILD_DIR="${FLT_PROJ_BUILD}/ios" \
    -sdk iphoneos \
    -quiet >/dev/null

  /usr/bin/env xcrun xcodebuild build \
    -configuration Debug ARCHS='x86_64' \
    -target "${pluginName}" BUILD_DIR="${FLT_PROJ_BUILD}/ios" \
    -sdk iphonesimulator \
    -quiet >/dev/null
}

# 合併靜態庫
MergeStaticFramework() {
  local pluginName=$1
  local pluginLibPath=$2

  local debugFramework="${FLT_PROJ_BUILD}/ios/Debug-iphonesimulator/${pluginName}/lib${pluginName}.a"
  local releaseFramework="${FLT_PROJ_BUILD}/ios/Release-iphoneos/${pluginName}/lib${pluginName}.a"

  lipo -create "${debugFramework}" "${releaseFramework}" -o "${pluginLibPath}"
}

# 編譯靜態庫
BuildStaticFramework() {
  local pluginName=$1
  local pluginLibPath=$2

  echo " ├───Generating lib${plugin_name}.a"
  GenerateStaticFramewrok "${plugin_name}"

  echo " ├───Merging lib${plugin_name}.a"
  echo " ├───Plugin Lib Path: ${pluginLibPath}"
  MergeStaticFramework "${plugin_name}" "${pluginLibPath}"
  echo " └───Merging lib${plugin_name}.a done"
  echo ""
}

# 編譯Flutter的插件和FlutterPluginRegistrant
BuildFlutterPlugin() {
  echo " ├─Building Flutter Plugin..."
  echo ""

  cd ${FLT_PROJ_iOS}
  # 更新插件依賴
  pod install
  cd -

  # 進入Flutter/Pods編譯插件
  cd "${FLT_PROJ_iOS}/Pods"

  if [ -f "${FLT_PROJ}/.flutter-plugins" ]; then
    # 聲明數組保存Plugin的路徑
    declare -a plugin_lib_path_arr
    # 獲取所有的插件名稱
    plugin_arr="$(cat "${FLT_PROJ}/.flutter-plugins" | awk -F "=" '{print $1}') FlutterPluginRegistrant"

    echo ""

    # ./build/ios/Release
    local pluginOutput="${FLT_PROJ_BUILD}/ios/Release"
    mkdir -p ${pluginOutput}

    for plugin_name in ${plugin_arr}; do
      if [ "${plugin_name}" = "FlutterPluginRegistrant" ]; then
        echo " └─Building Flutter Plugin done"
        echo ""
        echo " ├─Building FlutterPluginRegistrant..."
      fi

      local pluginLibPath="${pluginOutput}/lib${plugin_name}.a"
      BuildStaticFramework "${plugin_name}" "${pluginLibPath}"
      plugin_lib_path_arr=("${plugin_lib_path_arr[@]}" "${pluginLibPath}")
    done
  fi

  EchoDone "Building FlutterPluginRegistrant done"
  echo " ├─Merging Flutter Plugin and FlutterPluginRegistrant..."

  # 生成在build目錄下
  libtool -static -o "${FLT_PROJ_BUILD}/libFlutterPlugins.a" "${plugin_lib_path_arr[@]}" >/dev/null

  EchoDone "Merging Flutter Plugin and FlutterPluginRegistrant done"

  cd -
}

# 重置環境變量
UnsetVariable() {
  echo "Project ${FLT_PROJ} built and packaged successfully."

  unset FLT_ARCH               # 編譯的架構
  unset FLT_PROJ_BUILD_MODE    # 編譯的模式:release/profile

  unset PUB_HOSTED_URL         # flutter pub域名

  unset FLT_PROJ               # Flutter工程./根目錄
  unset FLT_PROJ_iOS           # Flutter工程./iOS目錄
  unset FLT_PROJ_iOS_FLT       # Flutter工程./iOS/Flutter目錄
  unset FLT_PROJ_BUILD         # Flutter工程./build目錄
  unset FLT_PROJ_BUILD_AOT_APP # Flutter工程./build/aot/App.framework目錄
}

# 主要流程
main() {
  # Flutter工程目錄
  cd ./flutter_module

  InitVariable       # 獲取輸入的變量
  InitEnv            # 初始化環境變量
  BuildAppFramework  # 編譯App.framework
  GeneratedSYM       # 生成dSYM
  StripdSYM          # Strip dSYM
  BuildFlutterAssets # 編譯Flutter資源
  BuildFlutterPlugin # 編譯Flutter插件
  UnsetVariable      # unset環境變量

  # 退出到原來目錄
  cd -
}

# 啓動腳本
main

附錄

iOS

發佈了184 篇原創文章 · 獲贊 218 · 訪問量 46萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章