Flutter CLI 鸿蒙化适配实战:改造命令层与构建流程

发布时间:2026/10/19 2:37:22
Flutter CLI 鸿蒙化适配实战:改造命令层与构建流程
最近我一直在鼓捣一件事把 Flutter 生态里常用的三方项目管理 CLI——blue_bird_cli——做鸿蒙化适配。这个工具原本是团队内部用来管 Flutter 工程的负责项目创建、依赖同步、构建打包和一点发布流程按官方说法叫“智能化的 Flutter 项目管理”。但现在产品线要出鸿蒙端版本原来的 CLI 一遇到鸿蒙工程目录就罢工所以必须让它不仅能管 Flutter 项目还能把鸿蒙端的一键式脚本集成和工作流自动化一并接进来。如果你也在做 Flutter 工具链或者准备把现有 CLI 接进鸿蒙构建流程这篇指南应该能帮你少走点弯路。我不准备讲太多理论就按实际改造的顺序来为什么这么设计、环境怎么配、核心模块怎么改、一键脚本怎么串、踩过的坑怎么避。适配工作没有想象中那么神秘核心思路就一句话不是去重写鸿蒙构建引擎而是让原有 CLI 学会正确发现鸿蒙工程结构、调用鸿蒙工具链、并能把 Flutter 构建产物在合适的位置注入进去。1. 项目背景与适配思路拆解1.1 blue_bird_cli 原本是干什么的blue_bird_cli 这类工具其实很常见就是把你平时在终端里反复敲的那几条命令打个包。它内部做了几层封装一层是交互式脚手架通过对话式提问生成 Flutter 工程模板一层是平台适配器把android/目录下的 Gradle 命令、ios/目录下的 Xcode 命令分别封装成统一的build、run、install动作还有一层是发布辅助负责收集构建产物、生成版本号、推送内部仓库。我之前用的痛点在于团队项目越来越多模板换了一版又一版每次都要手动执行“创建项目 - 改 bundleId - 拉依赖 - 打 APK”这一套流程。blue_bird_cli 解决的就是“少敲命令、少记路径、少踩低级错误”这件事。它底层维护了一套模板仓库和命令路由用户侧只暴露blue_bird create、blue_bird build、blue_bird run这类顶层命令。但问题也出在命令路由上它默认把“构建行为”硬编码成 Android 的gradlew assembleRelease把“设备安装”硬编码成adb install。鸿蒙工程目录下没有android/也没有ios/于是工具直接报错退出。这就不难理解鸿蒙化适配的第一步不是加一堆新功能而是让工具能“认出”鸿蒙工程。1.2 鸿蒙端到底需要适配哪些东西把需求拆细一点鸿蒙化适配不是一个没边界的“兼容鸿蒙”而是三个非常具体的改造点。第一项目识别。工具拿到一个工程目录后要能判断它是 Flutter 工程还是鸿蒙工程甚至是 Flutter鸿蒙混合工程。最直接的办法是检查关键文件是否存在根目录有oh-package.json5、build-profile.json5或者ohos/entry/src/main/module.json5基本就可以判定为鸿蒙工程。第二命令调用。Flutter 侧构建还可以继续用flutter build但鸿蒙侧打包必须调用hvigorw assembleHap。依赖管理也不再是单纯的pub get还需要执行ohpm install。设备安装从adb install切换为hdc install。这些命令的差异必须通过平台适配层去消化而不是散落在各个业务代码里。第三产物注入。Flutter 构建完成后产出的不是可直接安装的 APK而是FlutterEngine.har这类鸿蒙依赖文件。你需要把它拷贝到鸿蒙工程的entry/libs目录再重新执行鸿蒙打包最终产出.hap安装包。这中间涉及的路径映射关系、构建顺序、失败回滚是整套适配里最容易出错的环节。1.3 整体改造方案命令层适配而不是重写我见过不少项目在兼容新平台时选择“重写一套”原因大多是对老代码不信任。但重写工具链风险极高原本已经稳定运行的模板渲染、版本校验、交互输入逻辑一旦重写就要重新测试成本完全不可控。我的做法是保持 CLI 原有架构不变引入一个“平台适配器”概念。每个平台Android、iOS、鸿蒙实现同样的接口detectProject()、resolveDependencies()、build()、install()、collectArtifacts()。顶层命令只关心接口不关心具体平台命令。这样既把改造影响面控制在平台适配层也方便以后继续接入其他系统。另外还有三个原则贯穿始终平台探测统一不要在每个命令里单独判断平台入口处统一探测一次把结果注入后续流程。命令封装统一所有外部命令都通过同一套子进程执行器调用统一处理环境变量、工作目录、输出流。产物路径统一无论哪个平台构建结果都汇总到统一约定的build/platform/目录方便后续上传或安装。2. 环境准备与配置扩展2.1 本机环境验收清单动工之前先把本机环境过一遍。不要等代码写完了才发现命令根本跑不通。最关键的三个工具链是Flutter SDK、鸿蒙 SDK、鸿蒙命令行工具。命令行工具里主要用到hvigorw编译打包、ohpm鸿蒙依赖管理、hdc设备调试与安装。这三者一般随官方 IDE 一起安装但 IDE 装好不代表命令行可用因为命令行所在的tool目录不一定会自动写进系统 PATH。我建议在本地做一次验收直接在终端执行flutter --version hvigorw --version ohpm --version hdc version如果某一项命令找不到先把对应工具目录添加到 PATH或者在做 CLI 适配时在配置里显式指定工具路径。另外hvigorw依赖 Java 环境至少需要 JDK 17 以上版本低于这个版本会在构建阶段直接报错而且是那种“莫名其妙的 JVM 内存错误”很劝退。注意ohpm和hvigorw在不同版本的工具链里目录名可能不同。适配过程中不要硬编码全局 PATH最好在项目配置文件里记录工具链的绝对路径或者做一次自动探测。2.2 配置文件结构扩展blue_bird_cli 本身有一份工程配置文件一般是blue_bird_config.yaml。原来里面只描述 Android 和 iOS 的构建参数适配鸿蒙时需要增加一个harmony节点。下面是一份可直接参考的配置示例platforms: android: true ios: true harmony: true harmony: moduleName: entry compileSdkVersion: 18 targetSdkVersion: 18 signConfig: ./sign/release.p12 deviceType: - phone - tablet autoInstall: true extraBuildArgs: - -p - productdefault各字段含义说一下。moduleName鸿蒙工程里的模块名默认是entry绝大多数工程不用改。compileSdkVersion/targetSdkVersion对应鸿蒙 SDK 版本。这里要跟开发工具里创建的工程配置保持一致配置不一致时编译会报“SDK 版本校验失败”。signConfig签名文件路径。打包 release 的 HAP 必须有签名否则设备上装不上。deviceType声明目标设备类型编译阶段会校验。autoInstallrun命令执行完后是否自动调用hdc install安装到设备。extraBuildArgs透传给hvigorw的额外参数。不同项目的构建模式、产品组合不同留这个字段是为了避免频繁改代码。配置化带来的好处是换一个鸿蒙工程不需要改 CLI 代码只需要改这份 YAML。这也是我在改造过程中比较满意的设计取舍。2.3 平台探测与命令路由平台探测看似简单但如果写得不稳后面每个命令都会出错。我的实现思路是只看文件存在性不做复杂内容解析。解析 JSON 判断平台一旦官方改了字段名探测逻辑跟着失效局面很难看。简化后的 Dart 伪代码是这样bool isHarmonyProject(String root) { return File($root/oh-package.json5).existsSync() File($root/ohos/entry/src/main/module.json5).existsSync(); }拿到平台类型后所有命令都走同一个路由class BuildCommand { Futurevoid execute(ProjectContext context) { final adapter PlatformAdapterFactory.create(context.platformType); await adapter.resolveDependencies(); await adapter.build(); await adapter.install(deviceId: context.deviceId); } }route 层不感知具体命令Android 走gradlew鸿蒙走hvigorw。后续即使新增平台也只需要新增一个 adapter原有命令代码不用动。这就是“命令层适配”的核心价值。3. 核心模块改造与实操实现3.1 项目创建模板下载与工程填充create命令的改造逻辑比较直接。原来工具会根据交互参数从模板仓库拉取 Android/iOS 模板现在增加鸿蒙模板选项。模板目录一般长这样templates/ harmony/ build-profile.json5 hvigorfile.ts oh-package.json5 ohos/ entry/ src/main/ module.json5 ets/ pages/ Index.ets模板不是完整工程而是“骨架”。执行blue_bird create -p harmony --name demo --bundleName com.example.demo时CLI 负责把模板文件复制到目标目录再替换关键占位符。比较需要注意的替换点是module.json5中的bundleName和versionName。bundleName必须与准备申请的签名证书匹配否则安装阶段会被系统拦截。还有个字段是pages如果模板里没有对应页面文件启动时会白屏或直接崩溃。模板从仓库拉下来后我建议做一次内部自检检查页面文件在不在别让使用者在启动应用那一瞬间才发现问题。3.2 依赖管理Flutter 依赖与鸿蒙依赖并行解析鸿蒙工程和 Flutter 工程的依赖解析是两套体系。Flutter 侧用pub get管理pubspec.yaml鸿蒙侧用ohpm install管理oh-package.json5。适配的核心问题是什么时候跑哪个以及如何避免每次构建都重复解析浪费时间。我的经验是做“锁定文件变更检测”。pub get是否执行取决于pubspec.yaml和pubspec.lock是否发生变化ohpm install是否执行取决于oh-package.json5和oh-package-lock.json5是否发生变化。CLI 内部维护一个缓存目录把每次执行前的文件哈希记下来下次执行时比对。哈希一致就跳过解析命令直接进入构建阶段。这个优化对 CI 特别友好能省掉大量的网络请求和依赖解析时间。依赖本身也要注意来源差异。oh-package.json5里的依赖可能来自远程 ohpm 仓库也可能来自本地 har 包。适配 Flutter 产物时我们会往工程里注入一个本地依赖路径指向entry/libs/FlutterEngine.har。注入方式有两种一种是在oh-package.json5里直接写flutter_engine: file:./libs/FlutterEngine.har另一种是放进ohos/entry/oh-package.json5的dependencies中。两种都行但选一种并保持一致就好否则容易出现“明明拷贝了文件却一直报找不到依赖”的怪问题。{ name: entry, version: 1.0.0, dependencies: { flutter_engine: file:./libs/FlutterEngine.har, ohos/hypium: 1.0.6 } }注意本地依赖路径是相对ohos/entry目录的不是项目根目录。写错路径ohpm install会直接报错而且报错信息是让你去检查远程仓库很容易误导排查方向。3.3 构建流程把 Flutter 产物塞进鸿蒙工程再打 HAP这一节是整个适配最核心的部分。鸿蒙工程不能直接编译 Flutter 的 Dart 代码它只能通过依赖 Flutter 引擎的 har 包来运行 Flutter 模块。所以构建流程必须分成两段先用 Flutter 工具链生成 har 产物再用鸿蒙工具链把 har 打进 HAP。一个可参考的完整构建脚本如下#!/bin/bash set -e # 1. Flutter 侧构建 flutter build flutter_engine --target-platform harmony --release # 2. 拷贝产物到鸿蒙工程 mkdir -p ohos/entry/libs cp build/flutter_engine/harmony/release/FlutterEngine.har ohos/entry/libs/ # 3. 更新本地依赖声明如果还未写入 # 手动确认 ohos/entry/oh-package.json5 中存在 file:./libs/FlutterEngine.har # 4. 鸿蒙侧依赖解析 ohpm install # 5. 鸿蒙工程编译打包 ./hvigorw assembleHap --mode module -p productdefault -p moduleentrydefault这里有几个细节容易踩坑。第一Flutter 构建命令不是所有版本都叫flutter build flutter_engine或者flutter build hap不同 Flutter 分支的产物命令会不同。建议在适配层做一次命令探测看看本机 Flutter 是否支持flutter build --help里出现了flutter_engine目标。第二hvigorw assembleHap的产物目录默认在ohos/entry/build/default/outputs/下每个模块对应自己的输出目录。如果你发现打出的 HAP 路径总是找不到直接在构建日志里搜outputs关键字比猜路径快得多。第三release 包默认需要签名签名配置不对时HAP 虽然能生成但是安装到鸿蒙设备时会提示签名校验失败。所以在 CI 场景下建议优先构建 debug 包验证流程跑通后再切 release。3.4 安装与调试hdc 的接入安装环节原来调adb install鸿蒙端要换成hdc install。基础命令并不复杂hdc list targets hdc install -r ohos/entry/build/default/outputs/entry-default-unsigned.hap但实际接进 CLI 的时候我加了几个保护逻辑。设备可能存在多台必须先指定序列号否则hdc install会提示设备不唯一或者直接卡住。hdc服务偶尔会掉线执行安装前先调用一次hdc start确保服务在线。如果目标设备已经安装过同包名应用需要加-r参数否则安装失败。安装结束后最好顺便拉起应用验证一下。命令是hdc shell aa start -a EntryAbility -b com.example.demo。一次性把“安装启动”链路打通体验会好很多。自动化执行时建议打印“当前设备、包名、HAP 路径”三要素出问题时一眼就能定位。4. 一键式脚本集成与工作流自动化4.1 一键命令的设计原则把前面的所有步骤封装成一条命令是所有适配工作的最终目的。执行blue_bird run --platform harmony --device serial内部自动完成clean - 依赖解析 - Flutter 构建 - 拷贝产物 - 鸿蒙构建 - 签名 - 安装 - 启动这个流程看似简单但设计上我坚持三个原则。第一阶段可见。每一步输出一个阶段标记比如[依赖] 解析开始、[Flutter] 构建引擎产物。跑挂了看到最后一个阶段标记就能定位到问题模块。第二失败快速回滚。前面步骤失败时不要继续执行后面的构建否则往往输出一堆无关报错。脚本层面用set -eDart 层面用异常中断都是同一思路。第三增量跳过。不是每次都要重新clean。依赖锁文件没变、Flutter 产物已经存在且时间戳没变就跳过对应步骤。增量逻辑能明显缩小本地迭代和 CI 的耗时尤其 Flutter 全量构建动不动就几分钟。4.2 Dart 侧子进程调用的细节blue_bird_cli 本身是 Dart 写的所以外部命令调用主要用Process.start。这里有个细节特别值得说不要用Process.run去接管耗时构建命令因为它会把全部 stdout/stderr 攒到内存里构建输出一大要么卡死要么内存暴涨。正确方式是Process.start之后用流式方式消费输出。一段可复用的命令执行代码Futurevoid runCommand( String command, ListString args, { String? workingDirectory, MapString, String? env, }) async { final process await Process.start( command, args, workingDirectory: workingDirectory, environment: env, ); process.stdout .transform(utf8.decoder) .listen((chunk) stdout.write(chunk)); process.stderr .transform(utf8.decoder) .listen((chunk) stderr.write(chunk)); final code await process.exitCode; if (code ! 0) { throw ToolException($command 执行失败退出码 $code); } }还要注意 Windows 平台下的命令差异。hvigorw在 Windows 上对应的是hvigorw.bat在 macOS/Linux 上才是无后缀脚本。适配层要按平台拼接命令名否则会出现“命令找不到”。环境变量继承也是一个容易忽略的点。CLI 里如果自定义了环境变量一定要在调用子进程时把它们合并进env否则你在配置里指定的 SDK 路径不会生效工具会退回系统默认值表现就是“明明配了路径构建还是报 SDK 找不到”。4.3 工作流接入 CIJSON 输出与缓存策略工作流自动化的另一半是 CI 流水线。工具不能只给人看还要给机器看。所以我在 CLI 里增加了一个--json参数让命令完成时输出机器可读的结果摘要比如{ platform: harmony, buildSuccess: true, artifactPath: ohos/entry/build/default/outputs/entry-default-signed.hap, artifactSize: 245760, installSuccess: true, deviceSerial: 1234567890 }CI 流水线拿到这个 JSON 后可以决定是继续上传产物还是标记当前构建失败、发通知。这样工具和流水线的耦合降到最低后续流水线平台怎么换CLI 都不用再动。CI 场景下还有一个缓存问题是本地不会遇到的。ohpm install和pub get都会产生大量临时文件每个流水线任务都从头解析依赖特别耗时间。解决方案是缓存~/.ohpm目录和项目内的build目录。在通用流水线配置里伪代码如下harmony_build: stage: build script: - blue_bird prepare --platform harmony - blue_bird run --platform harmony --device $TARGET_SERIAL artifacts: paths: - ohos/entry/build/default/outputs/*.hap expire_in: 1 week配套 CI 里的缓存配置至少能把二次构建时间压缩一半。这个收益在跑完整回归验证时特别明显。5. 常见问题与排查实录5.1 高频报错速查表下面整理的这些报错都是在适配过程中真实遇到过的按出现频率排个序报错信息根因解决方式hvigorw : 无法识别工具目录没进入 PATH或 Windows 下未用.bat调用前检测系统类型拼接正确的命令名配置工具链绝对路径ohpm install 报错 404ohpm 仓库连接异常或本地依赖路径写错检查oh-package.json5中file:路径确认相对目录构建产物为空Flutter 构建还没完成就执行了拷贝在脚本中使用set -e或者检查产物文件存在再继续SDK 版本校验失败配置的 compileSdkVersion 与工程实际不符统一用 IDE 默认参数生成工程后再对齐配置签名校验失败bundleName、证书、profile 三者不匹配签名三件套一起更新不要只换其中一个hdc install 卡住不动多设备未指定序列号先执行hdc list targets再指定-s参数安装5.2 三个容易踩的坑第一个坑是关于.gitignore的。鸿蒙工程里的ohos/.cxx、ohos/entry/build、ohos/entry/libs这些目录默认大概率被忽略掉。本地构建没问题但 CI 机器上 clone 代码后工程缺少entry/libs/FlutterEngine.harohpm install会解析失败。解决方式很粗暴在仓库里保留libs目录和目录下的README.md或者.gitkeep确保目录始终存在har 包是否提交仓库看团队策略至少要让构建脚本自己检查产物并给出明确报错。第二个坑是 hdc 服务不稳定。设备连着 USB 一夜之后再跑构建hdc install可能直接连不上。单纯在 CLI 里执行一次hdc install还不够我会在安装前先跑hdc start然后注册设备在线状态。整条调用链增加重试逻辑比如失败后重试三次每次间隔 5 秒。实测下来这个操作能解决绝大多数偶发掉线问题。第三个坑是目录结构变更导致 module.json5 里配置的入口失效。鸿蒙工程的页面路径、模块名都是显式声明在module.json5里的如果你为了对齐 Flutter 目录结构把ets/pages挪了位置却没有同步更新pages列表应用启动后会白屏或者直接报错找不到页面。所以模板设计阶段就要定好目录规范后面不要随意改名。5.3 配置化优先的一点体会整个鸿蒙化适配下来我最大的感受是工具链适配最怕硬编码。一开始我为了快速跑通在代码里直接写了/path/to/hvigorw结果同事一换电脑就废。后来改成配置驱动工具路径、签名文件、SDK 版本全部放到blue_bird_config.yaml里并提供一次自动探测。这样新同事接入时只需要把配置文件复制过去改几行命令就能跑通不用再翻代码。另一个体会是命令的执行结果一定要可观测。每个阶段都记录“开始时间、结束时间、退出码、输出摘要”出问题时打开日志就能定位到秒级。CLI 工具本质上是给人偷懒用的但偷懒的前提是“出问题时能快速接手”否则跑挂一条命令大家又退回手敲命令的模式适配工作就白做了。如果团队里同时维护着 Flutter 和鸿蒙两条工具链建议先把 blue_bird_cli 的“一键 run”流程跑通再逐步把签名、多渠道、发布归档这些高级能力加进来。流程通了剩下的事情就是往稳定里打磨了。