仓库: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 上的执行流是:

  1. update_dart_sdk.ps1官方 storage 下载 dart-sdk-windows-x64.zip;
  2. 官方 Dart SDK 的 dart:ffi 只有 22 个 ABI,没有 ohosArm/ohosArm64/ohosX64;
  3. bootstrap 编译 fluttertpc_dart_native(它的 ohos 补丁引用了这些 ABI)时,编译器在 architecture.dart 里找不到成员,直接报错。

同时还有第二个隐藏断裂:.sh 会在 bootstrap 时幂等补齐 bin/cache/dart-sdk-ohos(HAP/AOT 构建专用的 frontend_server + dartaotruntime),.ps1 同样没有——就算主 SDK 换对了,artifacts.dartplatform.isOhos 时把 frontendServerSnapshotForEngineDartSdkengineDartAotRuntime 都解析到 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.stampdart-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 上要记住三件事:

  1. Process.run('xxx') 找不到 .bat,要显式写扩展名;
  2. .bat 本身不可靠(父进程环境敏感、可能爆栈),能绕就绕;
  3. 最稳的路径往往是"找到脚本,用解释器(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

几点通用思考

  1. 双脚本必须同步演进。 update_dart_sdk.sh.ps1 是同一逻辑的两份实现,上游 Flutter 靠"NOTE 注释 + code review 纪律"维持一致,但涉及平台特有逻辑(如 OHOS 覆盖)时,.ps1 最容易被漏掉。凡是改了 .sh,先问一句"Windows 的对应路径在哪"。
  2. Windows 的进程模型是独立的坑域。 PATHEXT 不被 CreateProcess 解析、bat 脚本对调用环境敏感,这两个问题在 macOS/Linux 上完全无法复现,只能在 Windows 上实测。跨平台 CI 矩阵里,Windows 的"冒烟"不该只是"能编译",而要覆盖 doctor 这类大量 Process.run 的路径。
  3. 版本元数据依赖 git 状态。 浅克隆、无 tag、detached HEAD 都会让版本解析退化。工具侧可以做更友好的回退提示,用户侧记住:见到 0.0.0-unknown / [user-branch],先检查 tag 和分支,而不是重装 SDK。
  4. 发版工件与覆盖表要一起发。 dart-sdk-url.ohos 指向 Release 附件,代码合了但附件没传,效果等于没修(bootstrap 会 404 后回退官方源,又回到最初的报错)。发版清单里应包含"每个覆盖键对应的附件"核对项。

欢迎大家关注:https://atomgit.com/CPF-Flutter

Logo

开源鸿蒙跨平台开发社区汇聚开发者与厂商,共建“一次开发,多端部署”的开源生态,致力于降低跨端开发门槛,推动万物智联创新。

更多推荐