From 92bfc412b708860fd2d2bfd9bd82418ace6a9dc6 Mon Sep 17 00:00:00 2001 From: sudo_free Date: Mon, 9 Nov 2026 00:19:58 +0800 Subject: [PATCH 1/3] chore: keep the handoff notes out of the repository HANDOFF.md is an agent-side document. It has nothing to do with the tool and it entered the tree by accident; the file stays on disk, untracked. --- .gitignore | 3 +++ HANDOFF.md | 67 ------------------------------------------------------ 2 files changed, 3 insertions(+), 67 deletions(-) delete mode 100644 HANDOFF.md diff --git a/.gitignore b/.gitignore index cdff8fd..eb89467 100644 --- a/.gitignore +++ b/.gitignore @@ -49,3 +49,6 @@ screenshot-*.jpeg # Local toolchain caches and virtualenvs (never committed). .cargo-home/ .venv/ + +# Agent-side handoff notes: local only, never committed. +/HANDOFF.md diff --git a/HANDOFF.md b/HANDOFF.md deleted file mode 100644 index 12d0722..0000000 --- a/HANDOFF.md +++ /dev/null @@ -1,67 +0,0 @@ -# Handoff — hvolt 26.0.0-9 - -工作区:`/Users/hongyuncong/workspace/better-harmony-app-build` -当前基线:`main` = `bc7f104`(release: 26.0.0-9 (#44)) - -## 状态 - -- PR 43(#43)已合并:资源编译树内化、SDK 平台二进制收敛到 `es2abc` + `llvm-strip`、npm 平台包对齐、产品变体、`src/` 拆领域模块、命令改名 `build`/`migrate`。 -- PR 44(#44)已合并:版本号 `26.0.0-9`,覆盖 `Cargo.toml`、`Cargo.lock`、`npm/package.json` 与五个 `npm/platforms/*`。 -- 发布由 `release.yml` 驱动:`main` 上版本比最新 tag 新就自动构建 + GitHub release + npm publish,不靠手动打 tag。 - -本文件替代此前那份「自建 ArkTS 编译链」handoff。旧的逆向细节已落进代码与 `docs/agents/`,最新状态以仓库为准。 - -## 发布链上最重要的一个坑:私有 overlay - -`agc-local/` **不在仓库里**(`.gitignore:33`),它是 AGC 后端,CI 从 secret `AGC_OVERLAY_B64` 解出 tar.gz 到仓库根,再以 `--features agc-local` 编译。 - -后果:**仓库代码里 `src/sign/backend.rs` 的调用面与 secret 里的 overlay 必须同时更新**。只改一边,本地因为 gitignore 里那份旧副本存在而照样能编过,CI 上三个目标机会一起炸。 - -本次就是这样失败的:`backend.rs` 调用 `add_device_cli` / `delete_device_cli`,secret(2026-10-24 版)提供的还是底层 `add_device`,linux-x64 与 win32-x64 报 `cannot find function`,darwin-arm64 被连带取消,publish 跳过。 - -判据:改 `src/sign/**` 或 `agc-local/**` 任一方的接口后,必须同步更新 secret: - -``` -tar -czf /tmp/agc-overlay.tgz agc-local -base64 -i /tmp/agc-overlay.tgz -o /tmp/agc-overlay.b64 -gh secret set AGC_OVERLAY_B64 --body "$(cat /tmp/agc-overlay.b64)" -``` - -本地验证覆盖不到这条:`cargo check --features agc-local` 用的是磁盘上那份,不是 secret。 - -## 推 GitHub 的通道 - -`github.com:443` 的 https 在本机不通(HTTP2 framing)。传输走 git over SSH-443: - -``` -GIT_SSH_COMMAND='ssh -o StrictHostKeyChecking=no -p 443' \ - git push ssh://git@ssh.github.com/hvolt-project/hvolt.git -``` - -`gh` 只管 API(`pr create/view/checks/merge`、`run list/watch`、`secret set`)。gh 不能传输 git 对象:远端没有该对象时 `git/commits/` 返回 404,`POST /git/refs` 只能指向已存在的对象。 - -注意 `git fetch` 同样要带 `GIT_SSH_COMMAND`,否则默认 22 端口连不上,报的是「检查访问权限」而不是网络错误。 - -## 本次的行为教训 - -1. **本职环节不转审批项。** 传输、构建、验 CI 是代理职守;把它们拆成「请批准」「你跑这个脚本」等于把活推回去。 -2. **失败两次换判据。** 同一命令连撞三次以上,说明没在判断错误类型。 -3. **交付前对账。** PR 正文要覆盖 `git log` 的全部提交,模板三段要照抄仓库 `.github/PULL_REQUEST_TEMPLATE.md`。 -4. **CI 与本地不一致时用环境差异复现。** 本次两个测试分别依赖私有绝对路径与本机 SDK,修法不是跳过而是换仓库内 fixture + 在无 SDK 的 HOME 下实测。 -5. **`git reset --hard` 前先看 `git status`。** 一次误操作丢掉了工作区里未提交的 `AGENTS.md` 与 `HANDOFF.md` 改动(前者无法恢复)。 - -## 工作区未提交项 - -`docs/agents/resources-index.md`、`docs/agents/sdk-dependencies.md` 未跟踪(agent 侧文档,按既定标准不进 PR)。`.scratch/` 下是临时脚本。 - -## 建议技能 - -- `harmony-build` — hvolt 构建、装包、抓日志 -- `arkts-pitfalls` — 改 ArkTS 编译相关代码 -- `docs/agents/clean-code.md` — 提交闸(fmt / clippy `-D warnings` / test 三绿) - -## 下次接手先做 - -1. `gh run list --workflow=release.yml --limit 3` 确认 26.0.0-9 的发布结果 -2. `gh release view v26.0.0-9`、`npm view @hvolt/cli version` 核对两个出口都到位 -3. 改任何 `sign/**` 或 `agc-local/**` 之前,先读上面「私有 overlay」一节 From b45d7fabbe98b0d2b64dc18d224449d0f12d842a Mon Sep 17 00:00:00 2001 From: sudo_free Date: Mon, 9 Nov 2026 00:35:57 +0800 Subject: [PATCH 2/3] chore: keep the local development notes out of the public tree MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This repository is public. These describe a local development setup rather than the tool, so they leave the tree: - scripts/debug/ — 16 one-off reverse-engineering probes, two of which carry a plaintext p12 password - docs/agents/ and AGENTS.md — agent-side notes Both are gitignored so a stray 'git add -A' cannot bring them back. docs/published/ stays: those three files are compiled into the binary by src/support/skill.rs. --- .gitignore | 5 +- AGENTS.md | 25 -- docs/agents/clean-code.md | 157 ----------- docs/agents/domain.md | 51 ---- docs/agents/harmony-device-flow.md | 67 ----- docs/agents/hvolt-skill.md | 373 -------------------------- docs/agents/hvolt-toml.md | 280 ------------------- docs/agents/issue-tracker.md | 19 -- docs/agents/triage-labels.md | 15 -- scripts/debug/ast-diff.js | 53 ---- scripts/debug/cleanup-agents.py | 177 ------------ scripts/debug/compare-trees.js | 163 ----------- scripts/debug/extract_ohos_keys.sh | 42 --- scripts/debug/find_urls.py | 13 - scripts/debug/inspect_hos_sdk_list.py | 34 --- scripts/debug/inspect_sdk_hos_core.py | 14 - scripts/debug/inspect_sdk_list.py | 18 -- scripts/debug/parse_grs.py | 84 ------ scripts/debug/probe_mode_error.py | 16 -- scripts/debug/probe_oh_loader.js | 25 -- scripts/debug/probe_version_mapper.py | 32 --- scripts/debug/read_sdk_props.py | 13 - scripts/debug/sign_with_ohos.sh | 134 --------- scripts/debug/test_token_exchange.py | 107 -------- scripts/debug/token-diff.js | 224 ---------------- src/device/target.rs | 8 +- 26 files changed, 8 insertions(+), 2141 deletions(-) delete mode 100644 AGENTS.md delete mode 100644 docs/agents/clean-code.md delete mode 100644 docs/agents/domain.md delete mode 100644 docs/agents/harmony-device-flow.md delete mode 100644 docs/agents/hvolt-skill.md delete mode 100644 docs/agents/hvolt-toml.md delete mode 100644 docs/agents/issue-tracker.md delete mode 100644 docs/agents/triage-labels.md delete mode 100644 scripts/debug/ast-diff.js delete mode 100644 scripts/debug/cleanup-agents.py delete mode 100644 scripts/debug/compare-trees.js delete mode 100755 scripts/debug/extract_ohos_keys.sh delete mode 100644 scripts/debug/find_urls.py delete mode 100644 scripts/debug/inspect_hos_sdk_list.py delete mode 100644 scripts/debug/inspect_sdk_hos_core.py delete mode 100644 scripts/debug/inspect_sdk_list.py delete mode 100644 scripts/debug/parse_grs.py delete mode 100644 scripts/debug/probe_mode_error.py delete mode 100644 scripts/debug/probe_oh_loader.js delete mode 100644 scripts/debug/probe_version_mapper.py delete mode 100644 scripts/debug/read_sdk_props.py delete mode 100755 scripts/debug/sign_with_ohos.sh delete mode 100644 scripts/debug/test_token_exchange.py delete mode 100644 scripts/debug/token-diff.js diff --git a/.gitignore b/.gitignore index eb89467..af95205 100644 --- a/.gitignore +++ b/.gitignore @@ -50,5 +50,8 @@ screenshot-*.jpeg .cargo-home/ .venv/ -# Agent-side handoff notes: local only, never committed. +# Agent-side notes: this repository is public and these describe a local +# development setup, so they stay on the machine instead of in the tree. /HANDOFF.md +/AGENTS.md +/docs/agents/ diff --git a/AGENTS.md b/AGENTS.md deleted file mode 100644 index 7787bdd..0000000 --- a/AGENTS.md +++ /dev/null @@ -1,25 +0,0 @@ -## Agent skills - -### Issue tracker - -Issues live as local markdown files under `.scratch//`. See `docs/agents/issue-tracker.md`. - -### Triage labels - -All five triage roles use their canonical names (`needs-triage`, `needs-info`, `ready-for-agent`, `ready-for-human`, `wontfix`). See `docs/agents/triage-labels.md`. - -### HarmonyOS device flow - -真机装包跨三台机器(构建/签名宿主、持有设备的主机、本机)与两条签名链(localSign 只被开发板信任,零售机必须走 AGC 覆盖层)。改构建/签名/推包相关的东西、或要往设备上装包之前,先读拓扑与命令:See `docs/agents/harmony-device-flow.md`. - -### Domain docs - -Single-context layout — one `CONTEXT.md` + `docs/adr/` at the repo root. See `docs/agents/domain.md`. - -### hvolt codebase - -hvolt 的架构、CLI 命令参考、配置系统和开发指南。在修改 hvolt 代码或添加新功能前参阅。See `docs/agents/hvolt-skill.md`. - -### Clean code - -代码规范与验收闸(格式化、clippy 严格模式、测试三道全绿才提交)。改 `src/**` 前参阅。See `docs/agents/clean-code.md`. diff --git a/docs/agents/clean-code.md b/docs/agents/clean-code.md deleted file mode 100644 index 7842dfb..0000000 --- a/docs/agents/clean-code.md +++ /dev/null @@ -1,157 +0,0 @@ -# hvolt clean-code 规范 - -适用于 `src/**`。改代码前必读;提交前必须让三道检查全部通过。 - -## 1. 验收命令(三条全绿才提交) - -```bash -cargo fmt --all --check # 空输出 = 通过 -cargo clippy --all-targets -- -D warnings # 末行 "Finished" = 通过 -cargo test # 全部通过 = 通过 -``` - -判定规则由 `rustfmt.toml` 与 `Cargo.toml` 的 `[lints]` 固定,不靠人工记忆。 -任一条不过,不提交。 - -## 2. 格式化 - -- 只认 `rustfmt.toml`(stable 选项,`edition=2021`、`max_width=100`、`use_field_init_shorthand`)。 -- 不手写列对齐、不用 tab、不留 `cargo fmt` 可改的 diff。 - -## 3. 命名与模块 - -- 一个模块一件事:`src/build/deps/pipeline.rs`=构建编排,`src/sdk/manage.rs`=SDK 解析/下载,`src/device/`=设备协议;新职责新文件,不塞进 `main.rs`。 -- 文件首行用一行 `// <域>: <干什么>` 起头(如 `// AGC: manage AppGallery Connect resources`)。 -- `pub` 只给 `main.rs`/`cli.rs` 分派需要的入口;其余用 `pub(crate)` 或私有。二进制 crate 没有下游消费者,`pub` 不是 API 承诺。 -- CLI 子命令动词与 `src/support/cli.rs` 的变体一一对应,不另造同义词。 - -## 4. 错误处理 - -- 到 CLI 边界统一 `Result<(), String>`;错误串必须带上下文:`format!("read {}: {}", path.display(), e)`。 -- **禁止 `let _ = ` 吞错**。可忽略的失败要么 `eprintln!("Warning: ...")`,要么紧邻一行注释说明为什么能丢(例:`let _ = fs::remove_file(...)` 清理临时文件)。 -- `expect()` 只用于「失败即不可能」的位置,消息写不变量(`"create element dir"`),不写 `"failed"`。 -- 用户输入错误走 `Err(...)`,不 `panic!`。 - -## 5. 规模与类型 - -- 参数 > 7 个:优先抽结构体;确属 CLI 透传(一个参数对一个 flag)时用 - `#[allow(clippy::too_many_arguments)]` + 同一行理由。 -- 返回元组 ≥ 3 元素:起 `type` 别名(现有:`ToolPaths`、`ModuleSets`)。 -- 单函数 > 约 80 行:拆分,或在函数头注明为什么不能拆。 - -## 6. 死代码 - -- 不留 "legacy entry point / kept for X wiring" 之类的兼容壳——需要时从 git 历史取回。 -- 必须保留的:平台门控函数、尚未接线的保留方言,用 `#[allow(dead_code)] // 理由`。 - -## 7. 文档注释 - -- 公开项一行摘要 + 必要时机制段;不写空话。 -- 注释里出现的 CLI 命令必须与 `hvolt --help` 实际一致:删/改命令的同一次提交里同步注释。 -- 机制段写「测得的机制 + 日期」,不写猜测;不确定就写「未验证」。 -- 用技术陈述:不写第一人称(we/our/my)、不写内部工单代号(`audit-1`、`A/C-1` 之类)、不写隐喻或俚语 - (honest / disease / explode / litter)。需要承载历史时用「(measured)」+ 日期。 -- 机器核对: - - ```bash - grep -rnE '^\s*(///|//|//!).*(audit[- ]?[0-9#]|\b(we|our|my|I)\b|honest|disease|explode|litter)' src/ - ``` - -## 8. unsafe 与平台分支 - -- 全 crate **有两处** `unsafe`,都在本表登记,新增必须同步本文: - - `main.rs` 把 SIGPIPE 复位为 `SIG_DFL`(`hvolt ... | head` 不 panic); - - `support/platform.rs` 的 `gethostname`(`libc` 调用需要一个可写缓冲区,长度由返回的指针值给出)。 -- `#[cfg(windows)]` / `#[cfg(unix)]` 两支都要能独立读懂:目录用 `remove_dir`、符号链接用 `remove_file` 这类差异,注释写明为什么。 - -## 9. 测试 - -- 与实现同文件 `mod tests`,且放在文件**末尾**(`clippy::items_after_test_module` 会拦)。 -- 新增行为必须带测试;测试名描述行为(`rejects_dot_dot_entry`),不描述实现细节。 - -## 10. 当前 allow 清单(审计用) - -| 位置 | 属性 | 理由 | -|---|---|---| -| `src/sign/package.rs` ×4(`create_provision`/`finalize_signing`/`sign_app`/`sign_package`) | `clippy::too_many_arguments` | 与签名 CLI 表面 1:1 | -| `src/main.rs` `reset_sigpipe` | `rustc::unsafe_code` | SIGPIPE 复位,见 §8 | -| `src/support/platform.rs` `gethostname` | `rustc::unsafe_code` | `libc::gethostname` 的缓冲区契约,见 §8 | -| `src/device/target.rs` `Shell::PowerShell` | `dead_code` | 保留方言:pwsh 默认主机接入后即用 | - -新增条目必须同时改这张表。 - -## 11. 提交前清单 - -1. `cargo fmt --all --check` → 空输出 -2. `cargo clippy --all-targets -- -D warnings` → Finished -3. `cargo test` → 全部通过(本次基线 169,改后不得减少) -4. 动过 CLI 或注释里的命令 → 用 `hvolt --help` 复核 -5. 动过注释或文档 → 跑 §7 的 grep,输出只允许剩下引号内的示例文本 -6. 动过行为 → 同轮跑一次真实调用并把读数写进交付说明 -7. 动过结构(合并重复、抽接缝、改签名)→ 跑 §12 的三项判据并给出读数 - -## 12. 结构判据(可机械核对) - -用于判断一次改动是否让结构变好,而不是只让测试变多。任一项不达标即为未完成的重构。 - -### 12.1 单一真源计数 - -同一个策略——默认值、参数表、判定规则、路径拼装——在 crate 内只允许一处实现。 -第二份出现即缺陷:删掉,或在注释里写明两份为何必须并存(例如平台分支)。 - -```bash -# 默认值与 runtimeOS 字面量,带上上下文一行,便于确认是否附了指回真源的注释 -grep -rn -B1 '"HarmonyOS"\|"OpenHarmony"' src --include='*.rs' \ - | grep -v test | grep -v '^src/support/config.rs' | grep -v '^--$' -``` - -判据:命中为 0,或每条命中上方紧邻一行注释说明它为何不是 runtimeOS 策略。 -当前两处合规例外:`src/sign/sign_local.rs` 的 profile `developer-id`、`src/translator/strategy.rs` -的 signingConfigs `type`,均为签名配置的固定字段。 - -真源位置:runtimeOS 的默认值与比较在 `src/support/config.rs`(`DEFAULT_RUNTIME_OS`、 -`effective_runtime_os`、`is_open_harmony`);平台判断在 `src/support/platform.rs`。 - -### 12.2 接缝纯度 - -判定与 IO 分离:一个函数要么决定「该不该做、是不是错」,要么执行「读写、起进程」, -不在同一函数里既判定又执行。 - -- 判定函数:签名与函数体内不出现 `&Path`、`Command`、`std::fs`、`SdkEnv`。 -- IO 函数:不自行推导策略;策略来自入参或专门的纯函数。 - -```bash -# 纯函数的签名只含数据 -grep -rn 'pub(crate) fn ' src/*.rs | grep -vE '&Path|Command|SdkEnv' -``` - -判据:命中项必须满足其一—— -(a) 函数体只做数据准备(候选路径列表、参数打包),无业务判定;或 -(b) 业务判定被抽到同文件的无 IO 函数里,并带单元测试。 - -路径候选列表、curl 参数打包中的 `if`/`match` 属 (a),不算规则。 -反例:`assemble_and_build` 曾把四处 `process::exit` 与构建设置混在一起,判定无法单独调用, -属 (b) 不成立。 - -### 12.3 改动可回退粒度 - -一个提交只承载一类改动:合并重复、抽出接缝、修复缺陷三者不混提。 -混提后无法单独回退其中一类,评审也无法逐条核对。 - -判据:`git show --stat ` 的文件集合若同时包含「仅搬移/重命名」与「行为变更」两类, -即判为不达标,拆分后重提。 - -已按本条拆分的实例:`sign.rs` 的 `SignRequest` 合并、`pipeline::native_build` 的拆分 -的字符串感知修复、`platform` 判断合并,属三类改动,应分三次提交。 - -### 12.4 行为变更须显式报备 - -结构改动若顺带改变行为(例如修复了原实现会写坏文件的扫描逻辑),必须在交付说明里单独列一段: -症状、造成该现象的原因、修改方式、复验读数。不得并入「重构」一并交付。 - -```bash -# 改动前后各跑一次真实调用,保存原始输出 -``` - -判据:交付说明中存在「行为变更」段落,且带命令原始输出。 - diff --git a/docs/agents/domain.md b/docs/agents/domain.md deleted file mode 100644 index c97d6a6..0000000 --- a/docs/agents/domain.md +++ /dev/null @@ -1,51 +0,0 @@ -# Domain Docs - -How the engineering skills should consume this repo's domain documentation when exploring the codebase. - -## Before exploring, read these - -- **`CONTEXT.md`** at the repo root, or -- **`CONTEXT-MAP.md`** at the repo root if it exists — it points at one `CONTEXT.md` per context. Read each one relevant to the topic. -- **`docs/adr/`** — read ADRs that touch the area you're about to work in. In multi-context repos, also check `src//docs/adr/` for context-scoped decisions. - -If any of these files don't exist, **proceed silently**. Don't flag their absence; don't suggest creating them upfront. The producer skill (`/grill-with-docs`) creates them lazily when terms or decisions actually get resolved. - -## File structure - -Single-context repo (most repos): - -``` -/ -├── CONTEXT.md -├── docs/adr/ -│ ├── 0001-event-sourced-orders.md -│ └── 0002-postgres-for-write-model.md -└── src/ -``` - -Multi-context repo (presence of `CONTEXT-MAP.md` at the root): - -``` -/ -├── CONTEXT-MAP.md -├── docs/adr/ ← system-wide decisions -└── src/ - ├── ordering/ - │ ├── CONTEXT.md - │ └── docs/adr/ ← context-specific decisions - └── billing/ - ├── CONTEXT.md - └── docs/adr/ -``` - -## Use the glossary's vocabulary - -When your output names a domain concept (in an issue title, a refactor proposal, a hypothesis, a test name), use the term as defined in `CONTEXT.md`. Don't drift to synonyms the glossary explicitly avoids. - -If the concept you need isn't in the glossary yet, that's a signal — either you're inventing language the project doesn't use (reconsider) or there's a real gap (note it for `/grill-with-docs`). - -## Flag ADR conflicts - -If your output contradicts an existing ADR, surface it explicitly rather than silently overriding: - -> _Contradicts ADR-0007 (event-sourced orders) — but worth reopening because…_ diff --git a/docs/agents/harmony-device-flow.md b/docs/agents/harmony-device-flow.md deleted file mode 100644 index bf88e24..0000000 --- a/docs/agents/harmony-device-flow.md +++ /dev/null @@ -1,67 +0,0 @@ -# 鸿蒙真机装包:谁构建、谁签名、谁推包 - -这份文档记录跨主机的工作拓扑。它存在的原因是:拓扑只在会话里说过一次,压缩之后就丢了,之后的 agent 会把「更新远端 hvolt」理解成「在本机造一个 Windows 二进制」,然后去装 mingw、改 CI、甚至提议用 DevEco 图形界面——四条路都绕开了既有工具链。 - -## 三台机器的职责 - -| 主机 | 地址 | 有什么 | 负责 | -|---|---|---|---| -| GPU 服务器 | `hongyuncong@192.168.3.4:52525` | Rust、SDK(`~/.hvolt/sdk` 26.0.0.38)、AGC 登录态 | **构建、签名** | -| macOS(本机) | 本机 | hvolt、SDK 编译器(es2abc/restool)、`~/DevEcoStudioProjects/` | 转发文件;同网段时也能直连设备 | -| Windows Surface | `xyzsu@100.64.0.2`(tailnet) | `~/.local/bin/hvolt.exe`、SDK 编译器、USB/无线连真机 | 推包 | -| 真机 | `192.168.1.3:38565`(PLR-AL00,API 26) | — | 安装与运行 | - -**关键约束**:GPU 服务器到不了 Surface(不在同一 tailnet)。 - -## 签名有两条链,选错就装不上 - -| 链 | 命令 | 被谁信任 | -|---|---|---| -| OpenHarmony localSign | `hvolt sign --local` | 开发板 / 模拟器 / 自编译 ROM。**零售机拒**:`code:9568257 error: fail to verify pkcs7 file` | -| AppGallery Connect | `hvolt sign`(无 `--local`) | 华为开发者账号签发的证书与 profile,零售机可装 | - -AGC 后端是 `agc-local/` 覆盖层,**不在公开源码树里**(`Cargo.toml` 的 `agc-local` feature)。没有它时 `hvolt agc` 与 `hvolt sign` 会明确报「后端缺失」。 - -```bash -cargo build --release --features agc-local # 需要 agc-local/ 目录在场 -``` - -## 完整流程(2026-10-24 实跑) - -```bash -# 1 构建(服务器) -cd ~/work/hvolt-current && ./target/release/hvolt build --cache-dir /tmp/hv-cache - -# 2 签名(服务器;设备在别处,用覆盖层预留的跨机入口交 UDID) -HVOLT_DEVICE_UDID=$UDID ./target/release/hvolt sign - -# 3 传递(服务器 → 本机 → Surface,服务器到不了 Surface) -scp output/*-signed.hap :/tmp/ && scp /tmp/*-signed.hap xyzsu@100.64.0.2:C:/tmp/ - -# 4 安装与取证(Surface) -hvolt push C:/tmp/-signed.hap -d -hvolt log -d -T -``` - -## 签名分两步,别在 `sign` 之后就找包 - -`hvolt sign` 只做三件事:生成 keystore 与 CSR、向 AGC 换证书与 provisioning profile、把 -`[app.signing.default]` 写进 `hvolt.toml`。**它不签 HAP。** HAP 的签名发生在下一次 -`hvolt build`(输出里出现 `Signing HAP... ✓ -signed.hap`)。 - -```bash -hvolt build # 出 unsigned HAP -hvolt sign # 换证书 + profile + 写配置(设备在别处时配合 HVOLT_DEVICE_UDID) -hvolt build # ← 这一步才签出 signed HAP -``` - -## 已遇到的问题(都经过实测) - -- **不要替换 Surface 上的 `hvolt.exe` 而不知道它是什么构建**。2026-10-24 我用 CI 的普通构建覆盖了它(自称 `0.1.0` 说明那是另一条构建线,普通构建打印 `26.0.0-1 (SDK 26.0.0)`),备份留在 `~/.local/bin/hvolt.exe.bak-20261024-1050`。 -- **`runtime_os` 与 `device_types` 必须匹配 SDK**:没有 hms 的机器上 `runtime_os = "HarmonyOS"` 会报 `00303168 SDK component missing`;改成 `OpenHarmony` 后 `device_types` 只能用 `[default, tablet, tv, wearable, car, 2in1]`,`phone` 会被拒。`hvolt doctor` 会把这条直接写出来,先跑它。 -- **AGC 登录态会过期**:`hvolt agc login` 打印一个 `apply?port=` 的链接,需要在本机浏览器打开完成回调;refresh token 过期时报 `jwToken check failed … 401`,只能重新登录。 -- **`~/.hvolt` 可能不可写**(沙箱或权限):把 `HOME` 指到工作区内一份副本跑,登录态与签名材料都会落在那里。 -- **生成的 keystore 口令不能是纯十六进制**:`build` 把「32 位以上全十六进制」判成 DevEco 编码口令并拒绝签名,而早期 AGC 生成器产出的正是 64 位十六进制——工具拒绝自己刚生成的密码。覆盖层生成器已改为非十六进制字母表(26.0.0-3 起)。手工绕过:`keytool -importkeystore` 重包成非十六进制口令,再同步改 `hvolt.toml`。 -- **发布版是否带 AGC 要当场验**:`hvolt agc whoami` 返回账号=后端在;返回 `this build has no AppGallery Connect backend` =公开构建。覆盖层通过仓库 secret `AGC_OVERLAY_B64` 在 CI 侧解码,源码不在仓库里。 -- **无线调试会掉线**:`hvolt device list` 看不到目标时,先 `hvolt device connect ` 校验可达并记入配置。 -- **`hvolt sign` 在设备不可见时报 `No device UDID found`**:把真机 UDID 通过 `HVOLT_DEVICE_UDID` 交给签名端(UDID 由持有设备的主机取得)。 diff --git a/docs/agents/hvolt-skill.md b/docs/agents/hvolt-skill.md deleted file mode 100644 index d3741f0..0000000 --- a/docs/agents/hvolt-skill.md +++ /dev/null @@ -1,373 +0,0 @@ -# hvolt 技能 — HarmonyOS Community Build Tool - -## 项目概述 - -hvolt(HarmonyOS Volt)是一个社区构建工具,将扁平化项目结构转换成 HarmonyOS 标准工程布局。 - -语言:Rust(edition 2021) -二进制:`hvolt` -仓库:`better-harmony-app-build` - -### 扁平项目布局(hvolt 自己的工程格式) - -hvolt 不只是构建工具,它**定义**工程格式:一棵扁平树,`assemble` 翻成标准 HarmonyOS 工程。 -权威示例:`examples/hello-world`(最小)与 `examples/hello-native`(含 C++/NAPI 原生模块)。 - -``` -/ -├── hvolt.toml # 应用级:[app] bundle/name/version/vendor/icon/sdk_version/members、[app.signing]、[dependencies] -├── src/ # 一个模块(目录名即模块名) -│ ├── hvolt.toml # 模块级:[module] name/routes/entry/device_types/obfuscation -│ ├── EntryAbility.ets -│ └── pages/Index.ets -├── resources/base/ # element/{string,color,float}.json + media/ -└── mylib/ # 原生模块(可选) - ├── hvolt.toml # 顶层字段 name / type="native" / types / version / description - ├── CMakeLists.txt # add_library( SHARED napi_init.cpp) - ├── napi_init.cpp # NAPI + __attribute__((constructor)) 注册 - └── Index.d.ts # 给 ArkTS 的类型声明 -``` - -配置格式的权威规范见 **`docs/agents/hvolt-toml.md`**:字段全表、`[dependencies]` 表项语法、 -`[registries]`、版本范围、lock 文件与 `hvolt dep sync`。改配置或加字段前先读它。 - -**原生模块三条约定**(实测,违反则构建失败或运行期找不到库): - -1. 原生模块**只声明在 `[dependencies]`**,不写进 `app.members`(`is_native_module` 按 `type="native"` 判定)。 -2. `mylib/hvolt.toml` 的 `name` 决定产物 `lib.so`;CMake target、`nm_modname`、ArkTS `import` 必须同一致 - (`name="hello"` → target `hello`、`nm_modname="libhello"`、`import { add } from 'libhello.so'`)。 -3. `types` 指向类型声明;`assemble` 生成 `src/main/cpp/types/<依赖名>/`(`oh-package.json5` + `Index.d.ts`), - 并在模块 `oh-package.json5` 里加 `"<依赖名>.so": "file:./src/main/cpp/types/<依赖名>"`, - 同时写 `buildOption.externalNativeOptions.path = ./src/main/cpp/CMakeLists.txt`、`abiFilters = ["arm64-v8a"]`。 - -## 架构 - -### 核心模块 - -| 模块 | 职责 | 关键函数 | -|---|---|---| -| `src/main.rs` | 入口,命令分发 | `main()` | -| `src/support/cli.rs` | CLI 定义(clap derive) | `Args`, `Command` enum | -| `src/sdk/manage.rs` | SDK 发现/管理/下载 | `detect()`, `list_remote()`, `download_sdk()`, `import_sdk()` | -| `src/build/deps/pipeline.rs` | 构建流程编排(原生链) | `native_build()`, `build_and_sign()`, `run()` | -| `src/sign/package.rs` | 远程签名(AGC API) | `sign()`, `sign_remote()` | -| `src/sign/native_sign.rs` | 本地签名(进程内,无 java) | `sign_package()` | -| `src/build/artifacts/hap_pack.rs` | HAP 打包(替代 app_packing_tool.jar) | `pack()` | -| `src/support/config.rs` | `hvolt.toml` 解析 | `AppConfig`, `ModuleConfig`, `NativeModuleConfig` | -| `src/translator/` | 扁平工程发现(模块/原生模块) | `discover_modules()`, `generate_main_pages()` | -| `src/project/scaffold.rs` | 项目脚手架生成 | `scaffold()` | -| `src/project/check.rs` | 包侧质量检查(直接读归档目录) | `check_package()` | -| `src/project/hap.rs` | HAP 内省(bundle/版本/权限) | `hap_info()` | -| `src/project/doctor.rs` | 环境/SDK/设备/凭据一致性预检 | `run()` | -| `src/device/registry.rs` | 设备枚举与管理 | `list()`, `connect()` | -| `src/device/devtools.rs` | 设备侧操作(日志/截图/布局/文件/卸载),走原生设备协议 | `log()`, `screenshot()`, `file_send()`, `file_recv()`, `uninstall()` | -| `src/device/usb.rs` | USB 传输层(nusb + USBHead 信封) | `UsbTransport::open()` | -| `src/sign/sign_local.rs` | 本地签名链(开发板/模拟器) | — | - -> hvolt 不依赖任何外部构建器 / 包管理器 / JDK / 设备通道二进制:ArkTS 编译、打包、签名、设备通信都是本仓实现。 -> 唯一保留的外部依赖是 SDK 里的**编译器**(`es2abc`、`restool` 等官方二进制)。 - -### 数据流 - -``` -hvolt.toml → build-native → assemble-hap(资源) → assemble-ets(ArkTS→abc) → pack-hap → sign → install → device - ↘ ↘ ↘ - SDK: es2abc / restool / ArkTS 声明文件(只取编译器) -``` - -## 配置系统 - -### `hvolt.toml`(项目级) - -```toml -[app] -bundle = "com.example.app" # 必填 -name = "my-app" # 必填 -version = "1.0.0" # 必填 -vendor = "example" # 必填 -icon = "$media:layered_image" # 必填 -sdk_version = "26.0.0" # 兼容 SDK 版本 -compile_sdk_version = "26.0.0" # 编译 SDK 版本 -members = ["src"] # 模块目录列表 - -[app.signing] # 可选,签名配置 -store_file = "signing/debug/debug.p12" -store_password = "..." -key_alias = "debugKey" -key_password = "..." -certpath = "signing/debug/debug.cer" -profile = "signing/debug/debug.p7b" -sign_alg = "SHA256withECDSA" - -[dependencies] # 模块依赖 -# type = "native" → C++/NAPI 原生模块 -``` - -### `hvolt.toml`(模块级,常用字段) - -```toml -[module] -name = "entry" # 模块名;build-profile 与 module.json5 都用它(≠ 目录名) -routes = ["pages/Index"] # → main_pages.json -entry = "EntryAbility.ets" # 入口 UIAbility -device_types = ["phone", "tablet"] -obfuscation = true # → build-profile release 的 ruleOptions.enable -obfuscation_rules = "obfuscation-rules.txt" # 自定义规则;缺省自动发现同名文件 - -# 入口 Ability 的外观覆盖(缺省:应用图标 / auto_rotation) -icon = "$media:startIcon" -start_window_icon = "$media:startIcon" -orientation = "auto_rotation_unspecified" - -# 安装语义覆盖(缺省按 bundle_type 推:atomicService → 免安装) -delivery_with_install = true -installation_free = true -``` - -`[module] icon` / `start_window_icon` / `orientation` 只作用于 `entry` 指的入口 Ability; -`[[module.abilities]]` 的 UIAbility 用各自的 `orientation`。 - ----|---|---| -| 独立 native 模块 | `/hvolt.toml` + `CMakeLists.txt`,并在 `[dependencies]` 声明 | 拷 cpp、写 types 桥、加 `externalNativeOptions` | -| 模块内联 cmake | `/cpp/CMakeLists.txt`(+ `/libs//*.so`) | 递归拷到 `src/main/cpp`、`src/main/libs`,写 `externalNativeOptions`(abiFilters 取 `[module] abi_filters`) | - -**预编译输入放源树**:`/cpp/prebuilt/` 之类的位置,并在 CMakeLists 里引用; -不要放进 `build/`——全量 assemble 会清空该目录。CMakeLists 中若有**越出模块**的相对路径 -(`../../...`),`hvolt migrate` 会以「可能」措辞给出 gap:扁平布局不保证这些路径存在, -hvolt 也不改写 CMake 文件。 - -`cpp/`、`libs/` 永远不会被镜像进 `ets/`——曾经会,结果是 `.cpp` 与 `CMakeLists.txt` 明码打进 HAP。 - -### 目录与资源的归属 - -| 路径 | 翻译到 | 说明 | -|---|---|---| -| `/resources/**` | 模块 `src/main/resources/` | 多语言、dark、resfile 放这里 | -| `resources/**`(根) | 同上(**与模块资源合并**,模块优先) | 应用图标等少量共享资源 | -| `app-resources/**` | `AppScope/resources/base/**` | 应用级 element/media(`$r('app.string.*')` 的归属) | - -### `~/.hvolt/config.json`(用户级) - -```json -{ - "sdk_path": "/path/to/sdk", - "node_home": "/path/to/node", - "device_target": "192.168.3.5:8710" -} -``` - -### SDK 布局 - -``` -~/.hvolt/sdk/ -├── v26.0.0.23/ -│ ├── sdk-pkg.json # 元数据 -│ ├── openharmony/ # OpenHarmony SDK 组件 -│ │ ├── ets/ -│ │ ├── js/ -│ │ ├── native/ -│ │ ├── previewer/ -│ │ └── toolchains/ # es2abc / restool 等官方编译器 -│ └── hms/ # HMS 组件 -└── default → v26.0.0.23 # 当前版本符号链接 -``` - -**同一 SDK 有四种形态,不要混用**(混用会导致构建能过、扫描或校验失败): - -| 形态 | 路径 | 谁在用 | -|---|---|---| -| store | `~/.hvolt/sdk/{default→v, v/openharmony/<组件>/oh-uni-package.json}` | hvolt 管理/下载 | -| 构建视图 | `~/.hvolt/sdk-ohos//{ets,js,native,previewer,toolchains}` | 原生编译链(只取编译器) | -| DevEco 布局 | `/sdk/default/{openharmony,hms}/{组件}` | 装了 IDE 的机器 | - -## 代码约定与验收检查 - -改 `src/**` 的规范本体在 `docs/agents/clean-code.md`。三条硬性检查(缺一不提交): - -```bash -cargo fmt --all --check -cargo clippy --all-targets -- -D warnings -cargo test -``` - -要点速览:错误处理到 CLI 边界统一 `Result<(), String>` 且不吞 `Result`;参数 > 7 用 -`#[allow(clippy::too_many_arguments)]` + 一行理由;返回元组 ≥3 元素起 `type` 别名; -不留 "legacy" 兼容壳;`mod tests` 放文件末尾;全 crate 唯一 `unsafe` 是 `main.rs` 的 SIGPIPE 复位。 - -## CLI 命令参考 - -### 项目命令 - -| 命令 | 说明 | -|---|---| -| `hvolt init ` | 生成新项目脚手架 | -| `hvolt migrate [--from ] [--out ] [--dry-run]` | DevEco 工程 → hvolt 布局;同时列出表达不了的项 | -| `hvolt build` | 原生链构建:编译 ArkTS → 打包 HAP → 签名(不调用外部构建器/包管理器/JDK) | -| `hvolt run` | 推送并启动当前包;配了 `[app.signing]` 时先 build + sign | -| `hvolt clean` | 清理构建产物 | -| `hvolt doctor` | 环境/SDK/工具/设备/凭据一致性预检(exit≠0 表示存在 ✗ 项) | -| `hvolt log` | 设备 hilog:`--level/--bundle/-T tag/-D domain/-k 关键词/--crash/--follow/--tail` | -| `hvolt file send·recv` | 设备文件传输(原生设备协议,双向,字节数与 sha256 可校验) | -| `hvolt uninstall [bundle]` | 卸载(bundle 缺省取 hvolt.toml) | - -### SDK 命令 - -| 命令 | 说明 | -|---|---| -| `hvolt sdk info` | 显示 SDK 路径 + 工具检测状态 | -| `hvolt sdk list-remote` | 列出远程可下载版本 | -| `hvolt sdk download ` | 下载 SDK(支持 `--components` 过滤) | -| `hvolt sdk import ` | 导入已有 SDK 到 `~/.hvolt/sdk/` | -| `hvolt sdk list` | 列出本地已安装版本 | -| `hvolt sdk use ` | 切换当前版本 | -| `hvolt sdk remove ` | 删除版本 | -| `hvolt sdk set ` | 设置 SDK 路径(`--global` 全局) | - -### 配置命令 - -| 命令 | 说明 | -|---|---| -| `hvolt config get` | 显示全部配置 | -| `hvolt config set node_home ` | 设置 Node.js 路径 | -| `hvolt config set sdk_path ` | 设置 SDK 路径 | -| `hvolt config unset ` | 删除配置项 | - -### 检查与扫描 - -| 命令 | 说明 | -|---|---| -| `hvolt check [--size-limit 2M]` | 包侧质量检查(直接读归档目录) | -| `hvolt hap info` | HAP 内省(bundle/版本/权限) | - -### 签名 / 部署 / 设备 - -| 命令 | 说明 | -|---|---| -| `hvolt sign [HAP]` | AGC 签名:CSR→证书→登记设备→profile(需先 `hvolt agc login`)。给了 `HAP` 时签那个裸包:默认走 localSign 测试链,profile 绑**包内 bundleName**(缺失时 `--bundle` 补),结果留在包所在目录的 `-signed.hap`(输入以 `-unsigned` 结尾则换成 `-signed`),签完由内置校验自证(`ApplicationVerifier` + `ProfileVerifier`);`--p12/--profile/--cert` 三项同时给出时改用这些材料签 | -| `hvolt sign --local [HAP]` | 官方公开测试链本地签名(**仅开发板/模拟器**;零售机装不上)。给了 `HAP` 时签该裸包,行为同上;**已带签名的包会被拒绝**(按包内 `META-INF` 判,不看文件名) | -| `hvolt push [-d serial] [--hap FILE]` | 推送 newest `output/*-signed.hap` 并安装;`--hap` 指定任意位置的包文件,**未签名的包直接报错**而不是丢给设备 | -| `hvolt screenshot -o x.jpeg [-d serial]` | 设备截图(设备侧输出 jpeg) | -| `hvolt ui dump` | ArkUI 布局树 dump(uitest dumpLayout) | -| `hvolt device list \| connect \| remote` | 设备列表 / TCP 连接(tconn)/ 登记跨机安装目标 | -| `hvolt emulator list\|start\|stop\|create\|delete\|images\|license` | 模拟器与系统镜像(**二进制与镜像随 DevEco Studio 分发**,仅 Windows/macOS;Linux 无官方二进制) | - -### AGC - -| 命令 | 说明 | -|---|---| -| `hvolt agc login` | 浏览器授权登录(token 落 `~/.hvolt/auth.json`;回调页需在**目标机**打开) | -| `hvolt agc certs \| devices \| whoami` | 证书列表/删除、已登记设备、当前账号 | - -## SdkEnv 结构体 - -传递给构建流程的环境快照: - -```rust -pub struct SdkEnv { - pub ohos_sdk: PathBuf, // SDK 根目录(~/.hvolt/sdk 或配置路径) - pub node_home: PathBuf, // Node.js 家目录(LSP/模拟器包装用) - pub emulator: Option, // 模拟器 -} -``` - -### 发现优先级 - -1. `hvolt config` 中的配置 -2. 环境变量(`NODE_HOME`) -3. `PATH` 查找 - -## 已知限制 - -### API -- `getSdkList` API 对 `linux/arm64` 返回空列表,`linux/x86_64` 报参数异常 -- 历史注记:早期代码曾对 linux 也发 `"darwin"`(osArch 传错值导致的误判);**已证伪并修正** — 现为 `("linux","x86_64") => ("linux","x64")`,linux/x64 toolchains 是 ELF 构建(2026-10-09 服务器实测,见 src/sdk/manage.rs 注释) - -### 构建 -- SDK 中 `restool`、`es2abc` 等编译器是**平台原生二进制**(Mach-O / ELF) -- Linux Docker 内无法运行 macOS 版本的 SDK toolchains - -### lint -- 副本树同步**不跟随链接**(已修:实测 Windows 上源 177 文件 → 副本 178 文件,此前跟随 junction 会到 18337)。 -- `--fix` 未实现(见上节判据里的实测原因)。 -- `doctor` 的 `[lint]` 段只做**警告**(payload / sdk bridge 是否就绪),不把缺 DevEco 的机器算成问题。 -- 规则集与引擎清单跟 IDE 版本绑定,换 DevEco 版本需重取 payload。 -- 曾把"引擎跳过"归因于 **node 版本下限**,**已证伪并撤回**:node 20/22 与 24/26 表现相同,真因是 SDK 形态。 - -### doctor 的 device_types 判据 -- 可用集合 = `modulecheck/module.json` 的 schema 合法集 **∩** 本 SDK `device-define` 实际存在的集合。实测:`phone` 不在 OpenHarmony schema;`car` 在本 SDK 无 `device-define`。 -- 只做只读排查;设备侧只允许装/启停应用与读日志,不代用户注入输入。 - -## 开发指南 - -```bash -# 构建 -cargo build -cargo build --release - -# 运行 -cargo run -- sdk list-remote -cargo run -- sdk info -cargo run -- config get -cargo run -- build # 在 hvolt 项目目录中执行 - -# 测试 -cargo test - -# debug(查看完整 stderr) -RUST_BACKTRACE=1 cargo run -- build 2>&1 -``` - -### 添加新命令 - -1. 在 `src/support/cli.rs` 的 `Command` enum 中添加变体 + 参数 -2. 在 `src/main.rs` 的 `match` 中添加处理分支 -3. 实现业务逻辑 -4. `cargo build` 验证 - -### 模块翻译 - -`assemble()` 流程: - -``` -read hvolt.toml → parse config → discover modules → translate each module: - 1. generate build-profile.json5(含 externalNativeOptions) - 2. generate module.json5(ability/extension config) - 3. generate oh-package.json5(依赖声明) - 4. mirror source files(ETS/JS/C++ → build/.../ets/) - 5. copy resources(media/string/color → AppScope/module resources) - 6. pack the module into a HAP and sign it -``` - -## 构建与签名验收纪律 - -(方法论来源:OpenHarmony 系统应用签名实践参考;原则:**验证最终产物,不验证中间声明**) - -### 验签验的是包,不是日志 - -1. 签名成功日志 ≠ 交付物正确。**验最终 signed HAP**: - `hvolt sign` 签完用内置校验(`ApplicationVerifier` + `ProfileVerifier`)自证,输出 - `✓ verified final package` 与 SHA-256;要外部复核时再用官方 jar 的 `verify-app` / - `verify-profile`,核对 `verifiedPassed: true`。 -2. 从 HAP 内解出 `module.json`,交叉核对四字段与构建目标一致: - `bundleName`、Profile 的 `bundle-info.bundle-name`、`apl`、`app-feature`; - **`validity` 按 UTC 秒读**(模板沿用旧值即过期身份,签名工具不会替你把关)。 -3. 记录 **HAP 的 SHA-256**:确认安装/交付的是同一份文件(hvolt build 后读盘 `md5sum`/`shasum` 即此纪律的轻量版)。 - -### 缓存检查(幂等重跑的前提) - -生成文件存在 ≠ 反映当前配置。`build/`、`output/`、签名中间产物(profile p7b)都按 -**输入 mtime** 判断有效性:模板/证书/配置更新后必须重生成;失败残留要清生成物 -(源材料与证书不动)。hvolt 的 download 重入保持幂等、build 前 `rm -rf build` 冷跑都属此条。 - -### 设备链路的成功是分层的 - -- 传输成功 ≠ 远端 `aa start` 成功:两层结果分别断言(hvolt push/run 输出对应两行)。 -- 安装失败归因顺序:**签名信任 → 包名/更新身份 → Profile 时效 → 设备约束**; - 运行时权限失败才对应 API/模块声明/ACL。修复发生在真实故障层,不用反复重签掩盖功能问题。 -- `aa start` 只对导出的 UIAbility 成立;非导出/UIExtension 组件沿宿主入口验收。 - -### 验签的诚实边界 - -OpenHarmony 公共测试证书链只对**预置该 CA 的设备**(源编译 ROM/开发板/emulator)成立; -零售 HarmonyOS 设备需 AGC 签发身份。两种身份域不可互替——`hvolt sign`(AGC)与 -`hvolt sign --local`(OpenHarmony localSign)分别对应这两个域。 diff --git a/docs/agents/hvolt-toml.md b/docs/agents/hvolt-toml.md deleted file mode 100644 index 8430b4a..0000000 --- a/docs/agents/hvolt-toml.md +++ /dev/null @@ -1,280 +0,0 @@ -# hvolt.toml 规范 - -hvolt 的唯一配置格式。一个应用由**一个根 `hvolt.toml`** 与**若干成员级 `hvolt.toml`** 组成, -每个第三方依赖包也以同样的格式描述自己。设计参照 Cargo:同一套字段定义在不同位置复用, -只有"能出现在哪些位置"不同。 - -- 根 `hvolt.toml`:[app](应用身份)+ [dependencies](全应用可见)+ [registries](依赖源) -- 成员 `hvolt.toml`:[module](模块身份)+ [dependencies](本模块追加) -- 依赖包 `hvolt.toml`:[module](包身份)+ 可选 [dependencies](它自己的依赖) - -## 基本规则 - -1. **一套字段定义**。`[app]` 与 `[module]` 共用同一组身份字段,语义一致;成员未写的字段从根继承。 -2. **扁平布局**。项目里只出现 `hvolt.toml` 描述的源码树。`oh-package.json5`、`build-profile.json5`、 - `hvigorfile.ts`、`package.json` 这类 hvigor/ohpm 布局文件**不进入**扁平项目;依赖包的它们由 - `hvolt dep sync` 在转换阶段消化掉。 -3. **依赖不是成员**。`members` 只列项目自己的源码模块;第三方包走 `[dependencies]`,不进 `members`。 -4. **表格只写事实**。凡是能从源码树推导的(模块名、入口、资源、设备类型)都有缺省值,写出来的只能是覆盖。 - -## 目录布局 - -``` -project/ -├── hvolt.toml # 根:[app] + [dependencies] + [registries] -├── app-resources/ # 应用级资源 -├── entry/ # 成员 -│ ├── hvolt.toml # [module] -│ └── ... -├── proxy_core/ # 成员 -│ ├── hvolt.toml -│ ├── cpp/ # 模块内联 cmake(可选) -│ └── libs//*.so # 预编译原生库(可选) -└── deps/ # registry 里的 ArkTS 包扁平化后落在这里 - └── sandboxfinder/ - ├── hvolt.toml - └── src/... -``` - -纯 JavaScript 包(如 `yaml`)**不落进项目**,编译时直接从 cache 引用。 - -## 根 `hvolt.toml` - -```toml -[app] -bundle = "org.example.app" -name = "MyApp" -version = "1.0.0" -vendor = "example" -icon = "$media:layered_image" -sdk_version = "26.0.0" -compile_sdk_version = "26.0.0" -compatible_sdk_version = "" -runtime_os = "HarmonyOS" -members = ["entry", "proxy_core"] -products = ["default", "release"] -``` - -### [app] 字段 - -| 字段 | 类型 | 缺省 | 说明 | -|---|---|---|---| -| `bundle` | string | 必填 | bundleName | -| `name` | string | 必填 | 应用名(用于 module.json5 的 label 推导) | -| `version` | string | 必填 | versionName;`version_code` 缺省由它推导 | -| `version_code` | u32 | 0 = 推导 | 显式 versionCode | -| `build_version` | string | `"1"` | app.json5 的 buildVersion | -| `vendor` | string | `"example"` | vendor | -| `icon` | string | `$media:layered_image` | 应用图标 | -| `sdk_path` | string | 自动探测 | SDK 根目录 | -| `sdk_version` | string | `"26.0.0"` | 运行时 SDK | -| `compile_sdk_version` | string | 同 `sdk_version` | 编译 SDK | -| `target_sdk_version` | string | 同 compile | targetSdkVersion | -| `compatible_sdk_version` | string | 同 `sdk_version` | 最低兼容版本 | -| `bundle_type` | string | `"app"` | `app` / `atomicService` | -| `products` | array | `["default"]` | 构建产物配置名 | -| `runtime_os` | string | `"HarmonyOS"` | `HarmonyOS` / `OpenHarmony` | -| `members` | array | `[]` | 成员目录(相对项目根) | - -### [app.signing.] - -```toml -[app.signing.default] -store_file = "signing/debug/debug.p12" -store_password = "..." -key_alias = "debugKey" -key_password = "..." -certpath = "signing/debug/debug.cer" -profile = "signing/debug/debug.p7b" -sign_alg = "SHA256withECDSA" -``` - -旧的单份写法 `[app.signing]` 等价于 `[app.signing.default]`。 - -## 成员 `hvolt.toml` - -```toml -[module] -name = "entry" -routes = ["pages/Index"] -entry = "EntryAbility.ets" -device_types = ["phone", "tablet"] -``` - -### [module] 字段 - -| 字段 | 类型 | 缺省 | 说明 | -|---|---|---|---| -| `name` | string | 目录名 | 模块名;module.json5 与 build-profile 都用它 | -| `routes` | array | 自动发现 `pages/` | 页面路由,写 `main_pages.json` | -| `entry` | string | `EntryAbility.ets` | 入口 UIAbility 源文件 | -| `device_types` | array | `["phone","tablet"]` | deviceTypes | -| `obfuscation` | bool | `false` | release 的 `ruleOptions.enable` | -| `obfuscation_rules` | string | 自动发现同名文件 | 自定义混淆规则 | -| `icon` / `start_window_icon` / `orientation` | string | 从 app 继承 | 只作用于 `entry` 指的入口 Ability | -| `delivery_with_install` | bool | 按 bundle_type 推 | 安装语义 | -| `installation_free` | bool | 按 bundle_type 推 | 免安装 | -| `abi_filters` | array | `["arm64-v8a"]` | 原生库 ABI | - -`[[module.abilities]]` 描述非入口的 Ability/ExtensionAbility(`type = ""` 为 UIAbility, -`type = "form"` 为表单扩展),字段与 `[module]` 的身份字段重叠,语义一致。 - -## [dependencies] - -第三方包、原生模块、其他成员都写在这里。表项支持两种写法: - -```toml -[dependencies] -# 1) 简写:只有版本,从 registry 拉 -yaml = "^2.8.0" - -# 2) 表:字段按需给 -sandboxfinder = { version = "^1.0.5", registry = "default", entry = "Index.ets" } -mccharts = { path = "deps/mccharts", version = "2.8.9", type = "arkts" } -flclash = { path = "proxy_core/cpp", type = "native" } -``` - -### 表项字段 - -| 字段 | 类型 | 缺省 | 说明 | -|---|---|---|---| -| `path` | string | 无 | 本地包目录(相对项目根)。省略则从 registry 取 | -| `version` | string | 无(path 依赖可省) | semver 范围,见下 | -| `registry` | string | `"default"` | 取哪个 [registries] 源 | -| `type` | string | 按内容探测 | `arkts` / `js` / `native` | -| `entry` | string | 按惯例 | 包入口,`Index.ets`(arkts)或 package.json 的 main/module(js) | -| `alias` | string | 同 key | ArkTS 里的导入名;`alias` 用于包名与导入名不一致时 | -| `optional` | bool | `false` | 缺失时不报错,只记 gap | - -`type` 的探测顺序:有 `src/main/ets/` 或 `Index.ets` → `arkts`;有 `package.json` 且无 arkts 根 → -`js`;有 `CMakeLists.txt` → `native`。 - -### 版本语法 - -支持完整 semver 范围:精确(`1.2.3`)、`^`、`~`、`>=`、`<`、`*`、`||` 组合。 -解析结果写进 `hvolt.lock`;构建按 lock 走,`hvolt dep sync --locked` 在 lock 缺失或漂移时报错。 - -## [registries] - -```toml -[registries.default] -url = "https://ohpm.openharmony.cn/ohpm/" - -[registries.local] -url = "file:///opt/packages" -``` - -- 支持 `http(s)://` 与 `file://`(本地目录 / 内网私服) -- `default` 缺省指向公开 ohpm registry;全局配置 `~/.hvolt/config.json` 可覆盖默认值 -- 依赖项用 `registry = ""` 选源,省略用 `default` - -## 依赖合并语义 - -与 hvigor 的实际行为一致: - -- 根 `[dependencies]` 对**所有成员可见**(相当于 hoisting 到应用级) -- 成员 `[dependencies]` 在此之上**追加**;同名时**成员声明胜出**,只影响该成员 -- 依赖包自己的 `[dependencies]` 只描述它自己的依赖,不向父级传播 - -同名冲突在 `hvolt dep sync` 阶段报错并列出双方来源,不做静默选择。 - -## 转换规则 - -依赖包是否需要"扁平化"由**包内布局**决定,不由来源决定: - -| 包内形态 | 判定 | 处理 | -|---|---|---| -| `oh-package.json5` + `src/main/ets/` + `Index.ets` | ArkTS 包(hvigor 布局) | 扁平化:生成 `hvolt.toml`,映射入口与资源,剥掉 hvigor JSON,落到 `deps//` | -| 只有 `package.json` + ESM 源码树 | 纯 JS 包 | **不转换**,不落盘,编译时从 cache 直接引用 | -| `CMakeLists.txt` + `cpp/` | 原生模块 | 按 `type = "native"` 编译,声明 `lib.so` | - -扁平化产出的 `deps//` 是普通 hvolt 成员形态(`hvolt.toml` + 源码),但它**不是** `members` -的一员:它由依赖解析引入,`members` 仍然只列项目自己的源码。 - -## hvolt.lock - -```toml -version = 1 - -[[package]] -name = "yaml" -version = "2.8.0" -registry = "https://ohpm.openharmony.cn/ohpm/" -hash = "sha256:..." -type = "js" - -[[package]] -name = "sandboxfinder" -alias = "@cxy/sandboxfinder" -version = "1.0.5" -registry = "default" -hash = "sha256:..." -type = "arkts" -``` - -lock 记录:解析出的精确版本、来源 registry、内容哈希、类型。构建时按 lock 取包; -`hvolt dep sync --locked` 校验 lock 与声明是否一致。 - -## hvolt dep sync - -``` -hvolt dep sync # 解析 → 下载 → 扁平化 → 写 deps/ 与 hvolt.lock -hvolt dep sync --locked # 只按 lock 取,缺包或漂移即失败 -hvolt dep sync --offline # 只用 cache,不联网 -``` - -`hvolt build` 会先跑一次 `dep sync`(行为等同不加参数)。cache 位于 `~/.hvolt/cache/`, -按 `///` 组织,命中即复用。 - -## 明确不做的事 - -- 不读扁平项目里的 `oh-package.json5` / `build-profile.json5` / `package.json` 作为**配置来源**: - 这些是 hvigor 布局,只在转换阶段作为输入被消化,转换后不留在项目里。 -- 不因为依赖引入了包就把它塞进 `members`。 -- 不在没有 lock 变化的情况下重新解析版本范围。 - -## 完整示例 - -根 `hvolt.toml`: - -```toml -[app] -bundle = "org.xbgroup.clashboxLTS" -name = "ClashBox" -version = "1.9.9" -vendor = "xbgroup" -icon = "$media:layered_image" -sdk_version = "26.0.0" -compile_sdk_version = "26.0.0" -members = ["entry", "proxy_core", "xb_components"] - -[app.signing.default] -store_file = "signing/debug/debug.p12" -store_password = "..." -key_alias = "debugKey" -key_password = "..." -certpath = "signing/debug/debug.cer" -profile = "signing/debug/debug.p7b" - -[dependencies] -yaml = "^2.8.0" -mccharts = { version = "^2.8.9", entry = "index.ets" } -sandboxfinder = { version = "^1.0.5", alias = "@cxy/sandboxfinder", entry = "Index.ets" } -flclash = { path = "proxy_core/cpp", type = "native" } - -[registries.default] -url = "https://ohpm.openharmony.cn/ohpm/" -``` - -成员 `entry/hvolt.toml`: - -```toml -[module] -name = "entry" -routes = ["pages/Index"] -entry = "EntryAbility.ets" - -[dependencies] -proxy_core = { path = "../proxy_core" } -xb_components = { path = "../xb_components" } -``` diff --git a/docs/agents/issue-tracker.md b/docs/agents/issue-tracker.md deleted file mode 100644 index a2f08fb..0000000 --- a/docs/agents/issue-tracker.md +++ /dev/null @@ -1,19 +0,0 @@ -# Issue tracker: Local Markdown - -Issues and PRDs for this repo live as markdown files in `.scratch/`. - -## Conventions - -- One feature per directory: `.scratch//` -- The PRD is `.scratch//PRD.md` -- Implementation issues are `.scratch//issues/-.md`, numbered from `01` -- Triage state is recorded as a `Status:` line near the top of each issue file (see `triage-labels.md` for the role strings) -- Comments and conversation history append to the bottom of the file under a `## Comments` heading - -## When a skill says "publish to the issue tracker" - -Create a new file under `.scratch//` (creating the directory if needed). - -## When a skill says "fetch the relevant ticket" - -Read the file at the referenced path. The user will normally pass the path or the issue number directly. diff --git a/docs/agents/triage-labels.md b/docs/agents/triage-labels.md deleted file mode 100644 index b716855..0000000 --- a/docs/agents/triage-labels.md +++ /dev/null @@ -1,15 +0,0 @@ -# Triage Labels - -The skills speak in terms of five canonical triage roles. This file maps those roles to the actual label strings used in this repo's issue tracker. - -| Label in mattpocock/skills | Label in our tracker | Meaning | -| -------------------------- | -------------------- | ---------------------------------------- | -| `needs-triage` | `needs-triage` | Maintainer needs to evaluate this issue | -| `needs-info` | `needs-info` | Waiting on reporter for more information | -| `ready-for-agent` | `ready-for-agent` | Fully specified, ready for an AFK agent | -| `ready-for-human` | `ready-for-human` | Requires human implementation | -| `wontfix` | `wontfix` | Will not be actioned | - -When a skill mentions a role (e.g. "apply the AFK-ready triage label"), use the corresponding label string from this table. - -Edit the right-hand column to match whatever vocabulary you actually use. diff --git a/scripts/debug/ast-diff.js b/scripts/debug/ast-diff.js deleted file mode 100644 index da25215..0000000 --- a/scripts/debug/ast-diff.js +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env node -// 打印 golden 与 actual 的第一个 AST 结构差异,供定位用;判据与 oracle 的 compare.js 一致。 -const fs = require('fs'); -const os = require('os'); -const path = require('path'); - -function loaderRoot() { - const sdkRoot = path.join(os.homedir(), '.hvolt', 'sdk'); - const versions = fs.existsSync(sdkRoot) - ? fs.readdirSync(sdkRoot).filter((v) => fs.statSync(path.join(sdkRoot, v)).isDirectory()).sort().reverse() - : []; - for (const version of versions) { - const candidate = path.join(sdkRoot, version, 'openharmony', 'ets', 'build-tools', 'ets-loader'); - if (fs.existsSync(path.join(candidate, 'main.js'))) return candidate; - } - throw new Error('ets-loader not found'); -} - -const ts = require(path.join(loaderRoot(), 'node_modules', 'typescript')); - -function canonical(node, out) { - if (ts.isParenthesizedExpression(node)) { - canonical(node.expression, out); - return; - } - let extra = ''; - if (ts.isIdentifier(node)) extra = `:${node.text}`; - else if (ts.isStringLiteralLike(node)) extra = `:${JSON.stringify(node.text)}`; - else if (ts.isNumericLiteral(node)) extra = `:${node.text}`; - out.push(ts.SyntaxKind[node.kind] + extra); - ts.forEachChild(node, (child) => canonical(child, out)); -} - -function fingerprint(file) { - const text = fs.readFileSync(file, 'utf8'); - const source = ts.createSourceFile(file, text, ts.ScriptTarget.ES2017, true, ts.ScriptKind.TS); - const parts = []; - canonical(source, parts); - return parts; -} - -const dir = process.argv[2]; -const golden = fingerprint(path.join(dir, 'golden.ts')); -const actual = fingerprint(path.join(dir, 'actual.ts')); -console.log(`length ${golden.length} vs ${actual.length}`); -for (let i = 0; i < Math.max(golden.length, actual.length); i += 1) { - if (golden[i] !== actual[i]) { - console.log('first diff at', i); - console.log(' golden:', golden.slice(Math.max(0, i - 6), i + 6).join(' | ')); - console.log(' actual:', actual.slice(Math.max(0, i - 6), i + 6).join(' | ')); - break; - } -} diff --git a/scripts/debug/cleanup-agents.py b/scripts/debug/cleanup-agents.py deleted file mode 100644 index e43e006..0000000 --- a/scripts/debug/cleanup-agents.py +++ /dev/null @@ -1,177 +0,0 @@ -#!/usr/bin/env python3 -"""Remove finished subagents from this session's catalog and delete their archives. - -Three steps per child, per the agent-cleanup skill: - - 1. `subagents/removeChild` — drops the catalog projection (`list_agents` / sidebar) - 2. `workspace/archiveSession` — archives the session store - 3. `workspace/deleteSession` — deletes it (hard, no recycle bin) - -Run this as a *background* job: calling the host's control RPC from inside a synchronous tool -call deadlocks the event loop that is waiting for that tool call. - -Usage: cleanup-agents.py [ ...] -""" - -import base64 -import hashlib -import hmac -import json -import os -import sys -import time -import urllib.request -import urllib.error - -DSH_HOME = os.environ.get("DSH_HOME", os.path.expanduser("~/.dsh")) -BASE = os.environ.get("DSH_WEB_URL", "http://127.0.0.1:49465") -PARENT = os.environ["DSH_SESSION_ID"] - -# Ids this script must never touch: the session itself, and children the user may still -# continue. Keep this list in the file that actually runs (skill §5). -KEEP = { - PARENT, - "fd26f04f-98ca-452b-9abe-9a630bcaf8a4", - "aab96d2d-763e-42c3-8ef3-598d6b0483b0", - "125ae2aa-4b7b-4c77-987e-d28dfd981df3", - "3f58b227-306e-457a-b363-2bd542345f85", - "42d707b1-76ce-4871-ad2a-1a945acd34ad", - "68d5e26b-a414-4c02-906a-9eea6cc8290f", - "af9956e4-d4f1-4495-bfda-a7f9e83e2d4e", -} - - -def b64url(data: bytes) -> str: - return base64.urlsafe_b64encode(data).decode().rstrip("=") - - -def stack_value(text: str, keys: list[str]): - """Read `key:` blocks from the YAML subset the credentials file uses.""" - wanted = list(keys) - current = None - values = {} - for raw in text.splitlines(): - if not raw.strip() or raw.lstrip().startswith("#"): - continue - indent = len(raw) - len(raw.lstrip()) - line = raw.strip() - key, _, rest = line.partition(":") - rest = rest.strip() - path = (indent, key) - if current is not None and indent <= current[0]: - current = None - if current is not None and indent == current[0] + 2 and key in wanted[1:]: - current = ((indent, key), key) - continue - if key == wanted[0] and not rest: - current = ((indent, key), "root") - continue - if current is not None and rest: - values.setdefault(id(current), {})[key] = rest - return values - - -def read_secret() -> bytes: - text = open(os.path.join(DSH_HOME, ".credentials.yaml")).read() - # Find the browser-session record block, then its `secret:` line inside it. - block = None - for raw in text.splitlines(): - if raw.strip().startswith("client-connection/browser-session:"): - block = [] - continue - if block is not None: - if raw and not raw.startswith(" "): - break - block.append(raw) - if block is None: - raise SystemExit("no client-connection/browser-session record") - for raw in block: - key, _, value = raw.strip().partition(":") - if key == "secret": - return base64.urlsafe_b64decode(value.strip() + "=" * (-len(value.strip()) % 4)) - raise SystemExit("browser-session record has no secret") - - -def cookie() -> str: - secret = read_secret() - authority = BASE.split("://", 1)[1] - name = "dsh-auth-" + b64url(hashlib.sha256(authority.encode()).digest()) - now = int(time.time() * 1000) - payload = { - "version": 1, - "authority": authority, - "issuedAt": now, - "expiresAt": now + 3600 * 1000, - } - body = b64url(json.dumps(payload, separators=(",", ":")).encode()) - mac = b64url(hmac.new(secret, body.encode(), hashlib.sha256).digest()) - return f"{name}=v1.{body}.{mac}" - - -def call(namespace: str, method: str, args: dict, rid: str) -> tuple[bool, str]: - url = f"{BASE}/api/{namespace}/{method}" - body = json.dumps( - { - "type": "client-request", - "rpcId": rid, - "method": f"{namespace}/{method}", - "payload": {"args": {"request": args}}, - } - ).encode() - request = urllib.request.Request( - url, - data=body, - headers={"content-type": "application/json", "cookie": cookie()}, - method="POST", - ) - try: - with urllib.request.urlopen(request, timeout=20) as response: - text = response.read().decode() - return True, text[:200] - except urllib.error.HTTPError as error: - return False, f"HTTP {error.code}: {error.read().decode()[:200]}" - except Exception as error: # noqa: BLE001 - report and continue with the next id - return False, f"{type(error).__name__}: {error}" - - -def main() -> int: - children = [c for c in sys.argv[1:] if c] - overlap = [c for c in children if c in KEEP] - if overlap: - print(f"refusing: {overlap} is in the keep set") - return 2 - if not children: - print("usage: cleanup-agents.py ...") - return 2 - print(f"parent: {PARENT}") - print(f"to remove: {len(children)}") - failures = 0 - for child in children: - ok, detail = call( - "subagents", - "removeChild", - {"parentSessionId": PARENT, "childSessionId": child}, - f"c-rm-{child[:8]}", - ) - print(f" removeChild {child[:8]}: {'ok' if ok else 'FAIL'} {detail}") - if not ok and "not-found" not in detail: - failures += 1 - ok, detail = call( - "workspace", "archiveSession", {"sessionId": child}, f"c-ar-{child[:8]}" - ) - print(f" archive {child[:8]}: {'ok' if ok else 'FAIL'} {detail}") - if not ok: - failures += 1 - continue - ok, detail = call( - "workspace", "deleteSession", {"sessionId": child}, f"c-dl-{child[:8]}" - ) - print(f" delete {child[:8]}: {'ok' if ok else 'FAIL'} {detail}") - if not ok: - failures += 1 - print(f"done, {failures} failure(s)") - return 0 if failures == 0 else 1 - - -if __name__ == "__main__": - sys.exit(main()) \ No newline at end of file diff --git a/scripts/debug/compare-trees.js b/scripts/debug/compare-trees.js deleted file mode 100644 index 3da037e..0000000 --- a/scripts/debug/compare-trees.js +++ /dev/null @@ -1,163 +0,0 @@ -#!/usr/bin/env node -// Compare every emitted `.ts` of the reference loader with hvolt's, at AST level. -// -// The fixture oracle (`scripts/oracle/compare.js`) does this per fixture; this one does it for -// a whole project, which is what a real app needs: a build that packs and installs still runs -// wrong if a field, an enum member or a whole declaration never made it into the output. -// -// Usage: node compare-trees.js -// reference-root: `/build/.../esmodule/` of a DevEco build -// hvolt-root: the staged `.ts` tree hvolt writes (HVOLT_KEEP_TS=1) - -'use strict'; - -const fs = require('fs'); -const os = require('os'); -const path = require('path'); - -function loaderRoot() { - const sdkRoot = path.join(os.homedir(), '.hvolt', 'sdk'); - const versions = fs.existsSync(sdkRoot) - ? fs - .readdirSync(sdkRoot) - .filter((v) => fs.statSync(path.join(sdkRoot, v)).isDirectory()) - .sort() - .reverse() - : []; - for (const version of versions) { - for (const layout of [ - ['openharmony', 'ets', 'build-tools', 'ets-loader'], - ['ets', 'build-tools', 'ets-loader'], - ]) { - const candidate = path.join(sdkRoot, version, ...layout); - if (fs.existsSync(path.join(candidate, 'main.js'))) return candidate; - } - } - throw new Error('ets-loader not found'); -} - -const ts = require(path.join(loaderRoot(), 'node_modules', 'typescript')); - -const IDENTITY_FIELDS = new Set(['bundleName', 'pageFullPath', 'pagePath', 'moduleName']); - -function normaliseProjectIdentity(node, out) { - if (ts.isPropertyAssignment(node) && node.name && (node.name.text || node.name.escapedText)) { - const name = String(node.name.text || node.name.escapedText); - if (IDENTITY_FIELDS.has(name) && ts.isStringLiteralLike(node.initializer)) { - out.push(`${name}: `); - return; - } - } - ts.forEachChild(node, (child) => normaliseProjectIdentity(child, out)); -} - -function canonical(node, out) { - const kind = ts.SyntaxKind[node.kind]; - let extra = ''; - if (ts.isIdentifier(node) || ts.isPrivateIdentifier(node)) { - extra = `:${node.text}`; - } else if (ts.isStringLiteralLike(node)) { - if (/^com\.example\./.test(node.text) || /\/src\/main\/ets\//.test(node.text)) { - extra = ':'; - } else { - extra = `:${JSON.stringify(node.text)}`; - } - } else if (ts.isNumericLiteral(node)) { - extra = `:${node.text}`; - } - if (ts.isParenthesizedExpression(node)) { - canonical(node.expression, out); - return; - } - if (ts.isObjectLiteralExpression(node) && node.properties.length > 0) { - const first = node.properties[0]; - if (ts.isPropertyAssignment(first) && String(first.name.text || first.name.escapedText) === 'page') { - out.push('LocMeta'); - return; - } - } - out.push(kind + extra); - if (ts.isNewExpression(node) && node.arguments && node.arguments.length >= 6) { - node.arguments.forEach((arg, index) => { - if (index === 5) out.push('LocMeta'); - else canonical(arg, out); - }); - return; - } - ts.forEachChild(node, (child) => canonical(child, out)); -} - -function nodes(file) { - const text = fs.readFileSync(file, 'utf8'); - const source = ts.createSourceFile(file, text, ts.ScriptTarget.ES2017, true, ts.ScriptKind.TS); - const parts = []; - canonical(source, parts); - return parts; -} - -function key_of(root, file) { - const rel = path.relative(root, file); - const parts = rel.split(path.sep); - // entry/src/main/ets/pages/Index.ts -> entry/pages/Index.ts - if (parts.length > 4 && parts[1] === 'src' && parts[2] === 'main' && parts[3] === 'ets') { - return [parts[0], ...parts.slice(4)].join('/'); - } - return parts.join('/'); -} - -function walk(dir, out) { - for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { - const full = path.join(dir, entry.name); - if (entry.isDirectory()) walk(full, out); - else if (entry.name.endsWith('.ts')) out.push(full); - } -} - -function main() { - const [referenceRoot, hvoltRoot] = process.argv.slice(2); - if (!referenceRoot || !hvoltRoot) { - console.error('usage: compare-trees.js '); - process.exit(2); - } - const files = []; - walk(referenceRoot, files); - - let same = 0; - const missing = []; - const differing = []; - for (const file of files) { - const key = key_of(referenceRoot, file); - const mine = path.join(hvoltRoot, key); - if (!fs.existsSync(mine)) { - missing.push(key); - continue; - } - const a = nodes(file); - const b = nodes(mine); - if (a.length === b.length && a.every((value, index) => value === b[index])) { - same += 1; - continue; - } - let at = 0; - while (at < a.length && at < b.length && a[at] === b[at]) at += 1; - differing.push({ - key, - at, - reference: a.slice(Math.max(0, at - 2), at + 4), - hvolt: b.slice(Math.max(0, at - 2), at + 4), - }); - } - - console.log(`reference files: ${files.length}`); - console.log(`same: ${same}`); - console.log(`missing in hvolt: ${missing.length}`); - for (const key of missing.slice(0, 20)) console.log(` MISS ${key}`); - console.log(`differing: ${differing.length}`); - for (const d of differing.slice(0, 20)) { - console.log(` DIFF ${d.key} @node ${d.at}`); - console.log(` reference: ${d.reference.join(' | ')}`); - console.log(` hvolt: ${d.hvolt.join(' | ')}`); - } -} - -main(); \ No newline at end of file diff --git a/scripts/debug/extract_ohos_keys.sh b/scripts/debug/extract_ohos_keys.sh deleted file mode 100755 index 93b34af..0000000 --- a/scripts/debug/extract_ohos_keys.sh +++ /dev/null @@ -1,42 +0,0 @@ -#!/bin/bash -# Extract OpenHarmony signing keys from OpenHarmony.p12 -set -e - -P12="/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/lib/OpenHarmony.p12" -PASS="123456" -JAVA="/Applications/DevEco-Studio.app/Contents/jbr/Contents/Home/bin" -OUTDIR="/tmp/ohos_keys" -rm -rf "$OUTDIR" -mkdir -p "$OUTDIR" - -echo "=== Extracting certificates ===" - -# Export all certs as PEM -for alias in "openharmony application root ca" "openharmony application ca" "openharmony application profile debug" "openharmony application profile release" "openharmony application release"; do - safe_name=$(echo "$alias" | tr ' ' '_') - echo " Exporting cert: $alias" - "$JAVA/keytool" -exportcert -alias "$alias" -keystore "$P12" -storetype PKCS12 -storepass "$PASS" -file "$OUTDIR/${safe_name}.cer" -rfc -done - -echo "=== Extracting private keys via openssl ===" - -# Extract private keys (PEM) via openssl pkcs12 -openssl pkcs12 -in "$P12" -passin "pass:$PASS" -nocerts -nodes -out "$OUTDIR/all_keys.pem" 2>/dev/null - -# Also extract individual keys by alias -for alias in "openharmony application root ca" "openharmony application ca" "openharmony application profile debug" "openharmony application profile release" "openharmony application release"; do - safe_name=$(echo "$alias" | tr ' ' '_') - echo " Extracting key: $alias" - # Extract key pair for this alias - openssl pkcs12 -in "$P12" -passin "pass:$PASS" -nocerts -nodes -name "$alias" 2>/dev/null | openssl ec -out "$OUTDIR/${safe_name}_key.pem" 2>/dev/null || echo " (no key for $alias, may be CA cert only)" -done - -echo "=== Files in $OUTDIR ===" -ls -la "$OUTDIR" - -echo "=== Creating cert chain for app signing ===" -# App signing chain: app cert -> intermediate CA -> root CA -cat "$OUTDIR/openharmony_application_release.cer" "$OUTDIR/openharmony_application_ca.cer" "$OUTDIR/openharmony_application_root_ca.cer" > "$OUTDIR/app_cert_chain.pem" -echo " Cert chain: $OUTDIR/app_cert_chain.pem" - -echo "Done!" diff --git a/scripts/debug/find_urls.py b/scripts/debug/find_urls.py deleted file mode 100644 index 94c9609..0000000 --- a/scripts/debug/find_urls.py +++ /dev/null @@ -1,13 +0,0 @@ -import zipfile -import re -import glob - -jars = glob.glob('/Applications/DevEco-Studio.app/Contents/plugins/openharmony/lib/sdk-*.jar') -for jar_path in jars: - print(f"=== {jar_path} ===") - with zipfile.ZipFile(jar_path) as z: - for name in z.namelist(): - if name.endswith('.class') or name.endswith('.json') or name.endswith('.properties'): - data = z.read(name) - for s in re.findall(b'https?://[a-zA-Z0-9_./-]+', data): - print(f" {name}: {s.decode('latin1')}") diff --git a/scripts/debug/inspect_hos_sdk_list.py b/scripts/debug/inspect_hos_sdk_list.py deleted file mode 100644 index 5919f38..0000000 --- a/scripts/debug/inspect_hos_sdk_list.py +++ /dev/null @@ -1,34 +0,0 @@ -import json -import urllib.request - -url = "https://devecostudio-drcn.deveco.dbankcloud.com/sdkmanager/v7/hos/getSdkList" -body = json.dumps({ - "osType": "mac", - "osArch": "arm64", - "supportVersion": "5.1-hos-single-1", - "deviceName": "DevEcoStudio" -}).encode('utf-8') - -headers = { - "Content-Type": "application/json", - "AppId": "DevEcoStudio_Lib", - "Version": "0.0.0.1" -} - -req = urllib.request.Request(url, data=body, headers=headers) -try: - with urllib.request.urlopen(req) as resp: - content = resp.read().decode('utf-8') - print(f"Status: {resp.status}") - data = json.loads(content) - print(f"Total items: {len(data) if isinstance(data, list) else data}") - if isinstance(data, list): - for i, item in enumerate(data[:15]): - print(f"[{i}] path={item.get('path')}, apiVersion={item.get('apiVersion')}, version={item.get('version')}, displayName={item.get('displayName')}, releaseType={item.get('releaseType')}") - print(f" archive={item.get('archive')}") - else: - print(data) -except urllib.error.HTTPError as e: - print(f"HTTP Error: {e.code} {e.read().decode('utf-8', errors='replace')}") -except Exception as e: - print(f"Error: {e}") diff --git a/scripts/debug/inspect_sdk_hos_core.py b/scripts/debug/inspect_sdk_hos_core.py deleted file mode 100644 index f50b978..0000000 --- a/scripts/debug/inspect_sdk_hos_core.py +++ /dev/null @@ -1,14 +0,0 @@ -import zipfile -import re - -jar_path = '/Applications/DevEco-Studio.app/Contents/plugins/openharmony/lib/sdk-hos-core-26.0.0.461.jar' -with zipfile.ZipFile(jar_path) as z: - for name in z.namelist(): - if name.endswith('.class'): - data = z.read(name) - # Find ascii strings - strings = re.findall(b'[\x20-\x7e]{4,}', data) - for s in strings: - s_str = s.decode('ascii', errors='ignore') - if any(k in s_str.lower() for k in ['getsdklist', 'sdkmanager', 'hms', 'hos', 'repo.harmonyos', 'dbankcloud', 'supportversion']): - print(f"[{name}] {s_str}") diff --git a/scripts/debug/inspect_sdk_list.py b/scripts/debug/inspect_sdk_list.py deleted file mode 100644 index e80340f..0000000 --- a/scripts/debug/inspect_sdk_list.py +++ /dev/null @@ -1,18 +0,0 @@ -import json -import urllib.request - -url = "https://repo.harmonyos.com/sdkmanager/v5/ohos/getSdkList" -body = json.dumps({ - "osType": "darwin", - "osArch": "arm64", - "supportVersion": "26.0-ohos-single-1" -}).encode('utf-8') - -req = urllib.request.Request(url, data=body, headers={"Content-Type": "application/json"}) -with urllib.request.urlopen(req) as resp: - data = json.loads(resp.read().decode('utf-8')) - -print(f"Total items: {len(data)}") -for i, item in enumerate(data): - print(f"[{i}] path={item.get('path')}, apiVersion={item.get('apiVersion')}, version={item.get('version')}, displayName={item.get('displayName')}, releaseType={item.get('releaseType')}") - print(f" url={item.get('archive', {}).get('url')}") diff --git a/scripts/debug/parse_grs.py b/scripts/debug/parse_grs.py deleted file mode 100644 index b0b3f01..0000000 --- a/scripts/debug/parse_grs.py +++ /dev/null @@ -1,84 +0,0 @@ -"""Parse Java class file constant pool to extract GRS-related strings.""" -import struct -import zipfile -import sys - -def parse_class_constants(data): - """Parse Java class file constant pool to extract all string constants.""" - if len(data) < 8 or data[:4] != b'\xca\xfe\xba\xbe': - return [] - - pos = 8 - cp_count = struct.unpack('>H', data[pos:pos+2])[0] - pos += 2 - - strings = [] - i = 1 - while i < cp_count and pos < len(data): - tag = data[pos] - pos += 1 - - if tag == 1: # CONSTANT_Utf8 - if pos + 2 > len(data): - break - length = struct.unpack('>H', data[pos:pos+2])[0] - pos += 2 - if pos + length > len(data): - break - value = data[pos:pos+length].decode('utf-8', errors='replace') - pos += length - strings.append(value) - elif tag == 3: # CONSTANT_Integer - pos += 4 - elif tag == 4: # CONSTANT_Float - pos += 4 - elif tag == 5: # CONSTANT_Long - pos += 8 - i += 1 - elif tag == 6: # CONSTANT_Double - pos += 8 - i += 1 - elif tag == 7: # CONSTANT_Class - pos += 2 - elif tag == 8: # CONSTANT_String - pos += 2 - elif tag in (9, 10, 11): # Fieldref, Methodref, InterfaceMethodref - pos += 4 - elif tag == 12: # CONSTANT_NameAndType - pos += 4 - elif tag == 15: # CONSTANT_MethodHandle - pos += 3 - elif tag == 16: # CONSTANT_MethodType - pos += 2 - elif tag in (17, 18): # Dynamic, InvokeDynamic - pos += 4 - elif tag in (19, 20): # Module, Package - pos += 2 - else: - break - i += 1 - - return strings - - -def main(): - jar_path = '/Applications/DevEco-Studio.app/Contents/lib/hwlib.jar' - with zipfile.ZipFile(jar_path) as z: - for name in sorted(z.namelist()): - if 'grs' in name.lower() and name.endswith('.class'): - data = z.read(name) - strings = parse_class_constants(data) - interesting = [s for s in strings if any( - kw in s.lower() for kw in - ['http', 'https', '/api', 'grs', 'url', 'connect', - 'route', 'service', 'sign', 'huawei', 'cloud', 'agc', - 'query', 'endpoint', 'address', 'region', 'country', - 'space', 'key', 'name'] - )] - if interesting: - print(f'\n=== {name} ===') - for s in interesting: - print(f' {s}') - -if __name__ == '__main__': - main() diff --git a/scripts/debug/probe_mode_error.py b/scripts/debug/probe_mode_error.py deleted file mode 100644 index 3affc4f..0000000 --- a/scripts/debug/probe_mode_error.py +++ /dev/null @@ -1,16 +0,0 @@ -#!/usr/bin/env python3 -"""在 ohos/hos SDK handler 里定位 00308018 mode-changed 的触发条件。""" -import re -import sys - -path = sys.argv[1] -src = open(path).read() - -anchor = src.find("mode has changed") -if anchor < 0: - for m in re.finditer(r"00308018|[Mm]ode.{0,30}changed", src): - anchor = m.start() - break -print("== file:", path, len(src)) -print("== context around anchor ==") -print(src[max(0, anchor - 2200): anchor + 400]) diff --git a/scripts/debug/probe_oh_loader.js b/scripts/debug/probe_oh_loader.js deleted file mode 100644 index 281ab9a..0000000 --- a/scripts/debug/probe_oh_loader.js +++ /dev/null @@ -1,25 +0,0 @@ -// 驱动 OhLocalComponentLoader:打印每个组件的 expected vs actual 位置。 -const fs = require("fs"); -const D = "/home/hongyuncong/.hvolt/tools/hvigor/node_modules/@ohos/hvigor-ohos-plugin/node_modules/@ohos/sdkmanager-common/build/src"; -const { OhLocalComponentLoader } = require(D + "/ohos/loader/oh-local-component-loader.js"); -const { SimpleProgress } = require(D + "/core/log/simple-progress.js"); -const p = new SimpleProgress(); -p.debug = m => console.log("DBG:", m); -p.warn = m => console.log("WRN:", m); -p.info = () => {}; -p.error = m => console.log("ERR:", m); - -const root = process.env.OHOS_BASE_SDK_HOME || "/home/hongyuncong/.hvolt/sdk"; -const l = new OhLocalComponentLoader(root, p); -const files = l.findPotentialSdks(root); -console.log("scanned:", files.length, "files"); -for (const f of files) { - const json = JSON.parse(fs.readFileSync(f, "utf8")); - const c = l.convertComponent(json, f); - const fav = c.getFullApiVersion ? c.getFullApiVersion() : undefined; - console.log("----", c.path, c.version); - console.log(" apiVersion=", json.apiVersion, "platformVersion=", json.platformVersion, "fullApiVersion=", json.fullApiVersion); - console.log(" fullApi(dto)=", fav ? fav.getValue() : "none", "valid=", fav ? require(D + "/core/util/utils.js").isValidApiVersion(fav) : "-"); - console.log(" expected=", l._getExpectedLocation(c)); - console.log(" actual =", c.getLocation()); -} diff --git a/scripts/debug/probe_version_mapper.py b/scripts/debug/probe_version_mapper.py deleted file mode 100644 index 5a2341c..0000000 --- a/scripts/debug/probe_version_mapper.py +++ /dev/null @@ -1,32 +0,0 @@ -#!/usr/bin/env python3 -"""探测 hvigor HosVersionMapper 的版本映射语义与数据源。""" -import re -import sys - -path = sys.argv[1] -src = open(path).read() - -print("== file size:", len(src)) - -for name in [ - "transferVersionIntoHosVersion", - "getSupportVersions", - "getAllVersion", - "getHosVersion", - "isSupportVersion", -]: - i = src.find(name + "(") - if i >= 0: - print(f"== {name} ==") - print(src[i : i + 520].replace("\\n", "\n")) - print() - -# 数据来源:内置映射表 or json 文件 -for m in re.finditer(r'require\("([^"]*\.json[^"]*)"\)', src): - print("JSON require:", m.group(1)) -for m in re.finditer(r'(SUPPORT_VERSION[A-Z_]*|HOS_VERSION[A-Z_]*|SDK_VERSION[A-Z_]*)\s*=', src): - print("const:", m.group(1)) - -# 打印任何含 "26.0" 的片段 -for m in re.finditer(r'.{80}26\.0\.\d.{80}', src): - print("CTX:", m.group(0)[:220]) diff --git a/scripts/debug/read_sdk_props.py b/scripts/debug/read_sdk_props.py deleted file mode 100644 index 6039021..0000000 --- a/scripts/debug/read_sdk_props.py +++ /dev/null @@ -1,13 +0,0 @@ -import zipfile -import glob - -jars = glob.glob('/Applications/DevEco-Studio.app/Contents/plugins/openharmony/lib/sdk-*.jar') -for jar_path in jars: - with zipfile.ZipFile(jar_path) as z: - for name in z.namelist(): - if name.endswith('.properties') or name.endswith('.json'): - print(f"=== {jar_path} : {name} ===") - try: - print(z.read(name).decode('utf-8')) - except Exception as e: - print(f"Error decoding: {e}") diff --git a/scripts/debug/sign_with_ohos.sh b/scripts/debug/sign_with_ohos.sh deleted file mode 100755 index c5db72b..0000000 --- a/scripts/debug/sign_with_ohos.sh +++ /dev/null @@ -1,134 +0,0 @@ -#!/bin/bash -set -e - -P12="/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/lib/OpenHarmony.p12" -PASS="123456" -JAVA="/Applications/DevEco-Studio.app/Contents/jbr/Contents/Home/bin" -SIGN_TOOL="/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony/toolchains/lib/hap-sign-tool.jar" -WORKDIR="/tmp/ohos_sign" -rm -rf "$WORKDIR" -mkdir -p "$WORKDIR" - -echo "=== Step 1: Export certificate chain files ===" - -# Profile signing cert chain: profile debug cert -> intermediate CA -> root CA -for alias in "openharmony application profile debug" "openharmony application ca" "openharmony application root ca"; do - safe_name=$(echo "$alias" | tr ' ' '_') - "$JAVA/keytool" -exportcert -alias "$alias" -keystore "$P12" -storetype PKCS12 -storepass "$PASS" -file "$WORKDIR/${safe_name}.pem" -rfc 2>/dev/null -done - -# Profile cert chain (3-level, leaf first) -cat "$WORKDIR/openharmony_application_profile_debug.pem" \ - "$WORKDIR/openharmony_application_ca.pem" \ - "$WORKDIR/openharmony_application_root_ca.pem" \ - > "$WORKDIR/profile_cert_chain.pem" -echo " Profile cert chain: $WORKDIR/profile_cert_chain.pem" - -# App signing cert chain: app release cert -> intermediate CA -> root CA -"$JAVA/keytool" -exportcert -alias "openharmony application release" -keystore "$P12" -storetype PKCS12 -storepass "$PASS" -file "$WORKDIR/openharmony_application_release.pem" -rfc 2>/dev/null - -cat "$WORKDIR/openharmony_application_release.pem" \ - "$WORKDIR/openharmony_application_ca.pem" \ - "$WORKDIR/openharmony_application_root_ca.pem" \ - > "$WORKDIR/app_cert_chain.pem" -echo " App cert chain: $WORKDIR/app_cert_chain.pem" - -echo "=== Step 2: Create profile JSON ===" - -# Get device UDID (if connected) -UDID=$(hdc list targets 2>/dev/null | head -1 | awk '{print $1}' || echo "") -if [ -z "$UDID" ]; then - UDID="69C7505BE341BDA5948C3C0CB44ABCD530296054159EFE0BD16A16CD0129CC42" - echo " No device connected, using default UDID" -fi -echo " Device UDID: $UDID" - -# Read app cert as single-line base64 -APP_CERT_B64=$(sed '1d;$d' "$WORKDIR/openharmony_application_release.pem" | tr -d '\n') -echo " App cert (base64): ${APP_CERT_B64:0:50}..." - -BUNDLE_NAME="com.example.hvolt.app" -NOW=$(date +%s) -NOT_AFTER=$((NOW + 365*24*3600)) - -cat > "$WORKDIR/profile.json" << JSONEOF -{ - "version-name": "2.0.0", - "version-code": 2, - "uuid": "$(uuidgen 2>/dev/null || echo 'fe686e1b-3770-4824-a938-961b140a7c98')", - "validity": { - "not-before": $NOW, - "not-after": $NOT_AFTER - }, - "type": "debug", - "bundle-info": { - "developer-id": "OpenHarmony", - "development-certificate": "-----BEGIN CERTIFICATE-----\n${APP_CERT_B64}\n-----END CERTIFICATE-----\n", - "bundle-name": "$BUNDLE_NAME", - "apl": "normal", - "app-feature": "hos_normal_app" - }, - "acls": { - "allowed-acls": [""] - }, - "permissions": { - "restricted-permissions": [""] - }, - "debug-info": { - "device-ids": ["$UDID"], - "device-id-type": "udid" - }, - "issuer": "pki_internal" -} -JSONEOF -echo " Profile JSON: $WORKDIR/profile.json" - -echo "=== Step 3: Sign profile ===" - -"$JAVA/java" -jar "$SIGN_TOOL" sign-profile \ - -mode localSign \ - -keyAlias "openharmony application profile debug" \ - -keyPwd "$PASS" \ - -profileCertFile "$WORKDIR/profile_cert_chain.pem" \ - -inFile "$WORKDIR/profile.json" \ - -signAlg SHA256withECDSA \ - -keystoreFile "$P12" \ - -keystorePwd "$PASS" \ - -outFile "$WORKDIR/debug.p7b" 2>&1 - -echo "=== Step 4: Sign HAP ===" - -UNSIGNED_HAP="$1" -if [ -z "$UNSIGNED_HAP" ]; then - # Try to find unsigned HAP - UNSIGNED_HAP=$(ls ~/workspace/better-harmony-app-build/examples/test-app/build/src/build/default/outputs/default/*-unsigned.hap 2>/dev/null | head -1) -fi - -if [ -z "$UNSIGNED_HAP" ] || [ ! -f "$UNSIGNED_HAP" ]; then - echo " No unsigned HAP found. Skip sign-app." - echo " To sign: sign_with_ohos.sh " - echo "" - echo "=== Generated files in $WORKDIR ===" - ls -la "$WORKDIR" - exit 0 -fi - -echo " Unsigned HAP: $UNSIGNED_HAP" -SIGNED_HAP="$WORKDIR/signed.hap" - -"$JAVA/java" -jar "$SIGN_TOOL" sign-app \ - -mode localSign \ - -keyAlias "openharmony application release" \ - -keyPwd "$PASS" \ - -appCertFile "$WORKDIR/app_cert_chain.pem" \ - -profileFile "$WORKDIR/debug.p7b" \ - -profileSigned 1 \ - -inFile "$UNSIGNED_HAP" \ - -signAlg SHA256withECDSA \ - -keystoreFile "$P12" \ - -keystorePwd "$PASS" \ - -outFile "$SIGNED_HAP" 2>&1 - -echo "=== Done ===" -ls -la "$WORKDIR" -echo "Signed HAP: $SIGNED_HAP" diff --git a/scripts/debug/test_token_exchange.py b/scripts/debug/test_token_exchange.py deleted file mode 100644 index 3151402..0000000 --- a/scripts/debug/test_token_exchange.py +++ /dev/null @@ -1,107 +0,0 @@ -#!/usr/bin/env python3 -"""Test OAuth2 token exchange with Huawei's endpoint.""" - -import sys -import urllib.request -import urllib.parse -import json -import hashlib -import base64 -import secrets - -CLIENT_ID = "6099200" -TOKEN_URL = "https://oauth-login.cloud.huawei.com/oauth2/v2/token" -REDIRECT_URI = "https://developer.huawei.com/aui2/handleLoginV2.html" -PROXY = "http://127.0.0.1:10808" - - -def pkce_challenge(verifier: str) -> str: - digest = hashlib.sha256(verifier.encode()).digest() - return base64.urlsafe_b64encode(digest).rstrip(b"=").decode() - - -def _do(opener, url, body): - data = urllib.parse.urlencode(body).encode() - req = urllib.request.Request(url, data=data, - headers={"Content-Type": "application/x-www-form-urlencoded"}) - try: - with opener.open(req, timeout=15) as resp: - result = json.loads(resp.read().decode()) - print(f" Status: {resp.status}") - print(f" Response: {json.dumps(result, indent=2, ensure_ascii=False)}") - if "access_token" in result: - print(" >>> GOT access_token!") - print(f" access_token: {result['access_token'][:50]}...") - except urllib.error.HTTPError as e: - print(f" Status: {e.code}") - print(f" Body: {e.read().decode()[:500]}") - - -def test(code, verifier=None): - proxy_handler = urllib.request.ProxyHandler({"http": PROXY, "https": PROXY}) - opener = urllib.request.build_opener(proxy_handler) - - tests = [ - ("no PKCE, no secret", { - "grant_type": "authorization_code", "code": code, - "client_id": CLIENT_ID, "redirect_uri": REDIRECT_URI}), - ("with PKCE, no secret", { - "grant_type": "authorization_code", "code": code, - "client_id": CLIENT_ID, "redirect_uri": REDIRECT_URI, - "code_verifier": verifier} if verifier else None), - ("empty client_secret", { - "grant_type": "authorization_code", "code": code, - "client_id": CLIENT_ID, "client_secret": "", - "redirect_uri": REDIRECT_URI}), - ("oauth-api URL", { - "grant_type": "authorization_code", "code": code, - "client_id": CLIENT_ID, "redirect_uri": REDIRECT_URI}), - ] - - for name, body in tests: - if body is None: - continue - print(f"\n{'='*50}") - print(f"Test: {name}") - url = TOKEN_URL - if "oauth-api" in name: - url = "https://oauth-api.cloud.huawei.com/oauth2/v2/token" - _do(opener, url, body) - - # Basic auth test - print(f"\n{'='*50}") - print("Test: HTTP Basic auth (client_id:empty)") - creds = base64.b64encode(f"{CLIENT_ID}:".encode()).decode() - body = {"grant_type": "authorization_code", "code": code, "redirect_uri": REDIRECT_URI} - data = urllib.parse.urlencode(body).encode() - req = urllib.request.Request(TOKEN_URL, data=data, - headers={"Content-Type": "application/x-www-form-urlencoded", - "Authorization": f"Basic {creds}"}) - try: - with opener.open(req, timeout=15) as resp: - result = json.loads(resp.read().decode()) - print(f" Status: {resp.status}") - print(f" Response: {json.dumps(result, indent=2, ensure_ascii=False)}") - except urllib.error.HTTPError as e: - print(f" Status: {e.code}") - print(f" Body: {e.read().decode()[:500]}") - - -if __name__ == "__main__": - if len(sys.argv) < 2: - verifier = secrets.token_urlsafe(32)[:43] - challenge = pkce_challenge(verifier) - auth_url = ( - f"https://oauth-login.cloud.huawei.com/oauth2/v2/authorize?" - f"client_id={CLIENT_ID}&response_type=code&" - f"redirect_uri={urllib.parse.quote(REDIRECT_URI, safe='')}&" - f"scope=openid+profile&state=hvolt_test&access_type=offline&" - f"code_challenge={challenge}&code_challenge_method=S256" - ) - print(f"Login URL:\n {auth_url}") - print(f"code_verifier: {verifier}") - sys.exit(0) - - code = sys.argv[1] - verifier = sys.argv[2] if len(sys.argv) > 2 else None - test(code, verifier) diff --git a/scripts/debug/token-diff.js b/scripts/debug/token-diff.js deleted file mode 100644 index f8b3c6c..0000000 --- a/scripts/debug/token-diff.js +++ /dev/null @@ -1,224 +0,0 @@ -#!/usr/bin/env node -// Count the AST tokens two TypeScript trees disagree about. -// -// The fixture oracle judges a file by whether its tree *is* the reference's; this tool says how -// far apart two trees are, in tokens, so a change that keeps a file different can still be shown -// to shrink the difference — and a change that enlarges one shows up as a number instead of a -// diff nobody reads. -// -// ## What a token is -// -// One canonical node of the TypeScript AST, as `typescript`'s own parser reads the emitted file. -// The reading is the same one `scripts/oracle/compare.js` and `scripts/debug/compare-trees.js` -// use, so the three tools cannot disagree about what a tree "says": -// -// * a node contributes `SyntaxKind` plus its payload — an identifier's name, a numeric or -// string literal's text; -// * a string that names the project rather than the translation (`com.example.*`, -// anything carrying `/src/main/ets/`) is `:`, because every fixture and every -// build has its own bundle name and source root; -// * `({ page, line, col })` — the source coordinate the compiler writes into a component's -// `observeComponentCreation2` — is one `LocMeta` node, and so is the sixth constructor -// argument of a component call; the two sides may carry it or omit it; -// * grouping parentheses are not structure: `(a && b) ? x : y` and `a && b ? x : y` walk to -// the same operator tree, so the wrapper is skipped and its expression walked; -// * everything else the parser visits is counted, in `ts.forEachChild` order: keywords, -// declarations, modifiers, operators and punctuation-bearing expressions alike. Nothing is -// skipped except the comments (the parser attaches them to the source, not the tree) and -// the empty file's EOF token. -// -// A token present on one side only counts once per side; the total is their sum, which is what -// makes the number comparable across runs. -// -// ## How two token streams are aligned -// -// Longest-common-subsequence (the classic table walk), which is order-preserving: a token that -// merely moved is reported as removed-and-added rather than matched across the move. The -// resulting count is therefore an upper bound on editing distance — stable, deterministic, and -// never zero for two different trees. -// -// ## Modes -// -// token-diff.js every `.ts` of the reference tree, per file -// token-diff.js one pair -// -// The reference tree's relative paths are mapped onto the hvolt tree the way -// `compare-trees.js` does: `/src/main/ets/` is `/`, because hvolt -// stages a module's sources without its source root. A file the hvolt tree does not have is -// reported as missing rather than as silently agreeing. -// -// The output is one line per differing file and a `total` line, sorted by path so two runs on -// the same input print the same bytes; `--json` prints the same numbers as one object. -// -// Usage: -// node scripts/debug/token-diff.js [--json] -// node scripts/debug/token-diff.js - -'use strict'; - -const fs = require('fs'); -const os = require('os'); -const path = require('path'); - -function loaderRoot() { - const sdkRoot = path.join(os.homedir(), '.hvolt', 'sdk'); - const versions = fs.existsSync(sdkRoot) - ? fs.readdirSync(sdkRoot).filter((v) => fs.statSync(path.join(sdkRoot, v)).isDirectory()).sort().reverse() - : []; - for (const version of versions) { - const candidate = path.join(sdkRoot, version, 'openharmony', 'ets', 'build-tools', 'ets-loader'); - if (fs.existsSync(path.join(candidate, 'main.js'))) return candidate; - } - throw new Error('ets-loader not found'); -} - -const ts = require(path.join(loaderRoot(), 'node_modules', 'typescript')); - -/// One canonical token per AST node, in `forEachChild` order. -function canonical(node, out) { - const kind = ts.SyntaxKind[node.kind]; - let extra = ''; - if (ts.isIdentifier(node) || ts.isPrivateIdentifier(node)) { - extra = `:${node.text}`; - } else if (ts.isStringLiteralLike(node)) { - extra = /^com\.example\./.test(node.text) || /\/src\/main\/ets\//.test(node.text) - ? ':' - : `:${JSON.stringify(node.text)}`; - } else if (ts.isNumericLiteral(node)) { - extra = `:${node.text}`; - } - // Grouping parentheses are not structure. - if (ts.isParenthesizedExpression(node)) { - canonical(node.expression, out); - return; - } - // The source coordinate is one placeholder on both sides. - if (ts.isObjectLiteralExpression(node) && node.properties.length > 0) { - const first = node.properties[0]; - if (ts.isPropertyAssignment(first) && String(first.name.text || first.name.escapedText) === 'page') { - out.push('LocMeta'); - return; - } - } - out.push(kind + extra); - // The sixth constructor argument of a component call is the same coordinate. - if (ts.isNewExpression(node) && node.arguments && node.arguments.length >= 6) { - node.arguments.forEach((argument, index) => { - if (index === 5) out.push('LocMeta'); - else canonical(argument, out); - }); - return; - } - ts.forEachChild(node, (child) => canonical(child, out)); -} - -function tokens(file) { - const text = fs.readFileSync(file, 'utf8'); - const source = ts.createSourceFile(file, text, ts.ScriptTarget.ES2017, true, ts.ScriptKind.TS); - const parts = []; - canonical(source, parts); - return parts; -} - -/// The tokens two streams disagree about, both sides summed. -function differing(a, b) { - const n = a.length; - const m = b.length; - const table = Array.from({ length: n + 1 }, () => new Int32Array(m + 1)); - for (let i = n - 1; i >= 0; i--) { - for (let j = m - 1; j >= 0; j--) { - table[i][j] = a[i] === b[j] ? table[i + 1][j + 1] + 1 : Math.max(table[i + 1][j], table[i][j + 1]); - } - } - let i = 0; - let j = 0; - let referenceOnly = 0; - let hvoltOnly = 0; - while (i < n && j < m) { - if (a[i] === b[j]) { - i++; - j++; - } else if (table[i + 1][j] >= table[i][j + 1]) { - referenceOnly++; - i++; - } else { - hvoltOnly++; - j++; - } - } - referenceOnly += n - i; - hvoltOnly += m - j; - return { referenceOnly, hvoltOnly, total: referenceOnly + hvoltOnly }; -} - -/// The reference tree's path for one file, in the hvolt tree's layout. -function stagedPath(referenceRoot, file) { - const parts = path.relative(referenceRoot, file).split(path.sep); - if (parts.length > 4 && parts[1] === 'src' && parts[2] === 'main' && parts[3] === 'ets') { - return [parts[0], ...parts.slice(4)].join('/'); - } - return parts.join('/'); -} - -function walk(directory, out) { - for (const entry of fs.readdirSync(directory, { withFileTypes: true })) { - const full = path.join(directory, entry.name); - if (entry.isDirectory()) walk(full, out); - else if (entry.name.endsWith('.ts')) out.push(full); - } -} - -function main() { - const args = process.argv.slice(2).filter((arg) => arg !== '--json'); - const json = process.argv.includes('--json'); - const [left, right] = args; - if (!left || !right) { - console.error('usage: token-diff.js [--json]'); - process.exit(2); - } - if (!fs.existsSync(left) || !fs.existsSync(right)) { - console.error(`missing input: ${!fs.existsSync(left) ? left : right}`); - process.exit(2); - } - - const single = fs.statSync(left).isFile(); - const per = {}; - let missing = []; - if (single) { - const { total } = differing(tokens(left), tokens(right)); - if (json) { - console.log(JSON.stringify({ total, files: total ? 1 : 0, per: { [path.basename(left)]: total } })); - return; - } - console.log(`${total} ${path.basename(left)}`); - return; - } - - const files = []; - walk(left, files); - files.sort(); - let total = 0; - for (const file of files) { - const staged = stagedPath(left, file); - const mine = path.join(right, staged); - if (!fs.existsSync(mine)) { - missing.push(staged); - continue; - } - const { total: count } = differing(tokens(file), tokens(mine)); - if (count) { - per[staged] = count; - total += count; - } - } - if (json) { - console.log(JSON.stringify({ total, files: Object.keys(per).length, missing: missing.length, per })); - return; - } - for (const staged of Object.keys(per).sort()) { - console.log(`${String(per[staged]).padStart(6)} ${staged}`); - } - console.log(`total ${total} (files ${Object.keys(per).length}, reference files ${files.length}, missing ${missing.length})`); -} - -main(); \ No newline at end of file diff --git a/src/device/target.rs b/src/device/target.rs index f18294c..1d0c1da 100644 --- a/src/device/target.rs +++ b/src/device/target.rs @@ -12,10 +12,10 @@ // `hdc install` runs on that machine (its own ~/.hvolt supplies hdc + udid) // The `remote_*` helpers below drive the ssh/scp deploy path to a host that -// holds the device (docs/agents/harmony-device-flow.md). Installs now go -// through the native hdc transport (`hdc_proto::install`), so nothing calls -// them yet; they are kept rather than deleted because that topology is still -// the documented one for machines that cannot reach the device directly. +// holds the device. Installs now go through the native hdc transport +// (`hdc_proto::install`), so nothing calls them yet; they are kept rather than +// deleted because that topology is still the one for machines that cannot +// reach the device directly. #![allow(dead_code)] use std::path::{Path, PathBuf}; From 3f30319ff4ab3795e5e5fb12bf322369132ae215 Mon Sep 17 00:00:00 2001 From: sudo_free Date: Mon, 9 Nov 2026 00:35:57 +0800 Subject: [PATCH 3/3] feat(release): build and install linux-arm64, drop the darwin-x64 package npm declared five platform packages while release.yml built three, so an Intel Mac or an arm64 Linux host installed a launcher whose binary never existed. - release.yml: add a linux-arm64 lane on ubuntu-24.04-arm, building aarch64-unknown-linux-gnu natively - install.sh: map aarch64/arm64 to that archive and name the real support list when it refuses - npm: drop @hvolt/cli-darwin-x64 rather than ship a package with nothing behind it Measured: npm pack --dry-run on @hvolt/cli lists 4 files; the launcher's optionalDependencies and npm/platforms/ now agree at four entries. --- .github/workflows/release.yml | 5 +++++ CHANGELOG.md | 8 ++++++++ install.sh | 3 ++- npm/package.json | 1 - npm/platforms/darwin-x64/package.json | 25 ------------------------- 5 files changed, 15 insertions(+), 27 deletions(-) delete mode 100644 npm/platforms/darwin-x64/package.json diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 0df0481..e0c1f68 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -87,6 +87,11 @@ jobs: target: x86_64-unknown-linux-gnu bin: hvolt archive: tar.gz + - key: linux-arm64 + os: ubuntu-24.04-arm + target: aarch64-unknown-linux-gnu + bin: hvolt + archive: tar.gz - key: win32-x64 os: windows-2022 target: x86_64-pc-windows-msvc diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a0c7d1..025be8b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ versions may change the CLI surface. ## [Unreleased] +### Changed + +- A prebuilt binary is published and installable for `linux-arm64`, built on a native arm64 + runner. `install.sh` resolves `aarch64`/`arm64` to it and names the real support list when it + refuses a platform. +- `@hvolt/cli-darwin-x64` is no longer declared. The launcher listed five platform packages + while the release built three, so an Intel Mac installed a package with no binary behind it. + ## [26.0.0-9] — 2026-11-08 ### Added diff --git a/install.sh b/install.sh index 33343e2..b03b7ef 100755 --- a/install.sh +++ b/install.sh @@ -26,7 +26,8 @@ case "$os" in linux) case "$arch" in x86_64|amd64) key="linux-x64" ;; - *) die "no prebuilt binary for Linux $arch (supported: x86_64)" ;; + aarch64|arm64) key="linux-arm64" ;; + *) die "no prebuilt binary for Linux $arch (supported: x86_64, aarch64)" ;; esac ;; *) die "unsupported OS $os; on Windows use install.ps1" ;; diff --git a/npm/package.json b/npm/package.json index f04b903..a3c2f5e 100644 --- a/npm/package.json +++ b/npm/package.json @@ -21,7 +21,6 @@ }, "optionalDependencies": { "@hvolt/cli-darwin-arm64": "26.0.0-9", - "@hvolt/cli-darwin-x64": "26.0.0-9", "@hvolt/cli-linux-arm64": "26.0.0-9", "@hvolt/cli-linux-x64": "26.0.0-9", "@hvolt/cli-win32-x64": "26.0.0-9" diff --git a/npm/platforms/darwin-x64/package.json b/npm/platforms/darwin-x64/package.json deleted file mode 100644 index 1f38b9d..0000000 --- a/npm/platforms/darwin-x64/package.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "name": "@hvolt/cli-darwin-x64", - "version": "26.0.0-9", - "description": "Prebuilt hvolt binary for darwin x64", - "license": "GPL-3.0-or-later", - "homepage": "https://github.com/hvolt-project/hvolt#readme", - "repository": { - "type": "git", - "url": "git+https://github.com/hvolt-project/hvolt.git" - }, - "os": [ - "darwin" - ], - "cpu": [ - "x64" - ], - "files": [ - "bin/hvolt", - "bin/hvolt.exe", - "LICENSE" - ], - "publishConfig": { - "access": "public" - } -}