Lift the APK assembly out of package.sh so CI can run it too
package.sh does two things: it decides how the host reaches the image, and it assembles an APK once inside it. Only the first half is host-specific. CI already runs in that image, so the second half was about to be copied into a workflow step -- two copies of aapt2/zipalign/apksigner ordering, drifting apart at whatever rate the toolchain moves. So it moves to docker/android/assemble-apk.sh, which assumes it is inside the image and takes its paths from the environment, because the callers disagree about them: the container mounts the repo at /work, the runner checks it out wherever it likes. Every default reproduces what package.sh did, so the host path is unchanged. Two things stop being hard-coded on the way. The build-tools version and the compile SDK are resolved from what is installed rather than written out as 36.0.0 and android-36 -- the versions are Dockerfile ARGs, and a second copy is a second thing to miss when they move. --min-sdk-version now comes from that same ARG instead of a literal 28, which is the number the API-level check in CI already reads. The intermediates are removed at the end. They were harmless in a cache directory nobody looks at; beside a published artefact they are four more files for a glob to pick up by mistake. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Executable
+142
@@ -0,0 +1,142 @@
|
||||
#!/usr/bin/env bash
|
||||
# Assemble a signed APK from an already-built libdarkroom.so.
|
||||
#
|
||||
# This runs *inside* the Android image, where the SDK lives. It is deliberately
|
||||
# separate from package.sh: package.sh is a host-side convenience that mounts
|
||||
# the repo into a container and drives the whole build, while CI already runs
|
||||
# in that image and needs only this half. Keeping the assembly in one file
|
||||
# means the APK a device gets from `package.sh --install` and the APK CI
|
||||
# publishes are built by the same code, rather than by two copies that drift.
|
||||
#
|
||||
# Everything is overridable, because the two callers disagree about paths: the
|
||||
# container mounts the repo at /work, CI checks it out wherever the runner
|
||||
# likes.
|
||||
#
|
||||
# REPO repo root (default: this script's ../..)
|
||||
# TARGET_DIR cargo target directory (default: $REPO/target-android)
|
||||
# JNILIBS where cargo-ndk wrote the .so (default: $TARGET_DIR/jniLibs)
|
||||
# OUT output directory (default: $TARGET_DIR/apk)
|
||||
# KEYSTORE signing keystore (default: $TARGET_DIR/debug.keystore)
|
||||
# ABI Android ABI (default: arm64-v8a)
|
||||
# RUST_TARGET Rust target triple (default: aarch64-linux-android)
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
|
||||
REPO="$(cd "${REPO:-${HERE}/../..}" && pwd)"
|
||||
TARGET_DIR="${TARGET_DIR:-${REPO}/target-android}"
|
||||
mkdir -p "${TARGET_DIR}"
|
||||
TARGET_DIR="$(cd "${TARGET_DIR}" && pwd)"
|
||||
JNILIBS="${JNILIBS:-${TARGET_DIR}/jniLibs}"
|
||||
OUT="${OUT:-${TARGET_DIR}/apk}"
|
||||
KEYSTORE="${KEYSTORE:-${TARGET_DIR}/debug.keystore}"
|
||||
ABI="${ABI:-arm64-v8a}"
|
||||
RUST_TARGET="${RUST_TARGET:-aarch64-linux-android}"
|
||||
|
||||
SDK="${ANDROID_HOME:-/opt/android-sdk}"
|
||||
|
||||
# Resolved rather than hard-coded: the versions live in the Dockerfile as ARGs,
|
||||
# and a second copy here is a second thing to forget when they move. The newest
|
||||
# installed build-tools wins.
|
||||
BT="$(find "${SDK}/build-tools" -maxdepth 1 -mindepth 1 -type d | sort -V | tail -1)"
|
||||
[[ -n "${BT}" ]] || { echo "error: no build-tools in ${SDK}" >&2; exit 1; }
|
||||
|
||||
# The Dockerfile sets ANDROID_JAR to the compile SDK; see its comment for why
|
||||
# that is not the same number as MIN_API.
|
||||
ANDROID_JAR="${ANDROID_JAR:-$(find "${SDK}/platforms" -maxdepth 1 -name 'android-*' \
|
||||
| sort -V | tail -1)/android.jar}"
|
||||
[[ -f "${ANDROID_JAR}" ]] || { echo "error: no android.jar at ${ANDROID_JAR}" >&2; exit 1; }
|
||||
|
||||
# MIN_API comes from the Dockerfile too, so the manifest the device reads and
|
||||
# the API the linker targeted cannot disagree.
|
||||
MIN_API="$(sed -n 's/^ARG MIN_API=\([0-9]*\).*/\1/p' "${REPO}/docker/android/Dockerfile")"
|
||||
[[ -n "${MIN_API}" ]] || { echo "error: no ARG MIN_API= in docker/android/Dockerfile" >&2; exit 1; }
|
||||
TARGET_API="$(basename "$(dirname "${ANDROID_JAR}")" | sed 's/^android-//')"
|
||||
|
||||
# The version, taken from the workspace rather than restated here. See
|
||||
# package.sh for why versionCode is packed the way it is.
|
||||
VERSION_NAME="$(sed -n 's/^version = "\(.*\)"$/\1/p' "${REPO}/Cargo.toml" | head -1)"
|
||||
[[ -n "${VERSION_NAME}" ]] || { echo "error: no version in Cargo.toml" >&2; exit 1; }
|
||||
VERSION_CODE="$(awk -F. '{ print $1 * 10000 + $2 * 100 + $3 }' <<< "${VERSION_NAME}")"
|
||||
echo "==> version ${VERSION_NAME} (code ${VERSION_CODE}), min API ${MIN_API}, target API ${TARGET_API}"
|
||||
|
||||
SO="${JNILIBS}/${ABI}/libdarkroom.so"
|
||||
[[ -f "${SO}" ]] || { echo "error: ${SO} not built" >&2; exit 1; }
|
||||
|
||||
rm -rf "${OUT}"
|
||||
mkdir -p "${OUT}/staging/lib/${ABI}"
|
||||
|
||||
# Slint compiles a Java helper (SlintAndroidJavaHelper) in its build script and
|
||||
# dexes it. The build-dir hash changes whenever its inputs change, so find it
|
||||
# rather than hard-coding a path; the newest wins if stale directories from
|
||||
# earlier builds are still around.
|
||||
DEX="$(find "${TARGET_DIR}/${RUST_TARGET}/release/build" \
|
||||
-path "*i-slint-backend-android-activity*/out/classes.dex" \
|
||||
-printf "%T@ %p\n" 2>/dev/null | sort -rn | head -1 | cut -d" " -f2-)"
|
||||
[[ -n "${DEX}" ]] || { echo "error: Slint classes.dex not found — did the backend build?" >&2; exit 1; }
|
||||
echo " dex: ${DEX}"
|
||||
|
||||
# A debug keystore. CI points KEYSTORE at a throwaway directory so nothing is
|
||||
# persisted or published; package.sh keeps one in the cache on purpose, because
|
||||
# Android refuses to update an installed app whose signature changed and a new
|
||||
# key every build would mean uninstalling before every install.
|
||||
#
|
||||
# Debug-signed only. This gets the app onto a test device; it is not a release
|
||||
# signature, and the store password is the Android convention rather than a
|
||||
# secret worth protecting.
|
||||
if [[ ! -f "${KEYSTORE}" ]]; then
|
||||
echo " generating debug keystore"
|
||||
mkdir -p "$(dirname "${KEYSTORE}")"
|
||||
keytool -genkeypair -keystore "${KEYSTORE}" -alias androiddebugkey \
|
||||
-storepass android -keypass android \
|
||||
-keyalg RSA -keysize 2048 -validity 10950 \
|
||||
-dname "CN=Android Debug,O=Android,C=US" >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
# The launcher icon is the only resource the app has, but resources go through
|
||||
# aapt2 in two steps regardless: compile turns the source tree into an
|
||||
# intermediate archive of flat files, link folds that into the APK and builds
|
||||
# the resources.arsc table that @mipmap/ic_launcher in the manifest resolves
|
||||
# against. Skipping compile and handing link the directory does not work — link
|
||||
# only reads compiled input.
|
||||
"${BT}/aapt2" compile \
|
||||
--dir "${REPO}/apps/darkroom-android/android/res" \
|
||||
-o "${OUT}/res.zip"
|
||||
|
||||
"${BT}/aapt2" link \
|
||||
-I "${ANDROID_JAR}" \
|
||||
--manifest "${REPO}/apps/darkroom-android/android/AndroidManifest.xml" \
|
||||
-R "${OUT}/res.zip" \
|
||||
--min-sdk-version "${MIN_API}" \
|
||||
--target-sdk-version "${TARGET_API}" \
|
||||
--version-name "${VERSION_NAME}" \
|
||||
--version-code "${VERSION_CODE}" \
|
||||
-o "${OUT}/base.apk" \
|
||||
--auto-add-overlay
|
||||
|
||||
cp "${SO}" "${OUT}/staging/lib/${ABI}/libdarkroom.so"
|
||||
cp "${DEX}" "${OUT}/staging/classes.dex"
|
||||
|
||||
# -0 "" stores the .so without compression so Android can mmap it directly
|
||||
# (extractNativeLibs=false territory); for a 37 MB library that also keeps
|
||||
# install times sane.
|
||||
cd "${OUT}/staging"
|
||||
cp "${OUT}/base.apk" "${OUT}/unaligned.apk"
|
||||
zip -q -0 -X "${OUT}/unaligned.apk" "lib/${ABI}/libdarkroom.so"
|
||||
zip -q -X "${OUT}/unaligned.apk" classes.dex
|
||||
|
||||
# zipalign before signing: apksigner preserves alignment, the reverse order
|
||||
# invalidates the signature.
|
||||
"${BT}/zipalign" -p -f 4 "${OUT}/unaligned.apk" "${OUT}/darkroom.apk"
|
||||
"${BT}/apksigner" sign \
|
||||
--ks "${KEYSTORE}" --ks-pass pass:android --key-pass pass:android \
|
||||
--min-sdk-version "${MIN_API}" \
|
||||
"${OUT}/darkroom.apk"
|
||||
"${BT}/apksigner" verify --print-certs "${OUT}/darkroom.apk" | head -2
|
||||
|
||||
# The intermediates are not the artefact, and leaving them beside it invites
|
||||
# the wrong file being picked up by a glob.
|
||||
rm -rf "${OUT}/staging" "${OUT}/res.zip" "${OUT}/base.apk" "${OUT}/unaligned.apk"
|
||||
|
||||
echo "==> ${OUT}/darkroom.apk"
|
||||
ls -la "${OUT}/darkroom.apk"
|
||||
Reference in New Issue
Block a user