前言

第 8 篇收尾时,ninja28 全绿 0 FAILED,四个 target 齐备——引擎「能编出来」这件事成立了。但编出来的东西躺在一台 Mac 的 out/ 目录里,对任何人都没有价值。本篇解决后半程:怎么把它变成别人机器上一条命令就能装到、装完就能 flutter build hap 的东西。

这条链路比想象中长,而且它的难点不在技术复杂度,在于四段之间任何一段"看起来成功"都可能是静默失败:构建产物取错目录、打包布局"顺手统一"了一下、发布时重传了同名附件、分发时改了覆盖行但没换新 tag——四种情况本机全都能跑通,只有别人装不上。

本篇主体是 3.44.9 → 3.47.4 批次 1.0.0 → 1.0.1 两次发布的实录,命令与产物均实测;其中 1.0.0 暴露的 flutter_patched_sdk.zip 装错产品档 bug,在 1.0.1 修正并复验,是第五章的主线案例。第五章同时把发布线拉到当前的 3.47.8-ohos-1.0.0:11 个自传附件、11 条覆盖键,并把常被漏掉的那一类产物讲清楚——Dart SDK 自己也是要为鸿蒙重编、为 mac 和 Windows 各发一份的工件。

前置阅读:第 8 篇(引擎构建链,产物怎么来)、第 7 篇(Engine 合并与契约对齐)、第 2 篇(消费侧安装视角,本篇第六章从生产侧解释它为什么会自动更新)。

一、链路全景:四段一条线

1.1 硬约束:用户零手工步骤

先把目标钉死,因为它决定了后面所有设计取舍:

用户侧:fvm install <tag> → flutter build hap
中间不允许出现任何"手工下载、手工解压、手工改路径"的步骤

这是这个 fork 的硬约束。所有分发设计都服从它——宁可维护一套复杂的覆盖行机制,也不给用户提供"装完再手动放个文件"的选项。这条约束反过来解释了本篇几乎所有看似绕弯的设计。

1.2 四段链路

构建(ninja → har)  →  打包(5 个 zip)  →  发布(atomgit Release + tag)  →  分发(覆盖行 → 用户零操作)
   §三                    §四                    §五                          §六

这条是引擎侧主链。还有一条平行支路容易被漏:Dart SDK 本身也是自编自发的产物——第 6 篇从源码把它编出来,这里要多打包、多发布 4 个 zip(macOS / Windows 各两档),走的却是同一个 Release、同一个覆盖文件(§5.8、§6.1)。所以本篇真正的口径是"一次发布 11 个工件、11 条覆盖键",而不是只有那 5 个引擎 zip。

在这里插入图片描述

上图三个板块对应本篇的三个核心论点:① 四段链路各自职责,用户侧收口在 fvm install + flutter build hap 两条命令;② 中间的红/绿对照是本篇最重要的一条铁律——同名 tag 重传附件,已装机器永远拿不到新工件,而且不报错;③ 底部四类工件的过期判据完全不同,这是所有"改了没生效"误判的来源。

1.3 为什么这条链路长这样

鸿蒙引擎的官方默认下载地址在华为云 OBS 上,指向 engine.ohos = 10a8d012——那是 3.44.9 时代的产物,内带 dart 3.12.2 / kernel 130。而本分支的 dart 是 3.13.3 / kernel 138。

直接构建会得到这样一条错误:

Unexpected Kernel Format Version 130 (expected 138)

官方地址不能改(改了就不是兼容上游的 fork 了),所以走覆盖行:在仓库里放一个映射文件,把特定工件的下载地址改指到自己发布的 atomgit Release 附件上。这就是 §六的全部内容。

二、版本锁:三套版本源别搞混

2.1 三个锁文件的分工

bin/internal/ 下有三个长得很像的文件,管三件不同的事:

版本文件当前值管什么谁会动它
bin/internal/engine.version06a2e2a1上游 FlutterSdk:common/flutter_patched_sdk[_product].zip、宿主工具(const_finder / impellerc / flutter_tester,实测已是 138 版,不需要重建)同步上游时
bin/internal/engine.ohos.version10a8d012FlutterSdkOhos(darwin-$arch/artifacts.zip、sky_engine.zip)+ OHOSGenSnapshotArtifacts(ohos-arm64{,-profile,-release}/darwin-x64.zip)同步上游时
bin/internal/engine.ohos.har.versione068c218OHOSInternalBuildArtifacts:三个 har同步上游时

后两个的完整值是 40 位提交号(10a8d012d86cd7ee13e0acc859fea796c8a01764、e068c218c3d7f693cc86df27fe8c1bb6d066823b),它们决定的是"要下 OBS 上哪个目录"这个上游口径。

2.2 关键结论:重发工件不动这三个文件

这是本节唯一需要背下来的东西:

# 发布新工件时,正确的做法是只改一个文件:
git diff --stat HEAD~1 -- bin/internal/
#  bin/internal/dart-sdk-url.ohos   ← 只有这一行变了
# 三个 *.version 文件保持原样

# 真正的"推进信号"是覆盖行里的 URL 变了,不是版本号变了
grep -c '3\.47\.4-ohos-1\.0\.1' bin/internal/dart-sdk-url.ohos

很多同学的直觉是"我发了 1.0.1,总得把 engine.ohos.version 改成 1.0.1 吧"。**不要。**这三个锁文件只在同步上游时才动,改它们等于把默认回退地址也一起改到一个不存在的 OBS 目录上,会直接搞坏没有覆盖行的场景。

2.3 三类版本信息的正确读法

一个常见的误判(消费侧 CP12 的生产侧版本):拿 engine.version 去对比鸿蒙引擎。三者关系是:

  1. engine.version 是上游 FlutterSdk 的口径,跟鸿蒙引擎无关;
  2. engine.ohos.version 是鸿蒙引擎默认(OBS)地址的口径,有覆盖行时它只是回退目标;
  3. 覆盖行 URL 里的 tag(3.47.4-ohos-1.0.1)才是实际生效的产物版本。

判断"我装的到底是哪个引擎",唯一可靠的方法是查产物本身——libflutter.so 里的版本字符串,第 2 篇的三层核验讲过这个。

三、构建:规范化入口是 wrapper,不是手搓 gn + ninja

3.1 engine/ohos 只有一行有效代码

# flutter_flutter/engine/ohos
python3 ./src/flutter/attachment/scripts/ohos.py "$@"

为什么要这么个壳?因为 ohos.py 的 checkEnvironment() 强制要求 cwd = 引擎构建根,cwd 错了直接被拒。所以正确调用姿势是:

cd $ROOT/flutter_flutter/engine          # 这一步不能省,否则直接被 checkEnvironment 拒掉
export PATH="$ROOT/depot_tools:/opt/homebrew/opt/llvm@19/bin:$PATH"
unset LUCI_CONTEXT                        # 见 §3.6 注意事项

./ohos -n config  -t debug profile release
./ohos -n compile -t debug profile release
./ohos -n har     -t debug profile release

3.2 参数表(已核 ohos.py 的 argparse)

参数取值说明
-n / --nameclean | config | har | compile | zip | zip2 | upload | host可多值;不传 -n 也不传 -b 时默认 ["config","compile","host"]
-t / --typedebug | profile | release可多值,外层循环遍历类型,内层遍历名称
-b / --build—与 -n 互斥的另一套入口

几个 name 的实际含义:

  • config → src/flutter/tools/gn --runtime-mode <type> ...,生成 out/ohos_<type>_arm64/(首次会先跑附件同步,见 §3.4);
  • compile → ninja -C src/out/ohos_<type>_arm64;
  • har → 产出 arm64_v8a_<mode>.har / flutter.har / flutter_embedding_<mode>.har;
  • host → 同时构建宿主工具(out/host_<type>[_unopt][_arm64]);
  • zip / zip2 → 打 har 包;
  • upload → 走 auto_compile.py,OBS 时代的老路,本项目不用。

3.3 --llvm18 是禁令,不是建议

./ohos -n config -t release --llvm18     # ✗ 绝对不要

它把 toolchain 指到 DevEco 自带的 clang 15,编 zlib 时就挂。args.gn 里必须落到 Homebrew 的 llvm@19:

# out/ohos_release_arm64/args.gn 中必须出现的一行
custom_toolchain = "/opt/homebrew/opt/llvm@19"

为什么偏偏是 19:第 8 篇 §三讲过,libc++ 有 Clang 18 的硬门槛(下限),而 LLVM 23 太新会引入告警毒化(上限)——19 是两个约束夹出来的可行区间。

3.4 args.gn 的脆弱性:换机必看

args.gn 位于 out/ 下,而 out/ 是未跟踪目录——换机、清 out/ 之后它就没了。更坑的是增量 gn gen 不会补新 args(out/ 已存在时 config 基本是空转)。所以:

  1. 拿到新机或清空 out/ 后:先跑 config,再手工把 custom_toolchain 等必要项写进对应 args.gn,然后才 compile;
  2. 生成的 args.gn 存一份到 sync-data/ 留档,别指望它能从仓库里恢复。

⚠️ 诚实口径:out/ 清空后首次 args.gn 的完整生成链在本机没有实证(本批次机器上 out/ 一直存在,只验过增量路径)。第一次在干净机上做,要把生成结果与原值逐项比对,不要假设 wrapper 会替你写全。

3.5 附件机制:端口源码怎么进树

src/flutter/attachment/scripts/config.json 定义 4 个 type:"files" 任务,把 attachment/ 下的内容复制到未跟踪的第三方树里:

nametarget内容
bootstrap./引擎构建辅助
skia_ohos./src/flutter/third_party/skia/src/ports华为 skia 的 ohos 端口实现
skia_ohos_api./src/flutter/third_party/skia/include/ports对应头文件
jsoncpp./src/third_partyjsoncpp 代理 target

由 ohos_setup.py 执行(config 阶段自动跑,updateCode 里也会跑)。这套机制的存在理由,正是第 8 篇 §五那条被反转的结论:"DEPS 拉来的 checkout 里没有、构建却需要"的文件,靠 attachment 托管,不靠就地改树(端口源码镜像见 fluttertpc_skia)。

⚠️ copy_dir 在目标目录已存在时是 no-op。这意味着升级模式不会覆盖旧的端口源码——端口源码改了、或者新批次要换端口,必须先删掉目标目录再让 attachment 重放。

# 换端口源码后的正确重放姿势
rm -rf src/flutter/third_party/skia/src/ports/ohos \
       src/flutter/third_party/skia/include/ports/ohos
./ohos -n config -t debug profile release   # attachment 重新 copy_dir

这条是本链路最容易复现的运维失败面:改了 attachment/ 里的文件,重新构建,树里还是旧的,而且没有任何提示。

3.6 gen_snapshot 双份,取错即 AOT 直接不可用

每个 out/ohos_<mode>_arm64/ 下有两个同名 gen_snapshot:

路径格式跑在哪用途
out/ohos_<mode>_arm64/clang_arm64/gen_snapshotMach-OMac 宿主AOT 编译时用——这个才是要发布的
out/ohos_<mode>_arm64/gen_snapshotELF / arm64OHOS 设备上设备侧运行时
file -b out/ohos_release_arm64/clang_arm64/gen_snapshot   # Mach-O 64-bit executable arm64 ✓
file -b out/ohos_release_arm64/gen_snapshot               # ELF 64-bit LSB pie executable ✓(但不是要发的)

打包脚本已经内置 file -b 断言(不是 Mach-O 就报错退出),取错会被拦住。手工操作时不要绕过这个断言——把 ELF 当 Mach-O 发出去,症状是用户 flutter build hap --release 在 AOT 阶段炸,且报错信息完全不指向 gen_snapshot。

另外记一条不在坑编号里的:src/flutter/.dart_tool/package_config.json 缺失会让 profile 的 vmservice kernel_blob.bin 失败,补一个 {"configVersion":2,"packages":[]} 即可。LUCI_CONTEXT 是否必要未独立验证(本机是在未设它的状态下走通的)。

四、打包:5 个 zip 的布局不能"顺手统一"

4.1 脚本与产出

bash $ROOT/scripts/package_ohos_engine_artifacts.sh [目标目录]
# 默认输出到 $ROOT/flutter_flutter/engine/src/ohos-engine-artifacts

脚本可重跑、带断言。产出 5 个 zip,布局逐个与 1.0.0 附件核对过:

zip内容
ohos-arm64-artifacts.zipLICENSE.flutter_har_zip.md + flutter_embedding_debug.har + arm64_v8a_debug.har + flutter.har
ohos-arm64-profile-artifacts.zip同上但 _profile
ohos-arm64-release-artifacts.zip同上但 _release
ohos-arm64-profile-darwin-x64.zipLICENSE.gen_snapshot.md + gen_snapshot(宿主 Mach-O)
ohos-arm64-release-darwin-x64.zip同上

4.2 为什么 debug 的名字少了后缀

注意第一行:debug 的 zip 名是 ohos-arm64-artifacts.zip,不带模式后缀,另两个带 -profile / -release。这是 OBS 原始布局的不对称,不要"顺手统一"——覆盖行的键是按 urlPath / basename 精确匹配的(§6.2),统一命名会直接让 debug 档失去覆盖通道。

for z in *.zip; do echo "=== $z"; unzip -l "$z" | sed -n '4,12p'; done
# 逐个核对包内文件名与布局表,尤其是 .har 的模式后缀

4.3 两个 LICENSE 文件从哪来

bin/cache/artifacts/engine/ohos-arm64/LICENSE.flutter_har_zip.md 与 ohos-arm64-profile/darwin-x64/LICENSE.gen_snapshot.md——从 OBS 时代的原始 bundle 里带下来的元数据,重打包时原样沿用。

仓库里没有生成它们的脚本,不要去找。找不到生成器不是 bug,是历史。

4.4 断言点

脚本内置两道断言,任一不满足直接 exit 1,不会产出半成品:

  1. 三个 har 存在;
  2. 宿主 gen_snapshot 存在且是 Mach-O。

五、发布:tag、附件与两类产物

5.1 上传这一环由人通过网页完成

atomgit 的上传环节由人手工在网页上做。本机只有 SSH key,没有 atomgit HTTP 凭据——所以这一环无法、也不应该被自动化。脚本化/API 上传属于未验证项(见 §十)。

5.2 tag 命名惯例

从既有 tag 反推,非官方规范:<上游版本>-ohos-<自版本>,例如 3.47.4-ohos-1.0.0、3.47.4-ohos-1.0.1;候选期另有一种 oh-3.47.4-rc1。新批次沿用 X.Y.Z-ohos-N.M.K 即可,但这是惯例不是铁律。

Release 附件的 URL 形态:

https://atomgit.com/oh-flutter/flutter_flutter/releases/download/<tag>/<附件名>

附件名必须与文件名逐字一致——覆盖行是按名字匹配的。

5.3 铁律:发新版必须换新 tag,不能重传同名附件

这是本篇最重要的一节。覆盖机制靠 <cacheDir>/.ohos-override-url 标记文件比对 URL 判断缓存是否过期:

  • cache.dart 的 isUpToDateInner 拿标记里记的 URL 与当前覆盖行比对,一致就判"最新"、不重下;下载后 updateInner 才写入标记;
  • 因此覆盖同一 URL ⇒ 标记仍匹配 ⇒ 改动传不到任何已装好的 SDK。
✗ 同名 tag 重传附件:URL 不变 → 已装机器标记比对通过 → 永远拿不到新工件(静默)
✓ 换新 tag 发布:    URL 变化 → 标记比对失败 → 自动重新下载 → 用户侧无需任何动作

发布流程必须是四步:新建 tag → 传附件 → 改覆盖行 → 提交推送。只做前三步中的前两步等于没发。

本批次实证:1.0.0 的 flutter_patched_sdk.zip 装错包(§6.6),1.0.1 换新 tag 修掉——如果当时选择"在 1.0.0 上重传同名附件",这个 bug 到今天还在所有已装机器上活着。

5.4 最小可验证动作

第一次用某个 tag 时,先做这个再批量传:

# 新建 tag → 先只传 1 个附件 → 直连验证 → 再传其余(1.0.1 那批是 6 个,当前批次是 10 个)
curl -sI 'https://atomgit.com/oh-flutter/flutter_flutter/releases/download/3.47.4-ohos-1.0.1/ohos-arm64-artifacts.zip' \
  | sed -n '1p;/[Cc]ontent-[Ll]ength/p'
# 期望:HTTP/2 200,且 Content-Length 与本地文件字节数完全一致
stat -f%z ohos-arm64-artifacts.zip

先传一个验证的价值在于:附件名拼错、上传截断、URL 大小写不一致这类问题,在传第 1 个之后发现的成本是传 7 个的七分之一。

5.5 1.0.1 实例:7 个附件

附件大小说明
ohos-arm64-artifacts.zip31.9 MBdebug 引擎 har(含字体端口)
ohos-arm64-profile-artifacts.zip18.0 MBprofile 引擎 har
ohos-arm64-release-artifacts.zip15.5 MBrelease 引擎 har
ohos-arm64-profile-darwin-x64.zip2.6 MBprofile 宿主 gen_snapshot
ohos-arm64-release-darwin-x64.zip2.2 MBrelease 宿主 gen_snapshot
flutter_patched_sdk.zip4.1 MB1.0.1 的关键修复(§6.6)
flutter_patched_sdk_product.zip4.1 MB内容同 1.0.0,为让新 tag 自包含而重传

这一批是纯引擎侧的 5+2:3 个 har + 2 个宿主 gen_snapshot(5 个,出自 §4.1 的打包脚本)+ 2 个 patched_sdk。当时的 dart-sdk-ohos.zip 还挂在 3.47.4-ohos-1.0.0 上没随新 tag 重传,所以覆盖行里那一条必须继续指旧 tag(提交 947610d6e0c1 修的就是这个)。

大小口径说明:本文的"MB"是十进制(字节数 ÷ 1e6),与托管平台网页显示一致;AtomGit API 的 assets[].size 返回字节,换算成 MiB 会小一圈(31,941,046 B = 31.9 MB = 30.5 MiB)。核对时以字节数为准,别拿 MB 去对 Content-Length。

5.6 当前批次 3.47.8-ohos-1.0.0:11 个自传附件(网页显示 15)

发布线走到 3.47.8,附件从 7 个长到 11 个——多出来的正是 4 个 Dart SDK 包(§5.8):

组附件字节数MiB是什么
Dart SDKdart-sdk-darwin-arm64-ohos.zip225,700,875215.2macOS(arm64) 宿主的定制 Dart SDK
Dart SDKdart-sdk-windows-x64-ohos.zip216,595,889206.6Windows(x64) 宿主的定制 Dart SDK
Dart SDKdart-sdk-ohos.zip8,908,9108.5HAP 构建运行时(macOS / Linux 用)
Dart SDKdart-sdk-ohos-windows-x64.zip8,561,7228.2HAP 构建运行时(Windows 用,PE 二进制)
引擎 harohos-arm64-artifacts.zip31,941,04630.5debug 引擎 har(含字体端口)
引擎 harohos-arm64-profile-artifacts.zip18,040,53917.2profile 引擎 har
引擎 harohos-arm64-release-artifacts.zip15,485,64114.8release 引擎 har
宿主 AOTohos-arm64-profile-darwin-x64.zip2,574,5322.5profile 档 gen_snapshot(Mach-O)
宿主 AOTohos-arm64-release-darwin-x64.zip2,164,9482.1release 档 gen_snapshot(Mach-O)
patched_sdkflutter_patched_sdk.zip4,109,4603.9非产品档平台 dill(P29a 修的就是它)
patched_sdkflutter_patched_sdk_product.zip4,109,4633.9产品档平台 dill

为什么网页上写 15:托管平台会为每个 Release 自动生成 4 个源码包(.zip / .tar.gz / .tar.bz2 / .tar,API 里 size 为 null),和 11 个自传附件一起计数。核对覆盖行时只看这 11 个,15 是页面计数,不是工件数。

5.7 附件数演进:1 → 8 → 7 → 13 → 11

把每个 tag 的自传附件数(不含自动源码包)排出来,能直接读出发布策略的变化:

tag发布时刻自传附件关键变化
oh-3.47.4-rc12026-09-14 10:161只挂 dart-sdk-darwin-arm64-ohos.zip,引擎工件仍指 OBS
3.47.4-ohos-1.0.009-14 11:318首次成组:5 个引擎工件 + 2 个 patched_sdk + dart-sdk-ohos.zip
3.47.4-ohos-1.0.109-14 23:417P29a 修复(§6.6);dart-sdk-ohos.zip 内容未变没重传 → 覆盖行留旧 tag
3.47.4-ohos-1.0.209-15 08:477内容逐字节未变,随新 tag 重挂载,只为让 flutter doctor -v 报出的版本号配套
3.47.4-ohos-1.0.309-15 17:578只重建了 release 档,debug/profile 仍是 1.0.1 的旧二进制(黑屏未修)
3.47.4-ohos-1.0.409-16 00:2313首次出现 Windows 工件:两个 Windows Dart SDK + 两个 ohos-arm64-{profile,release}-windows-x64.zip;9 行覆盖全部并入本 tag
3.47.5-ohos-1.0.009-23 23:5711Dart 升到 3.13.4(kernel 仍 138);两个 Windows gen_snapshot 没随批次重传 → 宿主 AOT 回到 darwin 两档
3.47.7-ohos-1.0.010-13 20:0911覆盖行首次加两个 Windows Dart SDK 键,从 9 行长到 11 行
3.47.7-ohos-1.0.110-21 15:5611纯框架批次(第 4 篇自动签名);11 个附件逐字节重挂载
3.47.8-ohos-1.0.010-24 20:5911纯框架批次;engine.ohos.version 与 Dart SDK 均未变,逐字节重挂载

两条可直接抄去用的经验:

  1. 附件数和覆盖行数必须一起核对。1.0.4 已经把 Windows 的两个 gen_snapshot 传上去了,但覆盖文件里当时还没有对应的键——上传了 ≠ 用户拿得到,反之键留着而附件没重传就是 404。发布前把两边的清单做一次 comm(§6.9 给了命令)。
  2. "逐字节相同也重挂载"是有意的:1.0.2 / 3.47.7-1.0.1 / 3.47.8-1.0.0 三个批次的内容都没变,重挂载只为了让 flutter doctor -v 显示的版本号与实际拉到的工件一一对应。这不是浪费带宽,是把"版本号 ↔ 工件"这条映射做成可核对的。

5.8 Dart SDK 是第二类自编自发的工件:mac 与 Windows 各一份

前面 §4.1 的打包脚本只产 5 个引擎 zip。另外 4 个 Dart SDK 包不在那条链路上,它们是第 6 篇"Dart 必须从源码构建"这件事在发布侧的延续——编一次,要发好几份。

为什么官方 Dart SDK 不能用(bin/internal/dart-sdk-url.ohos 文件头原文):

官方 dart-sdk 的 dart:ffi 缺少 Abi.ohosArm / ohosArm64 / ohosX64,编译
fluttertpc_dart_native(code_assets / hooks_runner 的 ohos 补丁)时必然报
"Member not found: 'ohosArm'"。

第 6 篇已经实证过:给 bin/cache/dart-sdk 打补丁是零影响的(JIT 二进制内嵌 platform kernel)。所以定制版只能"基于官方 Dart + ohos 补丁按目标架构重新构建",而且注释里写得很硬:二进制全量重建,跨架构不可复用。这一句就是"要发好几份"的根因——每支持一个宿主平台,就多一整套要编要发的产物。

四份产物分别是什么、给谁用:

覆盖键(zip 名)落地位置内容谁读它
dart-sdk-darwin-arm64.zipbin/cache/dart-sdkmacOS arm64 宿主的完整 Dart SDK(dart / dartanalyzer / snapshots)update_dart_sdk.sh
dart-sdk-windows-x64.zipbin/cache/dart-sdkWindows x64 宿主的完整 Dart SDKupdate_dart_sdk.ps1
dart-sdk-ohos.zipbin/cache/dart-sdk-ohosHAP 构建专用运行时:dartaotruntime(Mach-O)+ frontend_server_aot.dart.snapshot(18,620,752 B),kernel 138update_dart_sdk.sh
dart-sdk-ohos-windows-x64.zipbin/cache/dart-sdk-ohos同上,但 dartaotruntime.exe 是 PE32+,snapshot 17,158,320 Bupdate_dart_sdk.ps1

这里有两个实测出来的硬约束,任何一条违反都会在构建期炸:

# 1) 运行时是宿主二进制,macOS 那份拿到 Windows 上用不了
file -b x_dart-sdk-ohos/dart-sdk-ohos/bin/dartaotruntime
#   Mach-O 64-bit executable arm64
file -b x_dart-sdk-ohos-windows-x64/dart-sdk-ohos/bin/dartaotruntime.exe
#   PE32+ executable (console) x86-64, for MS Windows

# 2) 两份 frontend_server snapshot 大小不同(18,620,752 vs 17,158,320),
#    说明它们是各自主机上重新产出的产物,不能跨平台复制粘贴

所以 update_dart_sdk.ps1 在解压后专门校验 PE magic,不匹配就告警:

# bin/internal/update_dart_sdk.ps1
Write-Host "WARNING: dart-sdk-ohos/bin/dartaotruntime.exe is not a Windows PE; `
  dart-sdk-ohos-windows-x64.zip may be built for another platform."

键名怎么拼出来的:.sh 与 .ps1 先按宿主算出 DART_ZIP_NAME(Darwin → dart-sdk-darwin-${ARCH}.zip,Linux → dart-sdk-linux-${ARCH}.zip,MINGW*/MSYS* → dart-sdk-windows-x64.zip),主 SDK 用它去查覆盖行;HAP 运行时则拼 "dart-sdk-ohos-$DART_ZIP_NAME",于是 Windows 上要找的键就是 dart-sdk-ohos-windows-x64.zip。两个脚本必须保持同一套逻辑(.sh 文件头有明确 NOTE 提醒),改一边不改另一边是这类双实现最常见的失配。

没配键会怎样:覆盖行未命中时脚本不会失败,而是打印告警后回退官方源——

WARNING: OHOS customized Dart SDK (dart-sdk-linux-x64.zip) not published;
falling back to official SDK (build may fail on Abi.ohos* members).

这正是 Linux 宿主的现状:覆盖文件里 11 行没有任何 linux 键,Release 里也没有 Linux 的 Dart SDK 附件,所以 Linux 上 bootstrap 会装上官方 Dart SDK,直到编译 fluttertpc_dart_native 时才报 Member not found: 'ohosArm'。Windows 则是先补脚本、后补键:宿主 bootstrap 的改造是提交 18e36518663f(fix: Windows 宿主 dart-sdk bootstrap(补齐 OHOS 定制 SDK 与 dart-sdk-ohos))与合并提交 77ae67d1b007,而覆盖行里那两条 Windows 键直到 3.47.7-ohos-1.0.0 批次才加上。

一句话收口:"适配鸿蒙"要编的不只是引擎,Dart SDK 本身也是产物;它是"每个宿主架构一份"的,发布清单里必须和引擎工件一起数。

六、分发:覆盖行机制

6.1 覆盖行长什么样

bin/internal/dart-sdk-url.ohos 一个文件承担两件事(update_dart_sdk.sh 与 cache.dart 共用):

当前形态(3.47.8-ohos-1.0.0,11 条键,… 代表 https://atomgit.com/oh-flutter/flutter_flutter/releases/download):

# ① Dart SDK(update_dart_sdk.sh / .ps1 读,键 = 按宿主算出的 DART_ZIP_NAME)
dart-sdk-darwin-arm64.zip:…/3.47.8-ohos-1.0.0/dart-sdk-darwin-arm64-ohos.zip
dart-sdk-windows-x64.zip:…/3.47.8-ohos-1.0.0/dart-sdk-windows-x64-ohos.zip
dart-sdk-ohos.zip:…/3.47.8-ohos-1.0.0/dart-sdk-ohos.zip
dart-sdk-ohos-windows-x64.zip:…/3.47.8-ohos-1.0.0/dart-sdk-ohos-windows-x64.zip

# ② 引擎工件(cache.dart 的 _ohosArtifactOverrideUrl 读,键 = cache 里的 urlPath,也接受文件名)
ohos-arm64/artifacts.zip:…/3.47.8-ohos-1.0.0/ohos-arm64-artifacts.zip
ohos-arm64-profile/artifacts.zip:…/3.47.8-ohos-1.0.0/ohos-arm64-profile-artifacts.zip
ohos-arm64-release/artifacts.zip:…/3.47.8-ohos-1.0.0/ohos-arm64-release-artifacts.zip
ohos-arm64-profile/darwin-x64.zip:…/3.47.8-ohos-1.0.0/ohos-arm64-profile-darwin-x64.zip
ohos-arm64-release/darwin-x64.zip:…/3.47.8-ohos-1.0.0/ohos-arm64-release-darwin-x64.zip
flutter_patched_sdk.zip:…/3.47.8-ohos-1.0.0/flutter_patched_sdk.zip
flutter_patched_sdk_product.zip:…/3.47.8-ohos-1.0.0/flutter_patched_sdk_product.zip

对着这 11 行能读出一条命名约定:引擎侧的键是 cache 里的 urlPath(带 /),Release 上的附件名是把那个 / 换成 -(ohos-arm64-profile/artifacts.zip → ohos-arm64-profile-artifacts.zip);Dart SDK 侧的键与附件名只差一个 -ohos 后缀(dart-sdk-darwin-arm64.zip → dart-sdk-darwin-arm64-ohos.zip)。这不是代码要求,是 11 条键与 11 个附件名逐条对照出来的惯例——但既然是惯例就得沿用,因为 §5.2 说过覆盖行是按名字精确匹配的。

历史形态更值得看(3.47.4-ohos-1.0.1 时只有 9 条,且三个 tag 混用):

dart-sdk-darwin-arm64.zip:…/oh-3.47.4-rc1/dart-sdk-darwin-arm64-ohos.zip
dart-sdk-ohos.zip:…/3.47.4-ohos-1.0.0/dart-sdk-ohos.zip
ohos-arm64/artifacts.zip:…/3.47.4-ohos-1.0.1/ohos-arm64-artifacts.zip
# …其余 6 条同样指 1.0.1

这不是随意的:1.0.1 那次只重传了 7 个引擎侧附件,两个 Dart SDK 包没动,所以它们的键必须继续留在 rc1 与 1.0.0 上(提交 947610d6e0c1 就是修这个错指)。直到 1.0.4 才做到"全部 9 条指同一个 Release"(覆盖行注释原文:“使全部 9 个附件指向同一个 Release,不再混用 oh-3.47.4-rc1 与 3.47.4-ohos-1.0.3”)。

一条容易忽略的判据差异:Dart SDK 这四条与引擎工件的过期逻辑相反。引擎工件改 URL 会让已装机器重下(§5.3);Dart SDK 走的是 update_dart_sdk.sh / .ps1,改 URL 不会让已装副本重下,只影响全新安装(主 SDK 看 ENGINE_VERSION 比对,运行时看"文件在不在")。覆盖行注释专门用粗体提醒了这一点。

6.2 键的匹配规则与一个历史 bug

_ohosArtifactOverrideUrl 按 urlPath 或 basename 匹配。历史 bug(1.0.0 之前,已修):旧代码用 urlPath.startsWith('flutter_patched_sdk') 判断,而实际 urlPath 是 common/flutter_patched_sdk.zip——永不命中,覆盖形同虚设。

这个 bug 的教训值得单独记:覆盖机制"配了"和"生效了"是两件事,必须用行为验证(换新 tag 后看是否真的重下),不能只读文件内容。

6.3 ⚠️ 四类工件的行为都不一样

这是整条链路最容易误判的地方,也是所有"我改了怎么没生效"提问的标准答案:

工件走哪条通道改覆盖行会重下吗强制刷新
三个 har、两个 gen_snapshot、两个 patched_sdk各 artifact 类的 getBinaryDirs() → 覆盖机制生效✅ 会(换新 tag 即触发)—
sky_engine / flutter_gpuFlutterSdk.getPackageDirs() → pkg 循环,完全不查覆盖行❌ 不会,且没有覆盖通道只能改 OBS 默认址或自建分发
dart-sdk-ohosupdate_dart_sdk.sh❌ 不会(“缺了才下载”)rm -rf bin/cache/dart-sdk-ohos
主 dart-sdkENGINE_VERSION 版本比对❌ 不会(URL 变但版本没变)删 bin/cache/dart-sdk

6.4 通道分界的源码证据

不是猜的,是核过 cache.dart 的:

// packages/flutter_tools/lib/src/cache.dart
// _ohosArtifactOverrideUrl 只在 getBinaryDirs() 的两处被调用:
//   isUpToDateInner  (:1072)
//   updateInner      (:1123)
// getPackageDirs() 的循环(:1057 / :1099)从不调用它。

而 FlutterSdk 这一个类同时实现了两条通道:

  1. getPackageDirs() = ['sky_engine', 'flutter_gpu'];
  2. getBinaryDirs() = ['common', 'flutter_patched_sdk.zip', 'common', 'flutter_patched_sdk_product.zip']。

这正是"patched_sdk 能覆盖、sky_engine 不能"的原因——同一个类,两条通道,只有一条查覆盖行。

6.5 sky_engine 为什么长期未修

pkg 循环里 sky_engine 被特判成 $ohosStorageBaseUrl/.../$ohosEngineVersion/sky_engine.zip,即 OBS 上 3.44.9 时代的版本,无法通过覆盖行改指。

它长期未修的原因是影响面不阻塞构建:sky_engine 只服务 analyzer(flutter analyze 用的 dart:ui 镜像)。但若将来新引擎改了 dart:ui 接口,这条链路传不出去——这是本机制上已知的、有记录的缺口(对应待办"补 sky_engine 覆盖")。

dart-sdk-ohos 的判据在 bin/internal/update_dart_sdk.sh:218:

[ -n "$OHOS_RUNTIME_URL" ] && [ ! -x "$FLUTTER_ROOT/bin/cache/dart-sdk-ohos/bin/dartaotruntime" ]

行为与引擎工件完全相反:只看"文件在不在",不看 URL。3.47.4 批次没撞上它,只因为该目录当时天然不存在(首次 bootstrap 补齐)。换 Dart 版本的批次必然踩——改 URL 不会让任何已装 SDK 重下它。

6.6 patched_sdk:为什么它也在覆盖行里

上游的 flutter_patched_sdk.zip 不含鸿蒙的 Platform.isOhos / Abi.ohosArm(实测 ohosArm = 0),编译鸿蒙应用必报 Member not found,所以这两个条目必须覆盖。

1.0.0 的历史 bug(P29a):flutter_patched_sdk.zip 包内 flutter_patched_sdk/platform_strong.dill 装的其实是产品档 dill,导致 profile 模式一启动就 FATAL:

Dart Error: Type '_NetworkProfiling' not found in library 'dart.io'
→ FATAL:flutter/lib/io/dart_io.cc(38)] Check failed: !CheckAndHandleError(setup_result)

根因:非 PRODUCT 引擎会去查 _NetworkProfiling,而该类型靠 @pragma('vm:entry-point', !dart.vm.product) 在产品 dill 里已被树摇掉。

教训:非 PRODUCT 引擎 + 非产品平台 dill 是必须同时对上的两件事;debug 不触发不代表 profile 不触发。

1.0.1 已修,修复后的量化证据:libapp.so = 6,685,576 B,_NetworkProfiling 符号计数 0 → 2。

6.7 P29b:两个 entry 共用一个 marker(未根治)

flutter_patched_sdk.zip 与 flutter_patched_sdk_product.zip 解压到同一个 artifact 目录 engine/common/,但标记文件只有一个,两者互相覆盖 ⇒ 每次构建至少有一个被判过期而重下。

对正常流程无害(发布物是对的,重下装回来的就是对的),但有一条推论必须记住:任何"直接改 cache 里文件"的手工热修都会被下一次构建抹掉——验证这类改动必须改发布物。根治要给两个 entry 各配一个标记文件,属 cache.dart 的独立改动,本批未做。

6.8 用户侧怎么拿到更新

fvm 侧的事实(本机 fvm 4.3.1 实测)。自定义 fork 源存在 ~/Library/Application Support/fvm/.fvmrc 里,注意这个文件是 JSON 不是 YAML,本系列用的那条别名节选如下:

{
  "forks": [
    { "name": "nutpi", "url": "https://atomgit.com/oh-flutter/flutter_flutter.git" }
  ]
}

版本引用形如 nutpi/3.47.8-ohos-1.0.0(<别名>/<tag>)。项目级引用有两处:根目录 .fvmrc 写 {"flutter": "nutpi/3.47.8-ohos-1.0.0"},.fvm/fvm_config.json 写 {"flutterSdkVersion": "nutpi/3.47.8-ohos-1.0.0"},.fvm/flutter_sdk 是指向 ~/fvm/versions/<别名>/<版本> 的软链。

分发链路可以一条命令验证:全新执行 fvm install <别名>/<新 tag>,日志里就会出现覆盖行指向的 Release 附件地址(第 1 篇 5.2 节的真实输出):

OHOS: using customized Dart SDK (dart-sdk-darwin-arm64.zip) from
  https://atomgit.com/oh-flutter/flutter_flutter/releases/download/3.47.8-ohos-1.0.0/dart-sdk-darwin-arm64-ohos.zip

也就是说:换新 tag + 改覆盖行之后,用户侧什么都不用做——fvm install 新 tag 或已装 SDK 的下一次构建,都会从新地址把工件拉下来。

⚠️ fvm 装出来的副本在 detached HEAD 上(无当前分支),git pull 会失败——别误读成"传播机制没生效"。正确取法:

git -C ~/fvm/versions/nutpi/3.47.8-ohos-1.0.0 fetch origin
git -C ~/fvm/versions/nutpi/3.47.8-ohos-1.0.0 merge --ff-only origin/oh-3.47-dev
# 快进 detached HEAD,不动 fvm 布局

该副本的 origin = https://atomgit.com/oh-flutter/flutter_flutter.git,与 fork nutpi 一致。它是独立目录,有自己的 bin/cache,不是软链——所以覆盖行的改动要传播到它,必须先让它 merge 到新的覆盖行,再靠 flutter build hap 触发重下。

另外记一条:浅克隆没有 tag 会报 0.0.0-unknown 导致 pub 解析失败;真实 fvm 安装带 tag,不受影响(消费侧 CP9 的成因之一)。

用户侧全流程(零手工步骤):

  1. fvm install nutpi/<tag>;
  2. flutter build hap;
  3. bootstrap 自动下载 dart-sdk 与 dart-sdk-ohos;
  4. 首次构建时按覆盖行下载引擎 har 与 patched_sdk。

全新克隆端到端已验证:debug / profile / release 三种 flutter build hap --no-codesign 全过,产物 libflutter.so = 3.13.3。

6.9 反向清单:发布新批次到底要改哪几行、哪几个 URL

把前面所有机制收敛成一张"发布时的动作表"。要改的文件只有一个:bin/internal/dart-sdk-url.ohos;三个 *.version 锁文件保持不动(§2.2)。

#覆盖键目标附件谁读这行改了会不会立刻传播
1dart-sdk-darwin-arm64.zipdart-sdk-darwin-arm64-ohos.zipupdate_dart_sdk.sh❌ 只影响全新安装
2dart-sdk-windows-x64.zipdart-sdk-windows-x64-ohos.zipupdate_dart_sdk.ps1❌ 同上
3dart-sdk-ohos.zipdart-sdk-ohos.zipupdate_dart_sdk.sh❌ 只认"文件在不在",要 rm -rf bin/cache/dart-sdk-ohos
4dart-sdk-ohos-windows-x64.zipdart-sdk-ohos-windows-x64.zipupdate_dart_sdk.ps1❌ 同上(判据是 dartaotruntime.exe)
5ohos-arm64/artifacts.zipohos-arm64-artifacts.zipcache.dart✅ 换新 tag 即触发重下
6ohos-arm64-profile/artifacts.zipohos-arm64-profile-artifacts.zipcache.dart✅
7ohos-arm64-release/artifacts.zipohos-arm64-release-artifacts.zipcache.dart✅
8ohos-arm64-profile/darwin-x64.zipohos-arm64-profile-darwin-x64.zipcache.dart✅
9ohos-arm64-release/darwin-x64.zipohos-arm64-release-darwin-x64.zipcache.dart✅
10flutter_patched_sdk.zipflutter_patched_sdk.zipcache.dart✅(但每次构建至少重下一份,§6.7)
11flutter_patched_sdk_product.zipflutter_patched_sdk_product.zipcache.dart✅
—sky_engine / flutter_gpu没有覆盖通道FlutterSdk.getPackageDirs()❌ 只能改 OBS 默认址(§6.5)

两种 URL 形态与可覆写的基址:

# A. 自建分发(实际生效的通道)
https://atomgit.com/oh-flutter/flutter_flutter/releases/download/<tag>/<附件名>

# B. 默认回退址(OBS,3.44.9 时代产物,dart 3.12.2 / kernel 130)
{base}/flutter_infra_release/flutter/{engine.ohos.version}/{pkg}.zip
base 默认 https://flutter-ohos.obs.cn-south-1.myhuaweicloud.com
可用环境变量覆写:FLUTTER_OHOS_STORAGE_BASE_URL(鸿蒙侧) / FLUTTER_STORAGE_BASE_URL(官方侧)
非法 URI 会被 cache.dart 直接 throwToolExit 拦下,不会静默用错地址。

发布前后各跑一条命令,把"键写了但附件没传"和"附件传了但键没写"两类错都堵掉:

# 逐条 HEAD 覆盖行里的 URL:404 = 名字打错或该 tag 下没传这个附件
grep -E '^[a-z0-9_.-]+:https' bin/internal/dart-sdk-url.ohos | cut -d: -f2- |
  while read -r u; do
    printf '%s  %s\n' "$(curl -sS -o /dev/null -w '%{http_code}' -IL --max-time 20 "$u")" "${u##*/}"
  done

(比对口径用完整 URL,不要用文件名:三个 har 的键 basename 都是 artifacts.zip,用 basename 去 comm 会误报。)

三条最容易漏的:

  1. 没重传的附件,它的键要留在旧 tag——别"顺手统一"成新 tag,那是 404(§6.1 的 1.0.1 教训);
  2. Dart SDK 那四条改了也不会让老安装更新,验证只能全新克隆或删 bin/cache/dart-sdk{,-ohos};
  3. 宿主矩阵别只写 mac:Windows 的两个键(第 2、4 行)是 3.47.7 批次才补的,Linux 至今没有键——file -b 与 PE 校验(§5.8)说明这三档不能共用一份产物。

七、发布前回归验收清单

7.1 十一条,按执行顺序逐条过

[ ] 1 构建:三档 out/ohos_<mode>_arm64/ 齐全;ninja 0 FAILED
[ ] 2 字体端口在位(别跳过,丢了编译不报错)——按第 10 篇流程三档都查
[ ] 3 宿主 gen_snapshot 是 Mach-O:file -b out/ohos_<mode>_arm64/clang_arm64/gen_snapshot
[ ] 4 打包:package_ohos_engine_artifacts.sh 无断言失败,5 个 zip 的 unzip -l 布局与 §4.1 逐条一致;
    Dart SDK 4 个包按宿主平台齐备(macOS + Windows,键名与 §5.8 一致)
[ ] 5 新 tag:已建,且不是在旧 tag 上重传同名附件
[ ] 6 先传 1 个附件 → curl -I 直连验证(200 + Content-Length 与实际一致)→ 再传其余
[ ] 7 覆盖行:11 行逐条改成新 tag(内容未变、没重传的附件那几行**保留旧 tag**)
[ ] 7b 逐条 HEAD 覆盖行 URL,确认 11 条全 200(§6.9 的命令)
[ ] 8 提交推送:覆盖行改动推到发布分支(本批次为 hxa)
[ ] 9 本机传播验证:flutter build hap 观察是否触发重下;再对 fvm 副本跑一遍 §6.8 的 fetch+merge
[ ] 10 本机清缓存端到端:rm -rf bin/cache 后重跑三档 flutter build hap --no-codesign
[ ] 11 设备侧:三档 HAP 渲染正常(重点看文字与图标)

7.2 三条最容易跳过的

第 7 条用 grep 而不是行号定位(行号会随文件变动):

grep -n '3\.47\.4-ohos-1\.0\.0' bin/internal/dart-sdk-url.ohos
# 期望:无输出。有任何输出 = 还有旧 tag 残留 = 该工件不会更新

三条最容易被"我本地都好了"心态跳过的:

  • 第 2 条(字体端口):丢了编译不报错,只有跑起来看画面才发现全屏无文字——详见第 10 篇;
  • 第 6 条(先传一个验证):跳过它的代价是全部附件传完才发现名字不对;
  • 第 9 条(fvm 副本传播):本机仓库和 fvm 副本是两个独立 cache,本机过了不代表 fvm 那边过。

八、故障索引

现象根因 → 出路
Unexpected Kernel Format Version 130 (expected 138)覆盖行没生效或没拉到 → §1.3 / §6.1;先确认 git log -1 -- bin/internal/dart-sdk-url.ohos
改了覆盖行、已有安装没反应重传了同名附件(URL 未变)→ §5.3,必须换新 tag
换 Dart 版本后 dart-sdk-ohos 还是旧的它是"缺了才下载" → §6.5,rm -rf bin/cache/dart-sdk-ohos
每次构建都在重下 patched_sdkP29b 标记碰撞 → §6.7,正常现象,不是故障
手工改 cache 里的 dill 下次构建失效同上:重下会覆盖 → 改发布物,别热修
profile 一启动就 _NetworkProfiling FATALP29a:patched_sdk 装成产品档 dill → §6.6
通了 build/ohos/ 但 AOT 没重建P29c:AOT 快照缓存在 .dart_tool/flutter_build/
AOT 产物在设备上不能用取错了 gen_snapshot(ELF 当 Mach-O)→ §3.6
端口源码改了但树里还是旧的copy_dir 对已存在目录是 no-op → §3.5,先删目标目录
gn gen 编不过 zlib--llvm18 指到 clang 15 → §3.3,换 llvm@19
fvm 副本 git pull 失败detached HEAD → §6.8,用 fetch + merge --ff-only
Windows 上 HAP 构建报"dartaotruntime.exe is not a Windows PE"覆盖行指到了 macOS 的 dart-sdk-ohos.zip → §5.8,Windows 要用 dart-sdk-ohos-windows-x64.zip
Linux 宿主编 fluttertpc_dart_native 报 Member not found: 'ohosArm'覆盖文件没有 Linux 键,bootstrap 回退官方 Dart SDK 并告警 → §5.8
换了 Dart 版本但 bin/cache/dart-sdk-ohos 还是旧的它的判据是"文件在不在",不看 URL → §6.3 / §6.9,rm -rf bin/cache/dart-sdk-ohos
用户装完报 Abi.ohosArm Member not founddart-sdk 或 patched_sdk 覆盖行没生效 → §6.1 / §6.6
flutter analyze 的 dart:ui 与运行期不一致sky_engine 无覆盖通道 → §6.5,已知缺口

九、常见问题 FAQ

9.1 我改了引擎源码,重传同名附件行不行?

不行,而且这是最危险的做法。URL 没变 ⇒ 已装机器的 .ohos-override-url 标记仍然匹配 ⇒ 判定"已最新"不重下 ⇒ 改动传不出去,且不报任何错。必须新建 tag。

9.2 发布要改 engine.ohos.version 吗?

**不要。**三个 *.version 锁文件只在同步上游时动。重发工件的唯一推进信号是覆盖行 URL 变了(§2.2)。

9.3 为什么我改了覆盖行,sky_engine 没跟着更新?

因为它走 getPackageDirs() 的 pkg 循环,那段代码从不调用 _ohosArtifactOverrideUrl(§6.4)。当前它只影响 analyzer,不阻塞构建。

9.4 每次构建都重下 flutter_patched_sdk,是我配错了吗?

不是,是 P29b 的已知未根治问题:两个 entry 共用一个标记文件(§6.7)。对正确性无影响,只是浪费一点下载。

9.5 能不能把上传也自动化?

本批次没有验证过脚本化/API 上传路径。本机只有 SSH key,无 atomgit HTTP 凭据。想做的话请先在自己的仓库上验证,并把结论补进 §十。

9.6 Linux / Windows 宿主的工件在哪?

这个问题要分两类产物回答,口径不一样:

产物macOSWindowsLinux
宿主 AOT gen_snapshot✅ 两档(profile / release)❌ 1.0.4 传过,3.47.5 起未再随批次发布❌ 从未发布
定制 Dart SDK(bin/cache/dart-sdk)✅✅❌ 无键、无附件
HAP 运行时 dart-sdk-ohos✅✅(独立 PE 包)❌

没有覆盖键的宿主不会报错,而是回退 OBS 默认址。实测该址上 ohos-arm64-profile/windows-x64.zip 与 ohos-arm64-release/linux-x64.zip 都是 200,包内分别是 gen_snapshot.exe(PE32+)与 ELF 二进制,但 strings 出的版本是:

3.12.2 (stable) (Tue Aug 11 09:49:18 2026 +0800) on "windows_simarm64"
3.12.2 (stable) (Tue Aug 11 09:49:18 2026 +0800) on "linux_simarm64"

——3.44.9 时代、kernel 130 的产物,与本分支的 3.13.4(kernel 138)不配套,症状就是 §1.3 那条 Unexpected Kernel Format Version 130 (expected 138)。

结论:Windows 上装 Dart SDK 与 HAP 运行时已经零操作,但 Windows / Linux 宿主还拿不到配套的 AOT 产物。这是明确的未做项,不是"某次发布漏传了几个包"。

十、未验证项与诚实口径

以下不要当成已确认的步骤:

  • 上传环节由人通过网页完成,本机无 atomgit HTTP 凭据;脚本化/API 上传路径完全未验证;
  • tag 命名规范是从既有 tag 反推的惯例,非官方约定;
  • out/ 清空后首次 args.gn 的完整生成链无实证(本机只验过增量路径);
  • package_ohos_engine_artifacts.sh 的目录名与 LICENSE 源路径是 3.47.4 批次写死的,新批次需复核;
  • --ohos_api_int 取值存在矛盾(ohos.py 默认 13 且注释标废弃,args.gn 里是 13,实测整链按 API 24 走)——未定论;
  • 宿主矩阵只验证了 macOS:Windows 与 Linux 上既没有配套的宿主 AOT 产物,update_dart_sdk.ps1 的行为也只做过源码级核对(本机是 macOS arm64,Windows 端到端未实跑)——键与附件都存在,但"在 Windows 机器上从零装一遍"这一步没有证据。

另有一处跨文档勘误要说明:早期资料把发布动作写成"上传 OBS、更新 engine.ohos.version",那是 OBS 时代的链路。本条链路上这两个动作都不需要——OBS 现仅作默认回退地址,版本锁文件只在同步上游时才会动。

十一、本篇小结与系列导航

本篇走完 M4(发布分发)阶段:

  • 版本锁:三套版本源分工清楚,重发工件不动锁文件,推进信号是覆盖行 URL;
  • 构建:engine/ohos wrapper 是唯一规范入口,--llvm18 是禁令,args.gn 在未跟踪的 out/ 里、换机即丢;
  • 附件机制:4 个 files 任务把端口源码复制进第三方树,copy_dir 对已存在目录是 no-op——换端口先删目标;
  • 两类来源、11 个附件:引擎侧 5 个 zip 出自打包脚本(3 个 har + 2 个宿主 gen_snapshot),加上 2 个平台 dill(flutter_patched_sdk{,_product})与 Dart SDK 侧 4 个包(macOS / Windows 各一套),凑成当前 Release 的 11 个自传附件;Linux 宿主从未发布;
  • 打包:5 个 zip 布局不对称(debug 不带后缀),不要顺手统一;两个 LICENSE 是 OBS 遗产;
  • 发布:铁律是换内容必须换新 tag,同名重传对已装机器静默无效;
  • 分发:覆盖行一个文件两用,四类工件过期判据各不相同,patched_sdk 的 P29a/P29b 是两个真实事故;
  • 收口:11 条发布前验收清单 + 13 行故障索引。

「鸿蒙Flutter框架 0day适配实战」全系列 10 篇,本篇为第 9 篇(生产侧 M4 发布分发):

#篇目侧一句话主题
01背景、分叉仓库、版本命名与 fvm fork 安装消费三层架构、三个接缝、七层工作量、fvm 直装 tag、版本墙
02tag 版本的手动安装与三层核验消费浅克隆 bootstrap、首次初始化、SSH 修复、异常分诊
03鸿蒙模拟器/真机跑 Flutter + 消费侧踩坑集消费工具链三件套、HAP 构建/安装/运行、占位签名、截图取证、CP1-CP14
04flutter build hap 自动签名消费P12/CSR、AGC 证书与 Profile、纯 Dart AES-GCM、C0-C10 复用
05上游同步总览、BC 全景与端到端 SOP生产批次解剖、五阶段验证门、12 项适配表、端到端 SOP 与 P1-P29 索引
06Phase 1+2:基线建立与 Framework 适配生产代理 HTTP/1.1 修障、421 文件重打、Dart SDK 源码构建
07Phase 3 Engine:三方重放与冲突裁决生产826 文件、15 真冲突两分法、契约对齐 16 项
08引擎构建链攻防生产依赖树、工具链版本、musl 交叉链接、测试面漂移
09构建发布与产物分发(本篇)生产版本锁、11 个工件(含 Dart SDK 双宿主)、11 条覆盖键、换新 tag 铁律
10验证与真机验收生产三档引擎、设备侧单测、字体图标静默故障

总结

这条链路在 3.47.4 批次已经闭环:从改引擎源码到用户零操作安装,1.0.0 → 1.0.1 完整走通一遍,1.0.0 暴露的 patched_sdk 产品档 dill bug 在 1.0.1 修正并实测。

六条要记住的:

  1. 规范化构建入口是 engine/ohos → ohos.py,不是手搓 gn/ninja;args.gn 在未跟踪的 out/ 里,换机即丢;
  2. 要发的是 11 个附件、两类来源:引擎 5 个 zip(3 har + 2 宿主 gen_snapshot)+ 2 个平台 dill + Dart SDK 4 个包(bin/cache/dart-sdk 与 bin/cache/dart-sdk-ohos),后者按宿主架构各编一份,Linux 至今没有;
  3. 发布时唯一要改的文件是 bin/internal/dart-sdk-url.ohos 的 11 条键,三个 *.version 不动;改完逐条 HEAD 验证(§6.9);
  4. 发新版必须换新 tag——同名重传对已装机器静默无效;
  5. 四类工件的过期判据不一样:引擎工件看 URL 标记,dart-sdk-ohos 只看"文件在不在",sky_engine 根本没有覆盖通道;
  6. 发布前必须单独查字体端口——它丢了编译不报错(详见第 10 篇)。

到这里,"编出来"和"发出去"都成立了。下一篇(第 10 篇)是这个系列的收口:验证与真机验收——四道质量闸、设备侧单测 1723/1723 的真实跑法、三档引擎全部落地时的两个装错包事故,以及那个把整条链路逼出"发布前必查字体"制度的静默故障。

如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!


相关资源:

环境与版本

本文的示例与截图基于以下环境组合(2026-10 实测):

组件版本说明
上游 Flutter SDK3.47.8github.com/flutter/flutter
Flutter SDK(OH 适配版)3.47.8-ohos-1.0.0oh-flutter/flutter_flutter,oh-3.47-dev 分支
DevEco Studio26.0.0 Release提供 hvigor / ohpm / hdc / JBR 等工具链
OpenHarmony SDK / APIAPI 26(26.0.0)模拟器与 SDK 均为 API 26
运行设备OpenHarmony 模拟器API 26,1280×2832

环境搭建(分叉获取、DevEco Studio 与 SDK 配置、模拟器创建)请按官方指导操作:OpenHarmony-flutter 环境搭建指导。

本文正文是 2026-09 3.44.9 → 3.47.4 同步批次的实录:其中的版本号、提交号与实测数字(适配清单文件数、构建边数、单测用例数、工件字节数等)都是那一批的现场值,不随上表更新;上表是当前可复现、可照做的环境组合。方法论、验证门与坑模型对 3.47.8 线同样适用。

Logo

讨论HarmonyOS开发技术,专注于API与组件、DevEco Studio、测试、元服务和应用上架分发等。

更多推荐