Skip to content

feat: HarmonyOS NEXT toolchain + NAPI + HAP bundle (Phase 1 for #113)#122

Open
proggeramlug wants to merge 7 commits intomainfrom
harmony-os
Open

feat: HarmonyOS NEXT toolchain + NAPI + HAP bundle (Phase 1 for #113)#122
proggeramlug wants to merge 7 commits intomainfrom
harmony-os

Conversation

@proggeramlug
Copy link
Copy Markdown
Contributor

Phase 1 for #113 — HarmonyOS NEXT support end-to-end from perry compile foo.ts --target harmonyos to a .hap file on disk. Stacked on top of PR #121 (PR A — scaffolding); merging this after #121 is merged to main.

What's in scope

Three commits, each self-contained:

v0.5.124 — Toolchain (PR B.1)

  • find_harmonyos_sdk() probes $OHOS_SDK_HOME, then DevEco defaults (~/Library/Huawei/Sdk, ~/Huawei/Sdk, %USERPROFILE%\Huawei\Sdk).
  • harmonyos_cross_env() emits CC_ / CXX_ / CFLAGS_ / CXXFLAGS_ / CARGO_TARGET_*_LINKER / _RUSTFLAGS for cargo build, both hyphen and underscore triple forms.
  • New is_harmonyos linker branch: clang -shared -fPIC --target=aarch64-linux-ohos --sysroot=<sdk>/sysroot -D__MUSL__ -Wl,-Bsymbolic -Wl,--warn-unresolved-symbols.
  • exe_path default: auto-selects lib<stem>.so for --target harmonyos[-simulator].
  • Knock-on guards: !is_macho (ELF), !is_harmonyos in jsruntime_lib, --gc-sections dead-strip, Android-style companion-lib copy.
  • Fail-fast with a single clear error when the SDK is missing — no two-message chain.

v0.5.125 — NAPI + ArkTS shim (PR B.2)

  • crates/perry-runtime/src/ohos_napi.rs behind ohos-napi feature flag: opaque NAPI types, napi_module_register via #[link_section = ".init_array"] constructor, one exported function run() that calls Perry's compiled main() and returns its exit code as NAPI int32.
  • Feature is auto-enabled when --target harmonyos is passed (both --features-present and --features-absent paths covered).
  • emit_harmonyos_arkts_stubs() writes ets/entryability/EntryAbility.ets + ets/pages/Index.ets next to the .so. Import specifier is templated off the actual output filename.

v0.5.126 — HAP bundler (PR B.3)

  • crates/perry/src/commands/harmonyos_hap.rs assembles the full .hap archive ourselves (no hvigor — per Decision 3). Stages {app.json5, module.json5, pack.info, ets/, libs/arm64-v8a/<stem>.so, resources/base/{media,string,color,profile}/}, zips via the existing zip workspace dep.
  • .ets.abc compilation via SDK's es2abc / ets-loader when discoverable. Falls back to shipping source + warning otherwise.
  • Signing (Decision 4a): $PERRY_HARMONYOS_P12 / _P12_PASSWORD / _PROFILE + resolved hap-sign-tool.jar (or $PERRY_HARMONYOS_HAPSIGN override), shells out to java -jar .... Missing creds → unsigned HAP + one-line remediation.
  • Bundle name: $PERRY_HARMONYOS_BUNDLE_NAME overrides; fallback com.perry.app.<sanitized_stem>.
  • Two unit tests verify HAP layout + bundle-name sanitization; pass in ~10ms.

Decisions (agreed before implementation)

# Question Answer
1 Auto-select .so output for --target harmonyos? Yes
2 ArkTS shim calls entry.run() returning number? Agreed
3 Skip hvigor — assemble HAP zip ourselves + shell out to hap-sign only? Yes
4 Signing via $PERRY_HARMONYOS_* env vars + perry setup harmonyos wizard? a + b; wizard deferred to follow-up PR (env-var-only is sufficient for first on-device validation)

What's explicitly not in this PR

  • perry setup harmonyos wizard — Decision 4b. Env-var-only path covers @cavivie's first validation; wizard is its own PR if the UX warrants it.
  • User-provided icon / resource bundle — currently a 68-byte placeholder PNG. perry.harmonyos.icon in package.json is a follow-up.
  • x86_64 ABI dir for --target harmonyos-simulatorlibs/arm64-v8a/ is hardcoded for now.
  • perry-ui-harmonyos + TS→ArkTS emitter — that's Phase 2 (PR C).
  • Platform enum variant — deliberately not touched (would force premature publish/run behavior decisions). compile takes Option<String> so the string-level wiring is sufficient.

Verification

  • cargo build --release -p perry — clean (only pre-existing warnings).
  • cargo test --release -p perry --bins harmonyos_hap — 2/2 pass.
  • cargo check -p perry-runtime --features ohos-napi --no-default-features — clean on host.
  • perry compile hi.ts (default host), --target macos, --target ios — byte-equivalent to pre-B output.
  • perry compile hi.ts --target harmonyos without SDK — single fail-fast with remediation naming OHOS_SDK_HOME.
  • Mock-SDK integration: observed CC_aarch64-unknown-linux-ohos=<sdk>/llvm/bin/clang flowing through cc-rs invocations.
  • HAP unit test: produces a valid zip with all 10 required layout members, PNG magic bytes survive roundtrip, app.json5 carries the sanitized bundle name.

Test plan for @cavivie (on real NEXT hardware)

  • Install DevEco Studio, note the SDK path, export OHOS_SDK_HOME=<path>
  • export PERRY_HARMONYOS_P12=<path>, PERRY_HARMONYOS_P12_PASSWORD=<pw>, PERRY_HARMONYOS_PROFILE=<p7b>, PERRY_HARMONYOS_BUNDLE_NAME=<cert-bundle>
  • perry compile hello.ts --target harmonyos — expect libhello.so, ets/, hello.hap in output dir
  • hdc install hello.hap on physical NEXT device — expect install to succeed
  • Tap the app icon — expect the Perry program to run (console output visible via hdc hilog)
  • Report back on (a) whether .ets compilation worked or fell back to source, (b) whether signing worked, (c) any runtime errors

Expected rough patches needed: hap-sign-tool.jar arg set may drift between OHOS SDK versions; x86_64-simulator ABI dir; any NAPI symbol we missed.

Follow-ups if Phase 1 lands cleanly

  • Phase 2 (PR C): perry-ui-harmonyos crate + TS→ArkTS emitter for perry/ui HIR → ArkUI components. Bridges State<T>@State/@Link.
  • Phase 3: explicitly out of v1 per @cavivie; custom GL widget set on XComponent.

PR A for #113 — additive scaffolding only. Every edit is a new arm in
an existing match, unreachable until the user passes --target harmonyos
or --target harmonyos-simulator. No behavior change for any other target.

  * rust_target_triple / resolve_target_triple: harmonyos → aarch64-unknown-linux-ohos,
    harmonyos-simulator → x86_64-unknown-linux-ohos (DevEco emulator).
  * find_library: _harmonyos suffix convention next to the perry binary,
    mirroring _watchos / _ios / _tvos.
  * find_ui_library: libperry_ui_harmonyos.a (crate lands in PR C).
  * parse_native_library_manifest target_key → "harmonyos" so package.json
    authors can declare perry.nativeLibrary.targets.harmonyos.
  * UI-crate selector → perry-ui-harmonyos.
  * is_mobile feature filter includes harmonyos[-simulator] so --features plugins
    is stripped (dlopen isn't practical under HarmonyOS's sandbox).
  * __platform__ = 7 (HarmonyOS); ordered before the linux arm since the OHOS
    triple is *-unknown-linux-ohos — naive contains("linux") would misclassify.

Platform enum is deliberately untouched. publish.rs:627 and run.rs:1687-1802
exhaustively match every variant; adding one would force premature behavior
decisions (what does `perry publish --platform harmonyos` do?) that belong
with PR B alongside the HAP bundler. `compile` takes Option<String>, so
--target harmonyos works without the enum change.

Verified:
  * cargo build --release -p perry -p perry-codegen — clean
  * perry compile hi.ts (default host) — byte-equivalent output, runs
  * perry compile hi.ts --target harmonyos — fails fast with
    "Could not find libperry_runtime.a (for target harmonyos)"; search
    paths include target/aarch64-unknown-linux-ohos/release/... and
    the _harmonyos suffix, proving both new arms are live. rustc
    additionally emits "consider: rustup target add aarch64-unknown-linux-ohos",
    confirming the triple is upstream-recognized (Tier 2 with host tools).
PR B.1 for #113. First functional slice of HarmonyOS NEXT support: a
user with OHOS_SDK_HOME set can now invoke
  perry compile foo.ts --target harmonyos
and get a valid musl-ELF libfoo.so out the other end. No NAPI wrapper
yet (B.2), no ArkTS shim (B.2), no HAP bundler (B.3) — this slice stops
at "the .so is on disk."

Plumbing changes (all in crates/perry/src/commands/compile.rs):

  * find_harmonyos_sdk() — probes $OHOS_SDK_HOME first, then DevEco's
    defaults (~/Library/Huawei/Sdk on macOS, ~/Huawei/Sdk on Linux,
    %USERPROFILE%\Huawei\Sdk on Windows). Accepts either the SDK root
    or the native/ subdir, and walks openharmony/<api>/native if the
    DevEco API-level nesting is present. Normalizes to the native/
    dir that contains llvm/bin/clang and sysroot/.

  * harmonyos_cross_env() — emits env vars for cargo build so cc-rs
    picks up OHOS clang + --sysroot + -D__MUSL__. Sets both CC/CXX
    (the crate needs libmimalloc-sys, whose build.rs fails with
    "'pthread.h' file not found" otherwise) and in both hyphen and
    underscore triple forms (cc-rs prefers underscores; rustc has
    historically emitted the hyphen form from stable APIs, so we
    set both for robustness). Plus CARGO_TARGET_*_LINKER and
    _RUSTFLAGS so rustc links through clang with the sysroot.

  * auto_rebuild_runtime_and_stdlib wired: when target is harmonyos,
    prepend the cross env to the cargo_cmd before status().

  * Linker branch (new is_harmonyos arm, slotted after Android since
    they're both clang→.so): -shared -fPIC --target=<arch>-linux-ohos
    --sysroot=<sdk>/sysroot -D__MUSL__ -Wl,-Bsymbolic
    -Wl,--warn-unresolved-symbols. Symbolic binding prevents ArkTS's
    host process from interposing Perry's runtime symbols; warn-only
    keeps namespace-import shortname externs from blocking the link.

  * exe_path default: --target harmonyos[-simulator] auto-selects
    `lib{stem}.so`. The `lib` prefix matches the dlopen name that
    PR B.2's generated ArkTS shim will use
    (`import entry from 'libapp.so'`).

Knock-on guards (4 sites) so existing branches don't misfire:
  * is_macho falls through to mach-O on macOS hosts via
    `(!is_windows && !is_linux && !is_android && cfg!(target_os=macos))`.
    Added !is_harmonyos — OHOS is ELF, not mach-O.
  * jsruntime_lib guard: added !is_harmonyos (V8/jsruntime is not a
    thing on NEXT; same rationale as ios/android/watchos/tvos).
  * --gc-sections dead-strip path extended from (is_android || is_linux)
    to include harmonyos.
  * Android companion-.so copy-next-to-output extended to harmonyos —
    the B.3 HAP bundler will need them staged identically.

UX: fail-fast check at the top of compile() when --target harmonyos is
passed, the SDK is absent, and no prebuilt libperry_runtime_harmonyos.a
is on disk — bails with a single clear message naming OHOS_SDK_HOME and
the default probe paths. Without this, the user would see two messages
in sequence (auto-rebuild's "SDK not found" warning + find_runtime's
"libperry_runtime.a not found" with unrelated fixes).

Verified:
  * Default host / --target macos / --target ios paths produce byte-
    equivalent output to pre-B.1 main.
  * perry compile hi.ts --target harmonyos without OHOS_SDK_HOME:
    single-line fail-fast error listing remediation.
  * With OHOS_SDK_HOME=<mock-sdk-with-fake-clang>, cargo is invoked
    with --target aarch64-unknown-linux-ohos and cc-rs inherits
    CC_aarch64-unknown-linux-ohos + CXX_* + CFLAGS_* — observed in
    cc-rs trace output. Mock clang is a no-op shell script, so the
    compile itself doesn't complete, but the env plumbing is confirmed.
  * End-to-end on real OHOS SDK requires the Huawei-portal download;
    deferred to @cavivie's on-device validation once B.2 + B.3 land
    (there's no way to "run" a standalone .so without the ArkTS shim).

Scope not included (lands in B.2 / B.3):
  * napi_module_register Rust-side entry wrapper (ohos-napi feature).
  * ArkTS UIAbility / EntryAbility shim generator.
  * module.json5 / app.json5 / resources scaffolding.
  * HAP assembly (manual zip, not hvigor — per Decision 3).
  * hap-sign integration + $PERRY_HARMONYOS_P12 env discovery +
    perry setup harmonyos subcommand (Decision 4 = a+b).
PR B.2 for #113. Builds on B.1: the .so Perry emits for --target harmonyos
now has a NAPI entry point that ArkTS can actually call, plus the ArkTS
source files that call it.

crates/perry-runtime/src/ohos_napi.rs (new, ~110 lines, feature-gated):
  * Declares opaque NapiEnv/NapiValue/NapiCallbackInfo + NapiModule struct
    matching OpenHarmony's node_api.h layout.
  * Externs the four NAPI functions we need: napi_module_register (for
    the module registration in .init_array), napi_create_int32 (return
    value of run()), napi_create_function + napi_set_named_property
    (to wire run() onto the exports object during nm_register_func).
  * Externs `main` from the compiler-emitted TS entry. -Wl,-Bsymbolic
    (added in B.1) ensures this resolves to our own `main`, not the
    ArkTS host process's `main`.
  * `run(env, info)` NAPI callback: calls main(), wraps exit code as
    napi_int32. `napi_init(env, exports)` sets `exports.run = run`.
  * Module descriptor lives in a `static mut NAPI_MODULE_DESC` so
    register_module() can plug in nm_modname from static bytes at
    constructor time. Uses &raw mut to avoid unsafe-referent warnings.
  * Registration is triggered by `#[link_section = ".init_array"] #[used]
    static INIT: extern "C" fn() = register_module;` — the ELF
    constructor-array slot. `dlopen` walks this and invokes every
    function pointer before returning to ArkTS.

crates/perry-runtime/Cargo.toml + src/lib.rs:
  * New `ohos-napi = []` feature. Module is `#[cfg(feature = "ohos-napi")]`
    gated (not target_os — cross-build hosts see target_os = linux, the
    feature flag is the authoritative signal).

crates/perry/src/commands/compile.rs:
  * Feature auto-enable: the compiled_features block was the natural
    home. When target is harmonyos[-simulator], push "ohos-napi" into
    the feature list. Two paths covered — the existing `if features_str`
    branch (user passed --features too) and a new `else if target=harmonyos`
    branch (user passed no --features at all). Without the second, a
    bare `perry compile foo.ts --target harmonyos` would build the .so
    with no napi_module_register and ArkTS's `import entry from 'libfoo.so'`
    would fail at load with "module entry not found."
  * Feature passthrough to auto_rebuild_runtime_and_stdlib: ohos-napi
    added next to ios-game-loop/watchos-game-loop in the
    `perry-runtime/<feat>` rewrite list at compile.rs:1302.
  * `emit_harmonyos_arkts_stubs(output_dir, so_filename)` — writes two
    files to `<output_dir>/ets/`:
      - entryability/EntryAbility.ets: UIAbility subclass. onCreate calls
        `perryEntry.run()` once per ability instance. onWindowStageCreate
        loads `pages/Index`. Other lifecycle hooks (Destroy, Foreground,
        Background) are no-op for v1.
      - pages/Index.ets: ArkUI `@Entry @component struct Index` with a
        centered `Text('Perry app running')`. Enough to load; UI lands
        in PR C (TS→ArkTS emitter for perry/ui).
    The `import perryEntry from '<so_filename>'` specifier is templated
    off exe_path.file_name() so `-o libfoo.so` stays consistent with
    what ArkTS dlopen's.
  * Called from the post-link section (after the `link cmd.status()?`
    gate), before the android/harmonyos companion-.so copy block. Gated
    on `is_harmonyos`. A failure in shim-emission is logged as a warning,
    not a hard error — the .so is still useful without it if the caller
    wants to write their own ArkTS shim (unlikely, but possible).

Deliberately NOT in B.2 — lands in B.3:
  * module.json5 / app.json5 / resources (strings, icons, colors).
  * HAP .zip assembly + hap-sign + $PERRY_HARMONYOS_P12 env discovery.
  * `perry setup harmonyos` subcommand (Decision 4b).
  * ArkTS → .abc compilation step (the .ets files land in the HAP as
    either source or bytecode; B.3 decides and implements).

Verified:
  * `cargo check -p perry-runtime --features ohos-napi --no-default-features`
    — clean (only preexisting warnings).
  * `cargo build --release -p perry` — clean.
  * Standalone render of emit_harmonyos_arkts_stubs via a one-off binary:
    produces valid ArkTS, import specifier templates correctly.
  * `perry compile hi.ts --target harmonyos` (no SDK) still hits the B.1
    fail-fast with a single clear message.
PR B.3 for #113. Final Phase-1 piece: the .so + ArkTS shim from B.1/B.2
are now wrapped in a valid .hap archive that hdc can (in principle)
install. Per Decision 3, we do the HAP assembly ourselves instead of
shelling out to hvigor — no Node.js or DevEco-project dependency.

New crates/perry/src/commands/harmonyos_hap.rs (~560 lines incl. tests):

  * build_hap(HapBuildArgs) — top-level entry. Stages a directory:
      <stem>.hap_staging/
        app.json5         — bundleName, vendor, version, icon/label refs
        module.json5      — EntryAbility ability + entity.system.home skill
        pack.info         — summary + packages blocks hap-sign needs
        ets/              — copied from B.2's emit_harmonyos_arkts_stubs
        libs/arm64-v8a/   — the .so, renamed-preserved
        resources/base/
          media/icon.png  — inlined 68-byte 1×1 white placeholder
          string/string.json  — app_name + EntryAbility_label
          color/color.json    — start_window_background = #FFFFFFFF
          profile/main_pages.json — src: ["pages/Index"]
    Then zips into <stem>.unsigned.hap. If signing creds are available
    (see below) the signing step also runs and drops <stem>.hap next
    to the unsigned file (which is then removed — two HAPs in the
    output dir would be confusing).

  * Signing (Decision 4a). Reads three env vars:
      PERRY_HARMONYOS_P12, _P12_PASSWORD, _PROFILE
    If any is missing, emits unsigned + one-line remediation. With all
    three present, resolves hap-sign-tool.jar from the SDK (or the
    override PERRY_HARMONYOS_HAPSIGN) and shells out to
      java -jar <jar> sign-app -keyAlias perry-signing-key
          -signAlg SHA256withECDSA -mode localSign
          -appCertFile <profile> -profileFile <profile>
          -inFile <unsigned> -outFile <signed>
          -keystoreFile <p12> -keystorePwd <password>
    This is Huawei's documented minimum; users with existing cert
    pipelines may need env overrides once we get on-device feedback.

  * ets-loader detection. Probes four known SDK layouts for es2abc
    (standalone binary — preferred because no Node required) and
    falls back to a node-based ets-loader/main.js if it's there. If
    neither is found we ship .ets source and print a warning — the
    HAP still assembles, but will only install on DevEco emulator
    with source-mode enabled, not on real NEXT hardware.

  * Bundle naming. $PERRY_HARMONYOS_BUNDLE_NAME wins; otherwise
    `com.perry.app.<sanitized_stem>` (ASCII alnum only, leading-digit
    safe, consecutive-separator-safe). Real deploys must override —
    Huawei certs pin bundleName.

  * Placeholder icon. A real app's icon will come from
    perry.harmonyos.icon in package.json (not in this PR). Until then
    we inline a valid 68-byte PNG so the $media:icon ref resolves
    and hap-sign doesn't reject.

Wiring in compile.rs:

  * The existing is_harmonyos post-link block now chains
    emit_harmonyos_arkts_stubs → harmonyos_hap::build_hap. Both are
    gated behind the link succeeding. Logs a single summary line:
      Wrote HAP: libhi.unsigned.hap (unsigned, ets: source)
    HAP failures are eprintln! warnings — the .so is still useful
    for inspection even if the HAP step fails.

  * A new crates/perry/src/commands/mod.rs line (`pub mod harmonyos_hap`)
    registers the module.

Tests (crates/perry/src/commands/harmonyos_hap.rs#tests):

  * assembles_unsigned_hap_with_expected_layout — feeds a fake .so
    and fake ets/ to build_hap, verifies the produced zip contains
    every required layout member (10 entries), verifies the PNG
    magic bytes survive zip roundtrip, verifies the bundle-name
    fallback (`com.perry.app.hi`) appears in app.json5. Ignores
    signing by scrubbing env vars. Passes in ~10ms.
  * sanitize_bundle_segment_handles_edge_cases — covers hyphen
    rewrite, leading-digit prefix, consecutive-non-alnum collapse.

Deferred to follow-up PRs (not blocking @cavivie's on-device validation):

  * `perry setup harmonyos` wizard (Decision 4b). Env-var-only signing
    is enough for the first on-device install; a proper wizard with
    persisted config is its own PR.

  * User-provided icon / resource bundle (`perry.harmonyos.icon`,
    strings, colors).

  * x86_64 ABI dir for `--target harmonyos-simulator`. Currently
    libs/arm64-v8a/ is hardcoded; x86_64-linux-ohos output would
    need libs/x86_64/ and a runtime that can pick the right one.

  * `perry.toml` / `package.json` bundleName + version integration.

Verified:
  * `cargo test --release -p perry --bins harmonyos_hap` — 2/2 pass.
  * `cargo build --release -p perry` — clean.
  * `perry compile hi.ts --target harmonyos` without SDK — still
    single fail-fast from B.1 ("OHOS SDK not found").
  * Default host / macos / ios compile — byte-equivalent to pre-B.3.
  * Standalone render of a HAP via the unit test produces a valid
    ZIP that unzip(1) accepts, with the expected layout at every path.

End-to-end `hdc install` requires a real OHOS SDK + signing cert;
handing off to @cavivie for on-device validation once they pull.
Three parallel audits against Huawei's real sources caught seven
install-blocking bugs in the from-memory B.1-B.3 implementation.
Auditing against:
  - developtools_hapsigner (hap-sign-tool CLI spec)
  - developtools_packing_tool (pack.info canonical parser, C++ + Java)
  - arkui_napi / ace_napi (napi_module_register semantics)
  - arkcompiler_ets_frontend (es2abc CLI + supported extensions)
  - applications_app_samples (minimal reference HAPs)
  - openharmony/docs app.json5 + module.json5 references

Fixes landed in this commit:

1. NAPI modname ↔ .so filename mismatch (worst bug — silently fails).
   nm_modname was hardcoded "entry", default output was lib<stem>.so.
   OHOS's NativeModuleManager resolves `import X from 'libfoo.so'` by
   stripping lib/.so from the filename and looking up that name, so
   unless .so was literally libentry.so, the import resolved to
   undefined and every property access threw at load time.
   Fix: derive nm_modname at .init_array time via dladdr(register_module,
   &info), extract basename of dli_fname, strip "lib" prefix + ".so"
   suffix, copy into a 256-byte static buffer. Works for any -o output
   name. Falls back to "entry" if dladdr fails (matches DevEco's
   default-template modname). Single-threaded (init constructor runs
   before any ArkTS code) so no locking needed.

2. app.json5 missing minAPIVersion / targetAPIVersion / apiReleaseType.
   HAP install-time verification rejects without these. Added all three:
   minAPI=11 (HarmonyOS NEXT floor), target=12 (current DevEco 5.x),
   apiReleaseType="Release" (valid values: "Canary<N>" / "Beta<N>" /
   "Release"). Note apiReleaseType is spelled DIFFERENTLY from the
   releaseType key used inside pack.info — same semantics, different
   key name. Java parser checks both locations separately.

3. pack.info structural fixes.
   - summary.modules[0].name was bundleName; must be module name "entry".
   - summary.modules[0].package same — must be "entry".
   - packages[0].name same — must be "entry".
     Confusingly, summary.app.bundleName above IS the bundleName. Most
     common shape bug in hand-rolled HAPs per the audit.
   - apiVersion belonged under summary.app.apiVersion, not
     summary.modules[0].apiVersion. The Java parser reads the app-level
     path; a module-level duplicate is silently ignored. We put it in
     both locations for robustness across SDK versions.
   - deviceType (singular in pack.info, plural in module.json5) must
     match byte-for-byte; added "2in1" to pack.info side to align with
     module.json5's ["phone","tablet","2in1"]. packing_tool's HapVerify
     rejects mismatches before hap-sign even runs.
   - API target 10 -> 12. 10 = HarmonyOS 4.x, pre-NEXT.

4. hap-sign CLI reworked per developtools_hapsigner README lines 297-314.
   - -appCertFile and -profileFile are DIFFERENT files (cert chain
     .cer/.pem vs signed provisioning profile .p7b). B.3 passed the same
     PERRY_HARMONYOS_PROFILE path to both; hap-sign rejects (cert chain
     parser can't read p7b). Split into two env vars:
       PERRY_HARMONYOS_CERT     — cert chain (.cer)
       PERRY_HARMONYOS_PROFILE  — signed profile (.p7b)
   - -keyPwd added. DevEco-generated p12s have a separate key password
     from the keystore password. New env var PERRY_HARMONYOS_KEY_PASSWORD;
     falls back to PERRY_HARMONYOS_P12_PASSWORD if unset (common case
     where they're the same).
   - -keyAlias was hardcoded "perry-signing-key" which never matches
     anything. DevEco auto-signing uses "debugKey". New env var
     PERRY_HARMONYOS_KEY_ALIAS, defaults to "debugKey".
   - -signAlg configurable via PERRY_HARMONYOS_SIGN_ALG (defaults
     SHA256withECDSA; only other accepted value is SHA384withECDSA).
   - Explicit -profileSigned 1, -inForm zip, -signCode 1. These are
     defaults but explicit survives SDK-version drift.

5. ets-loader probe paths were all wrong.
   - B.3 probed <sdk>/build-tools/ets-loader/bin/ark_ts2abc_bin/es2abc
     + three variants under <sdk>/toolchains/. None exist in DevEco 5.x.
   - Real path: <sdk_root>/<api>/openharmony/ets/build-tools/ets-loader/.
     find_harmonyos_sdk returns <api>/openharmony/native/, so ets-loader
     is at `native/../ets/build-tools/ets-loader/`.
   - es2abc lives under ets-loader in a host-OS-specific subdir:
       build-mac/bin/es2abc   (macOS)
       build-win/bin/es2abc   (Windows)
       build/bin/es2abc       (Linux)
     We validate presence but don't invoke it directly — ets-loader
     spawns it internally.

6. Pipeline structurally rewritten. es2abc does NOT accept .ets — only
   js/ts/as. B.3's "run es2abc over each .ets file" approach was
   infeasible. Real pipeline is two-stage: ets-loader (Node/rollup npm
   package) desugars ArkUI decorators + bundles .ets -> .ts, then
   invokes bundled es2abc to emit .abc. compile_ets_to_abc now invokes
   ets-loader via `node <loader>/main.js --hap-mode=release <ets-dir>`
   from the staging dir. Hvigor orchestrates this with a richer
   build-profile.json5 we don't fully synthesize yet; our minimal
   invocation may or may not work on first try. First emulator run will
   tell us if we need to vendor more hvigor glue.

7. Source-fallback message updated. Physical NEXT devices reject
   .ets-source HAPs entirely (no debug-mode exception). Fallback now
   clearly states the HAP won't install and points the user at
   DevEco/hvigor for completion.

Tests extended:
  * serde_json strict-parse of pack.info (catches trailing commas).
  * Assert summary.modules[0].{name,package} == "entry".
  * Assert packages[0].name == "entry".
  * Assert summary.app.apiVersion is an object.
  * Assert app.json5 has minAPIVersion / targetAPIVersion /
    apiReleaseType: "Release".
  * Scrub all eight PERRY_HARMONYOS_* env vars at test start.

Also in this commit: ohos_napi.rs's modname buffer machinery (dladdr
+ basename extraction + "lib"/".so" stripping). 30-ish lines of
unsafe pointer walking — reviewed carefully, matches what every
OHOS NAPI example (including Huawei's own) does under the
NAPI_MODULE() macro.

NOT in this commit, staying as follow-up work:
  * Reading DevEco's build-profile.json5 auto-signingConfigs[] block
    directly instead of requiring users to set env vars manually.
  * More hvigor-glue if ets-loader's bare invocation fails on first
    emulator run.
  * --target harmonyos-simulator libs/x86_64/ ABI dir.
  * perry.toml / package.json bundle-name + icon integration.
  * perry setup harmonyos wizard.
…128)

PR B.5 for #113. First real-SDK run (DevEco Studio 6.0.1, API 21). Three
patches driven by what actually broke on the host:

1. DevEco 6.x SDK layout probe.
   5.x put the SDK at ~/Library/Huawei/Sdk/openharmony/<api>/.
   6.x bundles it inside the app: /Applications/DevEco-Studio.app/
     Contents/sdk/default/openharmony/.
   The per-API numeric subdir was replaced with a single `default/`.
   `find_harmonyos_sdk()`'s `normalize` gained two arms for the new shape,
   and the default candidate list picks up the app-bundle path on macOS.
   All downstream paths (toolchains/lib/hap-sign-tool.jar, ets/
   build-tools/ets-loader/bin/ark/build-mac/bin/es2abc, etc.) are
   unchanged — only the SDK root moved.

2. macOS host-side link args leaking into cross-compile.
   First real compile failed with:
     ld.lld: error: unknown argument '-framework'
     ld.lld: error: cannot open Security: No such file or directory
     ld.lld: error: unable to find library -liconv / -lobjc
   Root cause: the linker-command if/else chain had an `} else { if
   cfg!(target_os="macos") || is_cross_macos { -framework Security
   -framework CoreFoundation -framework SystemConfiguration -liconv
   -lresolv -lobjc } }` branch that fires whenever the target isn't
   explicitly ios/tvos/watchos/android/linux/windows. HarmonyOS fell
   through to it on a macOS host.
   Added dedicated `is_harmonyos` arm before `is_linux` with OHOS-correct
   libs: `-Wl,--allow-multiple-definition -lm -lpthread -ldl
   -lace_napi.z`. musl folds m/pthread/dl into libc.a so those -l flags
   are no-ops (harmless); libace_napi.z.so provides the NAPI symbols
   ohos_napi.rs imports (napi_module_register, napi_create_int32,
   napi_create_function, napi_set_named_property). OHOS convention is
   `<name>.z.so` and `-l` strips lib/.so but not the middle `.z`, so
   `-lace_napi.z` is the deliberate spelling.
   Also: `!is_harmonyos` added to the strip-debug-symbols guard — BSD
   strip on macOS emits a noisy "non-object and non-archive file"
   warning on ELF binaries (warning only, but confusing in the output).

3. ets-loader pipeline replaced with direct es2abc invocation.
   Empirical test against the installed SDK: ets-loader is a ~15-env-var
   Node/rollup pipeline that reads `aceModuleRoot`, `aceModuleBuild`,
   `aceModuleJsonPath`, `aceProfilePath`, `compileMode=moduleJson`, plus
   a full DevEco build-profile.json5. Without the full setup it silently
   exits 0 and produces no .abc. Synthesizing all of that is effectively
   re-implementing hvigor.
   Pivot: es2abc --extension ts empirically accepts plain-TypeScript
   content in a .ets file; it only rejects ArkUI-specific syntax
   (@Entry/@Component/struct). Phase 1 ships no ArkUI, just one plain
   UIAbility, so we invoke es2abc directly and skip ets-loader entirely.
   PR C (TS→ArkTS emitter) brings ets-loader back when it produces real
   ArkUI decorators.
   `compile_ets_to_abc` now:
     * finds es2abc at <sdk_native>/../ets/build-tools/ets-loader/bin/
       ark/build-<host>/bin/es2abc
     * collects every .ets under staging/ets/
     * one invocation: es2abc --module --merge-abc --extension ts
       --output ets/modules.abc <inputs...>
     * deletes the source .ets files
   HAPs ship a single merged `ets/modules.abc`, not per-file .abc.

4. Dropped pages/Index for Phase 1.
   EntryAbility.ets no longer imports `@ohos.window`, no longer has
   `onWindowStageCreate` with `windowStage.loadContent('pages/Index')`,
   and no longer needs a pages/Index.ets sibling. The UIAbility runs
   `perryEntry.run()` in onCreate; window stays blank but console.log
   output reaches hilog. That's enough to validate Phase 1's goal:
   cross-compile → NAPI bind → TS main() executes.
   Correspondingly:
     * module.json5 drops the `pages: "$profile:main_pages"` field.
     * main_pages.json no longer emitted.
     * resources/base/profile/ dir no longer created.
   PR C reintroduces all three when it can emit valid ArkUI pages.

Verified end-to-end (real DevEco 6.0.1 SDK):
  * perry compile hi.ts --target harmonyos succeeds with no warnings.
  * libentry.so: 8.6 MB, ELF 64-bit aarch64, NEEDED=[libc.so,
    libace_napi.z.so] (zero macOS cruft, zero glibc-only libs),
    .init_array has 2 function pointers (Rust's own init + our
    register_module), perry_runtime::ohos_napi::register_module
    present as GLOBAL FUNC, napi_module_register as UND (imported).
  * entry.unsigned.hap: 17 members, 9 MB, ets/modules.abc 1372 bytes
    with `PANDA` magic header, module.json5 bundle fallback through
    cleanly, no stray .ets source files.
  * Unit tests still pass after dropping pages/Index from expected
    layout (modified: required[] no longer includes profile/
    main_pages.json or ets/pages/Index.ets).

Blocking the first `hdc install` test:
  * Signing env vars (PERRY_HARMONYOS_P12/_P12_PASSWORD/_CERT/_PROFILE/
    _KEY_ALIAS/_KEY_PASSWORD/_BUNDLE_NAME) — user runs through DevEco's
    auto-signing flow in a throwaway project, then copies paths +
    passwords into their shell.
  * API-level compatibility. We set min=11, target=12. DevEco 6.x
    primary SDK is API 21; install-time verification may complain.
    Easy follow-up if it does: bump both, ideally read from the SDK
    root automatically.

Not yet touched:
  * `--target harmonyos-simulator` libs/x86_64/ path (hardcoded
    arm64-v8a today; emulator runs aarch64 anyway so not blocking).
  * Reading DevEco's build-profile.json5 signingConfigs.material to
    avoid manual env var setup. The seven env vars work but are fiddly.
…v0.5.129)

PR B.6 for #113. First time a Perry-emitted .so survives the OHOS
dynamic linker and `aa start EntryAbility` succeeds without crashing.
Before this patch, `hdc install` worked but onCreate threw
  TypeError: Cannot read property run of undefined
  Error relocating libentry.so: mi_malloc_aligned: symbol not found

Root cause: libmimalloc-sys's build.rs compiles its C sources to a
loose <hash>-static.o (362 KB, 154 mi_* symbols) AND emits a
libmimalloc.a wrapper — but on macOS→OHOS cross-builds the .a comes
out as a zero-member BSD-format archive (__.SYMDEF SORTED layout;
llvm-ar can't enumerate it). Rust's staticlib "bundle native libs"
path silently skips empty archives, so libperry_runtime.a shipped
with zero mi_* definitions.

On macOS/Linux targets this hasn't bitten us because Perry links
via rustc's driver there, which honors `cargo:rustc-link-lib=static=
mimalloc` directives and drags in the loose .o anyway. The harmonyos
branch invokes clang directly (we want --sysroot + --target=aarch64-
linux-ohos, rustc would wrap them awkwardly), so we miss all of
rustc's link-lib hints.

Fix: walk target/<variant>/<triple>/release/build/*/out/ recursively
(cc-rs can nest under c_src/mimalloc/v2/src/) and append every .o
to the clang link line. Triggers for both the perry-auto-<hash>/
(auto-rebuild) and plain <triple>/ (manual cargo build) roots.
Over-inclusion is safe: `--gc-sections` (already enabled for ELF
via the is_android || is_linux || is_harmonyos arm) dead-strips
anything the final binary doesn't actually reference.

Result on the wearable NEXT emulator (DevEco 6.0.1(21), SP3DEVC900E1):
  * libentry.so: 8.6 → 8.8 MB (+200 KB mimalloc C code).
  * mi_malloc_aligned: UND → 0x1aaed8 FUNC GLOBAL (llvm-readelf
    confirmed after the rebuild).
  * `bm install` + `aa start -a EntryAbility -b
    com.example.myapplication` both succeed.
  * "Hello world" default ArkUI page renders.
  * `perryEntry.run()` in onCreate returns (didn't throw).

Also in this commit (discovered during emulator validation, not
linker-related):

  * TARGET_API bumped 12 → 21 to match DevEco 6.0.1(21)'s primary
    SDK. minAPIVersion stays at 11 (HarmonyOS NEXT floor).
  * "wearable" added to deviceTypes (module.json5) and deviceType
    (pack.info) — phone emulator is gated behind Huawei real-name
    verification, wearable is the only image unverified accounts
    can download, so validation happens there.
  * ArkTS shim skill action: action.system.home →
    ohos.want.action.home (both accepted per OHOS spec, but the
    newer spelling matches DevEco 6 templates and surfaces fewer
    deprecation warnings).
  * Module-level docstring in harmonyos_hap.rs enumerates all 7
    PERRY_HARMONYOS_* env vars (was stale, listed only 3; B.4
    added CERT/KEY_ALIAS/KEY_PASSWORD/SIGN_ALG but didn't update
    the docs).

Open cutoff items (deliberate handoff to physical-device tester in
#113):

  * Does TS `console.log()` reach `hdc hilog`? perry-runtime writes
    to `println!()` → fd 1, which OHOS does not route into hilog by
    default. Likely needs a stdout→hilog shim in ohos_napi.rs's
    init_array constructor (dup2 fd 1 + fd 2 to a pipe-reader thread
    that calls OH_LOG_Print). Not tackled here — validation by
    @cavivie on physical metal will confirm whether the emulator
    behavior matches the device.

  * ArkTS import syntax: `requireNapi('entry')` resolved via a .d.ts
    + `file:` dependency in the DevEco project's oh-package.json5,
    not via direct `import ... from 'libentry.so'` (ArkTS's strict
    compiler rejects the bare import without a declaration). Clean
    path: have `perry compile --target harmonyos` emit the .d.ts +
    oh-package.json5 next to the .so. Follow-up.

  * `hdc install` of Perry's own assembled .hap: still gated on
    resolving DevEco-encrypted passwords or using the bundled
    OpenHarmony.p12 path. Current emulator validation uses the
    "splice .so + .ets into an existing DevEco project, let hvigor
    sign" workaround (documented in harmonyos_hap.rs module
    docstring).
@proggeramlug proggeramlug mentioned this pull request Apr 21, 2026
22 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant