在鸿蒙 PC 上用 Rust + Tauri 开发鸿蒙应用:从零到真机跑通
在华为 MateBook Pro 的 HarmonyOS 7.0 上,用 Tauri 的 OpenHarmony 分支把 Rust 应用跑进原生窗口:外层 ArkTS 壳加载 N-API 模块,内层 wry 起 ArkWeb。命令与报错都来自真机。

验证环境:HUAWEI MateBook Pro(HAD-W32),HarmonyOS 7.0.0.111 / OpenHarmony-7.0.0.109 / API 26,aarch64(2in1)。文中所有命令、报错原文、体积与耗时都来自这台真机的实测记录,不是从别的平台推断出来的流程。
用到的框架:tauri-apps/tauri 的 feat/open-harmony 分支(参考工程 richerfu/tauri-demo)。Rust 工具链是本机原生的 aarch64-unknown-linux-ohos host。
1. 先说结论:能跑到什么程度
最终形态是一个真正意义上的 Tauri 应用,跑在鸿蒙 PC 的原生窗口里:

- 外层是 ArkTS 壳:入口 Ability 继承 @ohos-rs/ability 提供的 NativeAbility,由它把 Rust 动态库作为 N-API 模块加载进应用进程;
- 内层是 Rust:tauri + wry 的 ohos 后端起了一个 ArkWeb(Chromium)webview,前端页面由 Tauri 的资源协议提供;
- 前端是零依赖静态页面:用 withGlobalTauri 暴露的全局对象直连 Rust 命令,不需要打包器;
- 前后端双向都通:前端按钮触发 app_info / greet 命令,返回值回填 DOM;同时保留了 Rust → ArkTS 的 N-API 回调(含 TSFN 异步线程)能力。
界面上第 1 张卡片返回的真实数据(这是从设备上抓的界面节点文本):
{
“product”: “harmony-tauri-app”,
“version”: “0.1.0”,
“os”: “linux”,
“arch”: “aarch64”,
“family”: “unix”,
“ohos”: true,
“ohos_library”: “tauri_demo_lib”,
“ohos_package”: “com.atomcode.harmonytauri”
}
第 2 张卡片返回 Hello, 鸿蒙开发者!You’ve been greeted from Rust!。
两个细节值得注意:
- “ohos”: true 来自编译期常量 cfg!(target_env = “ohos”),这是我特意加的判据;
- “os”: “linux” 是 Rust std::env::consts::OS 在 ohos 目标下的正常取值 —— ohos 目标的 target_os 是 linux、target_env 才是 ohos。想判断”是否鸿蒙”必须看 target_env,看 target_os 会误判。
版本与体积记录(便于你和自己的结果对照):
| 项目 | 实测值 |
|---|---|
| Rust / cargo | 1.99.0(harmonybrew 构建,host = aarch64-unknown-linux-ohos) |
| tauri / wry / tao | 2.8.5 / 0.55.1 / 0.35.3(均来自 ohos 分支) |
| openharmony-ability | 0.3.0(rev 295a276a) |
| @ohos-rs/ability
(ohpm 包) |
0.4.0-beta.5 |
| DevEco SDK | API 26(26.0.0.105) |
| hvigor / ohpm / hdc | 7.26.8 / 6.0.1 / 3.2.0f |
| Rust 动态库体积 | 10.7 MB → 9.4 MB(加上 custom-protocol 之后,见第 7 章) |
| 签名后的 HAP | 8.6 MB |
| 全量 Rust release 构建 | 2 分 02 秒(20 核 aarch64) |
| 增量重编译 | 30 ~ 44 秒 |
| hvigor 组装 HAP | 6 ~ 12 秒 |
2. 为什么鸿蒙 PC 上这套流程处处别扭
如果你只做过 Android/iOS 上的 Tauri,会有一段明显的“水土不服”。原因是鸿蒙 PC 的用户态和普通 Linux 发行版有四个硬性差别,它们分别对应后文的四个大坑。
2.1 用户态是 musl,且目标三元组是 ohos 专用
$ ls -l /lib/ld-musl-aarch64.so.1 /lib/ld-linux-aarch64.so.1
-rwxr-xr-x 1 root shell 2223008 2024-06-06 08:00 /lib/ld-musl-aarch64.so.1
ls: /lib/ld-linux-aarch64.so.1: No such file or directory
没有 glibc。目标三元组必须是 **aarch64-unknown-linux-ohos**,不是 aarch64-unknown-linux-musl,更不是任何 gnu。好在 Rust 官方把 aarch64-unknown-linux-ohos 列在 Tier 2 with Host Tools,也就是说上游会为本机发布 rustc/cargo/clippy/rustfmt 全套构件——这是整件事能成立的前提。
2.2 系统里没有 C 工具链
$ for c in rustc cargo gcc cc clang make cmake ninja python3; do printf ‘%s: ‘ “$c”; command -v “$c” || echo “(missing)”; done
rustc: (missing)
cargo: (missing)
gcc: (missing)
cc: (missing)
clang: (missing)
make: (missing)
cmake: (missing)
ninja: (missing)
python3: (missing)
这不是“没装”,是 Toybox 用户态本身就不带。后果:链接必须有链接器,而 Rust 默认调 cc;交叉编译的 C 代码也要有编译器。好在本机随 HarmonyOS Command Line Tools 装好的 OpenHarmony SDK 里带了一套能在设备上原生跑的 LLVM,可以直接借用(第 6.4 节)。
2.3 二进制必须自签名,否则内核拒绝执行
这是最容易忽略、也最致命的一条。任何未经签名的 ELF 在本机都无法执行:
$ ./hello
sh: ./hello: Operation not permitted
$ echo $?
126
而且限制与路径无关(~/.harmonybrew/Cellar、~/.local/bin 都试过,一样 126),只和文件内容里有没有合法签名有关。更细的观测是:报错会分两种,取决于文件里有没有 .codesign 段。
| 文件状态 | 报错 | 说明 |
|---|---|---|
| 完全没有 .codesign 段(例如拿别处编好的产物) | Permission denied
(126) |
只有 .note.ohos.ident 段 |
| 有 .codesign 段但内容是占位符(lld 默认行为,20 KB 全零) | Operation not permitted
(126) |
段在、签名不合法 |
| .codesign
段是 4 KB 的真实自签名 |
正常执行 | — |
所以本地工具链的每一个链接产物都要补一步签名。这个坑在后面第 6.4 节会以“链接器包装器”的形式一次性解决。
2.4 文件系统会间歇性返回 EINTR
这台机器的文件系统(应用沙箱 / HNP 挂载之上)会偶发让系统调用返回 EINTR。C 程序通常无害(会自动重试),Rust 程序则可能把它当成致命错误。本文后面有两个坑都是它造成的:
- cargo 的 git checkout 一撞上 EINTR 就整趟失败(第 6.2 节);
- 上一篇文章里 brew install 因为 Ruby 的目录枚举遇 EINTR 而必然失败。
记下这条:在这个平台上,“重试”往往比“修配置”更有效,但要分清楚哪些工具会自己重试、哪些不会。
2.5 一句话总结差异
| 维度 | 鸿蒙 PC 实际情况 | 对 Tauri 开发的影响 |
|---|---|---|
| libc | musl,目标三元组 …linux-ohos | 依赖里所有预编译产物都得换 |
| C 工具链 | 无 cc/gcc/clang/make | 必须指定链接器;C 依赖要走 SDK 的 clang |
| 可执行文件 | 未签名一律拒执行 | 链接后必须签名,且要自动化 |
| 文件系统 | 偶发 EINTR | cargo/git/hvigor 都可能莫名失败 |
| 应用签名 | HarmonyOS 设备不认 OpenHarmony 根证书 | 必须走 AGC 申请证书与 profile |
3. Tauri 在鸿蒙上是怎么跑起来的
先建立正确的心理模型,否则后面各种配置项会显得很随机。
3.1 分层结构
┌──────────────────────────────────────────────────────────┐
│ 鸿蒙应用(HAP,已签名) │
│ │
│ ArkTS 层 │
│ EntryAbility extends NativeAbility(@ohos-rs/ability) │
│ moduleName = “tauri_demo_lib” │
│ import rustNapi from ‘libtauri_demo_lib.so’ │
│ └─ 注册 ArkTS 回调,供 Rust 反向调用 │
│ │
│ 原生库层(libs/arm64-v8a/) │
│ libtauri_demo_lib.so ← 我们编译的 Rust cdylib │
│ ├─ napi-ohos N-API 绑定(Rust ↔ ArkTS) │
│ ├─ tauri 2.8.5 应用框架、IPC、资源协议 │
│ └─ wry 0.55.1 ohos 后端的 webview(ArkWeb) │
│ libc++_shared.so / libentry.so(模板遗留,可忽略) │
│ │
│ Web 层(ArkWeb / Chromium) │
│ index.html + main.js + style.css │
│ window.__TAURI__.core.invoke(“greet”, {name}) │
└──────────────────────────────────────────────────────────┘
关键点:鸿蒙上没有“Tauri 应用”这种进程,只有一个带 webview 的鸿蒙应用,Rust 是被加载进它进程里的动态库。 这与 Android 上的 Tauri 是同构的(Android 是 TauriActivity + lib<app>.so),只是这里的 Ability 由 ohos-rs 社区提供。
3.2 一次按钮点击的完整调用链
以第 1 章那个“打招呼”按钮为例:
- webview 里的 main.js 执行 window.__TAURI__.core.invoke(“greet”, { name: “鸿蒙开发者” });
- Tauri 的 IPC 把请求交给 Rust 侧注册的命令处理函数(本工程里是 src-tauri/src/lib.rs 的 #[tauri::command] fn greet(&str) -> String);
- Rust 返回字符串,经 IPC 回到 webview;
- main.js 把结果写进 DOM,界面上出现 Hello, 鸿蒙开发者! …。
反向(Rust → ArkTS)走的是另一条路:napi-ohos 把 ArkTS 传进来的回调函数包成 FunctionRef / ThreadsafeFunction,Cargo 侧用 #[napi] 导出注册函数,EntryAbility 在 onCreate 里调用它们完成注册。本工程的 lib.rs 里那套 register_main_thread_arkts_callback / register_async_thread_arkts_callback / register_main_thread_dispatcher 就是这条链路的三个样例(主线程函数调用、异步线程 TSFN、非阻塞派发)。
3.3 工程文件布局(鸿蒙侧)
Tauri 的移动端工程有个“生成目录”约定,鸿蒙侧是 src-tauri/gen/ohos,等同 Android 的 src-tauri/gen/android:
| 路径 | 作用 | 会不会被重新生成 |
|---|---|---|
| gen/ohos/AppScope/app.json5 | 包名 bundleName、应用名、版本号 | 手工维护 |
| gen/ohos/build-profile.json5 | 产品定义、signingConfigs、兼容 SDK 版本 | 签名由 devecocli 写入 |
| gen/ohos/entry/src/main/module.json5 | 模块名、入口 Ability、设备类型、权限 | 手工维护 |
| gen/ohos/entry/src/main/ets/entryability/EntryAbility.ets | ArkTS 壳,声明 Rust 库名与回调 | 手工维护 |
| gen/ohos/entry/src/main/ets/pages/Index.ets | 兜底页面(webview 才是主界面) | 模板生成 |
| gen/ohos/entry/src/main/cpp/ | 模板自带的 libentry.so(N-API 示例) | 可以忽略 |
| gen/ohos/entry/src/main/cpp/types/libtauri_demo_lib/ | 给 ArkTS 的 .d.ts 类型声明 | 与 Rust 导出保持一致 |
| gen/ohos/entry/libs/arm64-v8a/libtauri_demo_lib.so | 我们编译出来的 Rust 库 | 每次构建后放进来 |
| gen/ohos/assets/tauri.conf.json | 运行时真正读的配置
(第 7 章重点) |
必须与源码里的配置同步 |
EntryAbility.ets 里最关键的三行:
import { NativeAbility } from ‘@ohos-rs/ability’
import rustNapi from ‘libtauri_demo_lib.so’;
export default class EntryAbility extends NativeAbility {
public moduleName: string = “tauri_demo_lib” // 不带 lib 前缀、不带 .so
public defaultPage: boolean = true;
moduleName 与 Cargo.toml 里 [lib] name、以及 libs/arm64-v8a/ 下的文件名必须三者一致,否则加载不到。
3.4 前端资源到底存在哪
这是最容易搞错的一点,涉及两个地方:
| 位置 | 谁写 | 谁读 | 何时生效 |
|---|---|---|---|
| Rust 库内嵌的资源(tauri::generate_context! 展开时把 frontendDist 目录编进二进制) | tauri-build
/ tauri-codegen 在编译期 |
运行期的资源协议处理器 | 编译时确定 |
| HAP 内 assets/tauri.conf.json | tauri-cli 的 inject_resources 在每次构建时注入 | 运行期的 AppManager | 运行时读取 |
也就是说:**改了 src-tauri/tauri.conf.json,必须同时刷新 gen/ohos/assets/tauri.conf.json**,否则运行时读到的还是旧配置。我第一次翻车就翻在这里(第 7 章会看到那条经典报错)。
4. 环境准备
4.1 需要什么
| 组件 | 本机来源 | 版本 | 用途 |
|---|---|---|---|
| hdc | hmos-clt(软链到 ~/.local/bin/hdc) | 3.2.0f | 连设备、装 HAP、看日志 |
| hvigorw | hmos-clt | 7.26.8 | 组装 HAP |
| ohpm | hmos-clt | 6.0.1 | 拉 ArkTS 依赖(@ohos-rs/ability) |
| DevEco SDK | hmos-clt($DEVECO_SDK_HOME) | API 26 / 26.0.0.105 | ArkTS 编译器、Clang/LLD、打包与签名工具 |
| Rust 工具链 | harmonybrew 的 rust 包 | 1.99.0,host=aarch64-unknown-linux-ohos | 编译 Rust 侧 |
| Node.js | harmonybrew 的 node | v26.12.0 | ohpm / hvigorw / devecocli 的运行基础 |
| cmake + ninja | harmonybrew(或用 SDK 自带的) | 4.4.4 / 1.13.2 | hvigor 编译模板里的 C++ 库 |
| devecocli | npm 全局 @deveco/deveco-cli | 1.3.6 | AGC 签名材料 + 一键构建安装运行 |
配置 SDK 相关环境变量(写进 ~/.zshrc 更省事):
export HMOS_CLT_HOME=/data/service/hnp/hmos-clt.org/hmos-clt_1.0.0
export DEVECO_SDK_HOME=”$HMOS_CLT_HOME/sdk”
export PATH=”$HMOS_CLT_HOME/ohpm/bin:$HMOS_CLT_HOME/hvigor/bin:$HMOS_CLT_HOME/sdk/default/openharmony/toolchains:$PATH”
4.2 本机真实遇到的两个环境坑
坑一:坏的 node 会把整套鸿蒙工具链带崩。 本机 ~/.local/bin/node 指向一个 120 MB 的旧二进制,执行直接 Permission denied(未签名/架构不符),而它偏偏在 PATH 最前面:
$ ohpm -v
ERROR: node: …/ohpm/bin/ohpm[7]: /storage/Users/currentUser/.local/bin/node: Permission denied
ERROR: NODE_HOME: …/ohpm/bin/ohpm[12]: /node: inaccessible or not found
hvigorw 同样会中招。修法是把这几个软链指回能用的 node:
for b in node npm npx; do ln -sfn “$HOME/.harmonybrew/bin/$b” “$HOME/.local/bin/$b”; done
坑二:brew install 在这台机器上必然失败(Ruby 目录枚举遇 EINTR 致命退出)。装包一律走 brew fetch <包> + 手工解包装配。这是上一篇文章的主题,这里不展开;本文只需要 rust、cmake、ninja 这几个已经装好的东西。
4.3 设备连接
鸿蒙 PC 上做开发有个便利:idevice 就是本机自己,hdc 通过回环连过来。
$ hdc list targets
127.0.0.1:34953
$ devecocli device list
Name Serial Kind Device Type
HUAWEI MateBook Pro 127.0.0.1:34953 device 2in1
Device Type 是 2in1 —— 这意味着应用要能在 PC 形态下运行,module.json5 的 deviceTypes 必须包含 2in1(模板默认给了 phone/tablet/2in1/tv/car,先不用改)。
4.4 签名材料:两套,别混用
| 材料 | 位置 | 能用在哪 |
|---|---|---|
| OpenHarmony 自带(OpenHarmony.p12 / OpenHarmonyApplication.pem / UnsgnedDebugProfileTemplate.json) | $DEVECO_SDK_HOME/default/openharmony/toolchains/lib/ | 只能本地验签
,装不上 HarmonyOS 设备 |
| AGC 申请的(.p12 / .cer / .p7b) | ~/.ohos/config/
(由 devecocli 自动生成) |
可以装到真机 |
这一点非常关键,第 8 章会给出那条让人一脸问号的错误码。
5. 工程搭建:为什么我不用 tauri-cli 一条命令
5.1 官方推荐流程长什么样
参考工程 richerfu/tauri-demo 的 README 给的流程是标准 Tauri 移动端套路:
# 1) 装 CLI(从 ohos 分支源码编译)
cargo install tauri-cli –git https://github.com/tauri-apps/tauri –branch feat/open-harmony
cargo install ohrs
# 2) 前端依赖
pnpm install
# 3) 配置 SDK 与 PATH
export OHOS_HOME=/path/to/ohos-sdk/darwin
export PATH=/path/to/command-line-tools/bin:$PATH
# 4) 构建(会调用构建器、打包 HAP)
cd src-tauri
OHOS_HOME=$OHOS_HOME cargo tauri ohos build \
–config “{\”build\”:{\”beforeBuildCommand\”:\”pnpm –dir ‘${ROOT}’ build\”}}”
如果 CLI 卡在打包环节,README 还给了兜底:直接进 src-tauri/gen/ohos 手搓 ohpm install && hvigorw assembleHap,产物是未签名 HAP,签名交给 DevEco Studio 或自己配 signingConfigs。
这条路没错,但在这台机器上它有三个现实问题:
- cargo install tauri-cli 太贵:它要编译整个 tauri-cli 依赖树(数百个 crate)。而本机全量编译一次应用侧的 tauri 依赖树就已经 2 分 02 秒,CLI 只会更久,且本机文件系统偶发 EINTR,长时间编译中途翻车的概率不低。
- 构建期环境变量必须精确控制:tauri-cli 构建时会注入 4 个环境变量(WRY_OHOS_PACKAGE 等,见 6.3 节)并同步注入运行时配置。绕过 CLI 时这些都得自己给,反而更可控。
- 前端依赖树在这个平台上很脆:vite 6 依赖 rollup,而 rollup/rolldown、esbuild 这类包在鸿蒙上没有预编译的原生制品,装依赖阶段就会踩坑(这是本机环境的已知问题)。
于是我的选择是:手工构建 + 全部脚本化,并且把前端做成零依赖静态页面。
顺带说清楚:gen/ohos 这个鸿蒙工程目录,官方是由 cargo tauri ohos init 从 CLI 内置模板(crates/tauri-cli/templates/mobile/open-harmony/)生成的。我直接复用了参考 demo 已经生成好的 gen/ohos(两者同源),再改包名与应用名——效果等价于跑过一次 tauri ohos init。
5.2 零依赖前端:为什么值得这么做
Tauri v2 支持 app.withGlobalTauri = true,此时 webview 里会直接挂一个 window.__TAURI__ 全局对象,于是前端可以完全不引入 @tauri-apps/api,也就不需要任何打包器:
// tauri.conf.json
“app”: {
“withGlobalTauri”: true,
“windows”: [
{ “label”: “main”, “url”: “index.html”, “title”: “鸿蒙 Tauri 应用”, “width”: 900, “height”: 640 }
]
}
注意 “url”: “index.html” 这一行,它不是可选的装饰。省掉它,窗口默认会去请求根路径,进而触发第 7 章那个报错。
前端调用后端命令长这样(web/main.js):
// 通过 withGlobalTauri 暴露的全局对象直连 Rust 命令,不需要任何 npm 依赖
const invoke = window.__TAURI__.core.invoke;
asyncfunction call(label, cmd, args) {
try {
log(`→ ${cmd} ${args ? JSON.stringify(args) : “”}`);
const result = await invoke(cmd, args);
log(`← ${JSON.stringify(result)}`, “ok”);
return result;
} catch (error) {
log(`✗ ${label} 失败:${error}`, “err”);
}
}
document.getElementById(“btn-info”).addEventListener(“click”, async () => {
const info = await call(“应用信息”, “app_info”);
document.getElementById(“info-out”).textContent = JSON.stringify(info, null, 2);
});
Rust 侧对应的命令(src-tauri/src/lib.rs):
#[derive(serde::Serialize)]
struct AppInfo {
product: &’staticstr,
version: &’staticstr,
os: &’staticstr,
arch: &’staticstr,
family: &’staticstr,
/// 编译期常量:为鸿蒙目标编译时才为 true
ohos: bool,
/// 构建期由 tauri-cli 注入的环境变量;手工构建时可能为 None
ohos_library: Option<&’staticstr>,
ohos_package: Option<&’staticstr>,
}
#[tauri::command]
fn app_info() -> AppInfo {
AppInfo {
product: env!(“CARGO_PKG_NAME”),
version: env!(“CARGO_PKG_VERSION”),
os: std::env::consts::OS,
arch: std::env::consts::ARCH,
family: std::env::consts::FAMILY,
ohos: cfg!(target_env = “ohos”),
ohos_library: option_env!(“WRY_OHOS_LIBRARY”),
ohos_package: option_env!(“WRY_OHOS_PACKAGE”),
}
}
这个命令刻意做成了“自证式”的:ohos 字段用编译期 cfg 判断,ohos_library / ohos_package 直接把构建期注入的环境变量读回来。窗口里看到什么值,就说明构建环境是什么样,不用猜。
5.3 从 demo 到自己的工程:改动清单
| 文件 | 改动 | 为什么 |
|---|---|---|
| src-tauri/Cargo.toml | [package] name = “harmony-tauri-app”
([lib] name 保持 tauri_demo_lib) |
包名是自己的;库名先不动,它与 ArkTS 侧的 moduleName、import 语句、类型声明目录三处绑定,改一处不够 |
| src-tauri/Cargo.toml | tauri 依赖加 features = [“custom-protocol”] | 第 7 章的核心坑 |
| src-tauri/tauri.conf.json | productName
/ identifier(com.atomcode.harmonytauri)/ withGlobalTauri / 窗口 label+url / frontendDist: “../web” |
应用身份与前端来源 |
| src-tauri/src/lib.rs | 新增 app_info 命令并注册;保留 demo 的 N-API 回调样例 | 演示 IPC 与自证环境 |
| gen/ohos/AppScope/app.json5 | bundleName = “com.atomcode.harmonytauri” | 必须与 tauri 的 identifier 一致 |
| gen/ohos/AppScope/resources/base/element/string.json | app_name
改成中文应用名 |
桌面显示名 |
| gen/ohos/entry/src/main/resources/base/element/string.json | 模块描述 / Ability 标签 | 同上 |
| gen/ohos/assets/tauri.conf.json | 用本工程配置覆盖 | 运行时读的是这一份 |
| web/
(新增) |
index.html
+ main.js + style.css |
零依赖前端 |
| 删除 demo 的 src/、index.html、vite.config.ts、package.json 等 | 去掉 Vite/Vue 依赖 | 不再需要打包器 |
库名为什么先不改:它是整套里最容易牵一发动全身的地方。先把链路跑通,再改名是几分钟的事(改 Cargo.toml 的 [lib] name、EntryAbility.ets 的 moduleName 与 import、oh-package.json5 的依赖项、types/lib<库名>/ 目录名,然后重新构建)。
6. Rust 侧:依赖、编译与链接
6.1 依赖从哪来:一个 fork 加两处 patch
鸿蒙支持目前不在 tauri 的正式发布版里,必须指向 git:
[dependencies]
tauri = { git = “https://github.com/richerfu/tauri”, branch = “feat/open-harmony”, features = [
“custom-protocol”,
] }
tauri-plugin-opener = “2”
napi-ohos = “1.1”
napi-derive-ohos = “1.1”
# 把 openharmony-ability 固定到 2026-06-30 的 295a276a
# (上游 c6c4c9a 移除了 webview feature 之前的最后一个可用状态)
[patch.crates-io]
openharmony-ability = { git = “https://github.com/harmony-contrib/openharmony-ability.git”, rev = “295a276a699ba2352addc69b8cacb6acd3be6ab1” }
openharmony-ability-derive = { git = “https://github.com/harmony-contrib/openharmony-ability.git”, rev = “295a276a699ba2352addc69b8cacb6acd3be6ab1” }
# 把整条 tauri 家族都钉到同一个分支上,避免 crates.io 上的旧版本混进来
tauri = { git = “https://github.com/richerfu/tauri”, branch = “feat/open-harmony” }
tauri-runtime = { git = “https://github.com/richerfu/tauri”, branch = “feat/open-harmony” }
tauri-utils = { git = “https://github.com/richerfu/tauri”, branch = “feat/open-harmony” }
tauri-macros = { git = “https://github.com/richerfu/tauri”, branch = “feat/open-harmony” }
tauri-runtime-wry = { git = “https://github.com/richerfu/tauri”, branch = “feat/open-harmony” }
两点说明:
- 用户指定的 tauri-apps/tauri 的同名分支与这里的 fork 内容一致(fork 的 manifest 里已经声明了 wry/tao 的同源 rev)。参考 demo 把依赖指向 fork,是因为它同时把 wry、tao 也钉在了带 ohos 后端的提交上,我沿用了这套经过验证的组合。
- 上面那段 [patch.crates-io] 里的 tauri* 条目不是笔误:patch 会把传递依赖里从 crates.io 拉到的 tauri 家族 crate 也换成 git 版本,避免版本分裂。构建日志里能看到 tauri-utils v2.7.0 与 v2.8.3 两个版本同时存在,就是这个 patch 生效的痕迹。
实际拉到的关键版本(来自构建日志):
| crate | 版本 / 来源 |
|---|---|
| tauri | 2.8.5(fork feat/open-harmony @ c818cc1d) |
| tauri-runtime-wry | 2.8.1(同源) |
| wry | 0.55.1(richerfu/wry @ ae8b21b5) |
| tao | 0.35.3(richerfu/taofeat-ohos-webview @ 9d51be24) |
| openharmony-ability | 0.3.0(rev 295a276a) |
| napi-ohos | 1.1.6 |
| ohos-arkui-sys
/ ohos-web-sys / ohos-xcomponent-sys 等 |
0.0.1 ~ 0.2.0(ohos 系统能力绑定) |
6.2 坑一、坑二:cargo 在这台机器上取依赖
故障 A:cargo 内置 git 客户端 TLS 失败
error: failed to load source for dependency `openharmony-ability`
Caused by: unable to update https://github.com/harmony-contrib/openharmony-ability.git?rev=295a276a#295a276a
Caused by: failed to clone into: /storage/Users/currentUser/.cargo/git/db/openharmony-ability-7e6401132ac36172
Caused by: revision 295a276a699ba2352addc69b8cacb6acd3be6ab1 not found
Caused by: SSL error: unknown error; class=Ssl (16)
if a proxy or similar is necessary `net.git-fetch-with-cli` may help here
cargo 自带的 HTTP/git 传输栈在这台机器上握手失败(class=Ssl)。而系统 git 明明是好用的——同一个仓库 git ls-remote 2.29 秒就返回了。所以按 cargo 自己的提示,把 git 操作交给系统 git:
# ~/.cargo/config.toml
[net]
git-fetch-with-cli = true
故障 B:cargo 的 git checkout 不重试 EINTR
换用系统 git 之后,下载环节通了,但检出(checkout)环节开始翻车——checkout 是 cargo 自己用 Rust 实现的,而它把 EINTR 当致命错误:
Caused by: failed to stat ‘/storage/…/.cargo/git/checkouts/tauri-96ba84dc1b8a2872/c818cc1/crates/tauri-bundler/src/bundle/linux/util.rs’:
Interrupted system call; class=Os (2); code=Exists (-4)
之后每次重试都死在另一个文件上(tauri-schema-generator/…、tauri-cli/templates/…),因为 tauri 仓库文件太多,一整趟 checkout 里不撞上 EINTR 的概率很低。重试不解决问题。
思路转一下:下载和检出可以用别的工具做。git 是 C 程序,会自行重试 EINTR;tar 也一样。于是:
- 让 cargo 把仓库下载到它自己的 git db(这一步用系统 git,已经通了);
- cargo 检出失败时,从错误信息里取出出问题的 checkout 目录路径;
- 用 git archive + tar 手工把该目录铺好;
- 补上 cargo 认可的标记文件 .cargo-ok(空文件,只要存在且内容匹配就算检出有效),然后重试。
核心逻辑(完整脚本见第 11 章):
fix_checkout() {
root=”$1″ # …/git/checkouts/tauri-96ba84dc1b8a2872/c818cc1
repo=$(basename “$(dirname “$root”)”) # tauri-96ba84dc1b8a2872
rev=$(basename “$root”) # c818cc1
db=”$HOME/.cargo/git/db/$repo”
rm -rf “$root”; mkdir -p “$root”
git –git-dir=”$db” archive –format=tar “$rev” | tar -xf – -C “$root”
: >”$root/.cargo-ok” # cargo 用它判断 checkout 可用
}
实测效果:四个 git 依赖(openharmony-ability、tauri、tao、wry)逐个被铺好,之后 cargo 认账,继续去下载 crates.io 上的包:
/storage/Users/currentUser/.cargo/git/checkouts/openharmony-ability-…/295a276 文件数=159 ok=yes
/storage/Users/currentUser/.cargo/git/checkouts/tauri-96ba84dc1b8a2872/c818cc1 文件数=1109 ok=yes
/storage/Users/currentUser/.cargo/git/checkouts/tao-3928fb55dcb557e2/9d51be2 文件数=174 ok=yes
/storage/Users/currentUser/.cargo/git/checkouts/wry-9b73cbcb28a49192/ae8b21b 文件数=132 ok=yes
6.3 构建期必须注入的环境变量
绕过 tauri-cli 之后,它原本替我们做的注入要自己补。这些变量的取值口径在 crates/tauri-cli/src/mobile/open_harmony/mod.rs 的 get_config() 里写得很清楚:
| 变量 | 取值 | 谁在用 | 漏了会怎样 |
|---|---|---|---|
| WRY_OHOS_PACKAGE | 包名(com.atomcode.harmonytauri) | wry/tauri 的构建脚本 | 生成的原生侧包名信息不对 |
| TAURI_OHOS_PACKAGE_UNESCAPED | 同上,未转义 | 同上 | 同上 |
| WRY_OHOS_LIBRARY | Cargo.toml
的 [lib] name |
同上 | 库名对不上,加载失败 |
| TAURI_OHOS_PROJECT_PATH | gen/ohos
的绝对路径 |
同上 | 找不到鸿蒙工程,构建脚本报错 |
| OHOS_NDK_HOME | 含 native/ 的 SDK 目录 | ohos-arkui-sys
等 ohos-* crate 的 build.rs |
panicked at ohos-arkui-sys-0.0.1/build.rs:4:42: OHOS_NDK_HOME not set: NotPresent |
| OHOS_HOME | 同上 | 部分 ohos 工具链 | 同上(不同 crate 认的名字不一样) |
| TAURI_ENV_DEBUG=false | 固定值 | tauri
crate 的 build.rs |
被判成 dev 构建,前端资源加载失败(第 7 章) |
OHOS_NDK_HOME 那一条是我第一次构建时踩到的真实报错,原文:
error: failed to run custom build command for `ohos-arkui-sys v0.0.1`
thread ‘main’ panicked at …/ohos-arkui-sys-0.0.1/build.rs:4:42:
OHOS_NDK_HOME not set: NotPresent
本机上这些路径都能从环境推导,不需要写死:
export HMOS_CLT_HOME=/data/service/hnp/hmos-clt.org/hmos-clt_1.0.0
export OHOS_HOME=”$HMOS_CLT_HOME/sdk/default/openharmony”
export OHOS_NDK_HOME=”$OHOS_HOME”
6.4 坑三:没有cc,而且链接出来的东西还不能执行
环境变量补齐后,构建会走到一个很直接的错误:
error: linker `cc` not found
|
= note: No such file or directory (os error 2)
rustc 生成可执行文件/动态库时要调系统 C 链接器(默认名 cc),而本机没有。可用的替代品是 OpenHarmony SDK 里那套能在设备上原生运行的 LLVM:
SDK=/data/service/hnp/hmos-clt.org/hmos-clt_1.0.0/sdk/default/openharmony
ls “$SDK/native/llvm/bin” | head
# aarch64-unknown-linux-ohos-clang ← 171 字节的包装脚本
# aarch64-unknown-linux-ohos-clang++
# clang clang++ ld.lld llvm-ar llvm-objcopy llvm-readelf …
那个 aarch64-unknown-linux-ohos-clang 的内容值得一看,它替我们补了三件事:
#!/bin/sh
SOURCE=$(dirname — “$( readlink -f — “$0″; )”)
exec $SOURCE/clang \
-target aarch64-linux-ohos \
–sysroot=$SOURCE/../../sysroot \
-D__MUSL__ \
“$@”
但是——链路还没通。用它链接出的产物执行时会:
$ ./libtest_bin
sh: ./libtest_bin: Operation not permitted # 126
因为未签名。第 2.3 节那张三态表就是在这个阶段被测出来的。我试过两条弯路,记下来省得你重复:
| 尝试 | 结果 |
|---|---|
| 链接时加 -Wl,–code-sign(lld 确有这个开关) | .codesign
段变大(20 KB → 24 KB),但内核依然不认,Operation not permitted |
| 直接 binary-sign-tool sign -inFile x -outFile x -selfSign 1 | 报 [WriteCodeSignBlock] .codesign section already exists,工具拒绝在已有占位段上签 |
能用的组合是先剥段、再自签名:
SDK=/data/service/hnp/hmos-clt.org/hmos-clt_1.0.0/sdk/default/openharmony
OC=”$SDK/native/llvm/bin/llvm-objcopy”
BST=”$SDK/toolchains/lib/binary-sign-tool”
# 1) 摘掉 lld 留下的占位 .codesign 段
“$OC” –remove-section=.codesign ./out ./out.un && mv -f ./out.un ./out
# 2) 自签名(-selfSign 1 用内置自签证书,不需要额外密钥)
“$BST” sign -inFile ./out -outFile ./out -selfSign 1
# 3) 这次能执行了
./out
手工做是不可持续的:**cargo 每次重新链接都会生成新文件,包括过程宏的 .so**。正确做法是让链接器自己带签名——把上面三步封成一个“链接器包装器”:
#!/bin/sh
# ~/.harmonybrew/libexec/ohos-cc-sign
# 1. 系统里没有 cc/ld,链接交给 OpenHarmony SDK 自带的 clang;
# 2. 链接产物必须自签名,否则内核以 “Operation not permitted” 拒绝执行;
# 而 lld 会预留一个占位 .codesign 段,binary-sign-tool 见到它会报
# “.codesign section already exists”,所以先摘掉再签。
set -u
SDK=”${OHOS_SDK_HOME:-/data/service/hnp/hmos-clt.org/hmos-clt_1.0.0/sdk/default/openharmony}”
# 按调用名挑选 C / C++ 驱动:ohos-cc-sign -> clang,ohos-cxx-sign -> clang++
# (C++ 必须用 clang++,否则链接期不会带上 C++ 标准库)
case”${0##*/}”in
*cxx* | *++*) CLANG=”$SDK/native/llvm/bin/aarch64-unknown-linux-ohos-clang++” ;;
*) CLANG=”$SDK/native/llvm/bin/aarch64-unknown-linux-ohos-clang” ;;
esac
OBJCOPY=”$SDK/native/llvm/bin/llvm-objcopy”
SIGNTOOL=”$SDK/toolchains/lib/binary-sign-tool”
# 从参数里取出输出文件(cargo/rustc 与 cc-rs 都用 -o)
out=””
prev=””
for a in”$@”; do
case”$prev”in
-o | –output) out=”$a” ;;
esac
case”$a”in
-o?*) out=”${a#-o}” ;;
–output=*) out=”${a#–output=}” ;;
esac
prev=”$a”
done
“$CLANG””$@” || exit $?
[ -n “$out” ] && [ -f “$out” ] || exit 0
tmp=”$out.unsigned.$$”
if”$OBJCOPY” –remove-section=.codesign “$out””$tmp” 2>/dev/null; then
[ -x “$tmp” ] || chmod 755 “$tmp”
mv -f “$tmp””$out” || rm -f “$tmp”
fi
“$SIGNTOOL” sign -inFile “$out” -outFile “$out” -selfSign 1 >/dev/null 2>&1
exit 0
几个设计点:
| 代码 | 为什么 |
|---|---|
| case “${0##*/}”
按调用名切 clang/clang++ |
一个脚本兼作 C 与 C++ 驱动;C++ 用 clang 当驱动会漏掉标准库链接 |
| 解析 -o / –output | 输出路径只能从参数里取,rustc 与 cc-rs 都用 -o |
| 先 –remove-section=.codesign 再 sign | 唯一能过 binary-sign-tool 的顺序 |
| 签名失败不返回非零 | 只链 .o/.rlib 时本就不需要签名,不该让构建失败 |
| OHOS_SDK_HOME
可覆盖 |
hmos-clt 升级后 SDK 路径里的版本号会变 |
装好并建一个 C++ 名字的入口:
chmod 755 ~/.harmonybrew/libexec/ohos-cc-sign
ln -sfn ohos-cc-sign ~/.harmonybrew/libexec/ohos-cxx-sign # 名字带 cxx,脚本会自动切 clang++
然后把它告诉 cargo(~/.cargo/config.toml):
[net]
# 本机 cargo 内置的 git 客户端取 GitHub 时 TLS 会失败(SSL error, class=Ssl),
# 交给系统 git 去取。
git-fetch-with-cli = true
[target.aarch64-unknown-linux-ohos]
linker = “/home/<你的用户名>/.harmonybrew/libexec/ohos-cc-sign”
ar = “/data/service/hnp/hmos-clt.org/hmos-clt_1.0.0/sdk/default/openharmony/native/llvm/bin/llvm-ar”
[env]
# 给需要编译 C 代码的 crate(*-sys 那类,走 cc-rs)用
CC_aarch64_unknown_linux_ohos = “/home/<你的用户名>/.harmonybrew/libexec/ohos-cc-sign”
AR_aarch64_unknown_linux_ohos = “/data/service/hnp/hmos-clt.org/hmos-clt_1.0.0/sdk/default/openharmony/native/llvm/bin/llvm-ar”
6.5 构建脚本与实测耗时
把 6.3、6.4 节的东西合起来就是一个可重复的构建脚本(tools/build-rust.sh):
#!/bin/sh
set -eu
APP_DIR=$(cd”$(dirname “$0″)/..” && pwd)
RUST_DIR=”$APP_DIR/src-tauri”
OHOS_PROJECT=”$RUST_DIR/gen/ohos”
LIB_NAME=tauri_demo_lib # 与 src-tauri/Cargo.toml 的 [lib] name 一致
BUNDLE=com.atomcode.harmonytauri # 与 gen/ohos/AppScope/app.json5 的 bundleName 一致
TARGET=aarch64-unknown-linux-ohos
export HMOS_CLT_HOME=”${HMOS_CLT_HOME:-/data/service/hnp/hmos-clt.org/hmos-clt_1.0.0}”
export OHOS_HOME=”${OHOS_HOME:-$HMOS_CLT_HOME/sdk/default/openharmony}”
export OHOS_NDK_HOME=”${OHOS_NDK_HOME:-$OHOS_HOME}”
export PATH=”$HOME/.harmonybrew/bin:$HMOS_CLT_HOME/ohpm/bin:$HMOS_CLT_HOME/hvigor/bin:$PATH”
# tauri-cli 构建时会注入这些变量,手工构建必须自己给(见 6.3 节)
export WRY_OHOS_PACKAGE=”$BUNDLE”
export TAURI_OHOS_PACKAGE_UNESCAPED=”$BUNDLE”
export WRY_OHOS_LIBRARY=”$LIB_NAME”
export TAURI_OHOS_PROJECT_PATH=”$OHOS_PROJECT”
export TAURI_ENV_DEBUG=false
cd”$RUST_DIR”
# 同步运行时配置:OHOS 侧运行时读的是 HAP 里的 assets/tauri.conf.json
echo”== 同步 assets/tauri.conf.json ==”
mkdir -p “$OHOS_PROJECT/assets”
cp -f “$RUST_DIR/tauri.conf.json””$OHOS_PROJECT/assets/tauri.conf.json”
echo”== 编译 $LIB_NAME($TARGET,release)==”
cargo build –release –target “$TARGET” –lib
SO=”$RUST_DIR/target/$TARGET/release/lib$LIB_NAME.so”
DEST=”$OHOS_PROJECT/entry/libs/arm64-v8a”
mkdir -p “$DEST”
cp -f “$SO””$DEST/”
ls -l “$DEST/lib$LIB_NAME.so”
实测耗时(20 核 aarch64,release):
| 场景 | 耗时 |
|---|---|
| 首次全量(含 300+ crate) | 2 分 02 秒 |
| 改动 tauri.conf.json 后重编(配置嵌入二进制,需重新编译 tauri-codegen 与自身 crate) | 44 秒 |
| 改动特性(custom-protocol)后重编 | 30 秒 |
| 只改 lib.rs | 通常 10 ~ 20 秒 |
一个会在日志里出现、但完全无害的警告:
warning: hard linking files in the incremental compilation cache failed. copying files instead.
本机文件系统不支持硬链接,cargo 自动退化为复制文件,只影响增量编译的缓存效率,不影响正确性。
7. 三个坑:前端资源为什么加载不出来
这一章是整篇文章里最值得看的部分。它会经历一次完整的“界面能打开、但里面是空的”的排查,并且三个不同的根因指向同一句报错。
7.1 现象与第一课:这条错误不在日志里
应用装上了,进程也在跑:
$ hdc shell “ps -ef | grep -i harmonytauri | grep -v grep”
20020185 58455 670 0 16:46:31 ? 00:00:05 com.atomcode.harmonytauri
20100390 58695 5352 0 16:46:31 ? 00:00:00 com.atomcode.harmonytauri:gpu
20100391 58823 5352 0 16:46:31 ? 00:00:00 com.atomcode.harmonytauri:render
20100392 58849 5352 0 16:46:31 ? 00:00:00 com.atomcode.harmonytauri:render
窗口标题也是对的,但 webview 里只有一行字:
Failed to request tauri://localhost/: builder error for url (tauri://localhost/)
第一课:这句话是渲染在 webview 错误页里的文本,不会进 hilog。 我一开始在 hilog 里 grep 这个关键字,得到 0 次命中,就误判“已经修好了”——直到用界面节点树去读才打脸。
正确的验证手段是 devecocli 的 ui 子命令,它能直接读到设备上的界面节点(包括 webview 内部渲染出来的文本):
# 读界面节点树
devecocli ui layout
# 截图(我会把 PNG 拉下来直接看)
devecocli ui screenshot –path ./build-output/screenshot.png
出问题时的节点树长这样(节选):
DecorBar#ContainerModalTitleRow [597,362,2687,432]
Text#enhanceAppLabel [694,379,899,415] “鸿蒙 Tauri 应用”
Stack#ContainerModalStack [597,432,2687,1756]
genericContainer [612,456,2674,490] “Failed to request tauri://localhost/: builder error for url (tauri://localhost/)”
修好之后的同一位置(节选):
Stack#ContainerModalStack [597,433,2687,1558]
rootWebArea [597,433,2687,1558] scrollable
heading [852,493,2434,561] “鸿蒙 Tauri 应用”
button [891,837,1120,907] “获取应用信息” clickable
genericContainer [891,932,2395,1369] “{\n \”product\”: \”harmony-tauri-app\”,\n \”ohos\”: true, …”
paragraph [891,1703,1999,1734] “Hello, 鸿蒙开发者! You’ve been greeted from Rust!”
注意最后一行:连 greet 命令的返回都出现在节点文本里了——这同时也证明了 IPC 是通的。这种“读 UI 树”的验证方式在移动端调试里非常高效,不用靠肉眼比对截图。
7.2 根因一:这次构建被判成了 dev 构建
报错文案很好用,直接拿去搜源码,命中在 tauri 的资源协议处理器里:
// crates/tauri/src/protocol/tauri.rs:190
Err(e) => {
let error_message = format!(
“Failed to request {}: {}{}”,
url.as_str(),
e,
…
而这段代码位于 #[cfg(all(dev, mobile))] 的分支里——也就是说,只有“dev 构建 + 移动端”才会走进这条把请求转发给开发服务器的路径:
// crates/tauri/src/manager/webview.rs:40
pub(crate) const PROXY_DEV_SERVER: bool = cfg!(all(dev, mobile));
那 dev 是谁给的呢?顺着一条链往上追:
// crates/tauri-build/src/lib.rs:509
cfg_alias(“dev”, is_dev());
// crates/tauri-build/src/lib.rs:417
pub fn is_dev() -> bool {
env::var(“DEP_TAURI_DEV”).expect(“missing `cargo:dev` instruction, please update tauri to latest”)
== “true”
}
DEP_TAURI_DEV 来自 tauri crate 自己的构建脚本:
// crates/tauri/build.rs:252-261
alias(“custom_protocol”, custom_protocol);
alias(“dev”, dev);
println!(“cargo:dev={dev}”);
let target_os = std::env::var(“CARGO_CFG_TARGET_OS”).unwrap();
let target_env = std::env::var(“CARGO_CFG_TARGET_ENV”).unwrap_or_default();
let mobile = target_os == “ios” || target_os == “android” || target_env == “ohos”;
alias(“desktop”, !mobile);
alias(“mobile”, mobile);
结论很清楚:
- mobile 对鸿蒙是成立的(target_env == “ohos”);
- dev 的判定则是 Tauri 的老规矩——没有启用 custom-protocol 特性就视为 dev;
- 于是 PROXY_DEV_SERVER = true,资源请求被转发给”开发服务器”;而我们的配置里没有 devUrl,转发目标退化成 tauri://localhost/,reqwest 解析不了这个 scheme,抛出 builder error。
修法(两处一起做):
# src-tauri/Cargo.toml
tauri = { git = “…”, branch = “feat/open-harmony”, features = [“custom-protocol”] }
# 构建脚本里显式声明生产构建
export TAURI_ENV_DEBUG=false
修复前后的体积变化是个很好的旁证:.so 从 10.7 MB 降到 9.4 MB,掉的那 1.3 MB 正是 dev 专属代码。
这里也有个值得记住的结论:**用 cargo build –release 并不等于”生产构建”**。Tauri 的 dev/prod 判定走的是自己的 cfg 体系,与 cargo 的 profile 是两回事。
7.3 根因二:运行时读的那份配置没同步
Tauri 的 gen/ohos/assets/tauri.conf.json 是运行时真正读的配置(tauri-cli 的 inject_resources 会在每次构建时把解析后的配置写进去)。我们改成手工构建之后,这份文件还是参考 demo 的旧内容:
{
“productName”: “tauri-demo”,
“identifier”: “com.ranger.tauri-demo”,
“build”: { “devUrl”: “http://localhost:1420/”, “frontendDist”: “../dist” },
“app”: { “withGlobalTauri”: false }
}
它和我们编译进二进制的配置完全对不上。修法就是把它纳入构建流程——在 tools/build-rust.sh 里加一步同步(见 6.5 节脚本)。这也解释了为什么”改了 tauri.conf.json 却没有任何效果”是很常见的一种困惑。
7.4 根因三:窗口没有显式指定 url
再看 demo 那份“已解析”的配置,它的窗口是:
“windows”: [{ “label”: “main”, “url”: “index.html”, “title”: “tauri-demo”, … }]
而我们的窗口只写了标题和尺寸。缺了 url,窗口就会去请求根路径——**正好就是报错里的 tauri://localhost/**:
“windows”: [
{ “label”: “main”, “url”: “index.html”, “title”: “鸿蒙 Tauri 应用”, “width”: 900, “height”: 640 }
]
7.5 三条经验
- 同一句报错可能对应多个根因,改完一个别急着收工,用 ui layout 再读一次界面;
- 验证要用”能读到内容”的手段(界面节点树 / 截图),不要只看日志——错误页不进日志;
- 移动端 Tauri 有三份配置概念:源码里的 tauri.conf.json(编译期嵌入)、gen/ohos/assets/tauri.conf.json(运行期读取)、以及窗口/安全等默认值补全后的”已解析配置”。三者不一致时,症状往往就是”界面空白 + 一句含糊的错误”。
7.6 顺带一个不致命的警告
hvigor 构建时会有一条警告:
WARN: …/oh_modules/.ohpm/@ohos-rs+ability@0.4.0-beta.5/oh_modules/@ohos-rs/ability/src/main/module.json does not exist.
This library will not be merged. This may occur when the project is manually modified and the local library
is not declared in build-profile.json5.
它说的是 @ohos-rs/ability 这个 ohpm 包里没有 module.json,hvigor 不知道该怎么把它的资源合并进主模块。实测不影响运行(应用正常启动、webview 正常渲染),属于上游包元数据不全导致的提示。知道它的存在即可,不必为此改工程结构。
8. HAP 构建与签名
8.1 先把 HAP 组出来
Rust 侧的 .so 放进 gen/ohos/entry/libs/arm64-v8a/ 之后,就可以用 hvigor 组装 HAP 了:
cd src-tauri/gen/ohos
export PATH=”$HMOS_CLT_HOME/ohpm/bin:$HMOS_CLT_HOME/hvigor/bin:$PATH”
ohpm install
hvigorw assembleHap –no-daemon –stacktrace
ohpm install 会从鸿蒙仓库拉 ArkTS 依赖:
ohpm INFO: fetch package done 1 @ohos-rs/ability from https://repo.harmonyos.com/ohpm/@ohos-rs/ability/-/ability-0.4.0-beta.5.har
install completed in 1s 16ms
hvigor 的任务链走完这一串(节选)后就会产出产物:
Finished :entry:default@ConfigureCmake
Finished :entry:default@BuildNativeWithCmake # 编译模板自带的 C++ 库
Finished :entry:default@BuildNativeWithNinja
Finished :entry:default@ProcessLibs # 把 libs/arm64-v8a 下的 .so 收进包
Finished :entry:default@CompileArkTS
Finished :entry:default@PackageHap
Finished :entry:default@SignHap
Finished :entry:assembleHap
BUILD SUCCESSFUL in 12 s 217 ms
产物在:
entry/build/default/outputs/default/entry-default-unsigned.hap
构建期会有两条警告,都不致命:
WARN: The project has not explicitly set the ‘targetSdkVersion’ version at build-profile.json5. It is recommended to configure it.
WARN: …/@ohos-rs+ability@0.4.0-beta.5/…/module.json does not exist. This library will not be merged.
以及一条无害的 clang 提示(hvigor 给 C++ 传了它不用的参数):
clang++: warning: argument unused during compilation: ‘–gcc-toolchain=…/hms/native/BiSheng’ [-Wunused-command-line-argument]
8.2 坎一:hvigor 的 signingConfigs 不接受明文密码
我最初的思路很自然:SDK 里就带着 OpenHarmony 的签名材料(OpenHarmony.p12、OpenHarmonyApplication.pem、profile 模板),那把密码直接写进 build-profile.json5 不就行了?结果构建到 SignHap 直接失败:
hvigor ERROR: Failed :entry:default@SignHap
hvigor ERROR: 00303116 Configuration Error
Error Message: The length of the storePassword or keyPassword field in the signature configuration is less than 32.
At file: …/gen/ohos/build-profile.json5
at DecipherUtil.decryptPwd (…/hvigor-ohos-plugin/src/utils/decipher-util.js)
原因是 hvigor 会把这两个字段当密文解密(DevEco Studio 用本地密钥加密后写入,密文长度 ≥ 32),明文会被直接判为非法。这条路只有两个出口:
- 让 hvigor 产出未签名包,签名用 hap-sign-tool 自己来(CI 常用做法);
- **让 DevEco / devecocli 把加密后的签名配置写进 build-profile.json5**(8.4 节)。
我先把第 1 条走通了:
# build-profile.json5 里 “signingConfigs”: [],去掉产品上的 “signingConfig”
hvigorw assembleHap –no-daemon # 产出 entry-default-unsigned.hap
hap-sign-tool sign-app \
-keyAlias “openharmony application release” -signAlg “SHA256withECDSA” -mode “localSign” \
-appCertFile “$SDK_LIB/OpenHarmonyApplication.pem” \
-profileFile “./signing/harmony-tauri-app.p7b” \
-inFile “entry-default-unsigned.hap” \
-outFile “harmony-tauri-app-signed.hap” \
-keystoreFile “$SDK_LIB/OpenHarmony.p12” -keyPwd “123456” -keystorePwd “123456”
其中那个 .p7b 是我自己签出来的:把 SDK 的 UnsgnedDebugProfileTemplate.json 里的包名、设备 UDID、有效期改成本机信息,再用 hap-sign-tool sign-profile 签一遍。本地验签是通过的:
$ hap-sign-tool verify-app -inFile harmony-tauri-app-signed.hap -outCertChain /tmp/v.cer -outProfile /tmp/v.p7b
INFO – hap verify successed!
INFO – verify-app success
8.3 坎二:9568257 —— 签名合法,但设备不认
本地验签过了,装到设备上却是:
$ hdc install -r build-output/harmony-tauri-app-signed.hap
[Info]App install path:…/harmony-tauri-app-signed.hap msg:error: failed to install bundle.
code:9568257 error: fail to verify pkcs7 file.
这个错误码的官方解释很直白(华为开发者 FAQ / 社区答复):
NEXT 项目不允许 OH 根证书的应用安装,请使用商用证书签名安装。
出现该问题的原因是应用当前使用的签名不符合 HarmonyOS 应用签名要求。通常是由于当前使用的是 OpenHarmony 应用的签名,需替换为 HarmonyOS 应用的签名。
也就是说:SDK 自带的 OpenHarmony.p12 / OpenHarmonyApplication.pem 这一套 OpenHarmony 根证书,在 HarmonyOS(NEXT)设备上天生不被信任,只能用于本地验证与 OpenHarmony 设备/模拟器。要装到鸿蒙 PC 上,必须换成华为侧签发的材料:
- 在 AppGallery Connect(AGC) 申请调试证书(.cer)和密钥库(.p12);
- 在 AGC 设备管理里用它注册本机 UDID;
- 在 AGC 申请 HAP Provision Profile(选中该设备)得到 .p7b。
本机 UDID 的获取方式:
$ hdc shell bm get –udid
udid of current device is :
2B7F77180E0C23FA383DC8F24BD9CD0D3AF444A700351F27E46D6E06D6B3DF89
这是一条外部依赖:它需要华为开发者账号。所以我没有手工去 AGC 走一遍,而是用了 DevEco 官方 CLI —— devecocli 把这一整套自动化了。
8.4 用 devecocli 拿到 AGC 签名材料
devecocli 是 DevEco 的命令行工具(npm 包 @deveco/deveco-cli),它把 hvigor / ohpm / hdc / 签名流程都包了一层:
npm install -g @deveco/deveco-cli
$ devecocli auth status
Current user: 坚**** # 已登录华为开发者账号
$ cd src-tauri/gen/ohos && devecocli signature generate –product default
Executing signature generate command
Start generating p12
Start generating csr
Start generating certificate
Start generating profile
Signing config written to …/src-tauri/gen/ohos/build-profile.json5
Signature generation completed successfully.
它在本地产出 .p12 / .csr / .cer / .p7b 四件套(放在 ~/.ohos/config/),并把 signingConfigs 写进工程配置——注意密码是加密态,这正是 hvigor 需要的形态:
“signingConfigs”: [
{
“name”: “default”,
“type”: “HarmonyOS”,
“material”: {
“certpath”: “/storage/Users/currentUser/.ohos/config/default_ohos_….cer”,
“keyAlias”: “debugKey”,
“keyPassword”: “0000001b75313f897d24c8499e1abdde2e0f9982a81ca227f3d5bb29fad8bd4a9c4c0e6906f26f2fd25226”,
“profile”: “/storage/Users/currentUser/.ohos/config/default_ohos_….p7b”,
“signAlg”: “SHA256withECDSA”,
“storeFile”: “/storage/Users/currentUser/.ohos/config/default_ohos_….p12”,
“storePassword”: “0000001b75313f897d24c8499e1abdde2e0f9982a81ca227f3d5bb29fad8bd4a9c4c0e6906f26f2fd25226”
}
}
]
两个前提条件(脚本化时要注意):已登录(devecocli auth login,会拉起浏览器 OAuth)且设备已连接(要把 UDID 写进 profile)。
8.5 一条命令:构建 + 签名 + 安装 + 启动
签名配置就位之后,整个部署就是一条命令:
cd src-tauri/gen/ohos
devecocli run –module entry
真实输出(尾部):
[hvigor build] Running…
> hvigor Finished :entry:default@CompileArkTS… after 5 s 209 ms
> hvigor Finished :entry:default@PackageHap… after 170 ms
> hvigor Finished :entry:default@SignHap… after 532 ms
> hvigor BUILD SUCCESSFUL in 11 s 951 ms
Build completed successfully.
Installing artifacts to device 127.0.0.1:34953…
App installed successfully
Launching com.atomcode.harmonytauri/EntryAbility…
Application ‘com.atomcode.harmonytauri’: start ability successfully.
它内部做的事和 8.1 ~ 8.4 节手工做的一模一样(ohpm install → hvigor 构建 → SignHap → hdc 安装 → aa start),只是不用自己拼参数。常用参数:
| 参数 | 用途 |
|---|---|
| –module entry | 指定模块(多模块工程必须给) |
| –build-mode release | 出 release 包(默认 debug) |
| –uninstall | 先卸载旧包(换过签名密钥时很有用) |
| –skip-build | 跳过构建,只部署已有产物 |
| –device <serial> | 多设备时指定目标 |
8.6 签名工具速查(hap-sign-tool)
SDK 里的签名工具是原生二进制(不是 jar),用法是 <子命令> [参数],不吃 –help。常用三个子命令(均在 $DEVECO_SDK_HOME/default/openharmony/toolchains/lib/ 下):
T=$DEVECO_SDK_HOME/default/openharmony/toolchains/lib
# 1) 签 profile(把 Unsgned*ProfileTemplate.json 变成 .p7b)
“$T/hap-sign-tool” sign-profile \
-keyAlias “openharmony application profile release” -signAlg SHA256withECDSA -mode localSign \
-profileCertFile “$T/OpenHarmonyProfileRelease.pem” \
-inFile profile.json -keystoreFile “$T/OpenHarmony.p12” -outFile app.p7b \
-keyPwd 123456 -keystorePwd 123456
# 2) 签 HAP
“$T/hap-sign-tool” sign-app \
-keyAlias “openharmony application release” -signAlg SHA256withECDSA -mode localSign \
-appCertFile “$T/OpenHarmonyApplication.pem” -profileFile app.p7b \
-inFile unsigned.hap -outFile signed.hap \
-keystoreFile “$T/OpenHarmony.p12” -keyPwd 123456 -keystorePwd 123456
# 3) 本地验签(注意:验签通过 ≠ 设备会接受,见 8.3 节)
“$T/hap-sign-tool” verify-app -inFile signed.hap -outCertChain /tmp/v.cer -outProfile /tmp/v.p7b
顺带记一个“顺手发现”:OpenHarmony.p12 里读不出可读的别名(p12 是二进制/加密的),但官方文档给出的小写别名 openharmony application profile release / openharmony application release 在本机实测可用,密码都是 123456。
9. 安装、运行与调试
9.1 安装与启动的两条路径
路径一:devecocli(推荐,一步到位)
cd src-tauri/gen/ohos && devecocli run –module entry
路径二:纯 hdc(CI 或脚本化时用)
# 安装(-r 表示覆盖安装)
hdc install -r build-output/harmony-tauri-app-signed.hap
# 启动指定 Ability
hdc shell aa start -a EntryAbility -b com.atomcode.harmonytauri -m entry
# 卸载(换签名密钥后建议先卸载,否则可能报 “install sign info inconsistent”)
hdc uninstall com.atomcode.harmonytauri
官方文档里还有一种“推送再安装”的等价写法,适合设备端空间受限的场景:
hdc file send entry-default-signed.hap data/local/tmp/app.hap
hdc shell bm install -p data/local/tmp/app.hap
hdc shell rm -rf data/local/tmp/app.hap
9.2 看日志
# 应用侧日志(Rust 的 stdout/stderr 会带 RustStdoutStderr 标签)
hdc shell hilog | grep -iE ‘tauri|RustStdoutStderr’
# 用 devecocli 按包名取日志(含崩溃日志)
devecocli log –bundle-name com.atomcode.harmonytauri
启动一个 ArkWeb 应用时,日志里会出现大量 Chromium 内部行(chromium: 前缀),比如:
I A04510/com.atomcode.harmonytauri/chromium: [background_task_policy.cc:214] visible_page_num_: 1
D A04511/com.atomcode.harmonytauri:gpu/chromium: [vulkan_surface.cc:329] vulkan old swap chain width: 2090 height: 1324
visible_page_num_: 1 表示页面在前台可见——这类行可以用来确认 webview 是否真的起来了(但不是判断“内容渲染正确”的依据,见 9.3)。
9.3 验证界面:读节点树比看截图更有效
devecocli ui 提供了一整套设备 UI 操作:
devecocli ui layout # 打印界面节点树(能读到 webview 里的文本)
devecocli ui screenshot –path ./shot.png # 截图
devecocli ui click 1000 870 # 点击坐标(可用来点按钮)
devecocli ui text “输入内容” 1500 1640 # 往输入框打字
devecocli ui window list # 列出窗口
第 7 章已经演示过:节点树能直接读到 webview 渲染出来的文本。修好之后我读到的内容(节选,注意最后一行是 greet 命令的返回值):
heading [852,493,2434,561] “鸿蒙 Tauri 应用”
button [891,837,1120,907] “获取应用信息” clickable
genericContainer [891,932,2395,1369] “{\n \”product\”: \”harmony-tauri-app\”,\n \”version\”: \”0.1.0\”, … \”ohos\”: true, …}”
textField [891,1605,1999,1677] “鸿蒙开发者” clickable
paragraph [891,1703,1999,1734] “Hello, 鸿蒙开发者! You’ve been greeted from Rust!”
9.4 一个小插曲:别在锁屏状态下验证界面
我第一次跑 ui layout 时拿到的全是锁屏节点:
WindowScene#session18
Stack#SCBScreenLock_Screen_lock_home
Stack#slide_screen_lock
Text#ScreenLock-TimeView_Text_Digital_Text “07:42”
TextInput#screenLock_pc_TextInput_password clickable
Text “使用指纹或密码解锁”
ui layout 只返回屏幕上可见的节点,所以屏幕锁定/应用不在前台时,你看到的就不是应用界面。验证前顺手确认一下:应用是不是前台窗口(必要时 devecocli run 重新拉起一次,或先解锁屏幕)。
9.5 完整的验证清单
| 检查项 | 命令 | 期望 |
|---|---|---|
| 设备在线且是 2in1 | devecocli device list | device 2in1 |
| Rust 库已进包 | unzip -l build-output/*.hap | grep libtauri | 看到 libs/arm64-v8a/libtauri_demo_lib.so |
| 包信息正确 | unzip -p <hap> module.json | bundleName
对、deviceTypes 含 2in1 |
| 装上了 | hdc shell “bm dump -a | grep harmonytauri” | 列出包名 |
| 在运行 | hdc shell “ps -ef | grep harmonytauri” | 有主进程 + gpu/render 子进程 |
| 界面渲染正确 | devecocli ui layout | 能看到自己的界面文本 |
| IPC 通了 | 点按钮后看 ui layout | 节点里出现命令返回值 |
10. 踩坑总表
按“你在终端里看到了什么”组织,方便直接检索。同一句报错可能对应多个根因(尤其第 3 ~ 5 条)。
| 报错 / 现象 | 根因 | 修法 | 章节 |
|---|---|---|---|
| code:9568257 error: fail to verify pkcs7 file | HarmonyOS(NEXT)不信任 OpenHarmony 根证书签的应用 | 用 AGC 的调试证书 + 绑定本机 UDID 的 Profile;devecocli signature generate 可自动化 | 8.3 / 8.4 |
| 00303116 Configuration Error … storePassword … less than 32 | hvigor 要求签名密码是 DevEco 加密态,拒收明文 | 让 hvigor 出未签名包再 hap-sign-tool sign-app;或让 devecocli 写加密配置 | 8.2 |
| webview 里只有 Failed to request tauri://localhost/: builder error for url | 构建被判成 dev(未启用 custom-protocol),资源请求被转发给开发服务器 | tauri 依赖加 features = [“custom-protocol”],并 TAURI_ENV_DEBUG=false | 7.2 |
| 同上 | 运行时读的 gen/ohos/assets/tauri.conf.json 没同步 | 构建脚本里同步注入这份配置 | 7.3 |
| 同上 | 窗口没写 url,默认请求根路径 | 窗口加 “url”: “index.html” | 7.4 |
| error: linker \
cc` not found` |
系统无 C 工具链 | 用 SDK 的 clang 做链接器包装器 + ~/.cargo/config.toml | 6.4 |
| Operation not permitted
(126) |
产物的 .codesign 是占位段,签名不合法 | 剥段 + binary-sign-tool -selfSign 1(包装器已自动做) | 2.3 / 6.4 |
| Permission denied
(126) |
文件里完全没有.codesign 段(如上游官方构件) | 逐个二进制与 .so 都签名 | 2.3 / 6.4 |
| [WriteCodeSignBlock] .codesign section already exists | 直接签名时占位段还在 | 先 llvm-objcopy –remove-section=.codesign | 6.4 |
| panicked at ohos-arkui-sys-0.0.1/build.rs: OHOS_NDK_HOME not set: NotPresent | 缺构建期环境变量 | 注入 OHOS_NDK_HOME(指向含 native/ 的 SDK 目录) | 6.3 |
| SSL error: unknown error; class=Ssl (16)
(cargo 取依赖时) |
cargo 内置 git 客户端 TLS 在本机不可用 | ~/.cargo/config.toml
加 [net] git-fetch-with-cli = true |
6.2 |
| cannot access component in path ‘…’: Interrupted system call … | cargo 的 git checkout 不重试 EINTR | 用 git archive + 空 .cargo-ok 重铺 checkout,再重试 | 6.2 |
| ohpm
报 node: … Permission denied |
PATH 前面有个跑不起来的旧 node | 把 ~/.local/bin/{node,npm,npx} 指回可用的 node | 4.2 |
| WARN: … @ohos-rs/ability … module.json does not exist. This library will not be merged. | ohpm 包元数据不全 | 忽略,实测不影响运行 | 7.6 |
| WARN: The project has not explicitly set the ‘targetSdkVersion’ | 工程配置未显式声明 | 忽略(或按提示补上) | 8.1 |
| clang++: warning: argument unused … ‘–gcc-toolchain=…’ | hvigor 给 C++ 传了不用的参数 | 忽略 | 8.1 |
| warning: hard linking files in the incremental compilation cache failed | 文件系统不支持硬链接 | 忽略,只是增量缓存效率略降 | 6.5 |
| error:install sign info inconsistent | 设备上残留了用另一套密钥签的同包名应用 | devecocli run –uninstall
/ hdc uninstall <包名> |
9.1 |
| zcat: not gzip
+ tar: had errors(解 .tar.xz 时) |
本机 toybox 的 tar 不支持 xz | 用装好的 xz -dc x.tar.xz > x.tar 再解 | 上篇教程 |
11. 工程结构与一键复现
11.1 目录树
harmony-tauri-app/
├── web/ # 零依赖前端(编译期被嵌入 Rust 库)
│ ├── index.html
│ ├── main.js # 通过 window.__TAURI__ 调 Rust 命令
│ └── style.css
├── signing/ # 自签调试 profile(本地验签用)
│ └── debug-profile.json
├── tools/
│ ├── cargo-fetch-retry.sh # 拉依赖(自动绕开 EINTR checkout 故障)
│ ├── build-rust.sh # 编译 Rust 库 + 同步运行时配置 + 放进 gen/ohos
│ ├── build-hap.sh # hvigorw 出未签名包 + hap-sign-tool 显式签名
│ └── install-hap.sh # 纯 hdc 安装与启动(失败时打印 UDID 与指引)
├── build-output/ # 产物与验证截图(screenshot*.png)
└── src-tauri/
├── Cargo.toml # tauri fork + patch + custom-protocol
├── tauri.conf.json # 应用身份、窗口、frontendDist
├── src/
│ ├── lib.rs # 命令(greet/app_info)+ N-API 回调样例
│ └── main.rs # 桌面入口(鸿蒙不用)
├── tauri-plugin-ohos-demo/ # 参考用的插件工程(Rust + guest-js)
└── gen/ohos/ # 鸿蒙工程(等同 tauri ohos init 的产物)
├── AppScope/app.json5
├── build-profile.json5 # 内含 devecocli 写入的 signingConfigs
├── assets/tauri.conf.json# 运行时真正读的配置
└── entry/
├── src/main/ets/entryability/EntryAbility.ets
├── src/main/cpp/types/libtauri_demo_lib/Index.d.ts
└── libs/arm64-v8a/libtauri_demo_lib.so # 构建产物落点
11.2 三个”必须三处一致”的名字
这是配置类问题里最省时间的自查表:
| 概念 | 出现的位置(都要一致) |
|---|---|
| 包名 | tauri.conf.json
的 identifier;gen/ohos/AppScope/app.json5 的 bundleName;构建期 WRY_OHOS_PACKAGE / TAURI_OHOS_PACKAGE_UNESCAPED |
| 库名 | Cargo.toml
的 [lib] name;EntryAbility.ets 的 moduleName 与 import … from ‘lib<名字>.so’;entry/oh-package.json5 里的 “lib<名字>.so”: “file:./src/main/cpp/types/lib<名字>”;以及 types/lib<名字>/ 目录名 |
| 应用显示名 | gen/ohos/AppScope/resources/base/element/string.json
的 app_name;entry/…/string.json 的 EntryAbility_label |
11.3 从零到跑通的完整命令序列
# 0) 前置:Rust(aarch64-unknown-linux-ohos host)、ohpm/hvigorw、devecocli、可用的 node
# 以及 ~/.cargo/config.toml(linker + ar + [net] git-fetch-with-cli)
# 1) 准备工程:从参考 demo 复制 gen/ohos,改包名/应用名/(可选)库名
# 并写好自己的 web/ 前端与 src-tauri/src/lib.rs
# 2) 拉依赖(自动修复 cargo 的 EINTR checkout 故障)
sh tools/cargo-fetch-retry.sh
# 3) 编译 Rust 库并放进 ArkTS 工程
sh tools/build-rust.sh
# 4) 生成 AGC 签名材料(需已登录 + 设备在线)
cd src-tauri/gen/ohos && devecocli signature generate –product default
# 5) 构建 + 签名 + 安装 + 启动
devecocli run –module entry
# 6) 验证界面
devecocli ui layout | head -40
devecocli ui screenshot –path ../../build-output/shot.png
11.4 四个脚本各自解决什么
| 脚本 | 解决的问题 | 关键点 |
|---|---|---|
| cargo-fetch-retry.sh | cargo 取依赖的 TLS + EINTR 双故障 | 从错误信息里提取出问题的 checkout 目录,用 git archive + 空 .cargo-ok 重铺后重试(最多 N 轮) |
| build-rust.sh | 手工构建缺失的环境注入 | 注入 4 个 WRY_OHOS_*/TAURI_OHOS_* 变量 + OHOS_NDK_HOME + TAURI_ENV_DEBUG=false;同步 assets/tauri.conf.json;把 .so 复制进 libs/arm64-v8a/ |
| build-hap.sh | hvigor 只认加密密码 | hvigorw
出未签名包 → hap-sign-tool sign-app 签名;签名材料全部可用 SIGN_* 环境变量替换成 AGC 的材料 |
| install-hap.sh | 纯 hdc 部署 | hdc install -r
+ aa start;失败时直接打印本机 UDID 与“该去 AGC 做什么”的提示 |
其中 cargo-fetch-retry.sh 的核心是那段“读错误 → 修 checkout”的循环:
while [ “$i” -lt “$MAX” ]; do
i=$((i + 1))
if cargo fetch >”$LOG” 2>&1; then exit 0; fi
tail -4 “$LOG”
# 从 “cannot access component in path ‘…'” / “failed to stat ‘…'” 里取出 checkout 根目录
root=$(grep -oE “‘/[^’]*git/checkouts/[^’]*'” “$LOG” | head -1 | tr -d “‘” |
sed -E ‘s#(.*/git/checkouts/[^/]+/[^/]+).*#\1#’)
[ -n “$root” ] && [ -d “$root” ] && fix_checkout “$root” || exit 1
done
12. 已知限制与后续方向
12.1 限制
| 限制 | 说明与影响 |
|---|---|
| 依赖的是 feature 分支 | tauri
/wry/tao 的鸿蒙支持都在各仓库的分支上,与上游主线会有漂移;升级时要重新验证,不要盲目 cargo update |
| 需要 openharmony-ability 的固定 rev | 上游在 c6c4c9a 之后移除了 webview 相关能力,demo 与本文都钉在 295a276a;跟进上游前先确认这一点是否已恢复 |
| 签名与设备绑定 | 调试 profile 里写着 UDID,换设备要重新申请;release 签名还需要上架流程 |
| 库名不好改 | [lib] name
牵动 ArkTS 侧四处(见 11.2),建议先把链路跑通再改名 |
| 包体积 | HAP 约 8.6 MB,其中 Rust .so 9.4 MB(压缩后进包)、还有一份模板带来的 libc++_shared.so(1.2 MB);关掉模板的 C++ 库可以省掉后者 |
| 无 rustup | 本机 Rust 由 harmonybrew 的包提供,切版本要靠包管理器,而不是 rustup toolchain |
| 首次构建慢 | 全量 2 分钟起(20 核),改配置触发的重编 30 ~ 44 秒 |
| 热重载未实测 | devecocli run –hotreload
需要 DevEco ≥ 6.1.1 的 hvigor 能力,本文没有验证 |
| 前端依赖生态 | 零依赖静态前端规避了打包器问题,但反过来说,想上 React/Vue 就要先解决 rollup/esbuild 在本机的可用性 |
12.2 值得继续做的事
- 把库名改成自己的(4 处改动 + 一次重新构建),让工程看起来不像 demo;
- 收敛 deviceTypes:当前是 phone/tablet/2in1/tv/car,只做 PC 的话留 2in1 更严谨;
- 写一个真正的 Tauri plugin:工程里已带 tauri-plugin-ohos-demo 作为骨架(Rust 侧命令 + 权限 + guest-js),照它改即可;
- 走一遍 release 签名:AGC 申请发布证书与发布 Profile,把 –build-mode release 与上架流程补上;
- 试试官方 tauri-cli 路线:如果不在意首次编译时间,cargo tauri ohos build/dev 会省掉本文手工注入的那些变量(但要接受它的构建耗时与失败重试成本)。
12.3 一点体会
这套流程里真正“鸿蒙特有”的东西只有三件事:musl + ohos 三元组、必须签名、签名必须是华为侧签发。其余大部分挫折——cargo 的 TLS 与 EINTR、坏 node、打包器不可用、hvigor 只认加密密码——本质上是“在一个很新的、工具链尚不完整的平台上做工程”的通用问题。
所以排查这类问题时,我后来养成的习惯是:先把问题归类(平台差异 / 工具链缺陷 / 框架配置),再决定是“改配置”还是“绕过”。
- 看到 linker、cc、.codesign、Operation not permitted → 平台差异,改配置能解决;
- 看到 Interrupted system call、cannot load such file → 工具链缺陷,换工具比调参数有效;
- 看到 Failed to request tauri://…、界面空白但进程健在 → 框架配置(尤其是”三份配置”不一致),去读框架源码定位比猜快得多。
13. 参考资料
框架与工程
- Tauri 的鸿蒙分支:https://github.com/tauri-apps/tauri/tree/feat/open-harmony
- 参考工程(本文的 gen/ohos 与 N-API 回调样例来源):https://github.com/richerfu/tauri-demo
- Tauri CLI 里的鸿蒙实现(构建与环境变量注入的权威出处):crates/tauri-cli/src/mobile/open_harmony/
- Tauri CLI 内置的鸿蒙工程模板:crates/tauri-cli/templates/mobile/open-harmony/
Rust 侧
- Rust 官方平台支持文档(*-unknown-linux-ohos,Tier 2 with Host Tools):https://doc.rust-lang.org/stable/rustc/platform-support/openharmony.html
- ohos-rs(把 Rust 编译成鸿蒙原生模块 / N-API):https://ohos.rs/docs/basic/quick-start/
- openharmony-ability(Ability 的 Rust 绑定,本文钉在 295a276a):https://github.com/harmony-contrib/openharmony-ability
- cargo-ohos(交叉编译 + 自动拉起 SDK 的工具,CI 场景值得一看):https://crates.io/crates/cargo-ohos
鸿蒙侧
- DevEco CLI(devecocli)文档:https://www.npmjs.com/package/@deveco/deveco-cli
- 华为开发者文档「搭建流水线」(命令行构建、签名、安装运行的官方流程):https://developer.huawei.com/consumer/cn/doc/doccenter-deveco-studio/ide-command-line-building-app
- 华为开发者文档「自动签名」(AGC 与本地材料的对应关系):https://developer.huawei.com/consumer/cn/doc/doccenter-deveco-studio/ide-signing-auto
本文基于 2026-10-18 在 HUAWEI MateBook Pro(HarmonyOS 7.0.0.111 / API 26,aarch64,2in1)上的完整实操记录整理。文中所有命令、报错原文、版本号、体积与耗时均来自该机实测;标注为“未实测”的部分请以你机器上的结果为准。
本文由人人都是产品经理作者【nutpi】,微信公众号:【nutpi】,原创/授权 发布于人人都是产品经理,未经许可,禁止转载
题图来自Unsplash,基于 CC0 协议。
- 目前还没评论,等你发挥!

起点课堂会员权益



