一、写在前面

鸿蒙适配开源地址:https://atomgit.com/OpenHarmonyPCDeveloper/ohos_etcher

欢迎在 PC 社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper

环境搭建文章:https://blog.csdn.net/lbcyllqj/article/details/161286249

这篇文章记录的是 balenaEtcher 在 OpenHarmony / HarmonyOS Electron 运行环境中的适配过程。

balenaEtcher 是一个非常知名的开源工具,作用是把操作系统镜像(.iso / .img 等)安全地烧录到 SD 卡和 U 盘。它会自动保护本机硬盘、写完后逐字节校验,避免误写、写坏。它是一个典型的桌面 Electron 应用:

  • electron(37.x)
  • react + rendition(渲染层 UI)
  • electron-forge + webpack(构建/打包链路)
  • etcher-sdk / drivelist / @ronomon/direct-io(底层磁盘读写)
  • electron-updater(自动更新)

和大多数 Electron 应用不同,balenaEtcher 有一个很特别的架构:真正的"写裸盘"动作并不在 Electron 主进程里,而是交给一个独立的特权 sidecar 二进制 etcher-util 去做。GUI 负责界面,sidecar 负责以管理员权限直接写块设备,两者通过本地 WebSocket 通信。

这也决定了 balenaEtcher 的鸿蒙适配有两条边界清晰的主线:

一条是"让 GUI 在鸿蒙 Electron runtime 里跑起来、正常渲染";
另一条是"让底层 sidecar 在鸿蒙上真正能写盘"。

本次适配把第一条主线完整打通了:balenaEtcher 的主界面已经能在鸿蒙 PC 上正常启动、渲染、交互。第二条主线(实际烧录)受限于 sidecar 原生二进制还没有鸿蒙版本,留作后续工作,文末会专门说明。这篇记录会偏"工程复盘",把从 0 到真机跑通的每一步、以及中间踩的坑都讲清楚。


在这里插入图片描述

二、项目背景

balenaEtcher 的项目描述是:

Flash OS images to SD cards & USB drives, safely and easily.

当前适配项目版本:

2.1.6

技术栈关键点:

  • Electron 37 + electron-forge 7 + WebpackPlugin
  • React 17 渲染层,入口 lib/gui/app/renderer.ts,主进程入口 lib/gui/etcher.ts
  • 构建产物落在 .webpack/(这点后面是个关键坑)
  • sidecar:forge.sidecar.tspkglib/util 打成独立的 etcher-util 二进制
  • 权限提升:lib/shared/sudo(写裸盘需要管理员权限)

这个项目最大的特点:

它的核心能力(扫描可移动设备、写盘、校验)全部由独立 sidecar 进程承担,GUI 只是一个"控制台"。

所以鸿蒙适配的第一阶段目标非常明确——先让这个"控制台"在鸿蒙 Electron runtime 里干净地跑起来,把启动链路、自动更新、GPU、协议注册、sidecar 缺失这些桌面默认行为逐一在鸿蒙下做降级处理;至于 sidecar 本身的鸿蒙移植,是一个独立的、更重的工程。

本次适配复用了本地已经验证过的 marktext-ohos 的 OpenHarmony Electron 壳工程路线(同一套 ohos_hap 壳 + ohos:sync 同步脚本),把 balenaEtcher 的 webpack 产物塞进鸿蒙 runtime 资源目录里运行。


三、总体变化概览

3.1 新增鸿蒙工程壳

项目新增:

ohos_hap/
├── AppScope/                 # 应用级配置,显示名改为 balenaEtcher
├── electron/                 # entry 模块,含 EntryAbility 与 Electron so 库
├── web_engine/               # Electron runtime HAR 模块
│   └── src/main/resources/resfile/resources/app/   # ← balenaEtcher 运行资源
└── build-profile.json5       # HAP 构建与签名配置

鸿蒙侧运行资源目录:

ohos_hap/web_engine/src/main/resources/resfile/resources/app

3.2 新增构建脚本

新增:

scripts/build-ohos-package.js   # 跑 forge/webpack 构建 → 组装 resources/app
scripts/build-ohos-hap.js       # 同步后调用 hvigor assembleHap

并在 package.json 中增加:

{
  "build:ohos": "node scripts/build-ohos-package.js",
  "ohos:sync": "OHOS_ETCHER_OUT=ohos_hap/web_engine/src/main/resources/resfile/resources/app npm run build:ohos",
  "ohos:build": "node scripts/build-ohos-hap.js"
}

3.3 主进程 / 渲染层适配

  • lib/gui/etcher.ts(主进程):新增 ETCHER_OPENHARMONY 平台识别;鸿蒙下禁用 GPU 硬件加速、关闭自动更新、自定义协议 etcher:// 注册容错、get-util-path 给出 sidecar 不可用告警。
  • lib/gui/app/app.ts(渲染层):鸿蒙下 flasher 子进程拉起失败时降级为告警,避免 UI 抛未处理异常。

3.4 构建配置开关

  • forge.config.ts / webpack.config.ts:新增 ETCHER_OHOS_BUILD=1 开关。鸿蒙构建时跳过桌面端 sidecar 构建与 mac 代码签名,并以占位值定义 ETCHER_UTIL_BIN_PATH,保证主进程能干净打包。

在这里插入图片描述


四、原始项目直接迁移的问题

4.1 构建链路和参考项目不一样

本地之前适配过的 marktext 用的是 electron-vue(构建产物在 dist/electron),ARDM 用的是 vue-cli。而 balenaEtcher 用的是 electron-forge + WebpackPlugin,产物在 .webpack/,并且主进程入口里有 forge 注入的"魔法常量"(MAIN_WINDOW_WEBPACK_ENTRY 等)。所以同步脚本不能照抄,需要按 forge 的产物结构重写。

4.2 sidecar 会拖累、甚至阻断鸿蒙构建

forge.sidecar.tselectron-forge package 时会触发:

  • tsc 编译 sidecar 源码
  • npm rebuild mountutils(原生模块)
  • pkg --target node20-arm64(把 sidecar 打成独立二进制,会从 GitHub 拉 node 基础包)

这一整套对鸿蒙完全没用(sidecar 在鸿蒙跑不起来),而且 pkg 拉基础包在国内网络下会超时,直接把构建卡死。所以必须在鸿蒙构建时把 sidecar 这条链路整体跳过。

4.3 主进程默认面向桌面 Electron

原始 etcher.ts 默认就是桌面行为:自动更新、注册 etcher:// 协议、GPU 硬件加速、hiddenInset 标题栏。这些在 Win/macOS/Linux 上没问题,但鸿蒙 runtime 下需要重新判断,否则轻则报错、重则启动失败。

4.4 渲染层启动即拉 sidecar

lib/gui/app/app.ts 在渲染层一加载就立即 spawnChildAndConnect() 去拉起 etcher-util 扫描磁盘。鸿蒙上这个二进制不存在,spawn 必然失败——如果不处理,就会在渲染层抛出未处理异常。


五、鸿蒙适配核心路径

5.1 第一步:补齐 HAP 壳工程并改品牌

直接复用一份已验证的 OpenHarmony Electron 壳(marktext-ohos 的 ohos_hap),复制时排除掉可重新生成的构建产物(electron/build.hvigoroh_modules、旧的 app 资源),只保留 runtime so 库和 ArkTS 适配层,约 190MB。

随后把品牌从 MarkText 改成 balenaEtcher:

  • AppScope/app.json5vendor、版本号 2.1.6
  • 4 个 string.json 的 app 名 / EntryAbility_label
  • AppScope/.../media 下 3 个图标换成 etcher 自带图标

注意:bundleName 保持壳工程原有的 com.huawei.ohos_electron 不变——它要和签名 profile 对得上,改了反而签不了名。

5.2 第二步:主进程鸿蒙降级

lib/gui/etcher.ts 增加平台识别与降级:

// 由 ohos:sync 生成的启动引导 main.js 注入 ETCHER_OPENHARMONY=1
const isOpenHarmony = process.env.ETCHER_OPENHARMONY === '1';

// 鸿蒙下默认关闭自动更新
const packageUpdatable =
  !isOpenHarmony && updatablePackageTypes.includes(packageType);

// 鸿蒙下默认关闭 GPU 硬件加速(可用 ETCHER_DISABLE_GPU=0 覆盖)
if (isOpenHarmony && process.env.ETCHER_DISABLE_GPU !== '0') {
  electron.app.disableHardwareAcceleration();
}

并把 etcher:// 协议注册包进 try/catch,把 get-util-path 在鸿蒙下打一条"sidecar 暂不可用"的告警。

5.3 第三步:渲染层优雅降级

lib/gui/app/app.ts 里那条"拉 sidecar 失败就抛错"的逻辑,改成鸿蒙下只告警、不抛异常:

.catch((error) => {
  if (process.env.ETCHER_OPENHARMONY === '1') {
    console.warn(`[ohos] flasher sidecar unavailable, ...: ${error}`);
    return;            // 鸿蒙下降级,保证 UI 仍能加载
  }
  throw new Error(`Failed to start the flasher process. error: ${error}`);
});

这样磁盘扫描即便失败,主界面也照常渲染。

5.4 第四步:用 ETCHER_OHOS_BUILD 开关绕开 sidecar 与签名

forge.config.ts 里加一个环境开关,鸿蒙构建时跳过 sidecar 插件和 mac 签名:

const isOhosBuild = process.env.ETCHER_OHOS_BUILD === '1';
// ...
osxSign: isOhosBuild ? undefined : { /* 原签名配置 */ },
// ...
plugins: [
  new AutoUnpackNativesPlugin({}),
  new WebpackPlugin({ /* ... */ }),
  ...(isOhosBuild ? [] : [new sidecar.SidecarPlugin()]),   // 鸿蒙跳过 sidecar
],

由于跳过了 sidecar,原本由它注入的 ETCHER_UTIL_BIN_PATH 就没人定义了,于是在 webpack.config.ts 里给鸿蒙构建补一个占位定义:

const ohosSidecarDefine =
  process.env.ETCHER_OHOS_BUILD === '1'
    ? [new DefinePlugin({ ETCHER_UTIL_BIN_PATH: JSON.stringify('etcher-util') })]
    : [];

5.5 第五步:写同步脚本,把 webpack 产物组装成鸿蒙资源

scripts/build-ohos-package.js 做的事:

  1. ETCHER_OHOS_BUILD=1,用 node node_modules/@electron-forge/cli/dist/electron-forge.js package 跑一次生产构建;
  2. 定位 forge v7 产出的 .webpack/<arch>/{main,renderer}
  3. 把它复制成输出目录下的 webpack/注意:不带点,原因见第六节);
  4. 写入启动引导 main.js
process.env.ETCHER_OPENHARMONY = '1';
process.env.ETCHER_DISABLE_GPU = process.env.ETCHER_DISABLE_GPU || '1';
require('./webpack/main/index.js');
  1. 写入运行期 package.jsonmain 指向 main.js)。

跑通后终端会输出 OpenHarmony app resources written to: ...,鸿蒙 runtime 资源目录就组装完成了。

在这里插入图片描述


六、构建踩坑实录

这部分是整个适配里最"费劲"的地方。从环境到打包,前后踩了 6 个坑,按出现顺序记录如下:

坑 1 — Node 版本。 机器默认 Node 24,但 etcher 的原生模块在 24 上编译会失败。改用 .nvmrc 指定的 Node 18(fnm use 18.18.0)后正常。

坑 2 — npm 缓存权限。 ~/.npm 缓存里有历史遗留的 root 属主文件,npm ci 直接报 EACCES。不去动 sudo chown 改系统目录,而是加 --cache /tmp/xxx 用独立缓存绕过。

坑 3 — Electron 二进制下载超时。 安装时 node_modules/electron 的 postinstall 要从 GitHub 拉 Electron 二进制,国内网络 ETIMEDOUT,而且 npm ci 失败会回滚清空 node_modules。解决:配国内镜像

ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ npm install --cache /tmp/xxx

坑 4 — forge 的 .bin 执行位缺失。 脚本里 spawn node_modules/.bin/electron-forgeEACCES(该软链目标的可执行位丢了)。改成用 node 直接跑 CLI 的 JS 入口即可,脚本里已内置这个处理。

坑 5 — forge v7 产物在 arch 子目录。 electron-forge package 把 webpack 产物写到 .webpack/arm64/{main,renderer}(带架构子目录),不是老版本的 .webpack/{main,renderer}。脚本里加了自动定位 arch 目录的逻辑。

坑 6(最隐蔽)— 鸿蒙 HAP 会过滤掉点开头的目录。 这是最值得记下来的一个坑。第一次装到设备上启动,弹窗直接报:

Error: Cannot find module './.webpack/main/index.js'
  at .../electron/resources/resfile/resources/app/main.js:8

main.jspackage.json 都在,唯独 .webpack/ 不见了。把签名 HAP 解包一看:

unzip -l electron-default-signed.hap | grep -c webpack    # 输出 0

原因是 HAP 资源打包阶段会丢弃以 . 开头的隐藏文件 / 目录。修复很简单:把同步产物的目录名从 .webpack 改成 webpack(主进程 bundle 内部用 ../renderer 相对寻址,改目录名不影响解析),同步改一下引导里的 require 路径。改完再解包:

unzip -l electron-default-signed.hap | grep -c webpack    # 输出 16 ✅

这 6 个坑全部解决后,hvigor assembleHap 顺利出包:

在这里插入图片描述


七、安装与运行

签名 HAP 产物位于:

ohos_hap/electron/build/default/outputs/default/electron-default-signed.hap

用 hdc 安装到已连接的鸿蒙 PC,并拉起:

HDC=/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/hdc
$HDC install -r ohos_hap/electron/build/default/outputs/default/electron-default-signed.hap
$HDC shell aa start -b com.huawei.ohos_electron -a EntryAbility

终端依次返回 install bundle successfully.start ability successfully.

排查启动是否真的渲染,可以用设备截屏 + 查 hilog:

# 截屏拉回本地
$HDC shell snapshot_display -f /data/local/tmp/etcher.jpeg
$HDC file recv /data/local/tmp/etcher.jpeg ~/Desktop/etcher.jpeg
# 查是否还有主进程 JS 报错
$HDC shell "hilog -x | grep -iE 'Cannot find module|showErrorBox'"

修复坑 6 之后,报错消失,balenaEtcher 的主界面在鸿蒙 PC 上正常渲染出来了——标题栏、logo、经典的三步式界面(Flash from file / Select target / Flash!)、设置与帮助按钮、底部的数据采集提示一应俱全:

在这里插入图片描述


八、关于实际烧录能力的说明

需要诚实地交代一个边界:当前适配让 balenaEtcher 的界面在鸿蒙 PC 上完整跑起来了,但点 “Flash!” 真正写盘还不能用。

原因前面说过:balenaEtcher 的写盘动作由独立的特权二进制 etcher-util(基于 etcher-sdk / drivelist / @ronomon/direct-io 直接写裸块设备)完成,它目前没有 OpenHarmony 版本。鸿蒙下 get-util-path 返回的 etcher-util 不存在,扫描设备 / 烧录的子进程拉不起来,被适配逻辑优雅降级掉了。

要让烧录真正可用,后续还需要:

  1. 为 OpenHarmony(linux-arm64 基座)交叉编译 etcher-util 及其原生依赖;
  2. lib/shared/sudo 的权限提升方式适配到鸿蒙的权限模型;
  3. 把产物放进 resources/app 并让 get-util-path 在鸿蒙环境解析到它。

这是一条独立的、更重的工程主线,不在本次"让 GUI 跑起来"的目标范围内。


九、结语

balenaEtcher 这次适配的重点,是把一个"GUI + 特权 sidecar"双进程架构的 Electron 桌面工具,干净地迁入鸿蒙 Electron runtime:

  1. 复用 OpenHarmony Electron 壳,承载 React 渲染层;
  2. 主进程在鸿蒙下关掉自动更新 / GPU、做协议与 sidecar 容错;
  3. ETCHER_OHOS_BUILD 开关绕开对鸿蒙无用的 sidecar 与 mac 签名;
  4. 同步脚本按 forge v7 产物结构组装资源,并避开"点开头目录被 HAP 过滤"这个坑。

最终结果是 balenaEtcher 的主界面已经在鸿蒙 PC 上真机跑通、正常渲染、可交互。实际写盘能力则等待后续 sidecar 的鸿蒙移植。对于"传统桌面 Electron 工具迁鸿蒙"这类任务来说,这次适配把构建链路上的几个典型坑(Node 版本、镜像、forge 产物结构、HAP 资源过滤)都趟了一遍,可供后续同类项目直接复用。

Logo

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

更多推荐