Files
mail/platform/ohos/scripts/build-hap.sh
T
springmin 180f61084c ohos: add a pinned PDFium source-build path
Add build-pdfium-source.sh for the OHOS PDFium port at a pinned source revision, using the local SDK through the port's ohos_sdk/18 path and staging a stripped libpdfium.so plus its license. Keep the checksum-pinned prebuilt as an offline fallback and package the source license when present.
2026-10-28 22:43:28 +08:00

392 lines
19 KiB
Bash
Executable File

#!/bin/sh
# Build an OHOS hap for this repo: Rust cdylib -> ArkTS shell abc -> staging -> sign.
#
# APP=demo (default) builds the Slint backend demo; APP=mail builds the
# flectar-mail shell (both share the ArkTS project; the XComponent page
# template selects the native library).
#
# Signing modes:
# default DevEco auto-signing material under $HOME/Documents/ohos/config
# (bound to this machine's UDID) through the ohos-workload
# sign-huawei.sh helper. The encrypted store password is read
# at run time from the DevEco project that generated the
# material; it is never stored in this repository.
# RELEASE_SIGN=1 AGC release material passed through the environment
# (RELEASE_P12, RELEASE_CERT, RELEASE_PROFILE, key alias and
# passwords) and applied by hap-sign-tool directly.
# SKIP_SIGN=1 Stop at the unsigned hap (build-only CI and pull-request
# checks, which must never touch signing material).
set -e
HERE=$(cd "$(dirname "$0")" && pwd)
PLATFORM_DIR=$(dirname "$HERE")
REPO_DIR=$(cd "$PLATFORM_DIR/../.." && pwd)
# shellcheck disable=SC1091
. "$HERE/env.sh"
log() { printf '[build-hap] %s\n' "$*"; }
die() { printf '[build-hap] ERROR: %s\n' "$*" >&2; exit 1; }
SDK="$OHOS_SDK_ROOT"
SHELL_DIR="$PLATFORM_DIR/shell"
BUILD_DIR="$PLATFORM_DIR/build"
STAGE="$BUILD_DIR/hap-stage"
# The hap carries the application version: versionName is the root crate
# version and versionCode follows the Android sequence (alpha < beta < rc <
# release < the next patch), so store uploads stay monotonic.
# VERSION_NAME/VERSION_CODE override.
APP_VERSION="$(sed -n '/^\[package\]$/,/^\[/s/^version = "\([^"]*\)"/\1/p' "$REPO_DIR/Cargo.toml" | head -n 1)"
[ -n "$APP_VERSION" ] || die "cannot read the application version from Cargo.toml"
VERSION_NAME="${VERSION_NAME:-$APP_VERSION}"
if [ -z "${VERSION_CODE:-}" ]; then
VERSION_CODE=$(python3 "$REPO_DIR/scripts/android-version-code.py" "$APP_VERSION") \
|| die "unsupported application version: $APP_VERSION"
fi
[ "$VERSION_CODE" -gt 0 ] && [ "$VERSION_CODE" -le 2147483647 ] || die "versionCode out of range: $VERSION_CODE"
log "version: $VERSION_NAME ($VERSION_CODE)"
# APP selects which native module the shell loads (page template + cdylib).
APP="${APP:-demo}"
case "$APP" in
demo) CRATE="slint-ohos-demo"; LIB="slint_ohos_demo" ;;
mail) CRATE="flectar-mail-ohos"; LIB="flectar_mail_ohos" ;;
*) die "APP must be demo or mail (got: $APP)" ;;
esac
# The demo must not install over the real mail app. Keep the mail bundle stable
# for store uploads and signing profiles; BUNDLE still overrides either value.
if [ "$APP" = demo ]; then
BUNDLE="${BUNDLE:-com.flectar.demo}"
else
BUNDLE="${BUNDLE:-com.flectar.mail}"
fi
log "bundle: $BUNDLE"
UNSIGNED="$BUILD_DIR/$LIB-unsigned.hap"
SIGNED="$BUILD_DIR/$LIB-signed.hap"
NODE_BIN="$(command -v node || true)"
[ -n "$NODE_BIN" ] || NODE_BIN="$HOME/.harmonybrew/bin/node"
HVIGOR_JS="${HVIGOR_JS:-$HOME/arkts-build/node_modules/@ohos/hvigor/bin/hvigor.js}"
JAVA_HOME="${JAVA_HOME:-$HOME/.harmonybrew/opt/openjdk@17}"
DEVECO_PROJECT_PROFILE="$HOME/Documents/DevEcoStudioProjects/MyApplication/build-profile.json5"
SIGN_CFG="${SIGN_CFG:-$HOME/Documents/ohos/config}"
SIGN_HELPER="${SIGN_HELPER:-$HOME/springsources/ohos-workload/scripts/sign-huawei.sh}"
HVIGOR_LOG="$BUILD_DIR/hvigor.log"
[ -f "$HVIGOR_JS" ] || die "hvigor not found: $HVIGOR_JS"
[ -d "$JAVA_HOME" ] || die "JAVA_HOME not found: $JAVA_HOME"
if [ "${SKIP_SIGN:-0}" != "1" ] && [ "${RELEASE_SIGN:-0}" != "1" ]; then
[ -f "$SIGN_HELPER" ] || die "sign helper not found: $SIGN_HELPER"
fi
mkdir -p "$BUILD_DIR"
# 0. hvigor prerequisites ----------------------------------------------------
# hvigor resolves SDK components through a version-nested root referenced from
# local.properties (`sdk.dir=<root>/<platform-version>/<component>`). Both are
# generated here because they are machine-specific.
SDK_ROOT="$BUILD_DIR/sdk"
PLATFORM_VERSION="26.0.0"
if [ ! -e "$SDK_ROOT/$PLATFORM_VERSION/ets" ]; then
rm -rf "$SDK_ROOT"
mkdir -p "$SDK_ROOT/$PLATFORM_VERSION"
for component in ets js native previewer toolchains; do
[ -e "$SDK/$component" ] && ln -s "$SDK/$component" "$SDK_ROOT/$PLATFORM_VERSION/$component"
done
fi
printf 'sdk.dir=%s\nnodejs.dir=%s\n' "$SDK_ROOT" "$(dirname "$(dirname "$NODE_BIN")")" \
> "$SHELL_DIR/local.properties"
# 1. Rust cdylib -------------------------------------------------------------
# PROFILE=debug builds faster for first bring-up; release is the default.
PROFILE="${PROFILE:-release}"
if [ "$PROFILE" = debug ]; then
CARGO_PROFILE_ARG=""
SO_DIR="debug"
else
CARGO_PROFILE_ARG="--release"
SO_DIR="release"
fi
log "cargo build $PROFILE -p $CRATE"
( cd "$PLATFORM_DIR" && CARGO_BUILD_JOBS="${CARGO_BUILD_JOBS:-3}" cargo build $CARGO_PROFILE_ARG -p "$CRATE" )
SO="$REPO_DIR/target/$SO_DIR/lib$LIB.so"
[ -f "$SO" ] || die "cdylib not found: $SO"
# 2. ArkTS shell -> modules.abc ----------------------------------------------
# hvigor's own PackageHap step needs the packing tool jar the SDK mirror lacks;
# the ArkTS output below is what matters (same approach as the workload's
# build-arkts-shell.sh, which tolerates that failure).
# NODE_PATH lets the generated hvigorfile.ts resolve `@ohos/hvigor-ohos-plugin`
# from the shared hvigor install (this project is not nested under its
# node_modules directory).
# The XComponent page pins the `libraryname`; pick the template for this APP.
cp "$SHELL_DIR/page-templates/Index.$APP.ets" "$SHELL_DIR/entry/src/main/ets/pages/Index.ets"
# Drop the previous ArkTS build outputs: hvigor's compiled modules.abc must be
# produced by this run, or a failed compile would silently package the previous
# APP's abc (its XComponent libraryname would load the wrong native module).
rm -rf "$SHELL_DIR/entry/build"
# Align AppScope/app.json5 with the selected bundle for this build: hvigor
# generates the ArkTS BuildProfile from it, so a BUNDLE override must reach
# this file as well as the final module.json.
APP_JSON5="$SHELL_DIR/AppScope/app.json5"
APP_JSON5_BAK="$BUILD_DIR/app.json5.bak"
cp "$APP_JSON5" "$APP_JSON5_BAK"
restore_app_json5() { mv "$APP_JSON5_BAK" "$APP_JSON5" 2>/dev/null || true; }
trap restore_app_json5 EXIT INT TERM
sed -i "s/bundleName: '[^']*'/bundleName: '$BUNDLE'/" "$APP_JSON5"
sed -i "s/versionCode: [0-9]*/versionCode: $VERSION_CODE/" "$APP_JSON5"
sed -i "s/versionName: '[^']*'/versionName: '$VERSION_NAME'/" "$APP_JSON5"
log "hvigor assembleHap (compile step; PackageHap failure is tolerated)"
( cd "$SHELL_DIR" && \
env -i PATH="$(dirname "$NODE_BIN"):$JAVA_HOME/bin:/usr/bin:/bin" HOME="$HOME" \
NODE_PATH="$HOME/arkts-build/node_modules" \
JAVA_HOME="$JAVA_HOME" \
OHOS_BASE_SDK_HOME="$SDK" DEVECO_SDK_HOME="$SDK" \
externalApiPaths="$SDK/ets/api:$SDK/ets/kits:$SDK/ets/arkts" \
"$NODE_BIN" "$HVIGOR_JS" \
assembleHap -m module -p module=entry@default -p product=default -p buildMode="$PROFILE" \
> "$HVIGOR_LOG" 2>&1 ) || true
ABC="$(find "$SHELL_DIR/entry/build" -name modules.abc -path '*loader_out*' 2>/dev/null | head -1)"
[ -n "$ABC" ] || die "modules.abc not produced (see $HVIGOR_LOG)"
if grep -q 'Failed :entry:default@CompileArkTS' "$HVIGOR_LOG"; then
tail -20 "$HVIGOR_LOG" | sed 's/^/ /'
die "ArkTS compilation failed (see $HVIGOR_LOG)"
fi
log "modules.abc: $ABC"
# 3. hap staging -------------------------------------------------------------
log "staging hap layout"
rm -rf "$STAGE"
mkdir -p "$STAGE/ets" "$STAGE/libs/arm64-v8a"
cp "$ABC" "$STAGE/ets/modules.abc"
cp "$SO" "$STAGE/libs/arm64-v8a/"
# Prebuilt native libraries (libpdfium.so, fetching: scripts/fetch-pdfium.sh).
# The hap signing (signCode 1) covers every libs/** entry, so they ride along.
if [ -d "$PLATFORM_DIR/prebuilt/arm64" ]; then
for lib in "$PLATFORM_DIR"/prebuilt/arm64/*.so; do
[ -e "$lib" ] && cp "$lib" "$STAGE/libs/arm64-v8a/"
done
# The OHOS PDFium archive is distributed unstripped. Strip the staged
# copy for release haps; the source archive stays untouched so its pinned
# checksum remains meaningful.
if [ "$PROFILE" = release ] && [ -x "$SDK/native/llvm/bin/llvm-strip" ]; then
for lib in "$STAGE"/libs/arm64-v8a/*.so; do
[ -e "$lib" ] && "$SDK/native/llvm/bin/llvm-strip" --strip-unneeded "$lib"
done
fi
fi
MERGED_JSON="$(find "$SHELL_DIR/entry/build" -name module.json -path '*merge_profile*' 2>/dev/null | head -1)"
if [ -n "$MERGED_JSON" ]; then
cp "$MERGED_JSON" "$STAGE/module.json"
else
die "merged module.json not found under hvigor build output"
fi
# The module.json field injections happen after the resource compile below,
# because they need the compiler's resolved resource ids (id table output).
# hvigor's own CompileResource feeds restool a config carrying the
# `compression` option; that path emits a legacy "Restool" resources.index
# which this device's resource manager cannot load — every $string/$media
# reference (ability label, app icon, colors) silently resolves to nothing and
# the launcher shows the bundle name with a blank icon. Re-compiling from the
# same config without `compression` produces the "RestoolV2" index that
# DevEco-built haps carry and the device loads.
RES_CFG="$(find "$SHELL_DIR/entry/build" -name resConfig.json -path '*intermediates/res/*' 2>/dev/null | head -1)"
[ -n "$RES_CFG" ] || die "hvigor resConfig.json not found under the build output"
RES_OUT="$BUILD_DIR/res-out"
rm -rf "$RES_OUT"
mkdir -p "$RES_OUT"
python3 - "$RES_CFG" "$RES_OUT" <<'PY'
import json, sys
config = json.load(open(sys.argv[1]))
out = sys.argv[2]
config.pop("compression", None)
config["output"] = out
config["ResourceTable"] = [f"{out}/ResourceTable.h", f"{out}/ResourceTable.ts"]
# The index format follows the manifest's SDK coordinates: an OpenHarmony
# target makes restool emit the legacy format this device cannot load. Patch
# the manifest restool validates against to the device's HarmonyOS values.
manifest = json.load(open(config["configPath"]))
app = manifest["app"]
app["compileSdkVersion"] = "6.1.0.105"
app["compileSdkType"] = "HarmonyOS"
app["targetAPIVersion"] = 60100023
app["minAPIVersion"] = 60100023
app["apiReleaseType"] = "Release"
with open(f"{out}/module.json", "w") as f:
json.dump(manifest, f)
config["configPath"] = f"{out}/module.json"
# `ids`/`definedIds` keep the paths hvigor generated: restool reads the
# existing id table there and writes it back.
with open(f"{out}/resConfig.json", "w") as f:
json.dump(config, f)
PY
log "compiling resources (restool, V2 index)"
"$SDK/toolchains/restool" -l "$RES_OUT/resConfig.json" > "$BUILD_DIR/restool.log" 2>&1 || {
tail -5 "$BUILD_DIR/restool.log" | sed 's/^/ /'
die "restool failed (see $BUILD_DIR/restool.log)"
}
[ -f "$RES_OUT/resources.index" ] || die "restool produced no resources.index"
head -c 10 "$RES_OUT/resources.index" | grep -q "RestoolV2" \
|| die "restool produced a legacy resources.index (the device cannot load it)"
cp "$RES_OUT/resources.index" "$STAGE/resources.index"
rm -rf "$STAGE/resources"
cp -r "$RES_OUT/resources" "$STAGE/resources"
# Ship the notices and the PDFium license inside the hap. `resources/rawfile`
# is the standard location for arbitrary packaged files and is included by the
# zip step below.
mkdir -p "$STAGE/resources/rawfile/LICENSES"
cp "$REPO_DIR/THIRD_PARTY_NOTICES.md" "$STAGE/resources/rawfile/"
cp "$REPO_DIR"/LICENSES/*.txt "$STAGE/resources/rawfile/LICENSES/"
if [ -f "$PLATFORM_DIR/prebuilt/PDFium-LICENSE" ]; then
cp "$PLATFORM_DIR/prebuilt/PDFium-LICENSE" \
"$STAGE/resources/rawfile/LICENSES/PDFium-source-LICENSE.txt"
fi
cp "$PLATFORM_DIR/prebuilt/README.md" "$STAGE/resources/rawfile/pdfium-README.md"
# 3c. module.json injections -------------------------------------------------
# The device loads the ability from `ets/modules.abc` only when the manifest
# selects the panda VM (`virtualMachine`); without it the loader falls back to
# a per-file `ets/entryability/EntryAbility.abc` probe that fails for
# modules.abc-only haps. hvigor does not inject it; the workload's own
# packaging does (templates/module.json.template).
# deviceTypes: 2in1/tablet/phone (not 'default') — the desktop otherwise runs
# the window in phone-compatibility mode at ~0.59x and fonts look tiny.
# requestPermissions: without ohos.permission.INTERNET every socket operation
# (loopback OAuth listener, IMAP/SMTP) fails with EPERM, which surfaces as
# "OAuth failed: io error: Operation not permitted (os error 1)".
# READ_PASTEBOARD is declared by the module: pasting needs it, but the hap
# only installs with an ACL-enabled profile that lists the permission
# (otherwise the installer reports "grant request permissions failed").
# Copying needs no permission.
# SDK coordinates: the resource compiler only emits the V2 `resources.index`
# the device can load for HarmonyOS targets; DevEco-built haps carry these
# exact values.
# Resolved ids: hvigor's PackageHap step (unavailable here; it needs the
# packing-tool jar) normally resolves `$string:`/`$media:` references into the
# numeric `labelId`/`iconId`/... fields. Without them the launcher falls back
# to the bundle name with a blank icon, and the runtime lookup of the app's own
# resources fails ("Invalid resource ID ... not found in loaded hap/hsp
# packages"). Inject the same fields from the compiler's id table.
IDS_JSON="$(find "$SHELL_DIR/entry/build" -path '*ids_map/id_defined.json' 2>/dev/null | head -1)"
[ -n "$IDS_JSON" ] || die "id_defined.json not found under the hvigor build output"
python3 - "$STAGE/module.json" "$IDS_JSON" "$BUNDLE" <<'PY'
import json, sys
path, ids_path, bundle_name = sys.argv[1], sys.argv[2], sys.argv[3]
with open(ids_path) as f:
records = json.load(f)["record"]
ids = {f"{record['type']}:{record['name']}": int(record["id"], 16) for record in records}
with open(path) as f:
manifest = json.load(f)
module = manifest["module"]
module["compileMode"] = "esmodule"
module["virtualMachine"] = "ark13.0.1.0"
module["deviceTypes"] = ["2in1", "tablet", "phone"]
module.setdefault("libIsolation", True)
permissions = module.setdefault("requestPermissions", [])
if not any(entry.get("name") == "ohos.permission.INTERNET" for entry in permissions):
permissions.append({"name": "ohos.permission.INTERNET"})
# The packing tool resolves `$string:` reasons through the resource table; the
# permission dialog needs the numeric reasonId.
for entry in permissions:
reason = entry.get("reason")
if isinstance(reason, str) and reason.startswith("$string:"):
key = "string:" + reason[len("$string:"):]
if key in ids:
entry["reasonId"] = ids[key]
app = manifest["app"]
app["bundleName"] = bundle_name
app["compileSdkVersion"] = "6.1.0.105"
app["compileSdkType"] = "HarmonyOS"
app["targetAPIVersion"] = 60100023
app["minAPIVersion"] = 60100023
app["apiReleaseType"] = "Release"
def put(target, field, key):
if key in ids:
target[field] = ids[key]
put(app, "labelId", "string:app_name")
put(app, "iconId", "media:app_icon")
put(module, "descriptionId", "string:module_desc")
for ability in module.get("abilities", []):
put(ability, "descriptionId", "string:EntryAbility_desc")
put(ability, "labelId", "string:EntryAbility_label")
put(ability, "iconId", "media:app_icon")
put(ability, "startWindowIconId", "media:app_icon")
put(ability, "startWindowBackgroundId", "color:start_window_background")
with open(path, "w") as f:
json.dump(manifest, f, indent="\t")
PY
# 4. zip into an unsigned hap -----------------------------------------------
# -D: no directory entries. BMS's GetNativeLibraryFileNames counts every
# `libs/<abi>/...` zip entry - including the `libs/<abi>/` directory itself -
# as a native library, and the install then fails signature enforcement when
# the SoInfo segment has one entry fewer than that count.
# resources.index and resources/** are STORED (like DevEco-packed haps): the
# device's resource manager reads the index straight out of the installed hap
# and cannot load it from a deflated entry — the app then has no resources at
# all (raw "$string:" labels, default launcher icon, 9001001 lookups).
log "packing unsigned hap"
rm -f "$UNSIGNED" "$SIGNED"
python3 - "$UNSIGNED" "$STAGE" <<'PY'
import os, sys, zipfile
out, stage = sys.argv[1], sys.argv[2]
with zipfile.ZipFile(out, "w", zipfile.ZIP_DEFLATED) as archive:
for root, _dirs, files in os.walk(stage):
for name in files:
path = os.path.join(root, name)
entry = os.path.relpath(path, stage)
stored = entry == "resources.index" or entry.startswith("resources/")
archive.write(
path,
entry,
compress_type=zipfile.ZIP_STORED if stored else zipfile.ZIP_DEFLATED,
)
PY
log "unsigned: $UNSIGNED ($(stat -c%s "$UNSIGNED") bytes)"
# 5. sign --------------------------------------------------------------------
# SKIP_SIGN=1 stops at the unsigned hap (build-only CI; pull request checks
# never touch signing material). Default: the DevEco debug material (bound to
# this machine's UDID) through the ohos-workload helper. RELEASE_SIGN=1
# switches to the AGC release material supplied through the environment:
# RELEASE_P12 (keystore) RELEASE_CERT (app certificate) RELEASE_PROFILE (signed .p7b)
# RELEASE_KEY_ALIAS RELEASE_KEY_PWD RELEASE_STORE_PWD
if [ "${SKIP_SIGN:-0}" = "1" ]; then
log "SKIP_SIGN=1: keeping the unsigned hap"
SIGNED="$UNSIGNED"
elif [ "${RELEASE_SIGN:-0}" = "1" ]; then
: "${RELEASE_P12:?RELEASE_SIGN=1 needs RELEASE_P12 (AGC keystore .p12)}"
: "${RELEASE_CERT:?RELEASE_SIGN=1 needs RELEASE_CERT (AGC app certificate)}"
: "${RELEASE_PROFILE:?RELEASE_SIGN=1 needs RELEASE_PROFILE (signed .p7b profile)}"
: "${RELEASE_KEY_ALIAS:?RELEASE_SIGN=1 needs RELEASE_KEY_ALIAS}"
: "${RELEASE_KEY_PWD:?RELEASE_SIGN=1 needs RELEASE_KEY_PWD}"
: "${RELEASE_STORE_PWD:?RELEASE_SIGN=1 needs RELEASE_STORE_PWD}"
log "signing with the AGC release material (bundle $BUNDLE)"
"$HAP_SIGN_TOOL" sign-app -keyAlias "$RELEASE_KEY_ALIAS" -signAlg SHA256withECDSA \
-mode localSign -signCode 1 \
-appCertFile "$RELEASE_CERT" -profileFile "$RELEASE_PROFILE" \
-inFile "$UNSIGNED" -outFile "$SIGNED" \
-keystoreFile "$RELEASE_P12" \
-keyPwd "$RELEASE_KEY_PWD" -keystorePwd "$RELEASE_STORE_PWD" >/dev/null \
|| die "release signing failed"
"$HAP_SIGN_TOOL" verify-app -inFile "$SIGNED" \
-outCertChain "$BUILD_DIR/.verify-cert.cer" -outProfile "$BUILD_DIR/.verify-profile.p7b" \
2>&1 | grep -q "verify-app success" || die "release signature verification failed"
rm -f "$BUILD_DIR/.verify-cert.cer" "$BUILD_DIR/.verify-profile.p7b"
else
ENC_PWD="$(sed -n 's/.*"storePassword"[ \t]*:[ \t]*"\([0-9A-Fa-f]\{32,\}\)".*/\1/p' "$DEVECO_PROJECT_PROFILE" | head -1)"
[ -n "$ENC_PWD" ] || die "cannot read the encrypted storePassword from $DEVECO_PROJECT_PROFILE"
log "signing with the Huawei debug material (bundle $BUNDLE)"
OHOS_ENC_PWD="$ENC_PWD" sh "$SIGN_HELPER" "$UNSIGNED" "$SIGNED" "$SIGN_CFG" \
|| die "signing failed"
fi
log "hap: $SIGNED"