Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 15 additions & 15 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
name: Build HarmonyOS HAP
name: Build HarmonyOS APP

on:
push:
Expand Down Expand Up @@ -175,7 +175,7 @@ jobs:
fi
cat AppScope/app.json5

# ── Step 6: Build & sign the HAP ─────────────────────────────────────
# ── Step 6: Build & sign the APP ─────────────────────────────────────
- name: Build HarmonyOS app
run: ./scripts/build-app.sh --${{ github.event.inputs.build_mode || 'release' }}
env:
Expand All @@ -185,23 +185,23 @@ jobs:
KEY_PASSWORD: ${{ secrets.OHOS_KEY_PASSWORD }}
KEY_ALIAS: ${{ secrets.OHOS_KEY_ALIAS }}
# ── Step 7: Upload artifact ──────────────────────────────────────────
- name: Find HAP file
id: find_hap
- name: Find APP file
id: find_app
run: |
HAP_FILE=$(find entry/build -name "*.hap" -type f | head -1)
if [ -z "${HAP_FILE}" ]; then
echo "::error::No .hap file found!"
APP_FILE=$(find build/outputs -name "*.app" -type f | head -1)
if [ -z "${APP_FILE}" ]; then
echo "::error::No .app file found!"
exit 1
fi
echo "hap_path=${HAP_FILE}" >> $GITHUB_OUTPUT
echo "hap_name=$(basename ${HAP_FILE})" >> $GITHUB_OUTPUT
echo "Found HAP: ${HAP_FILE} ($(du -h ${HAP_FILE} | cut -f1))"
echo "app_path=${APP_FILE}" >> $GITHUB_OUTPUT
echo "app_name=$(basename ${APP_FILE})" >> $GITHUB_OUTPUT
echo "Found APP: ${APP_FILE} ($(du -h ${APP_FILE} | cut -f1))"

- name: Upload HAP artifact
- name: Upload APP artifact
uses: actions/upload-artifact@v4
with:
name: electerm-harmony-${{ github.sha }}
path: ${{ steps.find_hap.outputs.hap_path }}
path: ${{ steps.find_app.outputs.app_path }}
retention-days: 30

# ── Summary ──────────────────────────────────────────────────────────
Expand All @@ -217,7 +217,7 @@ jobs:
echo "| ohos-node | \`${{ env.OHOS_NODE_VERSION }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| electerm-web | \`${{ env.ELECTERM_WEB_REPO }}@${{ env.ELECTERM_WEB_REF }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| Build mode | \`${{ github.event.inputs.build_mode || 'release' }}\` |" >> $GITHUB_STEP_SUMMARY
if [ -f "${{ steps.find_hap.outputs.hap_path }}" ]; then
echo "| HAP file | \`${{ steps.find_hap.outputs.hap_name }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| HAP size | \`$(du -h ${{ steps.find_hap.outputs.hap_path }} | cut -f1)\` |" >> $GITHUB_STEP_SUMMARY
if [ -f "${{ steps.find_app.outputs.app_path }}" ]; then
echo "| APP file | \`${{ steps.find_app.outputs.app_name }}\` |" >> $GITHUB_STEP_SUMMARY
echo "| APP size | \`$(du -h ${{ steps.find_app.outputs.app_path }} | cut -f1)\` |" >> $GITHUB_STEP_SUMMARY
fi
10 changes: 5 additions & 5 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,20 +114,20 @@ GitHub Actions (ubuntu-latest, x64)
│ ├── OHOS_CERT_B64 → signing/electerm_publish.cer
│ └── OHOS_PROFILE_B64 → signing/electermRelease.p7b
├── 9. Configure bundle name (from OHOS_BUNDLE_NAME secret → app.json5)
├── 10. Build unsigned HAP (two-phase signing, phase 1)
├── 10. Build unsigned APP (two-phase signing, phase 1)
│ ├── Generate build-profile.json5 (empty signingConfigs)
│ ├── ohpm install
│ └── hvigorw assembleHap -p enableSignTask=false
├── 11. Sign HAP with hap-sign-tool.jar (two-phase signing, phase 2)
│ └── hvigorw assembleApp -p enableSignTask=false
├── 11. Sign APP with hap-sign-tool.jar (two-phase signing, phase 2)
│ └── java -jar hap-sign-tool.jar sign-app -mode localSign ...
└── 12. Upload .hap artifact (retained 30 days)
└── 12. Upload .app artifact (retained 30 days)
```

### Two-Phase Signing

This project does **not** use hvigor's built-in signer (which requires DevEco Studio's encrypted passwords). Instead:

1. **Build unsigned** — `hvigorw assembleHap` with `-p enableSignTask=false` and empty `signingConfigs`
1. **Build unsigned** — `hvigorw assembleApp` with `-p enableSignTask=false` and empty `signingConfigs`
2. **Sign separately** — `hap-sign-tool.jar` with plaintext passwords from GitHub Secrets

See [`BUILD.md §5`](./BUILD.md#5-how-signing-works) for details.
Expand Down
44 changes: 25 additions & 19 deletions docs/BUILD.md
Original file line number Diff line number Diff line change
Expand Up @@ -84,25 +84,31 @@ export KEY_ALIAS="electerm_key"

The script performs two phases:

1. **Build unsigned HAP** — generates `build-profile.json5` (with empty `signingConfigs`), runs `ohpm install`, then `hvigorw assembleHap` with `-p enableSignTask=false`.
1. **Build unsigned APP** — generates `build-profile.json5` (with empty `signingConfigs`), runs `ohpm install`, then `hvigorw assembleApp` with `-p enableSignTask=false`.

2. **Sign the HAP** — invokes `hap-sign-tool.jar` directly with plaintext passwords to produce the signed `.hap` file.
2. **Sign the APP** — invokes `hap-sign-tool.jar` directly with plaintext passwords to produce the signed `.app` file.

The signed HAP is at:
The signed APP is at:
```
entry/build/default/outputs/default/entry-default-unsigned.hap
build/outputs/default/electerm-harmony-default.app
```

> The output filename contains "unsigned" in its name, but the file is signed — the signed version replaces the unsigned one in place.
> The `.app` package is a ZIP containing the HAP(s) + `pack.info`. This is the format required by AppGallery Connect for uploading.

### Step 6 — Install on device
### Step 6 — Install on device or upload to AGC

**Upload to AppGallery Connect:**

Upload the `.app` file directly in the AGC console under your app's testing section.

**Install on device:**

```bash
# Connect your HarmonyOS device via USB
hdc list

# Install the HAP
hdc install entry/build/default/outputs/default/entry-default-unsigned.hap
# Install the APP
hdc install build/outputs/default/electerm-harmony-default.app
```

---
Expand Down Expand Up @@ -131,9 +137,9 @@ The workflow runs on:
8. Configure ohpm registry
9. Decode signing materials from GitHub Secrets → signing/
10. Configure bundle name from secret (injected into app.json5)
11. Build unsigned HAP (hvigorw assembleHap with enableSignTask=false)
12. Sign HAP with hap-sign-tool.jar (plaintext passwords from secrets)
13. Upload .hap as GitHub Actions artifact (retained 30 days)
11. Build unsigned APP (hvigorw assembleApp with enableSignTask=false)
12. Sign APP with hap-sign-tool.jar (plaintext passwords from secrets)
13. Upload .app as GitHub Actions artifact (retained 30 days)
14. Write build summary
```

Expand All @@ -156,7 +162,7 @@ gh workflow run build.yml --ref dev
gh workflow run build.yml --ref dev -f build_mode=debug
```

Or use the GitHub Actions UI: **Actions** tab → **Build HarmonyOS HAP** → **Run workflow**.
Or use the GitHub Actions UI: **Actions** tab → **Build HarmonyOS APP** → **Run workflow**.

---

Expand All @@ -168,7 +174,7 @@ The following scripts automate the build steps:
|--------|---------|
| [`scripts/prepare-node.sh`](../scripts/prepare-node.sh) | Downloads and extracts ohos-node prebuilt binary into `rawfile/node/` |
| [`scripts/prepare-web.sh`](../scripts/prepare-web.sh) | Clones, builds, and bundles electerm-web into `rawfile/electerm-web/` |
| [`scripts/build-app.sh`](../scripts/build-app.sh) | Builds unsigned HAP with hvigorw, then signs it with `hap-sign-tool.jar` |
| [`scripts/build-app.sh`](../scripts/build-app.sh) | Builds unsigned APP with hvigorw assembleApp, then signs it with `hap-sign-tool.jar` |
| [`scripts/gen-secrets.sh`](../scripts/gen-secrets.sh) | Generates GitHub Secrets values from `signing/` files and `temp/.env` |

Run them in order for a local build:
Expand All @@ -185,7 +191,7 @@ Run them in order for a local build:

This project uses a **two-phase signing approach** instead of the standard DevEco Studio workflow:

### Phase 1: Build unsigned HAP
### Phase 1: Build unsigned APP

`build-app.sh` generates a `build-profile.json5` with an **empty** `signingConfigs` array:

Expand All @@ -205,7 +211,7 @@ This project uses a **two-phase signing approach** instead of the standard DevEc
}
```

Then builds with `-p enableSignTask=false` to skip the hvigor plugin's built-in signer.
Then builds with `-p enableSignTask=false` to skip the hvigor plugin's built-in signer. The `hvigorw assembleApp` command produces a `.app` package (ZIP containing HAP + `pack.info`).

### Phase 2: Sign with hap-sign-tool.jar

Expand All @@ -218,11 +224,11 @@ java -jar hap-sign-tool.jar sign-app \
-keyPwd <KEY_PASSWORD> \
-appCertFile signing/electerm_publish.cer \
-profileFile signing/electermRelease.p7b \
-inFile entry-default-unsigned.hap \
-inFile electerm-harmony-default.app \
-signAlg SHA256withECDSA \
-keystoreFile signing/electerm.p12 \
-keystorePwd <KEYSTORE_PASSWORD> \
-outFile entry-default-signed.hap
-outFile electerm-harmony-default-signed.app
```

### Why not use hvigor's built-in signer?
Expand Down Expand Up @@ -310,12 +316,12 @@ fnm use 24

## 7. File Size Considerations

The final `.hap` package is approximately **230 MB** because it bundles:
The final `.app` package is approximately **230 MB** because it bundles:

- ohos-node binary (~50 MB)
- electerm-web server code + node_modules (~170 MB)

If the HAP needs to be smaller:
If the APP needs to be smaller:

- Strip the node binary: `strip entry/src/main/resources/rawfile/node/bin/node`
- Prune devDependencies from electerm-web: `npm prune --production`
Expand Down
71 changes: 41 additions & 30 deletions scripts/build-app.sh
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
#!/usr/bin/env bash
# build-app.sh — Build and sign the HarmonyOS HAP package.
# build-app.sh — Build and sign the HarmonyOS APP package.
#
# This script builds an UNSIGNED HAP using hvigorw, then signs it
# directly using hap-sign-tool.jar with plaintext passwords.
# This script builds an UNSIGNED .app using hvigorw assembleApp, then signs
# it directly using hap-sign-tool.jar with plaintext passwords.
# This bypasses the hvigor plugin's password encryption requirement
# (which needs DevEco Studio's encrypted passwords + material/ key dirs).
#
# The .app package is a ZIP containing the HAP(s) + pack.info, and is the
# format required by AppGallery Connect for uploading.
#
# Prerequisites:
# - HarmonyOS Command Line Tools installed (ohpm, hvigorw in PATH)
# - Signing materials in signing/ directory
Expand Down Expand Up @@ -121,7 +124,7 @@ echo " hvigorw: ${HVIGORW}"
echo " sign tool: ${SIGN_TOOL_JAR}"

# --- Generate build-profile.json5 (without signing config) ------------------
# We build an UNSIGNED HAP and sign it separately with hap-sign-tool.jar.
# We build an UNSIGNED APP and sign it separately with hap-sign-tool.jar.
# This avoids the hvigor plugin's password encryption requirement.

echo "==> Configuring build-profile.json5 ..."
Expand Down Expand Up @@ -216,43 +219,51 @@ echo "==> Installing ohpm dependencies ..."
cd "${PROJECT_ROOT}"
"${OHPM}" install

# --- Build the unsigned HAP -------------------------------------------------
# --- Build the unsigned APP -------------------------------------------------

echo "==> Building unsigned HAP (${BUILD_MODE}) ..."
echo "==> Building unsigned APP (${BUILD_MODE}) ..."

# Build with enableSignTask=false to skip the hvigor signing step.
# We sign separately using hap-sign-tool.jar with plaintext passwords.
# assembleApp produces a .app package (ZIP containing HAP + pack.info).
if [ "${BUILD_MODE}" = "debug" ]; then
"${HVIGORW}" assembleHap --mode module -p product=default \
"${HVIGORW}" assembleApp -p product=default \
-p buildMode=debug -p enableSignTask=false --no-daemon
else
"${HVIGORW}" assembleHap --mode module -p product=default \
"${HVIGORW}" assembleApp -p product=default \
-p buildMode=release -p enableSignTask=false --no-daemon
fi

# --- Locate the unsigned HAP ------------------------------------------------
# --- Locate the unsigned APP ------------------------------------------------

HAP_DIR="${PROJECT_ROOT}/entry/build/default/outputs/default"
UNSIGNED_HAP=$(find "${HAP_DIR}" -name "*.hap" -type f | head -1)
# assembleApp outputs to build/outputs/default/*.app (project root level)
APP_DIR="${PROJECT_ROOT}/build/outputs/default"
UNSIGNED_APP=$(find "${APP_DIR}" -name "*.app" -type f 2>/dev/null | head -1)

if [ -z "${UNSIGNED_HAP}" ]; then
echo " ✗ No .hap file found in ${HAP_DIR}"
exit 1
if [ -z "${UNSIGNED_APP}" ]; then
echo " ✗ No .app file found in ${APP_DIR}"
echo " Searching entire build tree ..."
UNSIGNED_APP=$(find "${PROJECT_ROOT}/build" -name "*.app" -type f 2>/dev/null | head -1)
if [ -z "${UNSIGNED_APP}" ]; then
echo " ✗ No .app file found anywhere in build/"
exit 1
fi
fi

echo " ✓ Unsigned HAP: ${UNSIGNED_HAP} ($(du -h "${UNSIGNED_HAP}" | cut -f1))"
echo " ✓ Unsigned APP: ${UNSIGNED_APP} ($(du -h "${UNSIGNED_APP}" | cut -f1))"

# --- Sign the HAP with hap-sign-tool.jar ------------------------------------
# --- Sign the APP with hap-sign-tool.jar ------------------------------------

echo "==> Signing HAP with hap-sign-tool.jar ..."
echo "==> Signing APP with hap-sign-tool.jar ..."

# Show Java version for debugging (PKCS12 keystore compatibility)
JAVA_VERSION=$(java -version 2>&1 | head -1)
echo " Java: ${JAVA_VERSION}"

SIGNED_HAP="${UNSIGNED_HAP%.hap}-signed.hap"
SIGNED_APP="${UNSIGNED_APP%.app}-signed.app"

# Sign the HAP.
# Sign the APP package.
# hap-sign-tool.jar can sign both .hap and .app files.
# NOTE: -mode localSign is required (COMMAND_ERROR code 101 if missing).
# NOTE: -compatibleVersion, -signCode, -pwdInputMode are NOT supported by
# this SDK version (COMMAND_PARAM_ERROR code 110). They were added later.
Expand All @@ -262,29 +273,29 @@ java -jar "${SIGN_TOOL_JAR}" sign-app \
-keyPwd "${KEY_PASSWORD}" \
-appCertFile "${CERT_PATH}" \
-profileFile "${PROFILE_PATH}" \
-inFile "${UNSIGNED_HAP}" \
-inFile "${UNSIGNED_APP}" \
-signAlg SHA256withECDSA \
-keystoreFile "${KEYSTORE_PATH}" \
-keystorePwd "${KEYSTORE_PASSWORD}" \
-outFile "${SIGNED_HAP}"
-outFile "${SIGNED_APP}"

if [ ! -f "${SIGNED_HAP}" ]; then
echo " ✗ Signing failed — no signed HAP produced"
if [ ! -f "${SIGNED_APP}" ]; then
echo " ✗ Signing failed — no signed APP produced"
exit 1
fi

# Replace the unsigned HAP with the signed one
mv -f "${SIGNED_HAP}" "${UNSIGNED_HAP}"
HAP_FILE="${UNSIGNED_HAP}"
# Replace the unsigned APP with the signed one
mv -f "${SIGNED_APP}" "${UNSIGNED_APP}"
APP_FILE="${UNSIGNED_APP}"

echo " ✓ Signed HAP: ${HAP_FILE} ($(du -h "${HAP_FILE}" | cut -f1))"
echo " ✓ Signed APP: ${APP_FILE} ($(du -h "${APP_FILE}" | cut -f1))"

# --- Done -------------------------------------------------------------------

echo ""
echo "==> Build complete!"
echo " Mode: ${BUILD_MODE}"
echo " HAP: ${HAP_FILE}"
echo " Size: $(du -h "${HAP_FILE}" | cut -f1)"
echo " APP: ${APP_FILE}"
echo " Size: $(du -h "${APP_FILE}" | cut -f1)"
echo ""
echo " Install with: hdc install \"${HAP_FILE}\""
echo " Upload to AppGallery Connect, or install with: hdc install \"${APP_FILE}\""
Loading