让 Flutter 鸿蒙版在 Windows 上开箱即用:一次完整的工具链修复实录
仓库:
oh-flutter/flutter_flutter(Flutter OpenHarmony 适配版,分支oh-3.47-dev,基线 3.47.5)
涉及提交:96072843655/5f7a7693731/fe3cb9a799a
背景
Flutter 鸿蒙适配版(下称 flutter_flutter)在 3.47.5-ohos-1.0.0 这个 Release 上,为 macOS 和 Linux 用户提供了"零操作安装"体验:克隆仓库后第一次运行 flutter 命令,bootstrap 脚本会自动从 Release 附件拉取含 Abi.ohosArm/ohosArm64/ohosX64 的定制 Dart SDK,以及 HAP 构建专用的 dart-sdk-ohos 运行时。
但这套体验在 Windows 上是断裂的——我们在这台 Windows 11 机器上克隆仓库后第一次跑 flutter --version,直接撞上一连串错误:
/C:/.../code_assets/architecture.dart:54:9: Error: Member not found: 'ohosArm'.
Abi.ohosArm: Architecture.arm,
^^^^^^^
顺着这一行错误往下排查,最终在工具链层面修掉了四个问题。它们彼此独立,又都指向同一个主题:跨平台脚本与 Windows 进程模型的差异。下面按排查顺序逐个展开。
问题一:Windows bootstrap 完全没有 OHOS 逻辑
现象
首次运行 flutter --version 时,flutter tool 的 bootstrap 编译 fluttertpc_dart_native 失败,报 Member not found: 'Abi.ohosArm'。
根因
flutter 的 Dart SDK 下载逻辑分两套脚本:
bin/internal/update_dart_sdk.sh——bash 版,macOS/Linux 走这里;bin/internal/update_dart_sdk.ps1——PowerShell 版,Windows 走这里。
社区之前在 .sh 里加上了 OHOS 覆盖逻辑(读取 bin/internal/dart-sdk-url.ohos 表,把 dart-sdk-windows-x64.zip 等键的下载地址改指向定制 SDK),但 .ps1 完全没有同步——文件里 ohos 出现 0 次。
于是 Windows 上的执行流是:
update_dart_sdk.ps1从官方 storage 下载dart-sdk-windows-x64.zip;- 官方 Dart SDK 的
dart:ffi只有 22 个 ABI,没有ohosArm/ohosArm64/ohosX64; - bootstrap 编译 fluttertpc_dart_native(它的 ohos 补丁引用了这些 ABI)时,编译器在
architecture.dart里找不到成员,直接报错。
同时还有第二个隐藏断裂:.sh 会在 bootstrap 时幂等补齐 bin/cache/dart-sdk-ohos(HAP/AOT 构建专用的 frontend_server + dartaotruntime),.ps1 同样没有——就算主 SDK 换对了,artifacts.dart 在 platform.isOhos 时把 frontendServerSnapshotForEngineDartSdk 和 engineDartAotRuntime 都解析到 dart-sdk-ohos 目录,该目录不存在,HAP 构建依然走不通。
修复(提交 96072843655)
cherry-pick oh-3.47.4-dev 分支上的 18e36518663,把 .sh 的等价逻辑补到 .ps1:
Get-OhosOverrideUrl函数:读取与.sh、flutter_tools 的Cache._ohosArtifactOverrideUrl三方共用的dart-sdk-url.ohos覆盖表(格式为每行<zip 名>:<url>);- 主 SDK 下载前查表,命中则替换 URL,未命中告警并回退官方源;
- 在 engine-stamp 判断之外幂等补齐
dart-sdk-ohos:已安装就跳过,不必重下 200MB 主 SDK;失败只告警,不阻断普通 flutter 命令; - 对下载的运行时校验 PE 头(
MZ),防止拿到 macOS 的 Mach-O 二进制直到 HAP 构建阶段才报格式错误。
同时在 dart-sdk-url.ohos 覆盖表里加上 Windows 两行,并把全部 dart-sdk 条目统一指向 3.47.5-ohos-1.0.0 Release:
dart-sdk-windows-x64.zip:https://atomgit.com/oh-flutter/flutter_flutter/releases/download/3.47.5-ohos-1.0.0/dart-sdk-windows-x64-ohos.zip
dart-sdk-ohos-windows-x64.zip:https://atomgit.com/oh-flutter/flutter_flutter/releases/download/3.47.5-ohos-1.0.0/dart-sdk-ohos-windows-x64.zip
配套地,把两个 Windows 工件(主定制 SDK 约 206MB、ohos 运行时约 8.2MB)上传到了该 Release 附件。这里有个小插曲:AtomGit 的附件上传没有直接的 REST 端点(attach_files 404),要走签名两步法——先 GET /releases/:tag/upload_url?file_name=... 拿带 OBS 签名的 URL 和一组 x-obs-* 请求头,再 PUT 文件并原样带回这些头,漏了任何一个对象存储都会拒绝或回调不生效。
验证
删掉 bin/cache/engine-dart-sdk.stamp 和 dart-sdk 目录强制重建,bootstrap 正确打印:
OHOS: using customized Dart SDK (dart-sdk-windows-x64.zip) from https://...
下载、解压、编译 flutter tool 一气呵成。用定制 SDK 里的 dart 跑一个 ABI 自检脚本,输出含 ohos_arm / ohos_arm64 / ohos_x64,且 dartaotruntime.exe 是合法 PE(MZ 头)。
问题二:doctor 检测不到 ohpm
环境装好后(DevEco Studio 26.0.0),flutter doctor -v 的 HarmonyOS toolchain 一栏报:
[X] HarmonyOS toolchain - develop for HarmonyOS devices
• OpenHarmony Sdk at ... , available api versions has [26:default]
X Ohpm is missing, please configure "ohpm" to the environment variable PATH.
• Node version v24.14.1
• Hvigorw binary at ...\hvigorw
有意思的是:Node 和 hvigorw 都能检测到,唯独 ohpm 缺失——而 ohpm --version 在终端里明明能跑。
根因:Windows 的两层"找不到"
ohos_doctor.dart 里的检测代码是:
final _VersionInfo? ohpmVersion = await _getBinaryVersion(<String>['ohpm', '--version']);
_getBinaryVersion 内部就是 _processManager.run(commands),拿不到 exit 0 就报 missing。
第一层:CreateProcess 不解析 PATHEXT。 Linux/macOS 上 execvp 会沿 PATH 找到 ohpm(一个 shell 脚本)并执行;Windows 上 Dart 的 Process.run 最终走 CreateProcess,它不会像 cmd.exe 那样按 PATHEXT(.COM;.EXE;.BAT;.CMD;...)把裸名字 ohpm 补全成 ohpm.bat。PATH 里明明有 ohpm.bat,进程却报"系统找不到指定的文件"。
那把命令改成 ohpm.bat 行不行?我们做了个最小复现:
// 独立 dart 脚本里:
Process.runSync('ohpm.bat', ['--version']); // exit=0, 输出 26.0.0.630 ✓
单独跑没问题——但在 flutter_tools 环境里跑,stderr 是这样的:
****** B A T C H R E C U R S I O N exceeds STACK limits ******
第二层:批处理递归爆栈。 这是第二处典型的"Windows 才会踩"的坑:ohpm.bat 开头有 setlocal enabledelayedexpansion,内部大量使用 call :label 递归(算参数长度、剥参数、找 node)。bat 脚本对"自己怎么被拉起"非常敏感——当它经由某个继承了复杂环境/非常规调用约定的父进程启动时,%~dp0、参数展开或环境变量转义会出问题,导致脚本递归调用自己的入口而不是预期的 label,cmd 的调用栈(深度约 32 层)瞬间耗尽。加临时调试输出确认了这一点:无论用 PATH 名还是 where 拿到的全路径直接拉起,都是同一个爆栈错误;换成 cmd /c ohpm.bat --version 也没用。
修复(提交 5f7a7693731)
既然 .bat 的两条路都堵死,就绕开它。ohpm.bat 的本质只是 node pm-cli.js 的包装:
/// check ohpm
// Windows 下通过 PATH 名或全路径拉起 ohpm.bat 都会触发批处理递归栈溢出
// ("BATCH RECURSION exceeds STACK limits"),且 CreateProcess 不解析
// PATHEXT;改为定位 pm-cli.js 后用 node 直接执行,绕开 .bat。
List<Object> ohpmCommand = <String>['ohpm', '--version'];
if (isWindows) {
try {
final whereResult = await _processManager.run(<String>['where', 'ohpm.bat']);
if (whereResult.exitCode == 0) {
final ohpmBatPath = (whereResult.stdout as String)
.split('\n')
.map((e) => e.trim())
.where((e) => e.isNotEmpty)
.first;
final cliJs = ohpmBatPath.replaceAll('ohpm.bat', 'pm-cli.js');
ohpmCommand = <String>['node', cliJs, '--version'];
}
} on ProcessException {
// ignore error.
}
}
final _VersionInfo? ohpmVersion = await _getBinaryVersion(ohpmCommand);
思路:先用 Windows 自带的 where 定位 ohpm.bat(它是可执行文件,不涉及 PATHEXT 问题),把路径里的 ohpm.bat 替换成同目录的 pm-cli.js,然后用 node 直接执行。实测 exit=0,输出 26.0.0.630。
修复后 doctor 输出:
[√] HarmonyOS toolchain - develop for HarmonyOS devices
• Ohpm version 26.0.0.630
经验
给跨平台工具写进程检测时,Windows 上要记住三件事:
Process.run('xxx')找不到.bat,要显式写扩展名;- 但
.bat本身不可靠(父进程环境敏感、可能爆栈),能绕就绕; - 最稳的路径往往是"找到脚本,用解释器(node/python/cmd)直接执行脚本的真实内容"。
另外还有个环境变量层面的坑顺带记下:ohpm.bat 找 node 的顺序是先 node --version 再回退 NODE_HOME。只把 node 加 PATH 不设 NODE_HOME 时,在某些调用链下仍会失败,持久化 NODE_HOME 指向 DevEco 自带的 node 目录(...\DevEco Studio\tools\node)最省心。
问题三:版本号 0.0.0-unknown
现象
[!] Flutter (Channel [user-branch], 0.0.0-unknown, ...)
根因
我们的本地克隆是 git clone --depth 1 拿的——省了 200MB 流量,但浅克隆不带 tag。flutter_tools 解析框架版本用的是 git describe --tags,没有 tag 时命令失败,版本号回退成 0.0.0-unknown。这个其实是文档里写过的行为(浅克隆 + 无 tag 的已知副作用),不算 SDK 的 bug。
修复
不用全量历史,按需取 tag 即可:
git fetch --depth 1 origin tag 3.47.5-ohos-1.0.0
rm bin/cache/flutter.version.json # 让 flutter 重新生成版本缓存
之后 flutter --version 正确显示 Flutter 3.47.5-ohos-1.0.0。
问题四:unknown channel 警告
现象
版本号修好后还剩一条:
[!] Flutter (Channel oh-3.47-dev, 3.47.5-ohos-1.0.0, ...)
Currently on an unknown channel. Run `flutter channel` to switch to an official channel.
根因
version.dart 里的 getBranchName() 有一段脱敏逻辑:
if (redactUnknownBranches || _branch!.isEmpty) {
// Only return the branch names we know about; arbitrary branch names might contain PII.
if (!kOfficialChannels.contains(_branch) && !kObsoleteBranches.containsKey(_branch)) {
return kUserBranch; // '[user-branch]'
}
}
上游只认 master/main/beta/stable 四个官方 channel。这个设计的初衷是隐私:任意分支名可能包含 PII(比如个人项目名),上报前统一脱敏成 [user-branch]。但对 flutter_flutter 这个仓库来说,oh-3.47-dev 恰恰是官方发版分支,被误杀了。doctor 的判定随之连锁:versionChannel == kUserBranch → 附加 unknown channel 警告,且校验结果从 [√] 降级为 [!]。
修复(提交 fe3cb9a799a)
给 ohos 发版分支的命名规则(oh-<版本>[-rcN][-dev])开白名单:
/// OHOS(鸿蒙适配)分支名规则,如 oh-3.47-dev / oh-3.47.4-rc1-dev。
/// 命中该规则的分支视为已知 channel,doctor 不再报 unknown channel。
final RegExp kOhosBranchPattern = RegExp(r'^oh-\d+(\.\d+)*(-rc\d+)?(-dev)?$');
if (!kOfficialChannels.contains(_branch) &&
!kObsoleteBranches.containsKey(_branch) &&
// OHOS(鸿蒙适配)分支(oh-x.y[-rcN][-dev])是本仓库的官方发版分支。
!kOhosBranchPattern.hasMatch(_branch!)) {
return kUserBranch;
}
正则刻意收紧(oh- 前缀 + 数字版本 + 可选 rc/dev 后缀),普通用户分支不会被误匹配,脱敏逻辑对其他场景依然生效。
验证
[√] Flutter (Channel oh-3.47-dev, 3.47.5-ohos-1.0.0, on Microsoft Windows ...)
[!] 变 [√],警告消失。
最终效果
同一台 Windows 11 机器,修复前后 flutter doctor -v 的对比:
修复前 修复后
───────────────────────────── ─────────────────────────────
[!] Flutter (0.0.0-unknown) [√] Flutter (3.47.5-ohos-1.0.0)
[X] HarmonyOS toolchain [√] HarmonyOS toolchain
X Ohpm is missing • Ohpm version 26.0.0.630
Windows 用户现在的体验与 macOS/Linux 完全对齐:克隆仓库 → 配好 DevEco/Node 环境变量 → 直接 flutter build hap。
几点通用思考
- 双脚本必须同步演进。
update_dart_sdk.sh与.ps1是同一逻辑的两份实现,上游 Flutter 靠"NOTE 注释 + code review 纪律"维持一致,但涉及平台特有逻辑(如 OHOS 覆盖)时,.ps1 最容易被漏掉。凡是改了.sh,先问一句"Windows 的对应路径在哪"。 - Windows 的进程模型是独立的坑域。 PATHEXT 不被 CreateProcess 解析、bat 脚本对调用环境敏感,这两个问题在 macOS/Linux 上完全无法复现,只能在 Windows 上实测。跨平台 CI 矩阵里,Windows 的"冒烟"不该只是"能编译",而要覆盖 doctor 这类大量
Process.run的路径。 - 版本元数据依赖 git 状态。 浅克隆、无 tag、detached HEAD 都会让版本解析退化。工具侧可以做更友好的回退提示,用户侧记住:见到
0.0.0-unknown/[user-branch],先检查 tag 和分支,而不是重装 SDK。 - 发版工件与覆盖表要一起发。
dart-sdk-url.ohos指向 Release 附件,代码合了但附件没传,效果等于没修(bootstrap 会 404 后回退官方源,又回到最初的报错)。发版清单里应包含"每个覆盖键对应的附件"核对项。
欢迎大家关注:https://atomgit.com/CPF-Flutter
更多推荐



所有评论(0)