运用Dart构建一套Flutter主动打包脚本

打包脚本预览

脚本装备

#!/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

企业微信截图

运用Dart构建一套Flutter主动打包脚本

运用Dart构建一套Flutter主动打包脚本

钉钉截图

运用Dart构建一套Flutter主动打包脚本

运用Dart构建一套Flutter主动打包脚本

打包流程图

运用Dart构建一套Flutter主动打包脚本

相对来说咱们的项目现在打包仍是比较杂乱的,究竟还需求出Unity相关包的操作,每一次出Unity包都需求好久。

我最近准备在搞一套根据Dart言语的主动化运转引擎,所以这个打包脚本能够作为我后续进行开发改造的基础。

技能完成

运用Dart构建一套Flutter主动打包脚本

关于依赖库首要是两个库argsprocess_run,其他的首要是咱们便利写打包的逻辑。

  • path

    这个常用的库,首要是组装成一个当时平台的文件途径,windows和macos是存在途径差异的。

  • args

    能够便利创立指令和子指令和全局参数和局部参数来完成一套指令行体系

  • process_run

    首要便利经过dart进行调用shll指令履行

  • color_logger

    来让打印的日志有对应相关的颜色

  • dio

    和网络进行通讯

  • yaml

    读取.yaml文件里边的装备

  • darty_json_safe

    能够安全进行访问字典、数组、类型转换等

executables是设置指令行指令姓名

创立一个build_winner_app.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指令来支撑子指令。

class BuildCommand extends Command {
  BuildCommand() {
    addSubcommand(IosCommand());
    addSubcommand(AndroidCommand());
  }
  @override
  String get description => '编译发布最新的安装包';
  @override
  String get name => 'build';
  @override
  FutureOr? run() async {}
}

具有子指令的指令是不具备履行的能力的。

咱们别离创立IosCommandAndroidCommand别离履行iOS打包的流程和Android的打包流程。其间iOS打包和安卓打包存在大部分的逻辑通用,所以咱们需求创立一个子类指令BaseBuildCommand来完成通用逻辑部分。

为了支撑打包者能够不需求更新Unity包,比如经过他人的缓存来打包,则需求越过Unity相关的逻辑。

咱们增加一个skipUnityUpdate参数,默许敞开更新Unity包。

argParser.addFlag('skipUnityUpdate', help: '越过Unity主动更新!');

已然支撑多人打包,能够存在需求在日志显示出当时打包人,所以咱们增加一个tag参数用来标识当时打包人的信息。

argParser.addOption('tag', help: '当时打包的Tag');

经过上面的操作,咱们就开端到了完成逻辑的部分了。逻辑完成的部分咱们需求在run办法进行完成。

获取咱们方才设置的变量的值

/// 是否越过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用来将信息打印到控制台的。

在咱们后续的开发流程中,肯定会需求很多的装备变量,为了让打包之前能够提示用户没有装备会比履行打包结束提示没有装备不能上传的过错,运用者肯定要疯掉。

所以为了提高运用者的提现,咱们就把初始化的操作放到了最前面的方位,咱们新建了一个类用于寄存一切需求初始化装备。

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文件内容装备

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文件内容装备

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

{
    "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

    git log -n 1 --pretty=format:"%H"
    
  • 获取当时项目长途最终一次Commit

    /// 获取代码的长途分支的最新哈希
    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();
    }
    

这里获取长途的仍是相关于比较杂乱一些的。

运用Dart构建一套Flutter主动打包脚本

  • 获取当时分支称号 git rev-parse --abbrev-ref HEAD

在打包之前先获取日志,这样也是为了避免日志获取存在问题不能很早的发现。

运用Dart构建一套Flutter主动打包脚本

关于获取区间的日志,咱们能够经过git log指令进行获取。

获取单个一条的日志

git log -1

获取多条区间的日志

git log $currentCommitId..$lastCommitId

获取Flutter的更新日志和上面同上。

日志的过滤,上面咱们主动获取的日志包括了git很多信息,其实咱们是不需求的,咱们只需求得到咱们自己提交的日志即可。

/// 假如当时行存在以下关键字 则疏忽
if (['commit', 'Author', 'Date', 'Merge', '# Conflicts', '#    ']
    .any((e) => log.toLowerCase().startsWith(e.toLowerCase()))) {
  continue;
}

咱们在每一行去掉空格之后检测出开端包括上面关键词就主动过滤当时行日志。

经过上面履行结束,咱们的 Unity 工程和 Flutter 也现已该更新代码更新代码了,接下来就需求看是否需求操作 Unity 导包。

运用Dart构建一套Flutter主动打包脚本

关于iOS Unity导包相关于会杂乱一些,不但要修复导出支撑Bitcode的问题,还要修复生成.a的问题。

到现在方位咱们现已拿到了Flutter是否需求更新和Unity是否需求更新,假如两个都不需求更新,咱们就能够退出当时的打包,来节省打包的功用。

关于打包就变的非常简略的,就能够直接调用Flutter的打包体系

flutter build apk/ipa --build-name [版别] --build-number [build号]

打包结束就到了上传的环节,咱们之前现已装备了fastlane,所以咱们后面上传就变的非常简略了。

上传ipa到Testflight

fastlane upload_testflight ipa:$WORK_SPACE/build/ios/ipa/$name.ipa

上传apk到蒲公英

fastlane deploy apk:$WORK_SPACE/build/app/outputs/apk/release/app-release.apk

上传结束,咱们就需求把当时打包的日志发布出来告诉测验人员下载,咱们能够经过Web Hook方式告诉到企业微信和钉钉等等。

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。

由于之前放在工程,我发现我上传的时分,其他人在打包期间更新了代码,这就导致最终上传的时分报错。

到此方位咱们的主动化打包的脚本现已介绍为止,是不是感觉逻辑很杂乱。全流程测验起来非常杂乱,所以我才想到打造一套主动化引擎,能够拆分功用,能够轻松测验。低侵入,简单将流程进行共享或许改动给其他项目运用。