HarmonyOS Next: the ArkUI backend (§9)
HarmonyOS Next dropped the AOSP layer; its UI framework is ArkUI. Day targets it with a native
backend (day-arkui) built on the ArkUI Native NodeAPI, using the same “real native widgets, Day
owns layout” model as every other backend, adapted to HarmonyOS’s ArkTS-hosted world. harmony-arkui
is Tier 3: tested, but not comprehensively, and
not yet exercised by shipping applications.
Architecture
It mirrors the Android backend: a managed UI runtime (ArkTS) hosts the window, native Rust builds the widget tree over a thin bridge, and Day owns absolute layout.
ArkTS host (Index.ets) libentry.so (Rust cdylib)
NodeContent slot ──start()──▶ day-arkui-sys (C++ shim over the ArkUI NodeAPI + NAPI)
ContentSlot(content) day-arkui (Toolkit/Platform: Stack/Text/Button/…)
▲ day-core / day-pieces / day-fluent / day-script
└── OH_ArkUI_NodeContent_AddNode ── the native tree Day builds is mounted here
The ArkTS host is the framework’s, not the app’s. It lives in the day-arkui crate at
toolkits/day-arkui/platform/harmony/ — ets/ holds EntryAbility, DayWindowAbility, the
Index page and the secondary-window DayWindow page; types/Index.d.ts declares the native
module’s exports, so it always matches the day-arkui-sys shim the app links. day build
(and day prepare/day open) resolves the crate through cargo metadata and stages the
directory into build/day/harmony/project/: the pages and abilities under
entry/src/main/ets/day/, the typings (with their oh-package.json5) under
entry/src/main/cpp/types/libentry/, plus the generated page list and start-window colors.
The staged project is a copy of platform/harmony/. Day writes permission strings, shortcut
labels and profiles, manifest changes, and raw resources into the copy, then runs hvigor
there. All generated output stays under build/day/. What an app checks in is the hvigor
skeleton: the build profiles,
AppScope/app.json5, module.json5 (whose srcEntry paths name the staged files), the
string tables, and the hvigor config. An app that predates this (2026-09) still carries its
own copies under ets/pages/ and ets/entryability/; day build leaves those alone and
prints a note on how to adopt the staged host.
day open -p harmony-arkui prepares and opens the staged project in DevEco Studio. Edit
native source files under platform/harmony/, then run day prepare -p harmony-arkui again;
edits made only in the staged copy are overwritten. Each preparation refreshes the source
copy and removes obsolete resources while keeping hvigor’s build and dependency caches.
Symlinked inputs, including generated icon media, are copied as files so native generators
cannot write through to their sources. The old platform/ohos/ source layout also works.
Flavors use build/day/flavors/<name>/harmony/project/.
Keep native module dependencies and custom build-script paths relative to the native host,
or use absolute paths for external tools. A path that climbs out of platform/harmony/
will resolve from a different directory in the staged project.
day-arkui-sys: a C++ shim (likeday-qt-sys/day-xaml-sys) exposing a flat C ABI overarkui/native_node.h(createNode/setAttribute/addChild/registerNodeEvent/measureNode),arkui/native_node_napi.h(NodeContent), andnapi/native_api.h. It also registers the NAPI module (entry) whosestart(nodeContent, widthVp, heightVp, density)kicks off Day, wires the global node-event receiver back to Rust, and posts to the main (JS) thread via libuvuv_async.day-arkui: theToolkit/Platformimpl. Pieces map to ArkUI node types (ARKUI_NODE_STACKfor containers,TEXT,BUTTON,TEXT_INPUT,TOGGLE,SLIDER), children get an explicit position + size in vp (≈ Day points), and events (click / text / toggle / slider) come back throughday_arkui_on_event.day::day_start_arkui!(root): exportsday_arkui_start, the symbol the shim’s NAPIstartcalls; it mounts the app’s root piece and runs the loop (day::arkui::start→launch_with). Apps reach it throughday::day_start!("App Name", root), which expands to this macro and to every other platform’s entry; the title argument is ignored here, since the label comes fromAppScope/resources/base/element/string.json.
Local development environment
-
OpenHarmony SDK / NDK. Easiest: install the command-line-tools (see Build & run), one bundle carrying the NDK + hvigor + ohpm + node + signing material, and point
OHOS_NDK_HOMEat itssdk/default/openharmony/native. For a Rust-only cross-compile you can instead grab just thenativeNDK component from the public SDK (no account needed):curl -LO https://repo.huaweicloud.com/openharmony/os/6.0-Release/L2-SDK-MAC-M1-PUBLIC.tar.gz # extract → .../ohos-sdk/packages/ohos-sdk/darwin/native-*.zip → unzip → .../native export OHOS_NDK_HOME=/path/to/ohos-sdk/native -
Rust OHOS targets (Tier 2 std via rustup; Homebrew rustc has none):
rustup target add aarch64-unknown-linux-ohos x86_64-unknown-linux-ohosThe NDK ships the linker wrappers
$OHOS_NDK_HOME/llvm/bin/<target>-clang; point cargo at them withCARGO_TARGET_<TARGET>_LINKER. (day buildsets this itself, along withCC_<target>/AR_<target>pointing at the NDK’s clang and llvm-ar socc-rsbuild scripts cross-compile too; the exports only matter if you drive bare cargo.)
Check your environment first
day doctor --toolkit harmonyos reports exactly which of the pieces below are present or missing,
with setup instructions:
day doctor --toolkit harmonyos
Bare day doctor (no --toolkit) scans every toolkit and reports a missing HarmonyOS setup as a
warning rather than an error, since you only need it if you build for HarmonyOS.
Build & run
The build has two halves with different tool needs:
- The Rust cross-compile (
libentry.so) needs only the OpenHarmony NDK, thenativecomponent of the public SDK, which downloads without a Huawei account. PointOHOS_NDK_HOMEat it.hdc(for install/launch) sits in the SDK’s siblingtoolchains/dir;dayfinds it there automatically, or you can put it onPATH. - Packaging the
.hapneedshvigor+ohpm. These are not in the public SDK; they ship with the OpenHarmony command-line-tools (bundled with DevEco Studio, or the Linux-x64 bundle atrepo.huaweicloud.com/harmonyos/ohpm/<ver>/). Put theirbin/onPATH.
The showcase’s platform/harmony/ project targets OpenHarmony (runtimeOS: "OpenHarmony",
compileSdkVersion/compatibleSdkVersion = the integer API level), which matches the Oniro emulator
and avoids the HMS-only libimage_transcoder_shared library that only DevEco Studio ships, so the
whole flow works login-free on macOS and Linux.
macOS note. The Linux command-line-tools hvigor/ohpm are pure JavaScript, so they run under
system node via a two-line wrapper even though the bundle is packaged for Linux:
cat > ~/ohos/bin/hvigorw <<'SH'
#!/usr/bin/env bash
exec node "$HOME/ohos-clt/command-line-tools/hvigor/bin/hvigorw.js" "$@"
SH
cat > ~/ohos/bin/ohpm <<'SH'
#!/usr/bin/env bash
exec node "$HOME/ohos-clt/command-line-tools/ohpm/bin/pm-cli.js" "$@"
SH
chmod +x ~/ohos/bin/hvigorw ~/ohos/bin/ohpm
Point OHOS_NDK_HOME at the public mac SDK’s native dir and OHOS_BASE_SDK_HOME at a versioned
SDK layout (<dir>/<api>/…, e.g. a symlink 18 -> .../openharmony).
cd Day-Showcase
day build -p harmony-arkui
# Inspect the prepared native project in DevEco Studio:
day open -p harmony-arkui
For native packaging work after a Day build, run hvigor from the staged project:
cd build/day/harmony/project
ohpm install
hvigorw assembleHap --mode module -p product=default -p buildMode=debug --no-daemon
Use DevEco Studio’s auto-sign configuration when installing on a physical device. Configure it in the source host if it must survive the next preparation.
You don’t run any of the above by hand; day launch -p harmony-arkui does the whole flow
(cross-compile → hvigor → sign → install → start), and day brings up the emulator too:
# A native OpenHarmony emulator window (QEMU cocoa on macOS; no VNC, no password, no DevEco).
# Point DAY_OHOS_EMULATOR at the Oniro image dir (default ~/ohos/emulator/images); --headless for CI.
day devices boot -p harmony-arkui
# Then build + install + launch the app on every connected target (see "Multiple devices" below):
day launch --project Day-Showcase -p harmony-arkui
Multiple devices / architectures
day launch -p harmony-arkui enumerates every reachable hdc target, queries each one’s arch
(uname -m → x86_64 emulator / arm64 device), builds a libentry.so for each arch, and packs
them all into the one .hap (libs/x86_64/ + libs/arm64-v8a/) so it installs on any of them.
It then installs + starts the app on every connected target. Android (adb, per-device
ro.product.cpu.abi) and iOS (every booted simulator) do the same: one day launch fans out to
all connected devices, building whatever ABIs they need.
Status
harmony-arkui is a maintained target. Pieces render as ArkUI Native NodeAPI nodes, verified
on the Oniro emulator:
-
Nav shell (
nav) — a scrollable list that pushes detail pages. -
Controls —
Text,Button,TextInput, nativeSlider/Toggle, a determinateProgressbar + an indeterminateLoadingProgressspinner, andDividerhairlines. -
Canvas (§11) — an
ARKUI_NODE_CUSTOMnode whose on-draw callback replays Day’s display list with OH_Drawing (arcs, fills, strokes, rounded-rects, ellipses, text): the gauge + shapes pages. -
List (§10) — an
ARKUI_NODE_LISTdriven by anOH_ArkUI_NodeAdapterwith cell reuse, so a 500-row list only builds the visible cells. -
Tabs — an
ARKUI_NODE_SWIPERpager with a dot indicator. -
Gestures —
.on_tap(NODE_ON_CLICK) and.on_drag(a native pan recognizer via the NDK gesture API, reported on the shared kind-11 wire in px). Long-press is not wired. -
Scrolling (§7.6) —
scroll()is a realARKUI_NODE_SCROLLwhose day children live in a shim-owned container sized byset_scroll_content; without it the Scroll measures a content extent of 0 (day’s content nodes are layout-only) and neither touch norscroll_tomoves. -
Frame clock (§8.4) —
Platform::request_framerides a ~16 ms one-shotuv_timeron the JS loop (the NodeAPI has no re-armable vsync callback), soframe_clockgame loops and self-driven animations run; Day-Games’ breakout/sirtet/2048 play on the emulator. -
Fullscreen cover (docs/cover.md) —
Cap::CoveranswersEmulated: the cover node is re-homed onto the window root at full bounds (no transition, no gesture dismissal). -
ArkTS-built pieces (docs/extending.md) — a piece whose component exists only in ArkTS (the declarative
Webhas no ArkUI C node kind) ships its own.ets;day buildstages it into the hvigor project and generatesentry/src/main/ets/daypieces/DayPieces.ets, which the host page registers with the shim (registerPiece) beforestart().day-piece-webviewis the first user. ArkWeb does not run on the x86_64 emulator: the image’sArkWebCore.hapcarries arm64-only native libs, solibarkweb_engine.sonever loads (GetNWeb: web engine is nullptr) and the component’s RosenWeb surface wedges the window’s compositor for the rest of the process; every later frame goes stale. The showcase walkthrough therefore skips the web-view page on harmony-arkui; the arm path is unaffected.
Images/lottie/map pieces are not yet wired (they render as placeholders). Packaging + signing
run headlessly via the command-line-tools (see Build & run) with no Huawei developer account: hvigor
assembles the .hap and sign-hap.mjs patches (compileSdkType → OpenHarmony) + signs it with the
public release material. libentry.so links the NDK’s shared libc++ and libnative_drawing, both
packed into the .hap.
CI
The harmony-arkui job in .github/workflows/ci.yml runs on every push/PR (macos-14). The build
gates hard (clippy + cross-compile + hvigorw assembleHap + sign + day doctor); only the
emulator boot + walkthrough are best-effort (continue-on-error per step) because the GitHub-hosted
TCG emulator is slow and occasionally flaky. It downloads + caches the OpenHarmony
command-line-tools (~2 GB) and then runs the real build pipeline:
- clippy the backend for the OHOS target, and cross-compile the full Day app to
libentry.sofor the emulator (x86_64) and device (arm64) using the darwin SDK’s NDK clang; ohpm install, thenhvigorw assembleHap, a full hvigor build of the ArkTS host +.hap;- patch + sign the
.hapwithsign-hap.mjs(compileSdkType → OpenHarmony + the public release material), then install/launch it on the Oniro emulator overhdcand drive the dayscript walkthrough, uploading screenshots for the gallery, like the other targets. CI boots the emulator withday devices boot -p harmony-arkui --headless(the same Oniro v6.1 image openharmony-rs’s emulator-action uses) rather than the action itself: the action’s QEMU command has no GPU device (-nographic), so the guest has no display: the keyguard never dismisses,aa startis refused with error 10106102, and screenshots capture nothing. Day’s launcher adds-device virtio-gpu-pciwith-display none: a headless framebuffer the apps can foreground on anduitest screenCapcan capture.
Declaring the app OpenHarmony (via the compileSdkType patch) lets it install: the emulator enforces app code signing but doesn’t trust the public cert, and OpenHarmony’s BMS skips code-sign verification for OpenHarmony-declared apps on devices without Huawei OH code signing.
The whole job (build gates and emulator screenshots) runs on a single macOS runner: the
x86_64 Oniro guest is TCG-emulated on every GitHub runner, and only the macOS ARM hosts run it
fast enough for the pipeline (first-boot keyguard render → wake + swipe-unlock → walkthrough);
the ubuntu hosts never got that far. Build steps gate hard; only the emulator boot + walkthrough
are per-step best-effort. The setup replicates the validated local macOS flow: the Linux
command-line-tools (hvigor/ohpm are pure JS) run through node wrapper scripts, the darwin
API-18 SDK from setup-ohos-sdk supplies the NDK + hdc + the versioned OHOS_BASE_SDK_HOME
view hvigor builds against, and Day’s QEMU launcher boots the image. OHOS_BASE_SDK_HOME
must be a host-platform SDK: hvigor spawns its native tools (syscap_tool, restool,
es2abc) directly, so pointing it at the Linux CLT’s bundled SDK fails on macOS with
spawn ENOEXEC at SyscapTransform.
The scripted channel depends on the following facts, each of which was a silent total failure until diagnosed:
-
The default hdc forward port 55555 is often already occupied, since GitHub’s macOS runners hold it, and so do some local services; QEMU then dies instantly (“Could not set up host forwarding rule”), leaving no reachable target and blank screenshot sets.
day devices boot -p harmony-arkuiprobes and slides to the first free port,tconns the chosen key (so device discovery finds it), and exportsDAY_OHOS_TARGETthroughGITHUB_ENVso later CI steps target it too. -
ohos.permission.INTERNETis required for the loopback dayscript socket; without it inmodule.json5the engine’sTcpListener::bindfails silently and no scripted run can ever connect (the Android manifest needs the same permission for the same reason). -
The ability is a singleton: a second
aa startforegrounds the existing process, whose engine (if any) listens on the previous run’s port.day launchforce-stops the bundle before every start so each run’s engine params take effect. -
The keyguard returns whenever the display sleeps and
aa startis refused while it shows;day launchwakes + swipe-unlocks (uitest + uinput) around every launch retry. -
ArkUI Navigation diffs the old and new stacks by destination name, so a same-turn pop+push of two destinations with the same generic name collapses into “page is not change — don’t transition”, leaving the old (already-detached, empty) destination on screen for good. The host page pushes each Day page under a unique
day-page-<key>name, and dayscript-driven sessions push/pop without transition animation (ArkUI drops an operation issued across an in-flight transition; scripted bursts outrun one). -
uitest screenCaptrails the UI tree, because the TCG guest’s RenderService composites a pushed page seconds after it has laid out, so a shot taken right after a navigation shows the previous page. The runner waits forui_idle(the pushed destination’s first area report), sleepsDAY_OHOS_SHOT_SETTLE_MS(default 4000) before capturing, and re-captures when a shot comes out byte-identical to the run’s previous one (the first push after app start can lag past the settle while the render tree warms up).
Follow-ups
Wire the remaining pieces (image / lottie / map), richer accessibility (roles beyond the
label), interactive tab-bar labels on the swiper, and the image/webview backends. The dayscript
engine’s TCP channel is flaky on the TCG emulator (occasional connection resets), so scripted
walkthrough screenshots on OHOS are best-effort. Canvas fonts and the font list
(docs/fonts.md) compile against OH_Drawing_FontMgr and are verified by CI on a
device. A family the ability registered through font.registerFont is NOT visible to
OH_Drawing_FontMgrMatchFamilyStyle (the 2026-09 showcase drew canvas text in the system face
while its labels were right), so the toolkit builds a typeface from each bundled font’s bytes
and the canvas draws from that.