ci(android): containerized APK build and CI job (Issue #45)

- scripts/setup-android-sdk.sh installs fixed versions of the Android
  command-line tools (checksum verified), platform 34, build-tools
  34.0.0 and NDK 28.2
- packaging/android/Containerfile + scripts/build-apk-container.sh build
  the APK in a container (podman or docker) with JDK 17 and Rust 1.99
- New CI job "Android APK (experimental)": builds the APK and keeps it
  as a workflow artifact, signed with the key from the secrets
  ANDROID_KEYSTORE_B64/ANDROID_KEYSTORE_PASS/ANDROID_KEY_ALIAS if set.
  Not part of the release yet; its failure doesn't block the release.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
haraldandClaude Opus 5.5 committed 2026-10-27 21:39:35 +01:00
1 parent 0be321837c
commit 7584827c04
6 files changed
+172 -2

No files matched your search

+57
View File
@@ -79,3 +79,60 @@ jobs:
GITEA_SERVER: ${{ github.server_url }}
REPO_OWNER: ${{ github.repository_owner }}
REPO_NAME: ${{ github.event.repository.name }}
android:
# Experimental (Issue #45): builds the APK and keeps it as a workflow artifact. It is not
# part of the release yet, and a failure here does not stop the release above.
name: Android APK (experimental)
runs-on: ubuntu-latest
continue-on-error: true
steps:
- name: Checkout Repository
uses: actions/checkout@v4.2.2
- name: Install JDK and Tools
run: |
export DEBIAN_FRONTEND=noninteractive
SUDO=""
if [ "$(id -u)" -ne 0 ] && command -v sudo >/dev/null 2>&1; then
SUDO="sudo"
fi
$SUDO apt-get update -y
$SUDO apt-get install -y --no-install-recommends \
openjdk-17-jdk-headless build-essential pkg-config curl unzip zip
- name: Setup Rust Toolchain
uses: actions-rust-lang/setup-rust-toolchain@v1.11.0
with:
toolchain: stable
target: aarch64-linux-android
cache: true
rustflags: ""
- name: Cache Android SDK
uses: actions/cache@v4.2.3
with:
path: ~/android-sdk
key: android-sdk-${{ hashFiles('scripts/setup-android-sdk.sh') }}
- name: Install Android SDK and NDK
run: ./scripts/setup-android-sdk.sh
- name: Build APK
run: |
if [ -n "${ANDROID_KEYSTORE_B64}" ]; then
KEYSTORE="${RUNNER_TEMP:-/tmp}/release.keystore"
printf '%s' "${ANDROID_KEYSTORE_B64}" | base64 -d > "${KEYSTORE}"
export ANDROID_KEYSTORE="${KEYSTORE}"
fi
./scripts/package-android.sh
env:
ANDROID_KEYSTORE_B64: ${{ secrets.ANDROID_KEYSTORE_B64 }}
ANDROID_KEYSTORE_PASS: ${{ secrets.ANDROID_KEYSTORE_PASS }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
- name: Keep APK as Artifact
uses: actions/upload-artifact@v4.6.2
with:
name: ink-notes-android-apk
path: target/ink-notes_*_arm64.apk
+1 -1
View File
@@ -156,7 +156,7 @@ Die CI prüft zusätzlich `cargo fmt --check` und `cargo clippy --all-targets --
Andere Systeme:
- **Windows** wird unter Linux übersetzt: `scripts/package-windows.sh` (braucht `gcc-mingw-w64-x86-64`); die Tests laufen mit `wine` als Runner (`CARGO_TARGET_X86_64_PC_WINDOWS_GNU_RUNNER=wine cargo test --target x86_64-pc-windows-gnu`).
- **Android** (experimentell, Issue #42): `scripts/package-android.sh` baut `target/ink-notes_<version>_arm64.apk` ohne Gradle. Nötig sind das Android-SDK (build-tools, Plattform 34; `ANDROID_HOME`, Standard `~/android-sdk`) und das NDK; der Signierschlüssel kommt aus `ANDROID_KEYSTORE`, `ANDROID_KEYSTORE_PASS` und `ANDROID_KEY_ALIAS`, sonst wird der Debug-Schlüssel verwendet. Installieren mit `adb install -r target/ink-notes_<version>_arm64.apk`, Log-Ausgaben mit `adb logcat -s ink-notes`. Der Stift wird von Fingern unterschieden (Stifttasten, Radierer-Ende, Handballen-Erkennung), Textfelder öffnen die Bildschirmtastatur, Import und „Bild einfügen“ nutzen den System-Dateidialog, und Exporte landen in `Download/ink-notes` (ab Android 10). Tastaturtext und Dateidialog kommen von der kleinen Java-Activity `packaging/android/java/…/InkActivity.java` (zum Bauen ist ein JDK nötig).
- **Android** (experimentell, Issue #42): `scripts/package-android.sh` baut `target/ink-notes_<version>_arm64.apk` ohne Gradle. Nötig sind das Android-SDK (build-tools, Plattform 34; `ANDROID_HOME`, Standard `~/android-sdk`) und das NDK; der Signierschlüssel kommt aus `ANDROID_KEYSTORE`, `ANDROID_KEYSTORE_PASS` und `ANDROID_KEY_ALIAS`, sonst wird der Debug-Schlüssel verwendet. Ohne SDK auf dem Rechner baut `scripts/build-apk-container.sh` sie in einem Container (podman oder docker, `packaging/android/Containerfile`); `scripts/setup-android-sdk.sh` installiert die festen SDK-/NDK-Versionen. Die CI baut die APK als Workflow-Artefakt (noch nicht im Release); ein Release-Schlüssel gehört in die Secrets `ANDROID_KEYSTORE_B64` (`base64 -w0 release.keystore`), `ANDROID_KEYSTORE_PASS` und `ANDROID_KEY_ALIAS`. Installieren mit `adb install -r target/ink-notes_<version>_arm64.apk`, Log-Ausgaben mit `adb logcat -s ink-notes`. Der Stift wird von Fingern unterschieden (Stifttasten, Radierer-Ende, Handballen-Erkennung), Textfelder öffnen die Bildschirmtastatur, Import und „Bild einfügen“ nutzen den System-Dateidialog, und Exporte landen in `Download/ink-notes` (ab Android 10). Tastaturtext und Dateidialog kommen von der kleinen Java-Activity `packaging/android/java/…/InkActivity.java` (zum Bauen ist ein JDK nötig).
### Projektstruktur
+1 -1
View File
@@ -156,7 +156,7 @@ CI additionally checks `cargo fmt --check` and `cargo clippy --all-targets -- -D
Other systems:
- **Windows** is cross-compiled on Linux: `scripts/package-windows.sh` (needs `gcc-mingw-w64-x86-64`); the tests run with `wine` as runner (`CARGO_TARGET_X86_64_PC_WINDOWS_GNU_RUNNER=wine cargo test --target x86_64-pc-windows-gnu`).
- **Android** (experimental, Issue #42): `scripts/package-android.sh` builds `target/ink-notes_<version>_arm64.apk` without Gradle. It needs the Android SDK (build-tools, platform 34; `ANDROID_HOME`, default `~/android-sdk`) and the NDK; the signing key comes from `ANDROID_KEYSTORE`, `ANDROID_KEYSTORE_PASS` and `ANDROID_KEY_ALIAS`, otherwise the debug key is used. Install with `adb install -r target/ink-notes_<version>_arm64.apk`, log output with `adb logcat -s ink-notes`. The pen is told apart from fingers (pen buttons, eraser end, palm rejection), text fields open the soft keyboard, import and inserting images use the system file dialog, and exports are saved to `Download/ink-notes` (Android 10 and newer). The small Java activity `packaging/android/java/…/InkActivity.java` (needs a JDK to build) provides keyboard text and the file dialog.
- **Android** (experimental, Issue #42): `scripts/package-android.sh` builds `target/ink-notes_<version>_arm64.apk` without Gradle. It needs the Android SDK (build-tools, platform 34; `ANDROID_HOME`, default `~/android-sdk`) and the NDK; the signing key comes from `ANDROID_KEYSTORE`, `ANDROID_KEYSTORE_PASS` and `ANDROID_KEY_ALIAS`, otherwise the debug key is used. Without SDK on the host, `scripts/build-apk-container.sh` builds it in a container (podman or docker, `packaging/android/Containerfile`); `scripts/setup-android-sdk.sh` installs the fixed SDK/NDK versions. The CI builds the APK as a workflow artifact (not yet part of the release); a release key goes into the secrets `ANDROID_KEYSTORE_B64` (`base64 -w0 release.keystore`), `ANDROID_KEYSTORE_PASS` and `ANDROID_KEY_ALIAS`. Install with `adb install -r target/ink-notes_<version>_arm64.apk`, log output with `adb logcat -s ink-notes`. The pen is told apart from fingers (pen buttons, eraser end, palm rejection), text fields open the soft keyboard, import and inserting images use the system file dialog, and exports are saved to `Download/ink-notes` (Android 10 and newer). The small Java activity `packaging/android/java/…/InkActivity.java` (needs a JDK to build) provides keyboard text and the file dialog.
New code comments and commit messages are written in English; older code and history still contain German.
+25
View File
@@ -0,0 +1,25 @@
# Build environment for the Android APK (Issue #45): fixed versions of the JDK, Android SDK,
# NDK and Rust, so that the APK can be built the same way anywhere.
# Used by scripts/build-apk-container.sh (podman or docker).
FROM docker.io/library/ubuntu:24.04
ARG RUST_VERSION=1.99.0
RUN apt-get update \
&& DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
ca-certificates curl unzip zip openjdk-17-jdk-headless build-essential pkg-config \
&& rm -rf /var/lib/apt/lists/*
ENV ANDROID_HOME=/opt/android-sdk \
RUSTUP_HOME=/opt/rustup \
CARGO_HOME=/opt/cargo \
PATH=/opt/cargo/bin:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin
COPY scripts/setup-android-sdk.sh /tmp/setup-android-sdk.sh
RUN /tmp/setup-android-sdk.sh && rm /tmp/setup-android-sdk.sh
RUN curl -sSf https://sh.rustup.rs | sh -s -- -y --profile minimal \
--default-toolchain "${RUST_VERSION}" --target aarch64-linux-android \
&& chmod -R a+rwX /opt/cargo /opt/rustup
WORKDIR /src
+48
View File
@@ -0,0 +1,48 @@
#!/usr/bin/env bash
# ==============================================================================
# Builds the APK inside the container from packaging/android/Containerfile (Issue #45),
# with podman (preferred) or docker. Needs nothing else on the host.
# Result: target/ink-notes_<version>_arm64.apk
#
# Signing key as in scripts/package-android.sh: ANDROID_KEYSTORE (path on the host),
# ANDROID_KEYSTORE_PASS, ANDROID_KEY_ALIAS; otherwise a debug key (kept below
# target/android-container/home, so that test builds can update each other).
# ==============================================================================
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
cd "${ROOT_DIR}"
if command -v podman >/dev/null 2>&1; then
ENGINE=podman
USER_ARGS=(--userns=keep-id)
elif command -v docker >/dev/null 2>&1; then
ENGINE=docker
USER_ARGS=(--user "$(id -u):$(id -g)")
else
echo "FEHLER: weder podman noch docker gefunden."
exit 1
fi
IMAGE="ink-notes-android"
echo "=== Baue Container-Image ${IMAGE} ==="
"${ENGINE}" build -t "${IMAGE}" -f packaging/android/Containerfile .
mkdir -p target/android-container/home
ARGS=(
--rm "${USER_ARGS[@]}"
-v "${ROOT_DIR}:/src"
-e HOME=/src/target/android-container/home
-e CARGO_TARGET_DIR=/src/target/android-container
# Downloaded crates stay between runs
-e CARGO_HOME=/src/target/android-container/cargo
)
if [ -n "${ANDROID_KEYSTORE:-}" ]; then
ARGS+=(
-v "$(realpath "${ANDROID_KEYSTORE}"):/keystore:ro"
-e ANDROID_KEYSTORE=/keystore
-e ANDROID_KEYSTORE_PASS -e ANDROID_KEY_ALIAS
)
fi
"${ENGINE}" run "${ARGS[@]}" "${IMAGE}" scripts/package-android.sh
+40
View File
@@ -0,0 +1,40 @@
#!/usr/bin/env bash
# ==============================================================================
# Installs the Android SDK parts that scripts/package-android.sh needs, in fixed versions
# (Issue #45): command-line tools, platform 34, build-tools 34.0.0 and NDK 28.2.
# Used by the CI and packaging/android/Containerfile; works for a local SDK as well.
#
# ANDROID_HOME target folder (default: ~/android-sdk)
#
# Needs curl, unzip and a JDK 17 or newer (sdkmanager is a Java program).
# ==============================================================================
set -euo pipefail
ANDROID_HOME="${ANDROID_HOME:-${HOME}/android-sdk}"
CMDLINE_TOOLS_ZIP="commandlinetools-linux-11076708_latest.zip"
CMDLINE_TOOLS_SHA256="2d2d50857e4eb553af5a6dc3ad507a17adf43d115264b1afc116f95c92e5e258"
PACKAGES=(
"platforms;android-34"
"build-tools;34.0.0"
"ndk;28.2.13676358"
)
SDKMANAGER="${ANDROID_HOME}/cmdline-tools/latest/bin/sdkmanager"
if [ ! -x "${SDKMANAGER}" ]; then
echo "=== Lade Android Command-line Tools ==="
TMP="$(mktemp -d)"
trap 'rm -rf "${TMP}"' EXIT
curl -sSfL -o "${TMP}/${CMDLINE_TOOLS_ZIP}" \
"https://dl.google.com/android/repository/${CMDLINE_TOOLS_ZIP}"
echo "${CMDLINE_TOOLS_SHA256} ${TMP}/${CMDLINE_TOOLS_ZIP}" | sha256sum -c -
unzip -q "${TMP}/${CMDLINE_TOOLS_ZIP}" -d "${TMP}"
mkdir -p "${ANDROID_HOME}/cmdline-tools"
rm -rf "${ANDROID_HOME}/cmdline-tools/latest"
mv "${TMP}/cmdline-tools" "${ANDROID_HOME}/cmdline-tools/latest"
fi
echo "=== Installiere ${PACKAGES[*]} nach ${ANDROID_HOME} ==="
# sdkmanager ends with an error when `yes` is cut off by a closed pipe; that's fine
(yes || true) | "${SDKMANAGER}" --sdk_root="${ANDROID_HOME}" --licenses >/dev/null
"${SDKMANAGER}" --sdk_root="${ANDROID_HOME}" --install "${PACKAGES[@]}" >/dev/null
echo "Android-SDK bereit: ${ANDROID_HOME}"