# Builds and signs the release APK.
#
# In a container rather than on the runner, for the same reason the OTP
# release is: the runner needs docker and nothing else, and a developer
# can reproduce exactly what CI did without installing an SDK.
#
# 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 android/Dockerfile .
#
# Run from the repository root — the build context is the repo, so the
# `android/` sources are at `android/`.
ARG JDK_IMAGE=eclipse-temurin:17-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-11076708_latest.zip
ARG ANDROID_PLATFORM=34
ARG BUILD_TOOLS=34.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 ~200 MB of Gradle and Maven downloads cache
# as one layer that survives every change to the Kotlin sources.
FROM sdk AS deps
WORKDIR /src
COPY android/gradle ./gradle
COPY android/gradlew android/settings.gradle.kts android/build.gradle.kts android/gradle.properties ./
COPY android/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=34.0.0
COPY android/app ./app
RUN ./gradlew --no-daemon :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 that
# covers them.
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" \
--key-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
/gitgud
Git Gud
public · Issues · Pulls · Labels · Forks · Compare · Actions success · Badges · Packages
⭐
1
Log in to mark this repository.
4.3 KiB · text
History
d90390e