使用Dart构建一套Flutter自动打包脚本
打包脚本预览
脚本配置
shell
#!/bin/bash
# 设置协议 可以防止一些依赖拉取不下来
export https_proxy=xxx
# 设置运行的PATH
export PATH=xxx
# 设置Flutter中国镜像
export FLUTTER_STORAGE_BASE_URL="https://storage.flutter-io.cn"
# 设置Flutter中国镜像
export PUB_HOSTED_URL="https://pub.flutter-io.cn"
# 设置iOS包发送日志到企业微信Hook URL
export IOS_HOOK_URL=xxx
# 设置android包发送日志到企业微信 Hook URL
export ANDROID_HOOK_URL=xxx
# 设置上传到Appstore Api Key
export APP_STORE_CONNECT_API_KEY_ID=xxx
# 设置上传Appstore Issuer ID
export APP_STORE_CONNECT_API_ISSUER_ID=xxx
# 设置上传Appstore Api Key的.p8本地路径
export APP_STORE_CONNECT_API_KEY_FILEPATH=xxx
# 设置iOS的包的标识符
export APP_IDENTIFIER=xxx
# 设置iOS包在Appstore的ID
export APP_ID=xxx
# 设置上传安卓包蒲公英的Api Key
export PGYER_API_KEY=xxx
# 设置Unity工程所在Flutter项目的路径
export UNITY_WORKSPACE=xxx
# 设置iOS Unity工程所在的相对路径
export IOS_UNITY_PATH=unity/meta_winner_unity_ios
# 设置android Unity工程所在的相对路径
export ANDROID_UNITY_PATH=unity/meta_winner_unity_android
# 设置Unity引擎运行文件所在的路径
export UNITY_ENGINE_PATH=xxx/2021.3.16f1c1/Unity.app/Contents/MacOS/unity
# 设置发送日志到钉钉iOS的Hook URL
export DINGDING_IOS_HOOK_URL=xxx
# 设置发送日志到钉钉android的Hook URL
export DINGDING_ANDROID_HOOK_URL=xxx
echo "-----------------------------------ENV---------------------------------"
env
echo "-----------------------------------ENV---------------------------------"
# 进行打包 $PLATFROM 打包的平台 ios/android --tag 设置当前打包的Tag
build_winner_app build $PLATFROM --tag "稳健医疗"
# 下面是将最新打包ID同步 支持多设备进行打包
cd $WORKSPACE
git add .
git commit -m "更新最后一次打包配置"
git pull origin dev_mobshare
git push origin dev_mobshare
企业微信截图
钉钉截图
打包流程图
相对来说我们的项目目前打包还是比较复杂的,毕竟还需要出Unity相关包的操作,每一次出Unity包都需要很久。
我最近准备在搞一套基于Dart语言的自动化运行引擎,所以这个打包脚本可以作为我后续进行开发改造的基础。
技术实现
对于依赖库主要是两个库args
和process_run
,其他的主要是我们方便写打包的逻辑。
-
path
这个常用的库,主要是组装成一个当前平台的文件路径,windows和macos是存在路径区别的。
-
args
可以方便创建命令和子命令和全局参数和局部参数来实现一套命令行系统
-
process_run
主要方便通过
dart
进行调用shll
命令执行 -
color_logger
来让打印的日志有对应相关的颜色
-
dio
和网络进行通信
-
yaml
读取
.yaml
文件里面的配置 -
darty_json_safe
可以安全进行访问字典、数组、类型转换等
executables
是设置命令行命令名字
创建一个build_winner_app.dart
文件,填写下面的代码。
dart
import 'package:args/command_runner.dart';
import 'package:build_winner_app/commands/build/build_command.dart';
void main(List<String> arguments) async {
final runner = CommandRunner(
'build_winner_app', // 命令的名称
'打包并上传到Testflight/到蒲公英 企业微信通知', // 命令的描述
)..addCommand(BuildCommand());
await runner.run(arguments);
}
创建一个BuildCommand
命令来支持子命令。
dart
class BuildCommand extends Command {
BuildCommand() {
addSubcommand(IosCommand());
addSubcommand(AndroidCommand());
}
@override
String get description => '编译发布最新的安装包';
@override
String get name => 'build';
@override
FutureOr? run() async {}
}
具有子命令的命令是不具备执行的能力的。
我们分别创建IosCommand
和AndroidCommand
分别执行iOS打包的流程和Android的打包流程。其中iOS打包和安卓打包存在大部分的逻辑通用,所以我们需要创建一个子类命令BaseBuildCommand
来实现通用逻辑部分。
为了支持打包者可以不需要更新Unity包,比如通过别人的缓存来打包,则需要跳过Unity
相关的逻辑。
我们添加一个skipUnityUpdate
参数,默认开启更新Unity
包。
dart
argParser.addFlag('skipUnityUpdate', help: '跳过Unity自动更新!');
既然支持多人打包,可以存在需要在日志显示出当前打包人,所以我们添加一个tag
参数用来标识当前打包人的信息。
dart
argParser.addOption('tag', help: '当前打包的Tag');
经过上面的操作,我们就开始到了实现逻辑的部分了。逻辑实现的部分我们需要在run
方法进行实现。
获取我们刚才设置的变量的值
dart
/// 是否跳过Unity自动更新
final skipUnityUpdate = JSON(argResults?['skipUnityUpdate']).boolValue;
logger.log('skipUnityUpdate: $skipUnityUpdate', status: LogStatus.debug);
final tag = JSON(argResults?['tag']).string;
logger.log('tag: $tag', status: LogStatus.debug);
从上面的代码我们发现我们需要从argResults
获取参数的值,可以按照字典取值一样。但是我们添加一层JSON(...).boolValue
是为什么?
JSON(...).boolValue
来源于我个人写的darty_json_safe
库是为了保障不管是什么类型值,就转换为bool
类型,如果成功就按照原来的值输出,如果转换失败就按照默认false
输出。
logger.log(...,status:)
这个方法是基于color_logger
用来将信息打印到控制台的。
在我们后续的开发流程中,肯定会需要很多的配置变量,为了让打包之前可以提示用户没有配置会比执行打包完毕提示没有配置不能上传的错误,使用者肯定要疯掉。
所以为了提升使用者的提现,我们就把初始化的操作放到了最前面的位置,我们新建了一个类用于存放所有需要初始化配置。
dart
class Environment {
/// 打包的工程路径
late String workspace;
/// 发送iOS端日志的Hook的企业微信的地址
late String iosHookUrl;
/// 发送Android端日志的Hook的企业微信的地址
late String androidHookUrl;
/// App Store Connect API Key ID
late String appStoreConnectApiKeyId;
/// App Store Connect API Issuer ID
late String appStoreConnectApiIssuerId;
/// App Store Connect API Key Filepath
late String appStoreConnectApiKeyFilepath;
/// 应用标识符
late String appIdentifier;
/// 应用的ID
late String appId;
/// 蒲公英上传的Key
late String pgyerApiKey;
/// 打包的名称
late String buildName;
/// 钉钉发送iOS日志的钉钉机器人地址
late String dingdingIosHookUrl;
/// 钉钉发送Android日志的钉钉机器人地址
late String dingdingAndroidHookUrl;
/// 当前打包的分支
late String branch;
UnityEnvironment? unityEnvironment;
setup(bool updateUnity) {
workspace = env('WORKSPACE');
iosHookUrl = env('IOS_HOOK_URL');
androidHookUrl = env('ANDROID_HOOK_URL');
appStoreConnectApiKeyId = env('APP_STORE_CONNECT_API_KEY_ID');
appStoreConnectApiIssuerId = env('APP_STORE_CONNECT_API_ISSUER_ID');
appStoreConnectApiKeyFilepath = env('APP_STORE_CONNECT_API_KEY_FILEPATH');
appIdentifier = env('APP_IDENTIFIER');
appId = env('APP_ID');
pgyerApiKey = env('PGYER_API_KEY');
if (updateUnity) {
final unityWorkspace = env('UNITY_WORKSPACE');
final iosUnityPath = env('IOS_UNITY_PATH');
final androidUnityPath = env('ANDROID_UNITY_PATH');
final unityEnginePath = env('UNITY_ENGINE_PATH');
unityEnvironment = UnityEnvironment(
unityWorkspace: unityWorkspace,
iosUnityPath: iosUnityPath,
androidUnityPath: androidUnityPath,
unityEnginePath: unityEnginePath,
);
}
buildName = env('BUILD_NAME');
dingdingIosHookUrl = env('DINGDING_IOS_HOOK_URL');
dingdingAndroidHookUrl = env('DINGDING_ANDROID_HOOK_URL');
branch = env('BRANCH').replaceFirst('origin/', '');
}
String env(String name) {
if (Platform.environment[name] == null) {
logger.log('$name 环境变量未配置', status: LogStatus.error);
exit(1);
}
return Platform.environment[name]!;
}
}
class UnityEnvironment {
/// Unity所在Flutter项目的工程目录
final String unityWorkspace;
/// iOS Unity工程的相对路径
final String iosUnityPath;
/// Android Unity工程的相对路径
final String? androidUnityPath;
/// Unity 引擎的路径
final String unityEnginePath;
const UnityEnvironment({
required this.unityWorkspace,
required this.iosUnityPath,
required this.androidUnityPath,
required this.unityEnginePath,
});
/// 安卓Unity工程的完整路径
String get androidUnityFullPath => join(unityWorkspace, androidUnityPath);
/// iOS Unity工程的完整路径
String get iosUnityFullPath => join(unityWorkspace, iosUnityPath);
}
从上面的代码可以了解到,我们将所有和Unity相关的配置都单独的提炼出来了,也是为了更好的支持skipUnityUpdate
参数来跳过Unity对应的操作逻辑。
我们对于需要设置的参数在获取不到的时候会强行的终端运行,提示用户配置。这样就可以很快的知道一些前置错误。
我们目前苹果是上传到Testflight进行分发安装的,没有基于UUID是因为可以不需要再次的打包,只要添加邀请就可以安装了。安卓则是上传到蒲公英的分发平台进行下载安装的。
我们使用fastlane
分别进行上传到Testflight
和蒲公英
。
iOS的Fastfile
文件内容配置
ruby
default_platform(:ios)
lane :upload_testflight do |options|
ipa = options[:ipa]
changelog = options[:changelog] || "新的版本发布了,快来下载呀!"
api_key = app_store_connect_api_key(
key_id: ENV['APP_STORE_CONNECT_API_KEY_ID'],
issuer_id: ENV['APP_STORE_CONNECT_API_ISSUER_ID'],
key_filepath: ENV['APP_STORE_CONNECT_API_KEY_FILEPATH'],
duration: 1200, # optional (maximum 1200)
in_house: false # optional but may be required if using match/sigh
)
upload_to_testflight(
api_key: api_key,
app_identifier: ENV['APP_IDENTIFIER'],
apple_id: ENV['APP_ID'],
ipa: ipa,
changelog: changelog,
skip_waiting_for_build_processing: true,
)
end
其中常用的值通过环境变量获取,发布的ipa的路径和日志通过执行命令去传递。
Android的fastfile
文件内容配置
ruby
default_platform(:android)
lane :deploy do |options|
apk = options[:apk]
pgyer(
api_key: ENV["PGYER_API_KEY"],
apk: apk,
)
end
蒲公英上传的key
从环境变量获取,.apk
文件从命令参数获取。
当多有上面都准备就绪的时候,接下来就到了执行打包等步骤了。但是怎么知道这次更新的日志,还有当前需要需要更新打包呢?
那就需要有一个上一次打包的值来对比,因此我就专门一个文件保存到当前打包Flutter工程下面,名字叫做.build_info.json
json
{
"ios": {
"flutter": "c2266712087714625101d05c3908423f5f563cd5",
"unity": {
"cache": "c8755ce0e347f79071a71c91837bb247b6f722e4",
"log": "c8755ce0e347f79071a71c91837bb247b6f722e4"
}
},
"android": {
"flutter": "412a0eb55b3deb7ebabf6042b4df409d91ac685b",
"unity": {
"cache": "4b337876ba1bb4e6934cfbd62ddb8b34b2678dde",
"log": "4b337876ba1bb4e6934cfbd62ddb8b34b2678dde"
}
}
}
上图就是目前我们工程打包完毕最新的值。
因为分别有iOS和安卓打包,所以对此进行了区分。
- flutter 代表上一次Flutter代码更新最新的commit值
- unity 代表Unity代码的配置
- cache 代表上一次Unity打包Commit的值
- log 代表上一次Unity打包时候日志获取Commit值
从上面配置可以看到,iOS和安卓的值是不一样的。因为打包需要时候,可能等打下一个平台的时候,代码已经发生了变更,所以要区分开来。
如果开启了Unity自动更新,则需要获取当前Unity工程本地提交和远程提交。但是怎么获取到呢?这个时候我们需要用到git
命令。
-
获取当前项目本地最后一次Commit
shellgit log -n 1 --pretty=format:"%H"
-
获取当前项目远程最后一次Commit
dart/// 获取代码的远程分支的最新哈希 Future<String> getGitLastRemoteCommitHash(String root) async { final localCurrentBranch = await getLocalBranchName(root); /// 更新远程仓库 await runCommand(root, ''' git reset --hard git fetch origin '''); final remoteCommitHashCode = await runCommand( root, 'git ls-remote --heads origin $localCurrentBranch | awk \'{print \$1}\'', ).then((value) { /// bb2b2fb44d073c7cbea05e11c905543072b10b63 refs/heads/ArtStyle_1.0 final reg = RegExp('[0-9+a-z]*'); return reg.firstMatch(value.first.stdout.toString())!.group(0); }); return remoteCommitHashCode!.trim(); }
这里获取远程的还是相对于比较复杂一些的。
- 获取当前分支名称
git rev-parse --abbrev-ref HEAD
在打包之前先获取日志,这样也是为了防止日志获取存在问题不能很早的发现。
对于获取区间的日志,我们可以通过git log
命令进行获取。
获取单个一条的日志
shell
git log -1
获取多条区间的日志
shell
git log $currentCommitId..$lastCommitId
获取Flutter的更新日志和上面同上。
日志的过滤,上面我们自动获取的日志包含了git很多信息,其实我们是不需要的,我们只需要得到我们自己提交的日志即可。
dart
/// 如果当前行存在以下关键字 则忽略
if (['commit', 'Author', 'Date', 'Merge', '# Conflicts', '# ']
.any((e) => log.toLowerCase().startsWith(e.toLowerCase()))) {
continue;
}
我们在每一行去掉空格之后检测出开始包含上面关键词就自动过滤当前行日志。
经过上面执行完毕,我们的 Unity 工程和 Flutter 也已经该更新代码更新代码了,接下来就需要看是否需要操作 Unity 导包。
对于iOS Unity导包相对于会复杂一些,不但要修复导出支持Bitcode的问题,还要修复生成.a的问题。
到目前位置我们已经拿到了Flutter是否需要更新和Unity是否需要更新,如果两个都不需要更新,我们就可以退出当前的打包,来节省打包的性能。
对于打包就变的十分简单的,就可以直接调用Flutter
的打包系统
shell
flutter build apk/ipa --build-name [版本] --build-number [build号]
打包完毕就到了上传的环节,我们之前已经配置了fastlane,所以我们后面上传就变的十分简单了。
上传ipa到Testflight
shell
fastlane upload_testflight ipa:$WORK_SPACE/build/ios/ipa/$name.ipa
上传apk到蒲公英
shell
fastlane deploy apk:$WORK_SPACE/build/app/outputs/apk/release/app-release.apk
上传完毕,我们就需要把当前打包的日志发布出来通知测试人员下载,我们可以通过Web Hook方式通知到企业微信和钉钉等等。
dart
final dio = Dio();
final response = await dio.post(
hookUrl,
options: Options(headers: {
'Content-Type': 'application/json',
}),
data: json.encode({
'msgtype': 'text',
'text': {'content': text},
}),
);
上面的代码就是上传日志的核心代码。下面的步骤就需要保存当前打包的Commit到本地,不过到目前写文章为止,我已经更改了存储和读取配置从Appwrite。
因为之前放在工程,我发现我上传的时候,其他人在打包期间更新了代码,这就导致最后上传的时候报错。
到此位置我们的自动化打包的脚本已经介绍为止,是不是感觉逻辑很复杂。全流程测试起来十分复杂,所以我才想到打造一套自动化引擎,可以拆分功能,可以轻松测试。低侵入,容易将流程进行分享或者改动给其他项目使用。