Files
crossdesk/.github/workflows/build-android.yml
T

255 lines
11 KiB
YAML

name: Build Android
on:
workflow_dispatch:
inputs:
patch:
description: "Optional patch number, matching the main build workflow."
required: false
type: string
default: "0"
workflow_call:
inputs:
source_tag:
description: "Existing source tag for a standalone Android release."
required: false
type: string
default: ""
patch:
description: "Optional patch number from the main build workflow."
required: false
type: string
default: "0"
outputs:
version:
description: "Android artifact version; combined releases match the desktop tag."
value: ${{ jobs.build.outputs.version }}
pull_request:
paths:
- ".github/workflows/build-android.yml"
- ".github/workflows/build.yml"
- ".github/workflows/release-mobile.yml"
- ".github/workflows/sign-android.yml"
- ".gitmodules"
- "LICENSE"
- "PRIVACY.md"
- "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:
# Exercise the minimum supported version; local builds may use newer Xmake.
XMAKE_MIN_VERSION: "3.1.1"
CROSSDESK_SOURCE_TAG: ${{ inputs.source_tag || '' }}
CROSSDESK_RELEASE_BUILD: ${{ inputs.source_tag && 'YES' || 'NO' }}
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
fetch-depth: 0
ref: ${{ inputs.source_tag || github.ref }}
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"
cache: pip
# Python build dependencies are declared in this workflow. This file
# also exists when source_tag checks out an older Android release.
cache-dependency-path: .github/workflows/build-android.yml
- name: Install native build tools
run: |
crossdesk_missing_packages=()
for crossdesk_package in build-essential cmake ninja-build pkg-config autoconf automake libtool nasm; do
if [[ "$(dpkg-query -W -f='${Status}' "${crossdesk_package}" 2>/dev/null || true)" != "install ok installed" ]]; then
crossdesk_missing_packages+=("${crossdesk_package}")
fi
done
if (( ${#crossdesk_missing_packages[@]} )); then
sudo apt-get update
sudo apt-get install -y --no-install-recommends "${crossdesk_missing_packages[@]}"
fi
python3 -m pip install setuptools wheel
# Use an isolated SDK directory so the cache contains only this toolchain,
# rather than every SDK/NDK version preinstalled on the runner.
- name: Select Android SDK directory
run: |
echo "ANDROID_HOME=${RUNNER_TEMP}/crossdesk-android-sdk" >> "${GITHUB_ENV}"
echo "ANDROID_SDK_ROOT=${RUNNER_TEMP}/crossdesk-android-sdk" >> "${GITHUB_ENV}"
- name: Restore Android SDK
id: android-sdk-cache
uses: actions/cache/restore@v5
with:
path: ${{ env.ANDROID_HOME }}
key: ${{ runner.os }}-${{ runner.arch }}-ubuntu24.04-android-sdk-v1-cli12266719-api36-build36.0.0-ndk28.2.13676358
- name: Set up Android SDK
if: steps.android-sdk-cache.outputs.cache-hit != 'true'
uses: android-actions/setup-android@v3
with:
cmdline-tools-version: "12266719"
log-accepted-android-sdk-licenses: false
packages: "platform-tools platforms;android-36 build-tools;36.0.0 ndk;28.2.13676358"
# Save immediately after installation, even if a later APK build fails.
- name: Save Android SDK
if: steps.android-sdk-cache.outputs.cache-hit != 'true'
uses: actions/cache/save@v5
with:
path: ${{ env.ANDROID_HOME }}
key: ${{ steps.android-sdk-cache.outputs.cache-primary-key }}
- name: Add Android tools to PATH
run: |
echo "${ANDROID_HOME}/cmdline-tools/16.0/bin" >> "${GITHUB_PATH}"
echo "${ANDROID_HOME}/platform-tools" >> "${GITHUB_PATH}"
- name: Install xmake
uses: xmake-io/github-action-setup-xmake@v1
with:
xmake-version: ${{ env.XMAKE_MIN_VERSION }}
actions-cache-folder: .xmake-toolchain-cache
actions-cache-key: ${{ runner.os }}-${{ runner.arch }}-ubuntu24.04-android-xmake${{ env.XMAKE_MIN_VERSION }}
- name: Cache Xmake package repository
uses: actions/cache@v5
with:
path: apps/android/.native/global/.xmake/repositories/xmake-repo
key: ${{ runner.os }}-${{ runner.arch }}-android-xmake-repo-v1-${{ hashFiles('apps/android/scripts/build_native.py') }}
# 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: Resolve Android version
shell: python
env:
PATCH_NUMBER: ${{ inputs.patch || '0' }}
run: |
import datetime
import os
from pathlib import Path
import re
import runpy
resolver = Path("apps/android/scripts/release_version.py")
if resolver.exists():
runpy.run_path(str(resolver), run_name="__main__")
else:
# Older standalone releases predate the resolver and Gradle properties.
# Keep their original package version when retrying an existing tag.
tag = os.environ["CROSSDESK_SOURCE_TAG"]
match = re.fullmatch(r"android-v([0-9]+\.[0-9]+\.[0-9]+)-([0-9]{8})", tag)
config = Path("apps/android/app/build.gradle").read_text()
name = re.search(r"versionName\s+'([^']+)'", config)
code = re.search(r"versionCode\s+([0-9]+)", config)
if not match or not name or not code or match[1] != name[1]:
raise ValueError("Legacy Android sources require a matching standalone release tag")
datetime.datetime.strptime(match[2], "%Y%m%d")
with open(os.environ["GITHUB_ENV"], "a") as stream:
stream.write(f"ANDROID_VERSION_NAME={name[1]}\nANDROID_VERSION_CODE={code[1]}\n"
f"ANDROID_ARTIFACT_VERSION={tag.removeprefix('android-')}\n")
- name: Build APKs
working-directory: apps/android
run: |
./gradlew --no-daemon --stacktrace \
-PcrossdeskVersionName="${ANDROID_VERSION_NAME}" \
-PcrossdeskVersionCode="${ANDROID_VERSION_CODE}" \
:app:assembleDebug :app:assembleRelease
- name: Verify APKs
working-directory: apps/android
run: |
for APK in app/build/outputs/apk/debug/app-debug.apk app/build/outputs/apk/release/app-release-unsigned.apk; do
python3 -B test/verify_apk.py "${APK}" \
--aapt2 "${ANDROID_HOME}/build-tools/36.0.0/aapt2" \
--version-name "${ANDROID_VERSION_NAME}" --version-code "${ANDROID_VERSION_CODE}"
done
"${ANDROID_HOME}/build-tools/36.0.0/apksigner" verify app/build/outputs/apk/debug/app-debug.apk
- name: Package APKs with the resolved Android version
id: package
shell: python
run: |
import json
import os
from pathlib import Path
import shutil
apk_dir = Path("apps/android/app/build/outputs/apk")
version = os.environ["ANDROID_ARTIFACT_VERSION"]
for variant in ("debug", "release"):
metadata = json.loads((apk_dir / variant / "output-metadata.json").read_text())
for element in metadata["elements"]:
if (element["versionName"] != os.environ["ANDROID_VERSION_NAME"] or
element["versionCode"] != int(os.environ["ANDROID_VERSION_CODE"])):
raise ValueError(f"Unexpected Android {variant} version: {element}")
output = Path(os.environ["RUNNER_TEMP"]) / "crossdesk-android"
for variant, source in (
("unsigned", "release/app-release-unsigned.apk"),
("debug", "debug/app-debug.apk"),
):
destination = output / variant
destination.mkdir(parents=True, exist_ok=True)
shutil.copyfile(apk_dir / source, destination / f"crossdesk-android-arm64-{variant}-{version}.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"
"Debug and Release APKs built and packaged; APK checks passed.\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 APK
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