Skip to content

HarmonyOS#

On HarmonyOS, your screens are drawn with ArkUI components inside a Stage-model app, packaged as a HAP for native HarmonyOS devices. Your Rust code runs as WebAssembly, as on iOS and Android, and talks to ArkUI through a Node-API bridge. It does not use an Android APK or ArkWeb.

What works and what doesn't#

WorksDoesn't work yet
View, Text, Button, TextInput, PasswordInput, ScrollView (vertical), Image, RiveEvery other component, including Switch, Checkbox, Radio, Slider, the progress indicators, Pressable, and Vector: they stop with an error
Backgrounds, uniform and per-edge borders (border-t, border-l-2, …), italic text, rounded corners, opacity, clipping, sizes, alignment, text colourRight-to-left layout, dark mode, and semibold text (drawn as medium)
Press, text change, submit, focus, and blur eventsScroll events, horizontal scrolling, native text measurement, and active: pressed styles (buttons keep ArkUI's own press effect)
An accessibility label on each elementAccessibility roles and states
Local file:// imagesImages from URLs
Synchronous Rust plugins and ArkTS plugins listed in native.plugins, including postureCode push, Swift and Kotlin plugin handlers, worker and async plugins such as crypto and http, and app-owned services: the build leaves them out with a warning, and their calls fail with Unavailable

HarmonyOS is not desktop Linux, even though Rust builds it with target_os = "linux". Plugins use their HarmonyOS or mobile code paths there, never their Linux desktop ones: os reports the platform as harmonyos, shell answers every call with Unavailable because HarmonyOS apps can't start processes, and the desktop APIs of clipboard, dialog, menu, tray, and the other desktop plugins aren't part of a HarmonyOS build.

Create a project#

Rive vector animations use native PixelMaps and DisplaySync callbacks. The host suspends callbacks while inactive or idle and releases its frame resources when the page leaves. Set AppStorage key leptoskit.rive.reduceMotion from your application's motion preference; automatic system preference integration is not provided. Text, images, layout, scripts, audio, and view models inside Rive files are unsupported. Validate playback and lifecycle on the device or emulator you intend to support.

HarmonyOS has its own starter. It isn't part of the six-platform project that leptoskit new creates by default:

leptoskit new my_app --platform harmonyos
cd my_app

The starter is a counter. Its Rust code is in guest/src/lib.rs, and the ArkUI app that hosts it is in hosts/harmonyos, which you can open and edit in DevEco Studio.

Build and run#

Install DevEco Studio and the HarmonyOS SDK. The project targets HarmonyOS 5.0.0 (API 12); its build-profile.json5 is yours and can select another installed SDK. Then:

  1. Set OHOS_NDK_HOME to the SDK's native directory, the one containing llvm/ and sysroot/.
  2. Put DevEco's hvigorw on your PATH, or set HVIGORW to its absolute path. Use DevEco's Node.js and ohpm.
  3. Open hosts/harmonyos in DevEco Studio and let it sync, or run ohpm install there.
  4. Build:
rustup target add wasm32-unknown-unknown aarch64-unknown-linux-ohos
leptoskit native build harmonyos            # arm64 devices and emulators
leptoskit native build harmonyos --release

For an x86-64 emulator, add its target and build that ABI instead:

rustup target add x86_64-unknown-linux-ohos
leptoskit native build harmonyos-x86_64

Plain leptoskit build also builds the arm64 app, because the starter sets native.targets = ["harmonyos"]. leptoskit build --platform harmonyos only works after you add "harmonyos" to app.platforms in leptoskit.toml; the starter doesn't list it, so without that the command stops with "platform harmonyos is disabled".

The CLI configures the SDK's Clang, sysroot, and archiver for the selected architecture; Rust's OpenHarmony platform guide documents the targets. The HAP is written to hosts/harmonyos/entry/build/default/outputs/default. To install it, open hosts/harmonyos in DevEco Studio, pick an emulator or device, and run. The project has no signing credentials: configure signing and provisioning in DevEco Studio before installing on a device or publishing to AppGallery.

Foldable phones#

HarmonyOS has book folds such as the Mate X5, wide folds such as the Pura X, and the Mate XT trifold. Folding resizes your app; the posture plugin also tells your Rust code how the phone is folded, where each fold crosses your window, and each hinge's angle, as on iOS and Android.

The HarmonyOS starter keeps its host and guest in two crates. Add the plugin to both, with the guest feature in the guest, and select it in leptoskit.toml:

# Cargo.toml (the host)
[dependencies]
leptoskit_plugin_posture = "0.1"

# guest/Cargo.toml
[dependencies]
leptoskit_plugin_posture = { version = "0.1", features = ["guest"] }

# leptoskit.toml
[native]
plugins = ["posture"]

Then read it with DevicePosture::watch, as Foldable phones shows. The starter's guest has no executor yet: poll the watch future from a leptoskit_poll export, which the runtime calls whenever a plugin answers.

On HarmonyOS, the plugin reports:

  • Fold state: closed, half open, or open, for each hinge. The Mate XT reports its two hinges separately; showing two of its three panels is open, with the third panel's hinge closed and left out.
  • Where each fold crosses your window, as fractions of the window, and whether it runs top to bottom or side to side. "Open" on the Pura X is one wide screen with a side-to-side fold, while on the Mate XT it is two panels side by side, so read the fold's direction rather than assuming one.
  • Each hinge's angle, from HarmonyOS 5.0 (API 12) on. Angles arrive as the hinge moves, so the first reading after launch can have none.

Folded onto the cover screen, or with your app in split screen beside the fold, no fold crosses the window: you get the fold state and no hinges. Flip phones such as the Pocket 2 tell apps they do not fold, so they report "unknown" rather than "rigid". HarmonyOS does not say whether a fold hides content, so that is unknown too.

Fold postures have not yet been checked on a HarmonyOS emulator or device. Use a foldable or trifold emulator in DevEco's Device Manager and its fold controls to try each state.

Testing#

Test on a full emulator or a device: that is the only way to exercise your Rust code, the Node-API bridge, and the ArkUI controls together. DevEco's ArkUI Previewer is useful for layout, but it doesn't run leptoskit's runtime.

Start an emulator and run the app#

  1. Install DevEco Studio and a compatible HarmonyOS SDK. Open hosts/harmonyos and let DevEco sync the project, including ohpm install.
  2. Open Tools → Device Manager → Local Emulator. Create a phone or tablet virtual device, download its image, and start it. The available images and host requirements depend on the installed DevEco version; follow Huawei's emulator creation guide.
  3. Add the SDK's toolchains directory to PATH and run hdc list targets. Select the booted target. Match its ABI: use leptoskit native build harmonyos for arm64 or leptoskit native build harmonyos-x86_64 for x86-64. Do not assume every emulator is x86-64.
  4. In DevEco's project signing settings, configure debug signing for the selected target if required. Select the entry run configuration and the emulator, then run the app. Check that it displays Count: 0 and that tapping Increment changes the value.

For a physical device, enable developer mode and debugging, connect it, authorize the computer, and select its hdc target instead. Use a device running native HarmonyOS with a compatible API level; an Android/EMUI device does not validate this HAP target.

Run the generated native UI test#

New projects include entry/src/ohosTest/ets/test/Counter.test.ets, a runner, and an ohosTest build target. The test uses Hypium and UiTest to launch EntryAbility, tap the native Increment button three times, and verify the displayed count after each tap. It re-finds controls and waits for visible results with a deadline. It does not mock Rust, the guest, the bridge, or ArkUI.

Stop the running app before each test run so its in-memory counter starts at zero. Right-click Counter.test.ets in DevEco and run it as an Instrument Test, selecting the same target. Inspect the test result panel: HarmonyCounter.incrementsThroughNativeWidgets must pass. Change this starter journey when you replace the counter; add journeys for text entry and other controls your app uses.

For a project generated before the test scaffold was added, run leptoskit native prepare harmonyos with the updated CLI. This adds missing test sources, but deliberately preserves your root oh-package.json5: add "@ohos/hypium": "1.0.22" to its devDependencies, then run ohpm install. This version supports the scaffold's API 12 baseline. The regenerated entry build profile includes the test target. Existing test sources remain application-owned.

Run instrumentation from a terminal#

First build the application for the target ABI with leptoskit native build as above. From hosts/harmonyos, build the test HAP using the same product and debug signing configuration:

hvigorw --mode module -p product=default -p module=entry@ohosTest \
  -p buildMode=debug assembleHap --no-daemon

Use $HVIGORW instead of hvigorw if that is how you configured your installation. The test HAP is under entry/build/default/outputs/ohosTest; newer DevEco versions can use entry/.test/default/outputs/ohosTest. Use the actual paths printed by Hvigor. Install both the app and test HAPs, signed for the selected device where required:

hdc list targets
export HDC_TARGET='target-id-from-the-list'
export BUNDLE_NAME='com.leptoskit.myapp' # AppScope/app.json5: app.bundleName
export APP_HAP='/absolute/path/to/entry-default-signed.hap'
export TEST_HAP='/absolute/path/to/entry-ohosTest-signed.hap'

hdc -t "$HDC_TARGET" install -r "$APP_HAP"
hdc -t "$HDC_TARGET" install -r "$TEST_HAP"
hdc -t "$HDC_TARGET" shell aa force-stop "$BUNDLE_NAME"
mkdir -p test-artifacts/harmonyos
hdc -t "$HDC_TARGET" shell aa test -b "$BUNDLE_NAME" -m entry_test \
  -s unittest /ets/testrunner/OpenHarmonyTestRunner \
  -s class HarmonyCounter -s timeout 60000 \
  > test-artifacts/harmonyos/instrument.txt 2>&1

Check the instrumentation summary, including OHOS_REPORT_RESULT: this starter suite must report one test run, one pass, zero failures, and zero errors, with no skipped cases. A successful hdc transport exit code alone is not a test verdict. Missing results, a timeout, or zero tests must fail a CI integration. DevEco Instrument Test provides the test verdict and reporting without writing a parser.

Capture failures and finish device validation#

Keep the instrumentation log, SDK/API and ABI information, HAPs, and DevEco test report for each run. With the app visible on the target, UiTest commands can capture the rendered result and component tree:

hdc -t "$HDC_TARGET" shell uitest screenCap -p /data/local/tmp/leptoskit.png
hdc -t "$HDC_TARGET" shell uitest dumpLayout -p /data/local/tmp/leptoskit.json
hdc -t "$HDC_TARGET" file recv /data/local/tmp/leptoskit.png test-artifacts/harmonyos/screen.png
hdc -t "$HDC_TARGET" file recv /data/local/tmp/leptoskit.json test-artifacts/harmonyos/layout.json

Also check rotation/resizing, background/foreground transitions, Back followed by relaunch (the counter resets), large text and accessibility labels, and repeated launch/exit cycles. Test hardware APIs and performance on a physical device once your app uses them. Retain screenshots from failed runs instead of treating a HAP build as a UI test.

leptoskit test run does not yet orchestrate HarmonyOS HAP installation or instrumentation. Use DevEco or hdc for this target. Automated hardware CI needs a provisioned runner with the licensed SDK, a booted emulator or authorized device, and appropriate debug signing. The dedicated hosted CI job above validates SDK builds and guest/render behavior; hardware UI execution is not enabled.

How it works#

Edit guest/src/lib.rs for your app and the sources under hosts/harmonyos for the ArkUI shell. Running leptoskit native prepare harmonyos again keeps your app sources, build configuration, and signing configuration. It regenerates the entry module's build-profile.json5, which selects the ABI, the embedded app.wasm, and the build inputs in .leptoskit.

The bridge owns each runtime through a Node-API object that closes once, explicitly or when it is garbage-collected. The runtime stays on the UI thread, and each render updates the ArkUI components with the frames leptoskit's layout engine computed. Text input, submit, focus, blur, and button events go back to your Rust code. Leaving the page cancels pending work and destroys the runtime.

ArkTS plugins run on the same UI thread. Each prepare regenerates entry/src/main/ets/leptoskit with the selected plugins and a registrant that creates them; don't edit it. The page registers them before the runtime starts, hands each render's plugin calls to them, and returns their answers on a later turn. When your Rust code stops waiting for a call, the plugin is told and its answer is dropped. Projects created before ArkTS plugins existed keep their own Index.ets and index.d.ts, which don't wire plugins up: copy both from a newly created starter to use them.

esc