工具链适配鸿蒙:我用 Rust 给 OpenHarmony 造了一条轻量命令行工具链(ark-cli)
工具链适配鸿蒙:我用 Rust 给 OpenHarmony 造了一条轻量命令行工具链(ark-cli)
前言
如果你做鸿蒙开发,多半在 DevEco Studio 里点过无数次"运行",也多半在 CI 服务器上为"装环境、连设备、搞签名"折腾到崩溃。DevEco 很强,但它把一切都收进了 GUI——一旦要在脚本、CI、远程服务器上批量操作,就会发现鸿蒙侧的命令行几乎是空白。
我做了 ark-cli:一个用 Rust 写的 OpenHarmony 工具链,把 DevEco 的核心能力(装运行时、连设备、构建、跑模拟器、自动签名)搬进一个单二进制命令行工具。这篇文章记录我"适配鸿蒙"的全过程:为什么是 Rust、架构怎么分、运行时怎么一键装齐、hdc 怎么零安装内嵌、hvigor 怎么被 Rust 编排、签名怎么自动化,以及踩过的坑。
先看一眼它长什么样:

本文所有命令截图均来自真实运行环境(macOS + OpenHarmony SDK API 24),可复现。
快速上手:5 分钟跑通
赶时间的话,下面这条最短路径能让你从 0 到"构建出一个 .hap":
- 安装 ark(单二进制,放进 PATH)。
ark runtime install一键装齐工具链。ark create Demo --bundle com.example.demo生成工程。cd Demo && ark build构建出包。
# 最短路径:从 0 到 .hap
ark runtime install # 装工具链(首次较慢)
ark create Demo --bundle com.example.demo # 生成工程
cd Demo
ark build --mode debug # 产出 entry-default-unsigned.hap
ark devices # 看有没有设备可装
ark run # 有设备就装 + 启动
下文所有命令都围绕"装运行时 → 建工程 → 构建 → 跑设备"这条主线展开,逐段深入。
一、为什么要做一个鸿蒙命令行工具链
1.1 DevEco 很强,但入口太"GUI"
DevEco Studio 是华为官方的鸿蒙 IDE,能力完整——编辑器、预览器、构建、设备管理一应俱全。但它的操作模型是图形化的:你点点点。这在桌面交互时很舒服,却带来三个"脚本化"难题:
- 进不了 CI:CI 服务器没有桌面、没有 GUI,DevEco 的很多能力(尤其签名、模拟器)根本触发不了。
- 不可复现:手动点出来的环境,换台机器就得重新配一遍,配置散落在各处的
settings里。 - 不可批量:一次性对 10 台设备装 10 个不同的包,GUI 里只能重复劳动。
1.2 命令行是工具链的"操作系统接口"
一个成熟的生态,命令行工具链往往是它的"操作系统接口":脚本调它、CI 调它、其他工具也调它。Android 有 adb、gradle、avdmanager;iOS 有 xcodebuild、xcrun。而鸿蒙侧,长期以来这些能力主要绑在 DevEco 里,散落的 hdc/hvigor 又各自为政,缺少一个统一、可脚本化、可分发的入口。
1.3 一个能脚本化的 CLI 能带来什么
| 场景 | 传统方式(GUI / 散命令) | 用 ark-cli |
|---|---|---|
| 新机器装环境 | 手动下 SDK + 配路径 + 装 JDK | ark runtime install 一条命令 |
| CI 构建出包 | 无 GUI,构建链断裂 | ark build 纯命令行 |
| 连设备看日志 | 找 hdc 路径 + 手动传参 | ark devices / ark logcat -m Screen |
| 起模拟器 | DevEco 图形界面 | ark emulator create / start |
| 生成签名 | DevEco 登录 + 点按钮 + 复制材料 | ark signature generate |
核心目标不是"再造一个 DevEco",而是把 DevEco 里可以命令化的那部分抽出来,做成一个稳定、可组合、单二进制分发的 CLI。
二、为什么选 Rust 做这条工具链
选语言时我纠结过 Go(更简单、生态熟)和 Rust(更重、但更强)。最终选 Rust,理由有三条,条条都踩在这条工具链的"要害"上。
2.1 单二进制分发:CI 友好的前提
这条工具链要在 CI 上用,"下载即用、零依赖"是硬指标。Rust 编译出的是单个静态二进制,不依赖运行时、不拖一堆 .so/.dll。对比一下:
| 维度 | Rust | Go |
|---|---|---|
| 产物 | 单二进制,静态链接 | 单二进制,静态链接 |
| 内存安全 | 编译期所有权保证 | 依赖 GC |
| 二进制解析 / 加密 | 无 GC 停顿,行为确定 | GC 停顿不可控 |
| 交叉编译 | cargo build --target 成熟 | 同样成熟 |
| 学习曲线 | 陡 | 平缓 |
2.2 内存安全是"碰密钥"的底线
工具链里有大量解析二进制、证书、密钥的活儿:读 .hap、处理 .p7b/.cer、加密签名密钥。这些代码一旦有内存漏洞,后果是签名材料泄露或产物被污染。Rust 的所有权系统在编译期就排除了 use-after-free、data race 这类问题——对"天天碰密钥"的工具链,这是硬需求,而不是锦上添花。
2.3 一套代码,三端编译
鸿蒙工具链要跑在 macOS(研发主力)、Windows、Linux(CI) 上。Rust 用 #[cfg(target_os)] + 条件依赖就能一套代码三端编译:
# 一条命令交叉编译到各平台
cargo build --release --target aarch64-apple-darwin # macOS (Apple Silicon)
cargo build --release --target x86_64-pc-windows-msvc # Windows
cargo build --release --target aarch64-unknown-linux-gnu # Linux (ARM / CI)
选 Rust 的代价是学习曲线陡,但对"要长期维护、要碰密钥、要进 CI"的工具链,这笔账很划算。
三、ark-cli 的整体架构
3.1 一个 workspace,三个 crate
ark-cli 是一个 Cargo workspace,逻辑按"能力"拆成三个 crate,避免"一个大 main 撑死":
ark-cli/
├── Cargo.toml # workspace 根
├── crates/
│ ├── core/ # 全部领域逻辑:运行时 / hdc / hvigor / 签名 / 模拟器
│ ├── cli/ # 命令行入口:clap 子命令 → 调 core
│ └── gui/ # Tauri 桌面入口:同一个 core,另一张脸
└── scripts/ # 发布 / 内嵌二进制构建脚本
核心设计:逻辑沉 core,入口保持薄。 cli 和 gui 都只做"参数解析 + 调 core + 打印结果",所有真正的活儿都在 core。这样同一套能力,命令行能用、桌面 GUI 也能用;将来再加 MCP 工具、加插件,都只是"再加一个薄入口"。
3.2 一个核心,两个入口
┌────────────────┐
│ core │ 运行时 / hdc / hvigor / 签名 / 模拟器
└───────┬────────┘
┌─────────┴─────────┐
cli (clap) gui (Tauri)
终端 / CI 用户 桌面用户
这种"厚 core + 薄入口"的拆法,是这条工具链能同时服务 CI 脚本和桌面 GUI 的关键——能力只写一遍。
四、统一运行时:一条命令装齐工具链
4.1 痛点:配环境最劝退
鸿蒙开发最劝退的不是写代码,是配环境。SDK 要下、路径要配、JDK 要装、hvigor / ohpm 要找到、hdc 要在 PATH 里……每一步都可能卡住,而且不同 DevEco 版本目录结构还不一样。
ark-cli 的解法是引入"统一运行时"概念:ark runtime install 一键把 SDK、Emulator、hvigorw、ohpm、node、hdc 全部下载并归置到一个受管目录 ~/.ark-cli/runtime/ 下,之后所有命令都从这个目录找工具,不再依赖 DevEco 装在哪儿。
4.2 ark runtime install 做了什么
- 探测本机架构(aarch64 / x86_64)和操作系统。
- 下载对应平台的 OpenHarmony SDK。
- 下载 Emulator 运行时。
- 安装 hvigorw / ohpm / node 到
runtime/bin、runtime/ohpm、runtime/tool。 - 写出
config.toml,记录每个工具的绝对路径。
装完用 ark runtime status 一键体检,每个组件都会给出 ✅ / ❌:

# 一键装齐 + 体检
ark runtime install # 下载并归置整套工具链
ark runtime status # 逐组件体检(SDK / Emulator / hvigorw / ohpm / node / hdc)
ark runtime list # 看已安装了哪些组件
4.3 分量级安装:按需下载
运行时各组件互相独立,所以支持分量级安装——只装你需要的那部分:
ark runtime install sdk # 只装 SDK
ark runtime install emulator # 只装模拟器
ark runtime install hvigorw # 只装构建器
设计原则:依赖关系下沉到 core 的路径解析里,命令之间不互相假设"装了什么"——装没装、装在哪,
status说了算。
五、零安装设备交互:把 hdc 内嵌进二进制
5.1 为什么内嵌 hdc
hdc(HarmonyOS Device Connector)是鸿蒙世界的"adb"——连设备、装包、传文件、抓日志全靠它。传统做法是把 SDK 里的 hdc 加进 PATH,但这又回到了"配环境"的老路:路径不对、版本不对、CI 上根本没有。
ark-cli 的做法:把 hdc 二进制直接内嵌进 ark 可执行文件(include_bytes!),首次使用时解压到 ~/.ark-cli/bin/hdc 再执行。用户机器上不需要单独装 hdc,ark 自带。
5.2 100% 透传 + 结构化封装
设备交互我做了两层,兼顾"能力全"和"体验好":
- 透传层:
ark hdc <任意参数>把参数原样传给内嵌 hdc,hdc支持什么它就能做什么,一个都不少。 - 封装层:高频操作做成子命令,带结构化输出(
--json可被脚本直接消费)。
5.3 设备 / 日志 / 端口转发
ark devices # 列出在线设备
ark hdc shell "bm dump -a" # 透传:列出已安装的应用
ark logcat -m Screen # 抓日志,Screen 模式只刷屏幕相关 tag
ark logcat -m Crash # 只看崩溃日志
ark forward tcp:8080 tcp:8080 # 端口转发
透传是底线:封装可以慢慢加,但 hdc 的任何能力都不能被 ark 挡住。
六、构建系统:用 Rust 编排 hvigor
6.1 注入环境 + 规避 daemon 陷阱
鸿蒙项目用 hvigor 构建。但直接在命令行调 hvigorw 有两个坑:环境变量不全(缺 DEVECO_SDK_HOME 等),以及 daemon 不继承父进程环境——第一次 build 注入了环境,后台 daemon 却没继承,第二次 run 又找不到 SDK,表现为"时好时坏"。
ark-cli 的 ark build 针对这两点做了处理:
- 从统一运行时解析出完整环境变量(SDK 路径、node、JDK),注入到每一次构建子进程。
- 强制前台构建(不依赖 daemon 继承环境),根除"第一次成功、第二次失败"的玄学。
6.2 从模板到运行:create + build + run
新项目不用手搓目录结构,ark create 按模板生成一套能直接构建的工程:

构建则是一条命令,--watch 支持增量热构建:

ark create MyApp --bundle com.example.myapp # 按模板生成工程
ark build --mode debug # 构建出 .hap
ark build --watch # 监听文件变化,增量构建
ark run # 构建 + 安装 + 启动到设备
6.3 产物与签名分离
构建产物默认是未签名的 .hap(entry-default-unsigned.hap)。签名是独立的一步(见下一章)——这样 CI 可以"先构建、后签名",签名材料不会泄漏进构建产物,职责清晰。
关键经验:跨进程"注入环境"不可靠。daemon 是独立的长驻进程,它只认自己启动时的环境。要么每次都前台跑,要么把环境写进 daemon 自己的配置里。
七、模拟器全生命周期管理
7.1 实例线:create / start / stop / delete
DevEco 的模拟器管理藏在图形界面里,命令行几乎是空白。ark-cli 把模拟器的实例做成一组命令:
ark emulator list # 看有哪些模拟器
ark emulator create phone01 # 创建实例
ark emulator start phone01 # 启动
ark emulator stop phone01 # 停止
ark emulator delete phone01 # 删除
7.2 镜像线:image-install 带进度下载
模拟器要跑,得有系统镜像。ark-cli 把镜像的下载也命令化,带实时进度:
ark emulator image-list # 看可下载的镜像
ark emulator image-install phone # 下载 phone 镜像(带进度条)
ark emulator list 会同时给出 Emulator 版本和当前实例状态:

设计上把模拟器拆成两条独立线:实例(create / start / stop / delete)管"实例的生命周期",镜像(image-list / image-install)管"系统镜像的来源",两者互不耦合。
八、自动签名:把 DevEco 的按钮变成一条命令
8.1 四种签名材料
鸿蒙应用上架 / 安装需要一整套签名材料,在 DevEco 里是"登录 → 点几个按钮 → 手动复制"。ark-cli 把它做成一条命令,覆盖四种材料:
| 材料 | 文件 | 用途 |
|---|---|---|
| 密钥库 | .p12 | 应用私钥,本地保管 |
| 调试证书 | .cer | 调试期证书 |
| 发布证书 | .cer | 上架用证书 |
| Profile | .p7b | 把应用 bundle 与证书绑定的描述 |
8.2 六阶段流程 + 幂等检查
ark signature generate 内部是一条六阶段流水线,每一步都做幂等检查,已存在且正确的材料自动跳过:
- 登录态检查:确认 AppGallery Connect 已登录。
- 生成 / 复用密钥:
.p12已存在则跳过,否则生成并加密保管。 - 申请证书:向 AGC 申请调试 / 发布证书。
- 生成 Profile:
.p7b,绑定应用 bundle 与证书。 - 落地配置:把材料写进
build-profile.json5的signingConfigs。 - 验证:重新构建,确认这次产出的就是已签名的
.hap。
8.3 Rust 驱动 Java 工具链
鸿蒙签名工具链底层是 Java 的。ark-cli 用 Rust 编排:Rust 负责流程、幂等、错误处理,把重活委托给底层成熟的 Java 工具,再用 Rust 解析结果。这样既复用了现成的 Java 能力,又保证流程可控、可脚本化:
ark login # 先登录 AGC
ark signature generate # 自动生成全套材料并配置到项目
ark build # 验证:这次构建产出已签名的 .hap
设计要点:幂等是自动签名的生命线。CI 里这条命令会被反复跑,任何一步"重复执行就报错"都会让流水线变脆。所以每个阶段都先查"有没有、对不对",再决定做不做。
九、踩过的坑(都是真金白银换的)
9.1 macOS 上的"假 java"
macOS 系统里自带一个 java,它能跑、退出码还是 0,但它只是一个"引导桩",并不是一个真正的 JVM——java -version 给的信息不可信。这导致 JDK 检测"看起来成功",可 hvigor 真正构建时却因找不到真 JDK 而失败,而且失败信息还很隐蔽。
解决办法:不能只信退出码,要校验 java 的真实身份(厂商 / 版本 / 路径),把系统桩排除掉,改用运行时自带的 JDK:
// 不能只信退出码:校验 java 的真实身份,排除 macOS 系统桩
fn is_real_jdk(path: &Path) -> bool {
let out = std::process::Command::new(path)
.arg("-version")
.output()
.ok()?;
// 注意:java -version 的输出在 stderr,不在 stdout
let info = String::from_utf8_lossy(&out.stderr);
info.contains("OpenJDK")
|| info.contains("Temurin")
|| info.contains("Java(TM)")
}
9.2 字节 vs 字符:一个"测试全绿"的下载 bug
下载进度条算"百分比"时,我一度把字节数当成了字符数来算长度。对纯 ASCII 两者相等,测试全绿;可一旦内容含中文(多字节 UTF-8),比例就错了,进度条一度"倒着走"。
// 进度条算比例:数"字符"而不是"字节",否则中文会让比例错乱
let total = name.chars().count(); // ✅ 字符数
// let total = name.len(); // ❌ 字节数(UTF-8 里一个中文字符 = 3 字节)
let ratio = done as f64 / total as f64;
教训:UTF-8 里"一个字符 ≠ 一个字节"。任何涉及"长度 / 进度 / 截断"的地方,先问清楚自己数的是
len()还是chars().count()。
9.3 Windows 中文路径的 GBK 陷阱
Windows 上不少老工具(包括部分 SDK 工具)用 GBK 而非 UTF-8 处理路径。一旦路径里出现中文,子进程就拿到乱码、找不到文件。修复方式是在跨平台边界上显式声明编码,绝不依赖系统默认。
适配鸿蒙这套工具链,我沉淀出六条经验:
- 单二进制分发是工具链进 CI 的前提,别让用户装依赖。
- 厚 core + 薄入口:能力只写一遍,命令行 / GUI / 插件共享同一套逻辑。
- 透传是底线:封装可以有,但不能挡住底层工具的原生能力。
- 跨进程注入环境不可靠:要么前台跑,要么写进对方自己的配置。
- 幂等是自动化的生命线:CI 会反复跑,每一步都要"先查再做"。
- 编码要显式声明:别假设 UTF-8,尤其在 Windows。
十、展望
10.1 覆盖度
- 补全 Windows 上的签名与模拟器端到端覆盖(目前 macOS 已跑通)。
- 输出更多
--json结构化字段,让第三方编排更稳定。 - 模拟器镜像支持断点续传,弱网环境下载更稳。
10.2 生态
- 把高频操作沉淀成 MCP 工具,让 AI 助手能直接驱动这条工具链。
- 面向 CI 提供"一行装环境 + 一行出包"的最小工作流模板。
- 与社区共建,把适配经验沉淀成可复用的最佳实践。
附录:常见问题
Q1:ark-cli 会和 DevEco 冲突吗?
不冲突。ark-cli 只是去调用 DevEco / SDK 里的工具(hvigor、hdc、签名工具),不修改 IDE 本身,两者可以并存,甚至同时用。
Q2:没装 DevEco 能用吗?
能。ark runtime install 会把 SDK 和工具链独立装到 ~/.ark-cli/runtime/ 下,不依赖 DevEco 是否安装。这也是它能进 CI 的原因。
Q3:支持 Linux CI 吗?
支持。Rust 交叉编译到 aarch64-unknown-linux-gnu,产出的单二进制直接就能跑,不需要在 CI 上再装任何依赖。
Q4:签名密钥安全吗?
.p12 私钥在本地加密保管;ark signature generate 是幂等的,重复跑不会重新生成、也不会把密钥写进构建产物或日志。
总结
ark-cli 想证明一件事:鸿蒙的工具链,可以像 Android 的 adb / gradle 一样,被命令、被脚本、被 CI 稳稳地驱动——而不必每次都打开一个 GUI。从 Rust 单二进制、统一运行时、内嵌 hdc,到编排 hvigor、自动化签名,每一步都是"把 DevEco 里能命令化的部分抽出来"。它不取代 DevEco,而是补上 DevEco 够不到的那一片——脚本、CI、批量、远程。
下面是全部命令的速查表:
命令速查表
| 命令 | 作用 |
|---|---|
ark runtime install | 一键装齐 SDK / Emulator / hvigorw / ohpm / node / hdc |
ark runtime install <组件> | 分量级安装(sdk / emulator / hvigorw) |
ark runtime status | 逐组件体检,给出 ✅ / ❌ |
ark devices | 列出在线设备 |
ark hdc <参数> | 100% 透传 hdc |
ark logcat -m <模式> | 抓日志(Screen / Crash 等模式) |
ark forward <local> <remote> | 端口转发 |
ark create <名> --bundle <b> | 按模板生成可构建工程 |
ark build [--mode debug] | 构建出 .hap |
ark build --watch | 监听文件变化,增量热构建 |
ark run | 构建 + 安装 + 启动到设备 |
ark emulator list | 列出模拟器与版本 |
ark emulator create/start/stop/delete | 模拟器实例生命周期 |
ark emulator image-list | 查看可下载的系统镜像 |
ark emulator image-install <设备> | 下载系统镜像(带进度) |
ark login | 登录 AppGallery Connect |
ark signature generate | 自动生成签名材料并配置到项目 |
如果这篇文章对你有帮助,欢迎点赞👍、收藏⭐、关注🔔,你的支持是我持续创作的动力!
相关资源:
- ark-cli 仓库:atomgit.com/nutpi/ark-cli
- Rust 官方:rust-lang.org
- Tauri(桌面 GUI 入口):tauri.app
- OpenHarmony 官方:gitee.com/openharmony
- DevEco Studio:developer.huawei.com
- 开源鸿蒙跨平台社区:openharmonycrossplatform.csdn.net
更多推荐



所有评论(0)