[feat] add Android build and release workflow

This commit is contained in:
dijunkun
2026-10-01 23:27:57 +08:00
parent b05a84198a
commit b2f6bd3204
5 changed files with 202 additions and 1 deletions
+170
View File
@@ -0,0 +1,170 @@
name: Build Android
on:
workflow_dispatch:
workflow_call:
outputs:
version:
description: "Independent Android version and build date."
value: ${{ jobs.build.outputs.version }}
pull_request:
paths:
- ".github/workflows/build-android.yml"
- ".github/workflows/build.yml"
- ".gitmodules"
- "LICENSE"
- "apps/android/**"
- "apps/ios/CrossDeskMobile/Resources/ThirdPartyLicenses.json"
- "apps/ios/licenses/**"
- "apps/ios/scripts/generate_third_party_licenses.py"
- "deps/submodules/minirtc"
- "libs/wire/**"
permissions:
contents: read
jobs:
build:
name: Build on Android (arm64)
runs-on: ubuntu-24.04
timeout-minutes: 120
outputs:
version: ${{ steps.package.outputs.version }}
env:
XMAKE_VERSION: "3.1.1"
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
fetch-depth: 0
submodules: recursive
persist-credentials: false
- name: Set up Java
uses: actions/setup-java@v5
with:
distribution: temurin
java-version: "17"
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.13"
- name: Install native build tools
run: |
sudo apt-get update
sudo apt-get install -y --no-install-recommends \
build-essential cmake ninja-build pkg-config autoconf automake libtool nasm
python3 -m pip install setuptools wheel
- name: Set up Android SDK
uses: android-actions/setup-android@v3
with:
packages: "platform-tools platforms;android-36 build-tools;36.0.0 ndk;28.2.13676358"
- name: Install xmake
uses: xmake-io/github-action-setup-xmake@v1
with:
xmake-version: ${{ env.XMAKE_VERSION }}
# Cache installed packages only. build_native.py checks Xmake 3.1.1 and pins
# its own package repository; never reuse another platform's build/config.
- name: Cache native dependencies
uses: actions/cache@v5
with:
path: apps/android/.native/packages
key: ${{ runner.os }}-${{ runner.arch }}-android-arm64-native-v1-${{ hashFiles('apps/android/scripts/build_native.py', 'apps/android/native/xmake.lua', 'deps/submodules/minirtc/xmake.lua', 'deps/submodules/minirtc/thirdparty/**') }}
- name: Set up Gradle
uses: gradle/actions/setup-gradle@v6
with:
cache-provider: basic
cache-read-only: ${{ github.event_name == 'pull_request' }}
- name: Verify shared license catalog
run: python3 -B apps/ios/scripts/generate_third_party_licenses.py
- name: Build APKs and run Lint
working-directory: apps/android
run: |
./gradlew --no-daemon --stacktrace \
:app:assembleDebug :app:assembleRelease :app:assembleDebugAndroidTest \
:app:lintDebug :app:lintRelease
- name: Verify APKs and native protocol
working-directory: apps/android
run: |
python3 -B scripts/verify_apk.py app/build/outputs/apk/debug/app-debug.apk
python3 -B scripts/verify_apk.py app/build/outputs/apk/release/app-release-unsigned.apk
python3 -B scripts/test_protocol.py
"${ANDROID_HOME}/build-tools/36.0.0/apksigner" verify app/build/outputs/apk/debug/app-debug.apk
"${ANDROID_HOME}/build-tools/36.0.0/apksigner" verify app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk
- name: Package APKs with the independent Android version
id: package
shell: python
run: |
import datetime
import json
import os
from pathlib import Path
import re
import shutil
from zoneinfo import ZoneInfo
apk_dir = Path("apps/android/app/build/outputs/apk")
metadata = json.loads((apk_dir / "release/output-metadata.json").read_text())
version = metadata["elements"][0]["versionName"]
if not re.fullmatch(r"[0-9]+\.[0-9]+\.[0-9]+", version):
raise ValueError(f"Invalid Android version: {version!r}")
build_date = datetime.datetime.now(ZoneInfo("Asia/Shanghai")).strftime("%Y%m%d")
# Match the dated release tags used by the desktop and iOS jobs.
if os.environ["GITHUB_REF_TYPE"] == "tag":
tag = os.environ["GITHUB_REF_NAME"]
match = re.fullmatch(r"v?[0-9]+(?:\.[0-9]+){1,3}-(?:[0-9]+-)?([0-9]{8})(?:-[0-9]+)?", tag)
if match:
build_date = match[1]
datetime.datetime.strptime(build_date, "%Y%m%d")
version = f"v{version}-{build_date}"
output = Path(os.environ["RUNNER_TEMP"]) / "crossdesk-android"
for variant, source in (
("unsigned", "release/app-release-unsigned.apk"),
("debug", "debug/app-debug.apk"),
("debug", "androidTest/debug/app-debug-androidTest.apk"),
):
destination = output / variant
destination.mkdir(parents=True, exist_ok=True)
suffix = "-test" if "androidTest/" in source else ""
shutil.copyfile(apk_dir / source, destination / f"crossdesk-android-arm64-{variant}-{version}{suffix}.apk")
with open(os.environ["GITHUB_OUTPUT"], "a") as stream:
stream.write(f"version={version}\n")
with open(os.environ["GITHUB_STEP_SUMMARY"], "a") as stream:
stream.write(f"Android {version}: arm64, API 26+, 16 KB aligned.\n\n"
"Debug APK uses an ephemeral CI debug key; it is for testing and may require uninstalling an existing build. "
"The Release APK is unsigned and needs your own signing before installation.\n\n"
"Lint, native protocol/frame tests and APK checks passed. Device tests were compiled, not executed.\n")
- name: Upload unsigned Release APK
uses: actions/upload-artifact@v6
with:
name: crossdesk-android-arm64-unsigned-${{ steps.package.outputs.version }}
path: ${{ runner.temp }}/crossdesk-android/unsigned/*.apk
if-no-files-found: error
- name: Upload Debug and device test APKs
uses: actions/upload-artifact@v6
with:
name: crossdesk-android-arm64-debug-${{ steps.package.outputs.version }}
path: ${{ runner.temp }}/crossdesk-android/debug/*.apk
if-no-files-found: error
retention-days: 14
- name: Upload Lint reports
if: ${{ !cancelled() }}
uses: actions/upload-artifact@v6
with:
name: crossdesk-android-reports
path: apps/android/app/build/reports/
if-no-files-found: ignore
retention-days: 14
+12 -1
View File
@@ -531,6 +531,13 @@ jobs:
path: ${{ env.PACKAGE_FILE }}
if-no-files-found: error
# Android (also available as a standalone workflow)
build-android:
name: Build Android
uses: ./.github/workflows/build-android.yml
permissions:
contents: read
# Windows
build-windows-x64:
name: Build on Windows x64
@@ -761,7 +768,7 @@ jobs:
release:
name: Publish Release
if: startsWith(github.ref, 'refs/tags/v')
needs: [build-linux, build-macos, build-ios, build-windows-x64]
needs: [build-linux, build-macos, build-ios, build-android, build-windows-x64]
runs-on: ubuntu-latest
steps:
@@ -826,6 +833,9 @@ jobs:
Clone that version with recursive submodules. The repository includes the build scripts and dependency patches.
[iOS source, modification and rebuild instructions](https://github.com/${GITHUB_REPOSITORY}/blob/${GITHUB_SHA}/apps/ios/README.md).
The iOS archive also contains the exact source revision and third-party source catalog with archive checksums.
[Android source and rebuild instructions](https://github.com/${GITHUB_REPOSITORY}/blob/${GITHUB_SHA}/apps/android/README.md).
The Android arm64 Release APK is unsigned; sign it with your own key before installation.
It includes the exact application and MiniRTC revisions, license documents and native build recipes.
## Code signing policy
@@ -840,6 +850,7 @@ jobs:
cp artifacts/crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-linux-amd64-${{ steps.version.outputs.VERSION_WITH_V }}.deb
cp artifacts/crossdesk-linux-arm64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-linux-arm64-${{ steps.version.outputs.VERSION_WITH_V }}.deb
cp artifacts/crossdesk-ios-arm64-unsigned-${{ needs.build-ios.outputs.version }}/* release/crossdesk-ios-arm64-unsigned-${{ needs.build-ios.outputs.version }}.zip
cp artifacts/crossdesk-android-arm64-unsigned-${{ needs.build-android.outputs.version }}/*.apk release/crossdesk-android-arm64-unsigned-${{ needs.build-android.outputs.version }}.apk
cp artifacts/crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-win-x64-${{ steps.version.outputs.VERSION_WITH_V }}.exe
cp artifacts/crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}/* release/crossdesk-win-x64-portable-${{ steps.version.outputs.VERSION_WITH_V }}.zip
+16
View File
@@ -71,6 +71,22 @@ Release 构建使用 `./gradlew :app:assembleRelease`,默认输出未签名 AP
修改原生代码后重新运行 Gradle 即可;修改依赖版本或配方时同步检查许可证内容,必要时移除 `.native/packages` 重新构建。MiniRTC 的源码和依赖配方变更直接保存在子模块中;提交时先提交 MiniRTC,再更新主仓库的子模块引用。
## GitHub Actions
[`Build Android`](../../.github/workflows/build-android.yml) 可在 Actions 页面单独手动运行;修改 Android、MiniRTC、共享协议或许可证资料的 Pull Request 也会触发。仓库原有的 [`Build and Release`](../../.github/workflows/build.yml) 在分支推送、标签推送及手动运行时调用同一工作流,`v` 标签发布会等待安卓构建成功,并将未签名 Release APK 加入 GitHub Release 和现有下载服务器的产物集合。推送主仓库前,先确保它引用的 MiniRTC 提交已推送到子模块远程仓库,否则 CI 无法完成检出。
工作流使用 Ubuntu 24.04、JDK 17、Python 3.13、SDK 36、Build Tools 36.0.0、NDK r28c 和 Xmake 3.1.1。Gradle 使用工程内的 Wrapper,原生构建器固定 Xmake 包仓库版本。Gradle 缓存与 Android 原生依赖缓存分别管理;原生缓存按宿主架构、构建脚本及 MiniRTC 配方区分,不复用桌面或 iOS 的配置和目标文件。
CI 构建 Debug、Release 和设备测试 APK,执行 Debug / Release Lint、本机 C++ 协议与画面队列测试,并校验 APK 的 arm64 ABI、16 KB ELF / ZIP 对齐、完整许可证及 Debug 签名。Ubuntu x64 构建机上不运行 arm64 设备测试;使用下面的设备验证流程在 arm64 手机或模拟器上执行。Lint 报告在构建失败时也会保留。
产物使用 Android 工程自己的 `versionName`,例如 `v0.1.0-20261001`,不跟随桌面版本号。构建日期采用上海时区;带日期的版本标签沿用标签中的日期,与 iOS 一致。Actions 提供:
- `crossdesk-android-arm64-unsigned-<版本>.apk`:未签名 Release APK,标签发布只收集此 APK,安装前需自行签名。
- `crossdesk-android-arm64-debug-<版本>.apk` 和对应的 `-test.apk`:可安装的调试应用和设备测试包,保留 14 天。
- `crossdesk-android-reports`:Lint 报告,保留 14 天。
当前 CI 不需要签名 Secrets。Debug APK 使用每次运行的临时调试证书,可能无法覆盖手机上已有的其他签名版本;卸载原应用会清除本机连接记录和密码。需要持续覆盖升级时,应使用同一私钥签名 Release APK,并在发布新版本时递增 `app/build.gradle` 中的 `versionCode`。未签名 APK 已完成对齐,可通过 SDK 的 `apksigner sign --ks <自己的密钥库> --out <已签名.apk> <未签名.apk>` 签名,再用 `apksigner verify` 检查。
## 使用与连接安全
授权后首页会自动连接服务器,绿色“已连接服务器”表示信令登录完成;失败时可点击状态重试。设置页显示本机 ID。电脑端保持在线,在 Android 首页填写 9 位设备 ID,首次点击“连接”后在底部弹层输入密码,可选择加密保存密码。成功保存后,点击最近连接或再次输入相同 ID 会直接使用该服务器下保存的密码,重启应用后仍然有效;密码无法读取或被远端拒绝时会重新显示输入弹层,新密码仅在连接成功后替换,取消或连接失败不会覆盖原密码。自托管服务器从“设置 → 服务器”展开修改主机名与端口,不能填写 `https://`、路径或完整 URL。默认是 `api.crossdesk.cn:9099`。
+2
View File
@@ -245,3 +245,5 @@ README 的桌面图片直接由当前 Slint UI 渲染,复现方式见 [截图
## Android
原生控制端位于独立的 `apps/android` Gradle 工程,要求 Android 8.0+ arm64 设备,使用 SDK 36、NDK r28c 和 Xmake 3.1.1。构建、APK 安装与模拟器测试见 [Android 开发说明](../apps/android/README.md)。
[Android CI](../.github/workflows/build-android.yml) 可独立手动运行,并由主构建工作流调用。CI 提供 Debug / 设备测试 APK、未签名 Release APK 和 Lint 报告,执行原生协议测试及 16 KB 对齐、许可证检查;标签发布收集未签名 Release APK,安装前需要自行签名。设备测试需要另行在 arm64 手机或模拟器上执行。
+2
View File
@@ -257,3 +257,5 @@ Desktop README images are rendered directly from the current Slint UI. See the [
## Android
The native controller is a separate Gradle project in `apps/android`, targeting Android 8.0+ arm64 with SDK 36, NDK r28c, and Xmake 3.1.1. See the [Android development guide](../apps/android/README.md) for APK builds, installation, and emulator tests.
[Android CI](../.github/workflows/build-android.yml) can run manually on its own and is also called by the main build workflow. It uploads Debug/device-test APKs, an unsigned Release APK and Lint reports, runs native protocol tests, and checks 16 KB alignment and license contents. Tag releases include the unsigned Release APK, which requires your own signing before installation. Device tests must be run separately on an arm64 phone or emulator.