我们必须全力以赴,同时又不抱持任何希望。不管做什么事,都要当它是全世界最重要的一件事,但同时又知道这件事根本无关紧要。
分析了一堆的内容,其实主要就是为了做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 # 所有的插件及FlutterPluginRegistrant11 directories, 4 files
本脚本可以应用于Jenkints,也可以做成其他的自动化的部分。
根据Flutter build ios产物分析、Flutter xcode_backend分析的分析结果,可以简单定下以下的启动流程,分别为如下流程:
# 主要流程
main() {# Flutter工程目录cd ./flutter_moduleInitVariable # 获取输入的变量InitEnv # 初始化环境变量BuildAppFramework # 编译App.frameworkGeneratedSYM # 生成dSYMStripdSYM # Strip dSYMBuildFlutterAssets # 编译Flutter资源BuildFlutterPlugin # 编译Flutter插件UnsetVariable # unset环境变量# 退出到原来目录cd -
}# 启动脚本
main
根据上述脚本,逐渐填充函数实现即可。
# 用于命令行的输出
EchoDone() {echo ""echo " └─$1"echo ""
}# 初始化从外部输入的变量
InitVariable() {echo " ├──Input variable..."export FLT_PROJ_BUILD_MODE='release' # profile/releaseEcho " ├────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 --versionecho ""echo " ├────Clean flutter building artifacts"echo ""flutter cleanEchoDone "Clean flutter building artifacts done"echo " ├────Fetch flutter project dependences"# 更新依赖flutter pub getflutter packages getEchoDone "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" ]]; thenunset FLT_PROJ_iOSexport 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"
}
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 ]]; thenecho "Failed to generate debug symbols (dSYM) file for ${FLT_PROJ_BUILD_AOT_APP}/App."exit -1fiEchoDone "Generating dSYM file done"
}
StripdSYM() {echo " ├─Stripping debug symbols..."echo ""xcrun strip -x -S "${FLT_PROJ_iOS_FLT}/App.framework/App"if [[ $? -ne 0 ]]; thenecho "Failed to strip ${FLT_PROJ_iOS_FLT}/App.framework/App."exit -1ficp "${FLT_PROJ_iOS_FLT}/AppFrameworkInfo.plist" ${FLT_PROJ_iOS_FLT}/App.framework/Info.plistEchoDone "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" \--precompiledEchoDone "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
(模拟器)
/usr/bin/env xcrun xcodebuild BUILD_DIR \-configuration Release ARCHS='arm64 armv7' \-target ${plugin_name} BUILD_DIR=../../build/ios \-sdk iphoneos \-quiet
/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=$1local pluginLibPath=$2local 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=$1local pluginLibPath=$2echo " ├───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-updatecd -# 进入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/Releaselocal pluginOutput="${FLT_PROJ_BUILD}/ios/Release"mkdir -p ${pluginOutput}for plugin_name in ${plugin_arr}; doif [ "${plugin_name}" = "FlutterPluginRegistrant" ]; thenecho " └─Building Flutter Plugin done"echo ""echo " ├─Building FlutterPluginRegistrant..."filocal pluginLibPath="${pluginOutput}/lib${plugin_name}.a"BuildStaticFramework "${plugin_name}" "${pluginLibPath}"plugin_lib_path_arr=("${plugin_lib_path_arr[@]}" "${pluginLibPath}")donefiEchoDone "Building FlutterPluginRegistrant done"echo " ├─Merging Flutter Plugin and FlutterPluginRegistrant..."# 生成在build目录下libtool -static -o "${FLT_PROJ_BUILD}/libFlutterPlugins.a" "${plugin_lib_path_arr[@]}" >/dev/nullEchoDone "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
。完整脚本如下:
EchoDone() {echo ""echo " └─$1"echo ""
}InitVariable() {echo " ├──Input variable..."export FLT_PROJ_BUILD_MODE='release' # profile/releaseEcho " ├────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 --versionecho ""echo " ├────Clean flutter building artifacts"echo ""flutter cleanEchoDone "Clean flutter building artifacts done"echo " ├────Fetch flutter project dependences"echo ""# 更新依赖flutter pub getflutter packages getEchoDone "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" ]]; thenunset FLT_PROJ_iOSexport 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 ]]; thenecho "Failed to generate debug symbols (dSYM) file for ${FLT_PROJ_BUILD_AOT_APP}/App."exit -1fiEchoDone "Generating dSYM file done"
}StripdSYM() {echo " ├─Stripping debug symbols..."echo ""xcrun strip -x -S "${FLT_PROJ_iOS_FLT}/App.framework/App"if [[ $? -ne 0 ]]; thenecho "Failed to strip ${FLT_PROJ_iOS_FLT}/App.framework/App."exit -1ficp "${FLT_PROJ_iOS_FLT}/AppFrameworkInfo.plist" ${FLT_PROJ_iOS_FLT}/App.framework/Info.plistEchoDone "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" \--precompiledEchoDone "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=$1local pluginLibPath=$2local 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=$1local pluginLibPath=$2echo " ├───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 installcd -# 进入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/Releaselocal pluginOutput="${FLT_PROJ_BUILD}/ios/Release"mkdir -p ${pluginOutput}for plugin_name in ${plugin_arr}; doif [ "${plugin_name}" = "FlutterPluginRegistrant" ]; thenecho " └─Building Flutter Plugin done"echo ""echo " ├─Building FlutterPluginRegistrant..."filocal pluginLibPath="${pluginOutput}/lib${plugin_name}.a"BuildStaticFramework "${plugin_name}" "${pluginLibPath}"plugin_lib_path_arr=("${plugin_lib_path_arr[@]}" "${pluginLibPath}")donefiEchoDone "Building FlutterPluginRegistrant done"echo " ├─Merging Flutter Plugin and FlutterPluginRegistrant..."# 生成在build目录下libtool -static -o "${FLT_PROJ_BUILD}/libFlutterPlugins.a" "${plugin_lib_path_arr[@]}" >/dev/nullEchoDone "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/profileunset 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_moduleInitVariable # 获取输入的变量InitEnv # 初始化环境变量BuildAppFramework # 编译App.frameworkGeneratedSYM # 生成dSYMStripdSYM # Strip dSYMBuildFlutterAssets # 编译Flutter资源BuildFlutterPlugin # 编译Flutter插件UnsetVariable # unset环境变量# 退出到原来目录cd -
}# 启动脚本
main