// Window-hosted HarmonyOS packaging.
// HarmonyOS HAP packaging now uses the window-hosted template at
// `window/modules/window/harmonyos/template/` +
// `examples//harmonyos_window_hosted` entrypoint.
//
// `build_harmonyos` generates MoonBit C, stages the window-hosted hvigor
// project template, applies app identity (bundleName / moduleName / appName),
// optionally injects a signing config, then invokes `ohpm install` +
// `hvigorw assembleHap` with the `MBW_*` env vars consumed by the template's
// `entry/src/main/cpp/CMakeLists.txt`.
///|
priv struct BuildHarmonyosOptions {
app : MobileApp
workspace_root : String
skia_root : String
arch : String
renderer : String
build_dir : String
output_hap : String
sdk_home : String?
hvigorw : String?
ohpm : String?
fallback_skia : Bool
prepare_only : Bool
signing_config_file : String?
}
///|
/// Strip a minimal subset of JSON5 syntax from `text` so a JSON parser can
/// accept DevEco Studio's `build-profile.json5`:
/// - block comments `/* ... */`
/// - line comments `// ...` (but not `:` URLs like `https://...`)
/// - trailing commas before `}` or `]`
fn strip_json5(text : String) -> String {
let result = StringBuilder()
// `String::op_get` returns `UInt16` (UTF-16 code unit), but we need `Char`
// for `StringBuilder::write_char` and `Char` literal comparisons. Convert
// once to `Array[Char]` so all indexing yields `Char` directly. JSON5 source
// is ASCII for the constructs we strip (comments, trailing commas), so this
// is safe and cheap.
let chars = text.to_array()
let length = chars.length()
let mut i = 0
let mut prev_significant = ' '
while i < length {
let ch = chars[i]
// Block comment
if ch == '/' && i + 1 < length && chars[i + 1] == '*' {
i += 2
while i + 1 < length && !(chars[i] == '*' && chars[i + 1] == '/') {
i += 1
}
i += 2
continue
}
// Line comment — skip `// ...` to end of line, but only when the
// preceding non-whitespace char is not `:`. This preserves `https://`.
if ch == '/' && i + 1 < length && chars[i + 1] == '/' {
if prev_significant == ':' {
result.write_char(ch)
i += 1
continue
}
i += 2
while i < length && chars[i] != '\n' {
i += 1
}
continue
}
// Trailing comma before `}` or `]` (with intervening whitespace).
if ch == ',' {
let mut j = i + 1
while j < length {
let next_ch = chars[j]
if next_ch == ' ' ||
next_ch == '\t' ||
next_ch == '\n' ||
next_ch == '\r' {
j += 1
continue
}
if next_ch == '}' || next_ch == ']' {
// Skip the comma without emitting it.
i += 1
break
}
result.write_char(ch)
i += 1
break
}
if j == length {
// Trailing comma at EOF — drop it.
i += 1
continue
}
if i < length && chars[i] == ',' {
// Already handled above; loop again.
continue
}
continue
}
result.write_char(ch)
prev_significant = if ch == ' ' || ch == '\t' || ch == '\n' || ch == '\r' {
prev_significant
} else {
ch
}
i += 1
}
result.to_string()
}
///|
/// Locate the OpenHarmony OHOS CMake toolchain file under `sdk_home`.
/// Returns an empty string when the toolchain cannot be found.
fn find_ohos_toolchain(sdk_home : String) -> String {
let candidates = [
"native/build/cmake/ohos.toolchain.cmake", "default/openharmony/native/build/cmake/ohos.toolchain.cmake",
"default/hms/native/build/cmake/ohos.toolchain.cmake", "openharmony/native/build/cmake/ohos.toolchain.cmake",
]
for candidate in candidates {
let path = join_path(sdk_home, candidate)
if path_is_file(path) {
return path
}
}
// Fall back to scanning `sdk_home` recursively. We only look two levels
// deep to avoid traversing huge SDK trees.
let entries = @fs.read_dir(sdk_home) catch { _ => return "" }
for entry in entries {
let child = join_path(sdk_home, entry)
if path_is_dir(child) {
let target = join_path(child, "native/build/cmake/ohos.toolchain.cmake")
if path_is_file(target) {
return target
}
let sub_entries = @fs.read_dir(child) catch { _ => continue }
for sub in sub_entries {
let grandchild = join_path(child, sub)
let target = join_path(
grandchild, "native/build/cmake/ohos.toolchain.cmake",
)
if path_is_file(target) {
return target
}
}
}
}
""
}
///|
/// Discover the HarmonyOS/OpenHarmony SDK home on macOS by scanning
/// well-known DevEco-Studio install locations.
fn find_harmonyos_sdk_home() -> String {
let home = @env.get_env_var("HOME").unwrap_or("")
let candidates = [
"/Applications/DevEco-Studio.app/Contents/sdk/default/openharmony",
"/Applications/DevEco-Studio.app/Contents/sdk/default/hms",
join_path(home, "Library/OpenHarmony/Sdk/20"),
]
for candidate in candidates {
if find_ohos_toolchain(candidate) != "" {
return candidate
}
}
""
}
///|
/// Discover the `hvigorw` wrapper on macOS by scanning DevEco-Studio paths.
fn find_deveco_hvigorw() -> String {
let candidates = [
"/Applications/DevEco-Studio.app/Contents/tools/hvigor/bin/hvigorw", "/Applications/DevEco Studio.app/Contents/tools/hvigor/bin/hvigorw",
]
for candidate in candidates {
if path_is_executable(candidate) {
return candidate
}
}
""
}
///|
/// Discover the `ohpm` executable on macOS by scanning DevEco-Studio paths.
fn find_deveco_ohpm() -> String {
let candidates = [
"/Applications/DevEco-Studio.app/Contents/tools/ohpm/bin/ohpm", "/Applications/DevEco Studio.app/Contents/tools/ohpm/bin/ohpm",
]
for candidate in candidates {
if path_is_executable(candidate) {
return candidate
}
}
""
}
///|
/// Resolve the DevEco SDK root from a given `sdk_home` by walking up the
/// path tree until `default/openharmony` or `default/hms` is found.
fn find_deveco_sdk_home(sdk_home : String) -> String {
let candidates = [
sdk_home,
join_path(sdk_home, ".."),
join_path(sdk_home, "../.."),
"/Applications/DevEco-Studio.app/Contents/sdk",
]
for candidate in candidates {
if path_is_dir(join_path(candidate, "default/openharmony")) ||
path_is_dir(join_path(candidate, "default/hms")) {
return candidate
}
}
""
}
///|
/// Write `value` as pretty-printed JSON to `path`.
fn write_json_file(path : String, value : Json) -> Result[Unit, String] {
let text = value.stringify(indent=2) + "\n"
write_file_bytes_result(path, @utf8.encode(text[:], bom=false))
}
///|
/// Read a JSON/JSON5 object file, stripping JSON5 comments/trailing commas.
fn read_json5_object(path : String) -> Result[Map[String, Json], String] {
let text = match read_file_string_optional(path) {
Some(value) => value
None => return Err("could not read: " + path)
}
let json = @json.parse(strip_json5(text)) catch {
_ => return Err("invalid JSON/JSON5: " + path)
}
guard json is Object(fields) else {
return Err("expected object root: " + path)
}
Ok(fields)
}
///|
/// Update a HarmonyOS string resource `{ "string": [ { "name", "value" } ] }`.
fn update_json5_string_resource(
path : String,
name : String,
value : String,
) -> Result[Unit, String] {
let root = match read_json5_object(path) {
Ok(fields) => fields
Err(message) => return Err(message)
}
let strings = match root.get("string") {
Some(Array(values)) => values
_ => return Err(path + ": missing string array")
}
let mut found = false
let updated : Array[Json] = []
for item in strings {
match item {
Object(fields) =>
match fields.get("name") {
Some(String(item_name)) if item_name == name => {
fields["value"] = Json::string(value)
updated.push(Json::object(fields))
found = true
}
_ => updated.push(item)
}
_ => updated.push(item)
}
}
if !found {
return Err(path + ": missing string resource " + name)
}
root["string"] = Json::array(updated)
write_json_file(path, Json::object(root))
}
///|
/// Inject DevEco signing configs into a staged `build-profile.json5` from a
/// JSON fragment file. The
/// window-hosted template ships with an empty `signingConfigs` array.
fn inject_harmonyos_signing_config(
project_dir : String,
signing_config_file : String,
) -> Result[Unit, String] {
if signing_config_file == "" {
return Ok(())
}
if !path_is_file(signing_config_file) {
return Err(
"MOUI_HARMONYOS_SIGNING_CONFIG_FILE not found: " + signing_config_file,
)
}
let profile_path = join_path(project_dir, "build-profile.json5")
let profile_text = match read_file_string_optional(profile_path) {
Some(value) => value
None => return Err("could not read build-profile.json5: " + profile_path)
}
let fragment_text = match read_file_string_optional(signing_config_file) {
Some(value) => value
None => return Err("could not read signing config: " + signing_config_file)
}
let stripped_profile = strip_json5(profile_text)
let stripped_fragment = strip_json5(fragment_text)
let profile_json = @json.parse(stripped_profile) catch {
_ => return Err("build-profile.json5 is not valid JSON after strip")
}
let fragment_json = @json.parse(stripped_fragment) catch {
_ => return Err("signing config fragment is not valid JSON after strip")
}
// Extract signingConfigs array from the fragment. Accept:
// - an array
// - { signingConfigs: [...] }
// - { material: { ... }, name?: ..., type?: ... }
// - a single material object with storeFile
let signing_configs : Array[Json] = []
match fragment_json {
Array(values) =>
for value in values {
signing_configs.push(value)
}
Object(fields) =>
match fields.get("signingConfigs") {
Some(Array(values)) =>
for value in values {
signing_configs.push(value)
}
_ =>
match fields.get("material") {
Some(_) => signing_configs.push(fragment_json)
None =>
match fields.get("storeFile") {
Some(_) =>
signing_configs.push({
"name": Json::string("default"),
"type": Json::string("HarmonyOS"),
"material": fragment_json,
})
None =>
return Err(
"MOUI_HARMONYOS_SIGNING_CONFIG must be a signingConfigs array, material object, or {signingConfigs:[...]}",
)
}
}
}
_ =>
return Err("MOUI_HARMONYOS_SIGNING_CONFIG must be a JSON array or object")
}
// Merge into profile.app.signingConfigs.
guard profile_json is Object(profile_root) else {
return Err("build-profile.json5 root must be an object")
}
let app = match profile_root.get("app") {
Some(Object(value)) => value
_ => {
let empty : Map[String, Json] = Map([])
empty
}
}
app["signingConfigs"] = Json::array(signing_configs)
profile_root["app"] = Json::object(app)
let updated = Json::object(profile_root).stringify(indent=2) + "\n"
match
write_file_bytes_result(profile_path, @utf8.encode(updated[:], bom=false)) {
Ok(_) => Ok(())
Err(message) => Err(message)
}
}
///|
/// Apply mobile app identity onto a staged window-hosted HarmonyOS template.
/// Updates bundleName, moduleName, app_name, module_desc, entry_desc and SDK
/// versions. Targets
/// the window-hosted template's file layout.
fn apply_harmonyos_window_hosted_identity(
project_root : String,
app : MobileApp,
_renderer : String,
) -> Result[Unit, String] {
let harmonyos = match app.harmonyos {
Some(value) => value
None => return Err("moui.mobile.json missing harmonyos section")
}
let bundle_name = harmonyos.bundle_name
let module_name = match harmonyos.module_name {
Some(value) => value
None => "entry"
}
let app_name = match harmonyos.app_name {
Some(value) => value
None => app.display_name
}
let module_description = match harmonyos.module_description {
Some(value) => value
None => app.display_name + " HarmonyOS app"
}
let entry_description = match harmonyos.entry_description {
Some(value) => value
None => app.display_name + " HarmonyOS entry ability"
}
// AppScope/app.json5 → app.bundleName + vendor + version.
let app_scope_path = join_path(project_root, "AppScope/app.json5")
let app_scope = match read_json5_object(app_scope_path) {
Ok(fields) => fields
Err(message) => return Err(message)
}
let app_obj = match app_scope.get("app") {
Some(Object(fields)) => fields
_ => return Err(app_scope_path + ": missing app object")
}
app_obj["bundleName"] = Json::string(bundle_name)
app_obj["vendor"] = Json::string("wzzc-dev")
app_obj["versionCode"] = Json::number(1.0)
app_obj["versionName"] = Json::string("0.1.0")
app_scope["app"] = Json::object(app_obj)
match write_json_file(app_scope_path, Json::object(app_scope)) {
Ok(_) => ()
Err(message) => return Err(message)
}
// Root oh-package.json5 name/description.
let root_pkg_path = join_path(project_root, "oh-package.json5")
let root_pkg = match read_json5_object(root_pkg_path) {
Ok(fields) => fields
Err(_) => {
// Tolerate missing file: write a minimal stub.
let fields : Map[String, Json] = Map([])
fields
}
}
root_pkg["name"] = Json::string(
app.id.replace_all(old="_", new="-") + "-harmonyos",
)
root_pkg["description"] = Json::string(
app.display_name + " window-hosted HarmonyOS app",
)
match write_json_file(root_pkg_path, Json::object(root_pkg)) {
Ok(_) => ()
Err(message) => return Err(message)
}
// build-profile.json5 module name + SDK versions.
let profile_path = join_path(project_root, "build-profile.json5")
let profile = match read_json5_object(profile_path) {
Ok(fields) => fields
Err(message) => return Err(message)
}
let modules = match profile.get("modules") {
Some(Array(values)) => values
_ => return Err(profile_path + ": missing modules array")
}
if modules.length() != 1 {
return Err(
profile_path + ": window-hosted template must contain one entry module",
)
}
let module0 = match modules[0] {
Object(fields) => fields
_ => return Err(profile_path + ": modules[0] must be an object")
}
module0["name"] = Json::string(module_name)
profile["modules"] = Json::array([Json::object(module0)])
let compatible_sdk = if harmonyos.compatible_sdk_version == 20 {
"6.0.0(20)"
} else {
"6.0.1(21)"
}
match profile.get("app") {
Some(Object(app_fields)) =>
match app_fields.get("products") {
Some(Array(products)) => {
let updated_products : Array[Json] = []
for product in products {
match product {
Object(pfields) => {
pfields["targetSdkVersion"] = Json::string("6.0.1(21)")
pfields["compatibleSdkVersion"] = Json::string(compatible_sdk)
updated_products.push(Json::object(pfields))
}
_ => updated_products.push(product)
}
}
app_fields["products"] = Json::array(updated_products)
profile["app"] = Json::object(app_fields)
}
_ => ()
}
_ => ()
}
match write_json_file(profile_path, Json::object(profile)) {
Ok(_) => ()
Err(message) => return Err(message)
}
// entry/src/main/module.json5 module.name
let module_path = join_path(project_root, "entry/src/main/module.json5")
let module_root = match read_json5_object(module_path) {
Ok(fields) => fields
Err(message) => return Err(message)
}
let module_obj = match module_root.get("module") {
Some(Object(fields)) => fields
_ => return Err(module_path + ": missing module object")
}
module_obj["name"] = Json::string(module_name)
module_root["module"] = Json::object(module_obj)
match write_json_file(module_path, Json::object(module_root)) {
Ok(_) => ()
Err(message) => return Err(message)
}
// entry/oh-package.json5
let entry_pkg_path = join_path(project_root, "entry/oh-package.json5")
let entry_pkg = match read_json5_object(entry_pkg_path) {
Ok(fields) => fields
Err(_) => {
let fields : Map[String, Json] = Map([])
fields
}
}
entry_pkg["name"] = Json::string(module_name)
entry_pkg["description"] = Json::string(
app.display_name + " window-hosted entry module",
)
match write_json_file(entry_pkg_path, Json::object(entry_pkg)) {
Ok(_) => ()
Err(message) => return Err(message)
}
// String resources (display name + descriptions).
match
update_json5_string_resource(
join_path(project_root, "AppScope/resources/base/element/string.json"),
"app_name",
app_name,
) {
Ok(_) => ()
Err(message) => return Err(message)
}
let entry_strings = join_path(
project_root, "entry/src/main/resources/base/element/string.json",
)
match update_json5_string_resource(entry_strings, "app_name", app_name) {
Ok(_) => ()
Err(message) => return Err(message)
}
match
update_json5_string_resource(
entry_strings, "module_desc", module_description,
) {
Ok(_) => ()
Err(message) => return Err(message)
}
match
update_json5_string_resource(entry_strings, "entry_desc", entry_description) {
Ok(_) => ()
Err(message) => return Err(message)
}
log_info(
"[moui-window-hosted-harmonyos] Applied identity bundleName=" +
bundle_name +
" moduleName=" +
module_name,
)
Ok(())
}
///|
/// Stage the window-hosted HarmonyOS template into `target_project` and
/// write the `.moui-window-hosted-stage` marker so future runs can replace
/// it safely.
fn stage_harmonyos_window_hosted_project(
template_root : String,
target_project : String,
) -> Result[Unit, String] {
let stage_marker = join_path(target_project, ".moui-window-hosted-stage")
if path_is_dir(target_project) && !path_is_file(stage_marker) {
return Err(
"refusing to replace an unowned HarmonyOS project: " + target_project,
)
}
remove_tree(target_project)
match mkdir_p(target_project) {
Ok(_) => ()
Err(message) => return Err(message)
}
match copy_tree(template_root, target_project) {
Ok(_) => ()
Err(message) => return Err(message)
}
match write_file_bytes_result(stage_marker, b"") {
Ok(_) => Ok(())
Err(message) => Err(message)
}
}
///|
/// Build the HarmonyOS HAP archive using the window-hosted template.
fn build_harmonyos(
options : BuildHarmonyosOptions,
host : HostPlatform,
) -> Result[BuildResult, String] {
let _ = host
// Make build_dir + workspace_root absolute so MBW_* env vars consumed by
// CMake (which runs with cwd=) resolve correctly regardless
// of the user's original cwd. hvigorw spawns CMake in a child cwd, so any
// relative path passed via env vars would fail `EXISTS` checks.
let workspace_root_abs = make_absolute(
options.workspace_root,
@env.get_env_var("PWD").unwrap_or("."),
)
let build_dir_abs = make_absolute(options.build_dir, workspace_root_abs)
// 2. Resolve the upstream window package root and template.
let window_root = window_package_root(workspace_root_abs)
let template_root = window_template_root(workspace_root_abs, "harmonyos")
if !path_is_file(
join_path(template_root, "entry/src/main/cpp/CMakeLists.txt"),
) ||
!path_is_file(join_path(template_root, "build-profile.json5")) ||
!path_is_file(join_path(template_root, "oh-package.json5")) {
return Err(
"MoUI window-hosted HarmonyOS template is incomplete: " + template_root,
)
}
// 3. Generate MoonBit C from the entrypoint package.
let entrypoint_package = match
mobile_entrypoint_package(
options.app.metadata_path,
"harmonyos",
workspace_root_abs,
) {
Ok(value) => value
Err(message) => return Err(message)
}
let moonbit_c = match
generate_moonbit_c(
workspace_root_abs,
entrypoint_package,
base_name(entrypoint_package) + ".c",
build_dir_abs,
) {
Ok(value) => value
Err(message) => return Err(message)
}
if !path_is_file(moonbit_c) {
return Err("MoonBit generated C was not found: " + moonbit_c)
}
// Resolve real Skia (same contract as Android/iOS). Stub-only builds present
// a black XComponent because moonbit_skia_available() returns 0.
let renderer_selected = if options.fallback_skia {
"skia-raster"
} else if options.renderer == "skia-raster" {
"skia-raster"
} else {
"skia-gpu"
}
// HarmonyOS auto/skia-gpu must use static: locked libskia.so hides Ganesh
// symbols required by libskia_ganesh_ext.a (see docs/examples.md).
let link_mode = @env.get_env_var("MOUI_SKIA_LINK_MODE").unwrap_or_else(() => {
if renderer_selected == "skia-gpu" {
"static"
} else {
"dynamic"
}
})
let gpu_env : Array[(String, String)] = if renderer_selected == "skia-gpu" {
[("MOUI_SKIA_ENABLE_GPU_EGL", "1")]
} else {
[]
}
let skia_root_abs = make_absolute(options.skia_root, workspace_root_abs)
let skia_link = match
resolve_skia(
skia_root_abs,
"harmonyos",
options.arch,
link_mode,
options.fallback_skia,
gpu_env,
) {
Ok(value) => value
Err(message) => return Err(message)
}
let native_dir = join_path(build_dir_abs, "native")
let skia_cxx_rsp = join_path(native_dir, "skia-cxx-flags.rsp")
let skia_link_rsp = join_path(native_dir, "skia-link-flags.rsp")
match write_rsp(skia_cxx_rsp, skia_link.stub_flags) {
Ok(_) => ()
Err(message) => return Err(message)
}
match write_rsp(skia_link_rsp, skia_link.link_flags) {
Ok(_) => ()
Err(message) => return Err(message)
}
// 4. Stage the window-hosted template into /hvigor-project.
let hvigor_project_dir = join_path(build_dir_abs, "hvigor-project")
match
stage_harmonyos_window_hosted_project(template_root, hvigor_project_dir) {
Ok(_) => ()
Err(message) => return Err(message)
}
match
apply_harmonyos_window_hosted_identity(
hvigor_project_dir,
options.app,
options.renderer,
) {
Ok(_) => ()
Err(message) => return Err(message)
}
match
inject_harmonyos_signing_config(
hvigor_project_dir,
options.signing_config_file.unwrap_or(""),
) {
Ok(_) => ()
Err(message) => return Err(message)
}
// Clean stale Hvigor state.
for
stale in [
".hvigor", "build", "entry/build", "oh_modules", "entry/oh_modules", "oh-package-lock.json5",
"entry/oh-package-lock.json5",
] {
remove_tree(join_path(hvigor_project_dir, stale))
}
if options.prepare_only {
log_info(
"[moui-window-hosted-harmonyos] Prepared " +
build_dir_abs +
" (moonbit_c=" +
moonbit_c +
")",
)
return Ok({ build_dir: build_dir_abs, output_artifact: "", })
}
// 5. Resolve SDK home + hvigorw + ohpm.
let requested_sdk_home = match options.sdk_home {
Some(value) => value
None =>
match @env.get_env_var("HARMONYOS_SDK_HOME") {
Some(value) => value
None => @env.get_env_var("OHOS_SDK_HOME").unwrap_or("")
}
}
let sdk_home = if requested_sdk_home != "" &&
find_ohos_toolchain(requested_sdk_home) != "" {
requested_sdk_home
} else {
let discovered = find_harmonyos_sdk_home()
if discovered != "" {
discovered
} else {
requested_sdk_home
}
}
if sdk_home == "" {
return Err(
"HARMONYOS_SDK_HOME or OHOS_SDK_HOME must point at a HarmonyOS/OpenHarmony SDK. Use --sdk-home to pass it explicitly.",
)
}
if find_ohos_toolchain(sdk_home) == "" {
return Err("HarmonyOS native CMake toolchain not found under " + sdk_home)
}
let hvigorw = match options.hvigorw {
Some(value) =>
if path_is_absolute(value) {
value
} else {
join_path(workspace_root_abs, value)
}
None =>
match @env.get_env_var("HVIGORW") {
Some(value) => value
None => find_deveco_hvigorw()
}
}
if hvigorw == "" || !path_is_executable(hvigorw) {
return Err(
"Hvigor wrapper not found. Pass --hvigorw or set HVIGORW for HarmonyOS builds.",
)
}
let ohpm = match options.ohpm {
Some(value) =>
if path_is_absolute(value) {
value
} else {
join_path(workspace_root_abs, value)
}
None =>
match @env.get_env_var("OHPM") {
Some(value) => value
None => find_deveco_ohpm()
}
}
if ohpm == "" || !path_is_executable(ohpm) {
return Err(
"ohpm executable not found. Pass --ohpm or set OHPM for HarmonyOS builds.",
)
}
let deveco_sdk_home = find_deveco_sdk_home(sdk_home)
if deveco_sdk_home == "" {
return Err(
"DevEco SDK root not found for " +
sdk_home +
". Set DEVECO_SDK_HOME or pass --sdk-home under DevEco Studio's sdk tree.",
)
}
// 6. ohpm install.
let ohpm_env = moon_build_env()
ohpm_env.push(("DEVECO_SDK_HOME", deveco_sdk_home))
ohpm_env.push(("HARMONYOS_SDK_HOME", sdk_home))
ohpm_env.push(("OHOS_SDK_HOME", sdk_home))
ohpm_env.push(("OHOS_BASE_SDK_HOME", sdk_home))
log_info(
"[moui-window-hosted-harmonyos] Installing project dependencies with ohpm",
)
match
process_exec_argv(
[ohpm, "install", "--all", "--no-link"],
cwd=hvigor_project_dir,
env=ohpm_env,
capture=false,
) {
Ok(result) if result.exit_code == 0 => ()
Ok(result) =>
return Err(
"ohpm install failed (exit " + result.exit_code.to_string() + ")",
)
Err(message) => return Err(message)
}
// 7. hvigorw assembleHap with MBW_* env vars consumed by the template's
// entry/src/main/cpp/CMakeLists.txt.
let moon_home = make_absolute(
@env.get_env_var("MOON_HOME").unwrap_or_else(() => {
let home = @env.get_env_var("HOME").unwrap_or("")
join_path(home, ".moon")
}),
workspace_root_abs,
)
let hvigor_env = moon_build_env()
hvigor_env.push(("DEVECO_SDK_HOME", deveco_sdk_home))
hvigor_env.push(("HARMONYOS_SDK_HOME", sdk_home))
hvigor_env.push(("OHOS_SDK_HOME", sdk_home))
hvigor_env.push(("OHOS_BASE_SDK_HOME", sdk_home))
hvigor_env.push(("MBW_WORKSPACE_ROOT", workspace_root_abs))
hvigor_env.push(("MBW_WINDOW_ROOT", window_root))
hvigor_env.push(("MBW_MOON_HOME", moon_home))
hvigor_env.push(("MBW_MOONBIT_C", moonbit_c))
hvigor_env.push(("MBW_SKIA_CXX_RSP", skia_cxx_rsp))
hvigor_env.push(("MBW_SKIA_LINK_RSP", skia_link_rsp))
// Do not force MOUI_SKIA_DISABLE_PREBUILD_SKIA here: CMake already consumes
// resolved rsp flags. Stripping disable is handled inside resolve_skia.
log_info(
"[moui-window-hosted-harmonyos] Building installable HAP with Hvigor (skia=" +
renderer_selected +
" link_mode=" +
link_mode +
")",
)
match
process_exec_argv(
[hvigorw, "--no-daemon", "assembleHap"],
cwd=hvigor_project_dir,
env=hvigor_env,
capture=false,
) {
Ok(result) if result.exit_code == 0 => ()
Ok(result) =>
return Err(
"hvigorw assembleHap failed (exit " + result.exit_code.to_string() + ")",
)
Err(message) => return Err(message)
}
// 8. Locate the produced HAP and copy it to options.output_hap.
let hap_search_dir = join_path(hvigor_project_dir, "entry/build")
let built_hap = find_first_file_with_suffix(hap_search_dir, "-signed.hap")
let built_hap = if built_hap != "" {
built_hap
} else {
find_first_file_with_suffix(hap_search_dir, ".hap")
}
if built_hap == "" {
return Err("Hvigor did not produce a HAP under " + hap_search_dir)
}
match mkdir_p(parent_dir(options.output_hap)) {
Ok(_) => ()
Err(message) => return Err(message)
}
match copy_file_atomic(built_hap, options.output_hap) {
Ok(_) => ()
Err(message) => return Err(message)
}
log_info(
"[moui-window-hosted-harmonyos] Wrote HAP archive " + options.output_hap,
)
log_info(
"This is build/install evidence only until a device or emulator smoke proves first frame, input, resize, lifecycle, and real Skia present.",
)
Ok({ build_dir: build_dir_abs, output_artifact: options.output_hap, })
}
///|
/// Walk `root` recursively looking for the first file whose name ends with
/// `suffix`. Returns an empty string when no match is found.
fn find_first_file_with_suffix(root : String, suffix : String) -> String {
let entries = @fs.read_dir(root) catch { _ => return "" }
for entry in entries {
let path = join_path(root, entry)
if path_is_dir(path) {
let found = find_first_file_with_suffix(path, suffix)
if found != "" {
return found
}
} else if path_is_file(path) && entry.has_suffix(suffix) {
return path
}
}
""
}