Flutter 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

发布了183 篇原创文章 · 获赞 217 · 访问量 46万+

猜你喜欢

转载自blog.csdn.net/Notzuonotdied/article/details/104083090