把 cc-switch 搬上鸿蒙 PC 的那些天:一个 Tauri 应用的移植笔记
这篇写给自己,也写给打算把 Tauri 应用搬到鸿蒙 PC 上的人。
全程真实记录,坑都是踩过的,代码都是能跑的。

项目地址:https://atomgit.com/qq8864/cc-switch-harmonyos
更多交流学习,欢迎加入开源鸿蒙PC社区:https://harmonypc.csdn.net/
欢迎在PC社区平台申请新建项目:https://atomgit.com/OpenHarmonyPCDeveloper
猫哥的博客:https://blog.csdn.net/qq8864
起因
事情得从某天下午说起。我盯着电脑屏幕上的一个 GitHub 仓库,心里冒出一个念头:「这玩意儿要是能跑在鸿蒙 PC 上,应该挺有意思的。」
我说的这个仓库,就是 cc-switch(github.com/farion1231/cc-switch)——一个火得离谱的开源项目,GitHub 上 12 万+ star,常年挂在 Trending 榜上。它是给 AI 编程工具用的「All-in-One 配置管理器」:Claude Code、Claude Desktop、Codex、Gemini CLI、Grok Build、OpenCode、OpenClaw、Hermes Agent,这些工具的环境变量、API 配置、供应商切换,它一个界面全给你管了,还带代理、用量统计、MCP 管理这些进阶功能。写 AI 代码的人基本人手一个,说它是这个品类的头部工具毫不夸张。
它本身是 Tauri v2 写的——React 前端 + Rust 后端,Windows/macOS/Linux 三端齐活。但偏偏没有鸿蒙版。鸿蒙 PC(那些能跑桌面应用的鸿蒙笔记本和二合一设备)这两年生态起来了,桌面应用却还少得可怜。我寻思:Tauri 应用能不能也搬过去?cc-switch 这种重后端(数据库、代理服务、OAuth 全都用 Rust 写死)的桌面工具,恰恰是检验鸿蒙桌面生态成色的好靶子。
于是我就这么开工了。说实话,一开始心里是有点打鼓的——鸿蒙 PC 的应用生态跟 Windows 完全不是一个体系,Rust 能不能交叉编译过去、WebView 用谁家的、界面怎么渲染,全是未知数。但这事儿越未知越有意思,干就完了。
我手头有个现成的"参考答案":社区里有人维护了一个专门给 OpenHarmony 用的 Tauri fork(atomgit.com/qq8864/tauri),把 tauri 2.8.x 魔改成了能跑在鸿蒙上的版本,原理是用 napi 桥接 + ArkWeb 渲染。也就是说:前端基本不用动,Rust 后端交叉编译成 .so,套一个 DevEco 工程壳子打包成 HAP,就能装到鸿蒙设备上。听着挺美,对吧?
后来的事证明,理想很丰满,现实很骨感。这一趟下来,光编译就炸了十几轮,真机上又炸了好几轮,前后折腾了整整一天半。
环境准备:先把地基打牢
我的开发机是 Windows 11。要交叉编译到鸿蒙的 aarch64,需要这些东西:
- Rust 工具链:
stable-x86_64-pc-windows-gnullvm(gnullvm 是给交叉编译用的,普通 gnu 工具链链接器会报-lgcc_eh找不到) - 鸿蒙目标:
aarch64-unknown-linux-ohos(rustup 直接加) - 鸿蒙 SDK 的 NDK 部分(里面有 clang、llvm-ar、sysroot)
- DevEco Studio(打包 HAP 用,自带 hvigor、ohpm、node)
- 一台鸿蒙 PC 真机(HUAWEI MateBook Pro,API 24,aarch64)
这里有个小坑先说:SDK 有两个,一个是纯 NDK(只有 native 工具链),一个是完整 SDK(含 ets/js/toolchains)。交叉编译 Rust 用 NDK,DevEco 打包必须指向完整 SDK,指错了会报 SDK component missing。我当时差点被这个绕进去。
工具链装完,验证一下:cargo tauri ohos --help 能看到 init/build 子命令,说明 fork 的 CLI 装好了。好,地基算打完了。
第一场硬仗:Cargo 解析器大战
按文档,把 Cargo.toml 里的 tauri 依赖指到 fork 的路径上,然后编译。结果第一炮就哑了:
error: failed to select a version for `tauri`
package `tauri` links to the native library `Tauri`, but it conflicts with a
previous package which links to `Tauri` as well: tauri v2.8.5 (path)
我盯着这个错误看了半天。什么意思呢?就是说 cargo 同时拉了两个 tauri:一个是我的路径依赖(fork),一个是 crates.io 上的官方版(插件们要求的)。两个都声明了 links = "Tauri",cargo 觉得这俩是死对头,直接罢工。
这玩意儿特别坑,因为它跟我以前理解的 cargo 行为完全不一样。我以为路径依赖会自动满足所有对 tauri 的版本要求,但实际是:解析器根本不会拿路径依赖去给其他 crate 的 registry 需求当候选。我甚至写了个空工程复现,就一个路径 tauri + 一个插件,照样炸。不是移植对象的问题,是这条路本身就走不通。
后来我试了 [patch.crates-io]——把 fork 的 tauri 系列七个 crate 全部 patch 进去,把声明保留成 tauri = "2.8.2"。这一下就通了。原理也简单:patch 是"指定替换",cargo 会把 registry 里的 tauri 整体换成 fork 的,不存在两个并存的问题。
这里还埋了个连环雷:改完 manifest 之后,解析还是失败,提示 patch 版本和 Cargo.lock 不一致。因为 lock 里还钉着官方 tauri 2.10.3。得跑 cargo update -p tauri -p tauri-build ...(把 patch 的那些包全部列上)让 lock 刷新。千万别手贱删 Cargo.lock——删了会全量重拉索引,我们这网络环境直接超时。
光解决 tauri 本身还不够。fork 的 tauri 是 2.8.5,但 cc-switch 生态早就走到 2.10+ 了,插件们一个个都要求新版本。于是开始了痛苦的锁版本环节:tauri-plugin-log 锁 2.7.1、dialog 锁 2.4.2、opener 锁 2.5.2……这些还都能靠查索引解决。
真正阴险的是传递依赖。tauri-plugin-dialog 依赖 tauri-plugin-fs,lock 里 fs 是 2.4.5,而 2.4.5 要求 tauri ^2.9.3——cargo 的依赖解析是全目标统一的,不管你 cfg 段怎么写,只要进了解析图,它就会把官方 tauri 拉回来,然后跟 patch 的 2.8.5 撞车。我一开始还以为是锁错了 dialog,排查了半天才意识到是 dialog 背后的 fs 在作妖。解决方式很粗暴:把 tauri-plugin-fs = "=2.4.4" 显式写进依赖,钉死。
这轮下来我最大的体会是:移植的第一步不是改代码,是跟依赖解析器搏斗。不把版本图理清楚,后面全是白搭。
第二场硬仗:编译连环炸
解析器消停了,编译又开始了它的表演。第一个炸的是 openssl-sys:
Could not find directory of OpenSSL installation
查了一下,是 reqwest 没关默认特性——默认特性带 native-tls,native-tls 在 Linux 系(鸿蒙的 target_os 就是 linux)要调 openssl。可鸿蒙哪有系统 openssl 给你用。解法:reqwest 加 default-features = false,改走 rustls,鸿蒙段再用 rustls-tls-webpki-roots(内置根证书,因为鸿蒙没有系统证书库)。
然后是 rquickjs-sys:
couldn't read bindings\aarch64-unknown-linux-ohos.rs
这个库的绑定是按 target 文件名生成的,没有鸿蒙的份。我看了眼它的 build.rs,它的 bindgen 特性会把 Rust triple 直接当 clang 的 --target 传——aarch64-unknown-linux-ohos 这玩意儿根本不是合法的 LLVM triple,bindgen 必然死。所以正经路子走不通,直接土办法:把 aarch64-unknown-linux-gnu.rs 复制一份改名成 aarch64-unknown-linux-ohos.rs。反正是 C API 声明,跟架构无关,能编过就行。注意这改的是 cargo 缓存,换台机器要重做——以后有空给上游提个 PR。
接着是链接器。按文档配了个 ohos-clang.cmd 包装脚本当链接器,结果:
linking with `...\ohos-clang.cmd` failed ... 不是内部或外部命令
Windows 上 rustc 是直接 CreateProcess 调链接器的,根本不会去解析 .cmd/.bat。得把链接器直接指到 NDK 的 clang.exe,target 和 sysroot 通过 rustflags 的 link-arg 传进去。这个改法实测有效。
然后 build.rs 又给我上了一课。它里面有个 Windows manifest 的逻辑:
#[cfg(target_os = "windows")]
{
println!("cargo:rustc-link-arg=/MANIFEST:EMBED");
}
看着挺正常对吧?但 build script 是按宿主编译的——我在 Windows 上交叉编译,这个 cfg 恒为真,于是 /MANIFEST:EMBED 这种 MSVC 链接器参数就泄漏给了鸿蒙的 clang,clang 直接报 no such file: /MANIFEST:EMBED。改成运行时判断 CARGO_CFG_TARGET_OS 环境变量才治本。这个坑特别隐蔽,因为它只在交叉编译时出现,平时桌面构建完全正常。
还有个小的:ohos-arkui-sys 这个 crate 的 build.rs 要求 OHOS_NDK_HOME 环境变量,不设就 panic。设了就行。
编译本身也磨人。全量依赖树(六百多个 crate)第一次编,十几分钟起步。我一开始用 async 后台跑,结果每次 300 秒就被杀——后来才知道是超时限制,改成同步跑加长超时才消停。中间还踩了个 rustup 的坑:仓库根目录有个 rust-toolchain.toml 钉了 1.95,我们这儿的镜像没有这个版本,导致所有 cargo 命令从根目录跑都会 404。解法就是所有构建都进 src-tauri/ 目录再跑,让子目录的 rust-toolchain.toml 生效。
等到依赖全绿,终于轮到应用本身的代码报错了——23 个编译错误,全是 no method named 'show'/'set_focus'/'hide'...。fork 的鸿蒙 WebviewWindow API 面比桌面窄得多,一大堆窗口操作方法都没有。道理也讲得通:鸿蒙上窗口是系统管的,轮不到你调。解法就是把所有用到这些方法的代码用 #[cfg(desktop)] 包起来,命令级的直接给鸿蒙分支返回"当前平台不支持"。
这一轮编译打完,.so 终于出来了:ELF 64-bit ARM aarch64,21MB。那一刻挺有成就感的。
第三场硬仗:真机首跑,秒崩
.so 有了,接着是套 DevEco 工程、打包 HAP、签名、上真机。中间踩了 hvigor 的钩子坑(fork 模板在 entry/hvigorfile.ts 里注册了个 cargo 钩子,打包时会去调 CLI 的 dev 脚本,报什么 server-addr 找不到——直接把钩子文件替换成干净版),签名这块必须用 DevEco Studio 图形界面生成(华为账号登录,自动写材料),这个绕不开。
HAP 装上去,aa start 启动。然后……进程秒没。
ps 看不到进程,hilog 里只有一句:
LastFatalMessage: [OnSurfaceCreated] crash occured on callback: 0x5b73fd2724
最气人的是,应用里的 panic hook 压根没被触发,crash.log 也没写出来。那一刻我意识到,鸿蒙上调试 Rust 崩溃,跟桌面上完全不是一个玩法:stderr 不可见,/data/local/tmp 被沙箱拦,faultlog 目录 shell 没权限读——几乎所有的常规诊断手段全废了。
我被逼出了一个土办法:里程碑文件。在 run() 和 setup() 的关键节点往应用可写、shell 可读的路径写日志文件,二分定位崩溃发生在哪一步。试了几个路径才发现:应用沙箱里 /data/storage/el2/base/files/ 是应用能写、shell 也能读的(shell 读 /data/app/el2/100/base/<bundle>/files/),而 /data/local/tmp 和 /storage/Users/currentUser 应用根本写不进去。
里程碑一跑,真相大白:
run() start
panic_hook ok
setup start
rustls ok
卡在 log 插件初始化:Operation not permitted (os error 1)。原因查明白了——dirs::home_dir() 在鸿蒙上没有 HOME 环境变量,返回 None,代码回退到进程工作目录(/),然后 log 插件要去 / 下面建目录,被系统按在地上摩擦。
这里有个小插曲:有人跟我说鸿蒙 PC 的 home 是 /storage/Users/currentUser,我改了,结果 EPERM 依旧——应用根本没权限写用户目录的根。最后实测下来,应用自己的数据目录(/data/storage/el2/base/files)才是真正可写的 home。把这个路径填进 get_home_dir() 的鸿蒙分支,log 插件瞬间就过了。
这一轮修完,应用活了:主进程、GPU 进程、render 进程三件套齐活,数据库迁移、模型定价、OAuth、代理服务全都初始化成功。那一刻的心情,真的只能用「热泪盈眶」来形容。
第四场硬仗:白屏与 localStorage
应用进程活了,但界面还是白屏。日志里躺着这么一行:
Failed to request http://localhost:3000/
localhost:3000?这不是 devUrl 吗?我明明打的是 release。查了 fork 的 build.rs,发现一行要命的逻辑:
let dev = !custom_protocol;
原来 Tauri 的"生产模式"是靠 custom-protocol 这个 feature 区分的,而它通常由 CLI 在构建时注入。我之前图省事用裸 cargo build 交叉编译,这个 feature 就没带上,结果整个程序被编译成了开发模式,webview 去加载 devUrl,当然什么都没有。
换成 cargo tauri ohos build 之后,前端终于从 tauri://localhost/assets/... 加载了,页面也渲染出来了。但紧接着又是新错误:
TypeError: Cannot read properties of null (reading 'getItem')
React 首屏直接炸。查了半天,是 ArkWeb 在自定义 scheme(tauri://localhost)下,window.localStorage 居然是 null。前端一堆地方初始化时直接 localStorage.getItem(...),一碰就挂。
这属于平台限制,前端绕不过去,只能适配:在 main.tsx 顶部加了个内存版 localStorage polyfill——检测到 window.localStorage 为空就用 Map 实现顶上,保证不崩。代价是重启不持久化,对这个场景可以接受。
这里还埋了个很隐蔽的坑:改完前端重新打包部署,错误居然还在,bundle 的 hash 还是旧的。排查发现,ArkWeb 缓存了旧页面,而更坑的是真正服务前端的是 .so 内嵌的资产(编译期嵌入的),不是 rawfile。所以改前端必须走完整链路:重新构建 dist → CLI 重建 .so(自动嵌入新 dist)→ 重新打包 HAP → 清缓存重装。漏一步都不行。
收尾
最后一版部署上去,三进程稳定存活,日志零错误,页面渲染正常。整个链路终于通了:
Rust 交叉编译 → libcc_switch_lib.so → DevEco 工程 → HAP 签名 → hdc 安装 → 真机跑起来
回顾这一趟,最值钱的经验大概是这么几条:
- fork 的 tauri 必须走
[patch.crates-io],别用 path 依赖。这是最大的坑,没有之一。 - 必须用
cargo tauri ohos build构建,裸 cargo 会缺custom-protocolfeature,编出来是 dev 模式。 - 鸿蒙上调试 Rust 崩溃,先解决日志可见性问题。里程碑文件 + 双路径写入,是最快的定位手段。
dirs::home_dir()在鸿蒙上是废的,应用的可写 home 是自己的沙箱数据目录。- 改前端要全链路重建,.so 内嵌资产才是真正服务页面的那份。
- ArkWeb 自定义 scheme 下 localStorage 为 null,前端要 polyfill。
至于那些限制——没有系统托盘、原生对话框用不了、自更新砍了、localStorage 不持久——都在预期内,桌面版功能不受影响。一个 12 万 star 的开源桌面工具,数据、数据库、代理服务全都正常地在鸿蒙 PC 上跑起来了,光是这一点,这趟折腾就值了。至于其他细节问题,待优化实测逐步完善,不过那些都问题不大了。
如果有人问我建不建议搞鸿蒙 PC 应用移植,我的答案是:工具链比想象中成熟,坑比想象中多,但是真的能跑起来的。希望这篇笔记能帮你少踩几个我踩过的坑。
更多推荐




所有评论(0)