# Builds and signs the release APK. Adapted from Ova's, itself from
# GitGud's android/Dockerfile.
#
# The context is the repository root, not droid/: the app packages the
# desktop wallpaper's shader from moire/shaders/.
#
# podman build (or docker buildx build) \
# --target android-export \
# --secret id=keystore,src=./release.jks \
# --secret id=keystore_password,src=./keystore.pass \
# --output type=local,dest=./apk \
# --file droid/Dockerfile .
# 21 to match gradle/gradle-daemon-jvm.properties; anything else and
# Gradle goes off to download a second JDK.
ARG JDK_IMAGE=docker.io/library/eclipse-temurin:21-jdk
FROM ${JDK_IMAGE} AS sdk
# Pinned. `…_latest.zip` would make the toolchain drift under a cache
# that has no way to notice.
ARG CMDLINE_TOOLS=commandlinetools-linux-14742923_latest.zip
# compileSdk is release(36) { minorApiLevel = 1 }, and AGP 9.2 builds
# with build-tools 36.0.0.
ARG ANDROID_PLATFORM=36.1
ARG BUILD_TOOLS=36.0.0
ENV ANDROID_HOME=/opt/android-sdk \
DEBIAN_FRONTEND=noninteractive
RUN apt-get update \
&& apt-get install -y --no-install-recommends unzip curl ca-certificates \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir -p "${ANDROID_HOME}/cmdline-tools" \
&& curl -fsSL "https://dl.google.com/android/repository/${CMDLINE_TOOLS}" -o /tmp/tools.zip \
&& unzip -q /tmp/tools.zip -d "${ANDROID_HOME}/cmdline-tools" \
&& mv "${ANDROID_HOME}/cmdline-tools/cmdline-tools" "${ANDROID_HOME}/cmdline-tools/latest" \
&& rm /tmp/tools.zip
ENV PATH="${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/platform-tools:${PATH}"
# `yes |` because sdkmanager prompts per licence and there is no
# non-interactive flag that covers all of them.
RUN yes | sdkmanager --licenses > /dev/null \
&& sdkmanager --install \
"platforms;android-${ANDROID_PLATFORM}" \
"build-tools;${BUILD_TOOLS}" \
"platform-tools" > /dev/null
# ── dependencies ─────────────────────────────────────────────────────
#
# Build files only, so the Gradle and Maven downloads cache as one layer
# that survives every change to the Kotlin sources.
FROM sdk AS deps
WORKDIR /src/droid
COPY droid/gradle ./gradle
COPY droid/gradlew droid/settings.gradle.kts droid/build.gradle.kts droid/gradle.properties ./
COPY droid/app/build.gradle.kts ./app/
RUN echo "sdk.dir=${ANDROID_HOME}" > local.properties
# A task that resolves the whole dependency graph without needing the
# sources. `--no-daemon` because a daemon outliving the RUN is just a
# process the image has to reap.
RUN ./gradlew --no-daemon :app:dependencies --configuration releaseRuntimeClasspath > /dev/null \
|| echo "dependency warm-up failed; the build below will fetch them"
# ── build ────────────────────────────────────────────────────────────
FROM deps AS build
ARG BUILD_TOOLS=36.0.0
# A container build inherits nothing from the runner's environment, so
# the version has to be handed in explicitly. Without these every run
# builds versionCode 1 -- the same filename with different bytes, which
# the registry refuses with a 409.
#
# Declared in this stage so they don't invalidate the SDK and
# dependency layers, which are most of the build time.
ARG GITHUB_RUN_NUMBER
ARG SOURCE_DATE_EPOCH
ARG RELEASE_VERSION
ENV GITHUB_RUN_NUMBER=${GITHUB_RUN_NUMBER} \
SOURCE_DATE_EPOCH=${SOURCE_DATE_EPOCH} \
RELEASE_VERSION=${RELEASE_VERSION}
# The shader lives with the desktop wallpaper, at the path the app's
# build.gradle.kts expects relative to droid/.
COPY moire/shaders /src/moire/shaders
COPY droid/app ./app
# AGP 9 only wires unit tests to the debug variant; they are pure JVM
# logic, so that still gates what gets signed.
RUN echo "building versionCode=${GITHUB_RUN_NUMBER:-1}" \
&& ./gradlew --no-daemon :app:testDebugUnitTest :app:assembleRelease
# Sign as a separate step from Gradle so the keystore never reaches a
# build script, a Gradle cache, or an image layer. BuildKit secrets are
# mounted for the life of one RUN and are not committed.
#
# zipalign before apksigner, not after: aligning rewrites offsets, and
# doing it to an already-signed APK invalidates the v2 signature.
#
# No --key-pass: apksigner reads each `file:` password sequentially, so
# passing the same one-line file twice hits EOF on the second read.
# Omitted, the key password defaults to the store password, which is
# what PKCS12 keystores use anyway.
RUN --mount=type=secret,id=keystore,target=/tmp/release.jks \
--mount=type=secret,id=keystore_password,target=/tmp/keystore.pass \
set -eu; \
tools="${ANDROID_HOME}/build-tools/${BUILD_TOOLS}"; \
unsigned=app/build/outputs/apk/release/app-release-unsigned.apk; \
mkdir -p /out; \
"${tools}/zipalign" -p -f 4 "$unsigned" /tmp/aligned.apk; \
"${tools}/apksigner" sign \
--ks /tmp/release.jks \
--ks-pass "file:/tmp/keystore.pass" \
--out /out/app-release.apk \
/tmp/aligned.apk; \
"${tools}/apksigner" verify --print-certs /out/app-release.apk
# ── export ───────────────────────────────────────────────────────────
#
# FROM scratch, so `--output type=local` lands the APK and nothing else.
FROM scratch AS android-export
COPY --from=build /out/app-release.apk /
neiam
/neiam-devices
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
Log in to mark this repository.
5.4 KiB · text
History
0fc5cce