ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

命令行工具链鸿蒙化:从 dcli_scripts 适配看 Flutter 工程迁移的关键改造

命令行工具链鸿蒙化:从 dcli_scripts 适配看 Flutter 工程迁移的关键改造 如果手上有一套基于 Dart 写的命令行脚本集平时靠它完成 Flutter 项目的初始化、模板生成、批量改配置和打包分发那么当团队开始做 HarmonyOS 适配时你很可能在某个周五下午收到一条消息“脚本在鸿蒙上跑不通了。”我这里说的就是围绕 dcliDart 生态里专门用来写命令行脚本的库维护的那批脚本业内通常叫它们 dcli_scripts。这次适配我前后折腾了两周核心收获一句话命令行工具链的鸿蒙化不是把命令换个名字那么简单而是要把脚本里对目标平台的每一个隐性假设都显式化。这篇文章适合正在把 Flutter 工程往鸿蒙迁移、同时还想保留原有自动化能力的团队。不管你手里的脚本是自己零零散散写的还是从开源项目集成来的只要它们最终要调用 flutter、hvigorw、ohpm、hdc 这一串命令行工具下面的思路和代码细节大概率都用得上。1. 为什么 dcli_scripts 在鸿蒙场景下成了“半残”工具链1.1 dcli_scripts 到底帮你省了哪些事先把背景说清楚。dcli 是 Dart 的命令行开发库写出来的脚本用dart run tool/some_script.dart执行底层跑在 Dart VM 上。它提供了which、run、start、read、write、cd等一批开箱即用的 API比直接用 Process 调 shell 顺手得多。我手里维护的这套 dcli_scripts平时承担四类工作第一类是项目脚手架从 Git 仓库拉模板工程批量替换包名、应用名、图标生成新业务线的工程目录。第二类是配置注入根据环境参数改写 pubspec.yaml、AndroidManifest.xml、Info.plist 里的字段。第三类是依赖检查拉取依赖树找出版本冲突自动更新到指定版本。第四类是构建分发执行 flutter build把产物签名、重命名、上传到内部分发平台。这些事如果全让开发手工做每接入一个新项目就要浪费大半天。脚本化的意义在于把重复劳动交给机器同时把过程标准化。当鸿蒙平台加入之后脚本需要多承担一类事处理鸿蒙工程的模块结构、调用鸿蒙构建工具链、安装 HAP 包到真机。麻烦的是鸿蒙工程的结构和 Android/iOS 差异很大原有脚本里很多“想当然”的路径和命令全部失灵。1.2 鸿蒙化适配不等于“Flutter 能跑起来就完事”很多团队一开始有一个误区鸿蒙 Flutter SDK 已经能让 Flutter 工程编出 HAP 包了脚本不是自然而然就能用吗实际不是。命令行脚本的鸿蒙化核心是让脚本理解鸿蒙工程结构。比如鸿蒙工程里模块配置在module.json5依赖管理用oh-package.json5构建脚本是hvigorfile.ts产物输出目录是build加模块名。而 Android 的 Gradle 配置、iOS 的 Xcode 工程在鸿蒙这里完全不适用。又比如构建命令Android 是gradlew assembleReleaseiOS 依赖 Xcodebuild鸿蒙则是hvigorw assembleHap。脚本里只要有下面任意一个假设鸿蒙场景就会出问题假设注释里写死了.gradle文件路径假设flutter build apk之后一定产生build/app/outputs/假设每个平台都有package.config假设命令输出一定是纯文本没有 ANSI 控制符。这些假设分布在脚本的路径拼接、进程调用、输出解析各个角落想一次改完必须先做整体梳理。2. 改造前先把“依赖地图”铺开再决定适配路线2.1 分析 dcli_scripts 的三类典型依赖动手改代码之前我先花了一个晚上把整套脚本的依赖关系摸了一遍。推荐你也做这个动作作用相当于写代码前画架构图。脚本对外部命令的依赖大概分三类第一类是Flutter SDK 命令包括flutter、dart。这类命令所有 Flutter 工程师电脑上都有但鸿蒙 Flutter SDK 和标准 Flutter SDK 的命令子集不完全一致比如有的版本用flutter build hap有的用flutter build ohos脚本里不能写死。第二类是鸿蒙工具链命令包括hvigorw、ohpm、hdc、ace等。这类命令在 DevEco Studio 的安装目录里不一定在系统 PATH 里。很多脚本报“找不到命令”本质是环境变量没有配好。第三类是系统基础命令比如git、tar、unzip、python3。这类在 mac 和 Linux 上常见但在 Windows 上经常缺失或名字不同比如unzip在 Windows 上可能叫tar.exe或者根本没有。我建议做一张表格把脚本里which()过的所有命令、它们的绝对路径来源、在三个平台上的可用性都列出来。这一步能过滤掉一半的无效排查。2.2 两条适配路线最小侵入 vs 平台适配层整理完依赖后需要决定改造路线。我总结出两条。路线 A 是最小侵入分支法在原有脚本里加isHarmonyOS判断按平台走不同的逻辑分支。优点是快改动量小适合脚本总数不超过十五六套、只想让现有功能在鸿蒙上先跑起来的团队。缺点是后续每加一个平台脚本里都要塞 if代码会越来越难看。路线 B 是平台适配层法把“找命令”“拼路径”“构建”“安装”这些动作抽象成接口为 Android、iOS、HarmonyOS 各写一套实现。优点是长期维护舒服新平台接入成本低适合脚本数量多、团队规模大、后续可能还要接 Windows ARM 或 Linux 的场景。缺点是前期改造量大抽象设计一旦走偏反而比不改更痛苦。我的建议是如果你只是临时让三五个脚本出活别过度设计走 A。但如果你像我一样发现自己维护的脚本已经超过二十个而且公司还在同时维护多套 Flutter 基建那就老老实实走 B。我这次是先用 A 快速跑通全部用例再在第二周抽出核心逻辑重构成轻量适配层。2.3 提前建立鸿蒙工具链的“路径契约”无论选哪条路线都要先确立一套路径契约。所谓契约就是脚本里约定以哪些环境变量为准、命令去哪里找、对方一定是哪种格式。我最后定的契约是这样环境变量/入口推荐取值用途OHOS_SDK_HOMEDevEco Studio 内置 SDK 根目录定位 hdc、交叉编译工具、系统镜像DEVECO_SDK_HOMEDevEco Studio 命令行工具目录定位 hvigor、ohpm 的辅助脚本FLUTTER_HARBOR_HOME若使用鸿蒙版 Flutter SDK鸿蒙 Flutter SDK 根目录让脚本调用正确的 flutter 命令hvigorw工程根目录或 wrapper 脚本执行鸿蒙构建ohpm鸿蒙 SDK 内包管理器安装模块依赖hdc鸿蒙 SDK 内设备连接工具连接真机/模拟器并安装 HAP注意DevEco Studio 在 mac 上安装后命令行工具通常在/Applications/DevEco-Studio.app/Contents/sdk附近在 Windows 上则在安装盘 Program Files 里。关键是不要假设这些命令一定在 PATH 里脚本里要提供自动探测逻辑先查环境变量再查默认安装路径最后才报错。提示这一份“路径契约”要同步给团队所有人并在 README 里用表格写清楚。脚本跑不起来时一半的问题出在环境变量不一致。3. 核心改造点脚本里最容易踩坑的五个位置3.1 路径拼接不要再用单一路径分隔符dcli 脚本里最常见的隐性假设是路径拼接直接写/。比如原来的代码final gradlePath projectDir /android/app/build.gradle;这在 mac/Linux 上都正常但一旦切到 Windows 上跑或者鸿蒙 SDK 所在目录带反斜杠这套拼接就崩。鸿蒙适配后工程里会出现entry/src/main/ohos这种长路径拼接次数暴增。我的建议是统一用package:path/path.dart的p.join而不是手写分隔符。比如import package:path/path.dart as p; final entryDir p.join(projectDir, entry, src, main, ohos); final moduleJson p.join(entryDir, module, module.json5);这样写有三个好处一是按当前操作系统自动选择分隔符二是p.join会处理多余斜杠不会出现//这种幽灵路径三是代码可读性明显提升维护的人一眼就知道路径结构。另外要注意的是鸿蒙工程里oh_modules这个目录名和 Android 的.gradle目录一样属于“可以删除再由构建工具重建”的缓存目录。脚本做清理时别把oh_modules之外的源代码目录误删了。3.2 进程调用run() 的 shell 参数和命令顺序dcli 的run()底层是Process.run在不同系统上对 shell 的处理不一样。默认情况下它直接执行命令不经过 shell 解释因此像hvigorw这种本身是 shell 脚本的命令在 Windows 上会碰到问题因为 Windows 没有 bash 解释.sh文件而 dcli 不会自动帮你找 bash。实际改造时我用了这样的调用方式final result run( hvigorw, args: [assembleHap, --mode, debug, --module, entry], workingDirectory: projectDir, runInShell: isWindows, );runInShell在 Windows 上会走cmd或powershell这样hvigorw才能被解释执行。但这里有个隐患如果你在 Windows 上全局安装的是标准 Flutter SDK而鸿蒙 Flutter SDK 是另一个目录那么脚本里必须先切换 PATH 再调用 flutter否则会把标准 Flutter SDK 的命令拿过去用。我建议在脚本入口处做一次环境切换临时把鸿蒙 SDK 的 bin 目录插入环境变量final flutterBinDir p.join(harmonyFlutterSdkHome, bin); env[PATH] flutterBinDir Platform.pathSeparator env[PATH]!;这样才能保证后续所有which(flutter)都找到鸿蒙版本。3.3 文件系统写操作权限、软链和 hvigor 缓存鸿蒙构建会在工程目录下生成.hvigor、.ohpm、oh_modules等缓存目录。脚本如果负责清理构建产物要注意别漏了这些隐藏目录。同时鸿蒙 SDK 或 DevEco Studio 安装在 mac 上时经常是只读挂载或者有访问控制脚本往 SDK 目录里写配置会因为没有权限直接把整条工具链打断。我的原则是只读 SDK不写 SDK。任何权限变更、签名文件、代理配置一律放在工程目录或用户目录下。另外符号链接问题值得单独说。mac 上很多人喜欢把 DevEco Studio 放在/Applications然后把它ln -s到一个自定义路径。脚本里如果用了File(/Applications/DevEco-Studio.app/...).existsSync()大概率没问题但如果脚本里对路径做了canonicalize可能得到/private/Applications/...导致后续拼接出的路径不一致。所以路径比较一律用解析后的完整路径不要用显示路径。3.4 命令输出编码与 stdout 解析鸿蒙构建工具链的输出里经常带 ANSI 颜色控制符和进度条控制字符。脚本如果直接对 stdout 做正则匹配或字符串相等判断经常会匹配不上。典型场景是解析版本号final ok rawOutput.contains(BUILD SUCCESSFUL);这句在 Android Gradle 输出里很有效但到 hvigor 这里输出可能是\033[32mBUILD SUCCESSFUL\033[0m肉眼看着是 BUILD SUCCESSFUL程序里 contains 匹配却失败因为中间多了一段颜色控制序列。解决办法是先做一次 ANSI 剥离final clean rawOutput.replaceAll(RegExp(r\x1B\[[0-9;]*[A-Za-z]), ); final ok clean.contains(BUILD SUCCESSFUL);我踩过这个坑后才意识到所有解析输出的地方都要统一加上这段清洗否则同样的判断在终端里正常、在日志管道里就诡异失败。3.5 Flutter 命令封装从 build apk 到 build hap脚本里对 flutter 的调用也需要封装。原来可能是run(flutter, args: [build, apk, --release]);鸿蒙侧要换成对应平台命令。需要强调鸿蒙 Flutter SDK 各版本的子命令有差异有的版本认hap有的认ohos绝大多数版本是flutter build hap --release --target-platform ohos-arm64这类写法。脚本里不要猜启动时先跑一次探测final probe run(flutter, args: [build, --help], runInShell: true); final supportsHap probe.contains(hap );如果探测结果里没有 hap就只能走 hvigorw 手动构建。把这层判断做进封装里比让每个脚本单独处理要省心得多。4. 实操记录从环境准备到完整跑通 hvigor 构建4.1 一次把开发机上的鸿蒙工具链配齐开始改脚本之前我先花了一个上午把开发环境配到“命令行可用”状态。这里说的可用不是 DevEco Studio 能打开 UI而是说下面四条命令在终端里都能跑通命令期望结果说明flutter --version输出鸿蒙 Flutter SDK 版本确认当前 flutter 指向正确hvigorw --version输出版本号不报错确认 wrapper 可执行ohpm --version输出版本号确认包管理器可用hdc list targets至少列出模拟器或真机确认设备通道正常devEco Studio 安装完成后hvigorw在工程根目录通常有一个 wrapper 脚本真正可执行文件在 SDK 或者 DevEco 的 tool 目录里。我建议在系统环境变量里把hvigorw的父目录加进去或者用绝对路径两种方式都行但必须在 README 里写明。配环境时最容易忽略的是ohpm的全局 registry 配置。它默认读取用户目录下的配置文件如果之前装过远古版本可能滞留一个过期的 registry导致安装模块时一直卡住。彻底做法是手工检查配置文件里的 registry 地址。这一步只涉及正常的仓库源配置和网络连通性无需其他任何多余设置。4.2 基线验证改造前先跑一遍收集“失灵”现象环境配好后我强烈建议不要着急改代码先拿最核心的两个脚本各跑一遍收集失败现象。我记录下来的典型失败包括flutter build apk直接执行产物目录不存在which(hvigorw)返回空脚本走throw解析hvigorw --version输出时因为 ANSI 控制符导致版本提取失败默认路径拼接使用/在 Windows 上报路径找不到构建完成后的 HAP 包被脚本当成 APK 去重命名后缀名检查不通过。这些现象我建议用表格记录每一行对应一个脚本、一条命令、一个失败原因。这个表就是后续改造的验收清单改一个划掉一个比靠记忆靠谱得多。4.3 动手改脚本从初始化模块到打包链路以一套典型的“初始化 构建 安装”脚本为例我梳理改造顺序是先工程结构识别再命令封装最后产物处理。工程结构识别部分脚本原本会检查android/和ios/目录是否存在改造后要增加ohos/或harmony/目录的判断。代码大致是这样const harmonyDirNames [ohos, harmony]; String? findHarmonyDir(String projectDir) { for (final name in harmonyDirNames) { final path p.join(projectDir, name); if (exists(path)) return path; } return null; }这里用了两个候选目录名是因为不同版本的鸿蒙 Flutter 模板目录名不完全一致脚本里做一次自动探测别写死。接下来是命令封装。我写了一个BuildRunner专门负责调用 hvigorwclass HarmonyBuildRunner { final String projectDir; Futurevoid buildDebugHap({required String entryName}) async { final result run( hvigorw, args: [assembleHap, --mode, debug, --module, entryName], workingDirectory: projectDir, runInShell: isWindows, // 超时给足hvigor 首次构建要下载依赖非常慢 timeout: const Duration(minutes: 15), ); final clean result.replaceAll(RegExp(r\x1B\[[0-9;]*[A-Za-z]), ); if (!clean.contains(BUILD SUCCESSFUL)) { throw Exception(hvigor build failed: $clean); } } }这里有两处细节值得注意。第一--module参数要求的是模块名通常是entry不要写目录路径。第二超时要给足因为ohpm install第一次跑会下载较多依赖我当时因为超时设短了反复失败。产物处理部分HAP 包的默认输出路径是build/模块名/outputs/下的default/子目录。脚本里要按这个约定去找.hap文件找到后用hdc install安装到设备final hapFiles find(*.hap, under: p.join(projectDir, build, entryName, outputs)) .toList(growable: false); if (hapFiles.isEmpty) { throw Exception(未找到 HAP 产物请检查构建输出目录); } run(hdc, args: [install, hapFiles.first], runInShell: true);find是 dcli 提供的文件查找函数很好用但要注意它默认可能不遍历隐藏目录而oh_modules是隐藏目录所以查找范围要限定到outputs内部避免误判。4.4 回归验证用脚本自检替代肉眼检查改完当然要回归。我发现最有效的回归方式不是人肉盯着命令行看而是写一个自检脚本把验证动作固化下来。比如void verifyOutput(String projectDir) { final outputDir p.join(projectDir, build, entry, outputs); final apkFiles find(*.hap, under: outputDir).toList(); if (apkFiles.length ! 1) { throw Exception(应当且仅应当生成一个 HAP 文件实际 ${apkFiles.length} 个); } final moduleJson p.join(projectDir, entry, src, main, module.json5); if (!exists(moduleJson)) { throw Exception(缺少 module.json5鸿蒙工程结构不完整); } }这类自检脚本可以直接放进 CI 流程。我是把验证动作抽象成一个selftest入口所有脚本改完都跑一次跑完再看人工抽查报告。5. 常见问题与排查技巧实录5.1 高频问题速查表两周适配过程中我整理了最常出现的几类问题做成速查表给你参考现象原因解决办法which(hvigorw)返回空hvigorw 不在 PATH 中设置DEVECO_SDK_HOME并在脚本启动时自动探测常见安装路径Windows 上执行hvigorw报“不是内部命令”是 shell 脚本需要 bash/cmd 解释调用时设置runInShell: true或改成直接调用hvigorw.bathvigor 构建输出匹配不到“BUILD SUCCESSFUL”ANSI 颜色控制符混入输出先剥离 ANSI 控制字符再匹配清理构建目录时误删oh_modules未识别缓存目录清理白名单里排除oh_modules、.hvigor、.ohpm保留源码目录ohpm install长时间卡住registry 配置残留或 DNS 解析慢手工检查 ohpm 配置文件的 registry 地址用 curl 验证连通性hdc list targets列表为空开发机与设备未连接/驱动未装重启 hdc 服务重新插拔设备确认打开 USB 调试构建产物找不到.hap文件构建模块名和 scripts 里设置不一致统一使用--module entry并核对hvigorfile.ts里的模块定义每一行背后都是实测过的不是从文档里抄的。尤其是 ANSI 那行你可能调试半小时都想不到代码逻辑明明没问题输出明明也有“BUILD”强匹配就是过不了这就是控制符的锅。5.2 我的独家避坑心得除了速查表还有几条更偏“长期维护”的心得。第一条是别把逻辑散落在 shell 脚本里。很多 Flutter 项目既有 dcli 脚本又有.sh和.bat辅助脚本鸿蒙适配时如果每个地方都改很快就会失控。我建议把所有外部命令的调用都收敛到 dcli 脚本里shell 层只负责找现成的命令不做业务判断。第二条是用日志辅助排查而不是用 print。dcli 提供了logger支持等级过滤。适配阶段信息很杂把四处输出的信息统一打到日志里出了问题直接翻日志定位比在终端里翻几千行输出强太多。第三条是CI 上和本地的“最小一致性”要提前拉齐。本地 DevEco Studio 自带一套 SDKCI 机器上可能另装一套两边的 SDK 路径、工具链版本不一致时脚本在本地正常、CI 崩掉的场景非常常见。解决方式是在 CI 脚本里显式设置同一套环境变量并固定 SDK 版本号。第四条也是我认为最重要的一条先把命令串手打一遍。我所有改造步骤在写进脚本之前都会先手动在终端执行一遍。手敲的意义在于让你亲眼看到真实输出长什么样尤其是有没有 ANSI、产物目录到底叫什么、首次构建要多久。脚本是根据真实行为写的而不是根据你以为的行为写的。6. 一点个人经验先把“最小闭环”跑通再谈效率说实话这次命令行工具链鸿蒙化让我最大改观的不是技术难度而是适配顺序。我一开始试图把整套脚本一次性改造完结果上午改构建、下午改安装两边都在半生不熟的状态出了问题根本分不清是哪个环节引入的。踩过几次坑之后我的做法变成先挑一个最最小号的脚本比如“检查鸿蒙工程结构”这种只读脚本改成支持鸿蒙目录跑通后加一个“构建 HAP”的脚本再跑通后加“安装到设备”。每一步都全链路验证完再进入下一步。这样虽然看起来慢但总时间反而最短。还有一个小技巧改造过程中所有外部命令的探测结果我建议直接输出到一个estimated报告文件里。例如哪个目录存在、哪个命令可用、版本号是什么一次性打印出来。这样不仅调试方便后续写 README、给同事答疑也都能直接引用省得一遍遍解释。命令行工具链的鸿蒙化说到底就是把原本“默认 Android/iOS”的命令脚本扩展成能把鸿蒙工程也当作一等公民的工具。这件事的产出不只是几个脚本能跑而是整个团队的 Flutter 鸿蒙开发节奏不被手工命令打断。等到你发现自己只需要敲一次dart run tool/build_all.dart就能依次产出 APK、IPA 和 HAP 的时候那套工具链才算真正完成了鸿蒙化。
返回列表