The Android job's "Verify minimum API level" step has never verified the minimum API level. It took the first `*.so` anywhere under the target directory, which is a host proc-macro from debug/deps — an x86-64 object built by the runner's gcc, whose .comment section cannot mention Android and so can never contradict the expected value. It now reads the artifact under the target triple, compares against MIN_API parsed from the Dockerfile rather than a second copy of the number, and fails on a mismatch. Both sides are checked non-empty first: two failed parses would otherwise compare equal and pass, which is the same silent success in a new costume. The Android image installs one SDK package per layer and keeps the output. sdkmanager is a JVM program that aborts when it cannot get memory, and the single `> /dev/null` step reported that as a bare "exit code 134" while a retry re-downloaded everything that had already succeeded. tools/ci-local.sh runs all four jobs — desktop, android, layering, traceability — against the host toolchain, which is pinned to the same 1.92.0 CI installs. Its matrix check compares regeneration against the working tree rather than against HEAD: CI starts from a clean checkout, so git's answer is the right one there and reports every local run stale here. The rest is rustfmt across the workspace, and the clippy findings that surfaced once it did: manual_contains in dr-thumbs and collections_ui, a map iterated as pairs for its keys, an index loop over a slice, and two runtime assertions on a constant now made at compile time. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
179 lines
8.7 KiB
Docker
179 lines
8.7 KiB
Docker
# DarkRoom — reproducible Android build environment
|
|
#
|
|
# Pins the entire toolchain: JDK, Android SDK, NDK, Rust, and the four Android
|
|
# targets. Both CI and local builds use this image, so "works on my machine"
|
|
# and "works in CI" are the same machine.
|
|
#
|
|
# Build: podman build -t darkroom-android:latest docker/android
|
|
# Use: ./docker/android/build.sh cargo ndk -t arm64-v8a build --release
|
|
|
|
FROM docker.io/library/debian:bookworm-slim
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Versions — pinned deliberately. Bumping any of these is a reviewable change,
|
|
# not something that drifts underneath the build.
|
|
# ---------------------------------------------------------------------------
|
|
ARG JDK_VERSION=17
|
|
# Compile SDK / target API.
|
|
ARG ANDROID_API=36
|
|
# Minimum supported API — the level native code links against (NFR-COMPAT-1).
|
|
# 28 (Android 9) matches the floor where Vulkan support is dependable.
|
|
# cargo-ndk otherwise defaults to 21, which is far below what this app needs.
|
|
ARG MIN_API=28
|
|
ARG BUILD_TOOLS=36.0.0
|
|
ARG NDK_VERSION=27.2.12479018
|
|
ARG CMDLINE_TOOLS=13114758
|
|
# Must satisfy cargo-ndk's MSRV (4.1.x needs >= 1.86) as well as our own crates.
|
|
ARG RUST_VERSION=1.92.0
|
|
# Gitea runs JavaScript actions (actions/checkout, actions/cache) with Node from
|
|
# inside the job container. Bookworm ships 18; current actions expect 20+.
|
|
ARG NODE_MAJOR=20
|
|
|
|
# JDK 17, not the host's 25: the Android Gradle Plugin supports 17 as its
|
|
# stable target, and newer JDKs regularly break Gradle in ways that cost more
|
|
# time than they save.
|
|
ENV DEBIAN_FRONTEND=noninteractive \
|
|
ANDROID_HOME=/opt/android-sdk \
|
|
ANDROID_SDK_ROOT=/opt/android-sdk \
|
|
JAVA_HOME=/usr/lib/jvm/java-${JDK_VERSION}-openjdk-amd64 \
|
|
CARGO_HOME=/opt/cargo \
|
|
RUSTUP_HOME=/opt/rustup \
|
|
PATH=/opt/cargo/bin:/opt/android-sdk/cmdline-tools/latest/bin:/opt/android-sdk/platform-tools:$PATH
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# System packages
|
|
# ---------------------------------------------------------------------------
|
|
RUN apt-get update && apt-get install -y --no-install-recommends \
|
|
ca-certificates curl unzip git \
|
|
# zip: package.sh adds the .so and dex to the aapt2-linked APK
|
|
zip \
|
|
openjdk-${JDK_VERSION}-jdk-headless \
|
|
# Slint / winit build-time needs
|
|
pkg-config libfontconfig1-dev \
|
|
# native deps that may need building for host-side tooling
|
|
build-essential cmake python3 \
|
|
&& rm -rf /var/lib/apt/lists/*
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Node — required by the CI runner, not by the Android build
|
|
#
|
|
# This image is the job container for the Android CI job, and Gitea executes
|
|
# actions/checkout inside it using the container's own Node. Without this the
|
|
# job fails at checkout with "Cannot find: node in PATH", before any Rust or
|
|
# Gradle step runs. Local builds never invoke it.
|
|
# ---------------------------------------------------------------------------
|
|
RUN curl -fsSL "https://deb.nodesource.com/setup_${NODE_MAJOR}.x" | bash - \
|
|
&& apt-get install -y --no-install-recommends nodejs \
|
|
&& rm -rf /var/lib/apt/lists/* \
|
|
&& node --version
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Android SDK + NDK
|
|
# ---------------------------------------------------------------------------
|
|
RUN mkdir -p ${ANDROID_HOME}/cmdline-tools \
|
|
&& curl -fsSL -o /tmp/tools.zip \
|
|
"https://dl.google.com/android/repository/commandlinetools-linux-${CMDLINE_TOOLS}_latest.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
|
|
|
|
# One package per layer, and the output kept.
|
|
#
|
|
# Both halves are scar tissue from the same build. sdkmanager is a JVM program
|
|
# that aborts (SIGABRT, exit 134) when it cannot get memory — which it will on a
|
|
# loaded machine, since the NDK alone unpacks some 4.5 GB. With the whole
|
|
# install in one `> /dev/null` step, that surfaced as "exit code 134" and
|
|
# nothing else, and a retry re-downloaded the three packages that had already
|
|
# succeeded before reaching the one that had not.
|
|
#
|
|
# pipefail matters here: without it the `tr | tail` pipeline would report the
|
|
# exit status of `tail`, which is exactly the masking this step is undoing.
|
|
# Progress bars are carriage returns, hence the tr — the tail keeps the summary
|
|
# without the several thousand redraws.
|
|
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
|
|
|
|
RUN yes | sdkmanager --licenses > /dev/null 2>&1 || true
|
|
|
|
RUN sdkmanager --install "platform-tools" 2>&1 | tr '\r' '\n' | tail -3
|
|
RUN sdkmanager --install "platforms;android-${ANDROID_API}" 2>&1 | tr '\r' '\n' | tail -3
|
|
RUN sdkmanager --install "build-tools;${BUILD_TOOLS}" 2>&1 | tr '\r' '\n' | tail -3
|
|
RUN sdkmanager --install "ndk;${NDK_VERSION}" 2>&1 | tr '\r' '\n' | tail -3
|
|
|
|
ENV ANDROID_NDK_HOME=${ANDROID_HOME}/ndk/${NDK_VERSION} \
|
|
ANDROID_NDK_ROOT=${ANDROID_HOME}/ndk/${NDK_VERSION}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Rust + Android targets
|
|
#
|
|
# All four ABIs. arm64-v8a covers essentially every current device; the others
|
|
# exist so an ABI-specific build break is caught here rather than at release.
|
|
# ---------------------------------------------------------------------------
|
|
RUN curl -fsSL https://sh.rustup.rs | sh -s -- \
|
|
-y --no-modify-path --profile minimal --default-toolchain ${RUST_VERSION} \
|
|
&& rustup target add \
|
|
aarch64-linux-android \
|
|
armv7-linux-androideabi \
|
|
x86_64-linux-android \
|
|
i686-linux-android \
|
|
&& rustup component add rustfmt clippy \
|
|
&& cargo install cargo-ndk --locked \
|
|
&& chmod -R a+rwX ${CARGO_HOME} ${RUSTUP_HOME}
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Linker configuration
|
|
#
|
|
# cargo-ndk normally handles this, but setting it explicitly means plain
|
|
# `cargo build --target …` works too, which matters for tooling that shells
|
|
# out to cargo directly (rust-analyzer, cargo-metadata).
|
|
# ---------------------------------------------------------------------------
|
|
ENV NDK_BIN=${ANDROID_NDK_HOME}/toolchains/llvm/prebuilt/linux-x86_64/bin
|
|
|
|
# Linkers target MIN_API, not ANDROID_API — the binary must run on the oldest
|
|
# supported device, while the SDK compiles against the newest.
|
|
ENV CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=${NDK_BIN}/aarch64-linux-android${MIN_API}-clang \
|
|
CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER=${NDK_BIN}/armv7a-linux-androideabi${MIN_API}-clang \
|
|
CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER=${NDK_BIN}/x86_64-linux-android${MIN_API}-clang \
|
|
CARGO_TARGET_I686_LINUX_ANDROID_LINKER=${NDK_BIN}/i686-linux-android${MIN_API}-clang
|
|
|
|
# cargo-ndk reads this; without it it defaults to API 21.
|
|
ENV CARGO_NDK_PLATFORM=${MIN_API} \
|
|
ANDROID_PLATFORM=${MIN_API}
|
|
|
|
# ANDROID_PLATFORM above means "link native code for API 28" to cargo-ndk, but
|
|
# the android-build crate reads the same variable as "compile Java against
|
|
# platforms/android-28/android.jar" — a directory that does not exist here,
|
|
# because only the compile SDK (ANDROID_API) is installed. Slint's Android
|
|
# backend builds a Java helper through that crate, so it panics with
|
|
# "No Android platforms found" while android.jar sits in android-36.
|
|
#
|
|
# ANDROID_JAR is checked ahead of the platform lookup and settles it: Java
|
|
# compiles against the compile SDK, native code still links against MIN_API.
|
|
# The two are meant to differ (see the README's compile-SDK-versus-min-API
|
|
# note); only the variable name is overloaded.
|
|
ENV ANDROID_JAR=${ANDROID_HOME}/platforms/android-${ANDROID_API}/android.jar
|
|
|
|
# Crates with C or assembly components (ring's crypto core, and anything else
|
|
# using the cc crate) need a compiler and archiver per target, not just a
|
|
# linker. cargo-ndk sets the linker only, so these are set explicitly —
|
|
# otherwise `ring` fails its build script and TLS cannot be built at all.
|
|
ENV CC_aarch64_linux_android=${NDK_BIN}/aarch64-linux-android${MIN_API}-clang \
|
|
AR_aarch64_linux_android=${NDK_BIN}/llvm-ar \
|
|
CC_armv7_linux_androideabi=${NDK_BIN}/armv7a-linux-androideabi${MIN_API}-clang \
|
|
AR_armv7_linux_androideabi=${NDK_BIN}/llvm-ar \
|
|
CC_x86_64_linux_android=${NDK_BIN}/x86_64-linux-android${MIN_API}-clang \
|
|
AR_x86_64_linux_android=${NDK_BIN}/llvm-ar \
|
|
CC_i686_linux_android=${NDK_BIN}/i686-linux-android${MIN_API}-clang \
|
|
AR_i686_linux_android=${NDK_BIN}/llvm-ar
|
|
|
|
# Shared cargo registry cache — bind-mount over this to persist across runs.
|
|
VOLUME ["/opt/cargo/registry"]
|
|
|
|
WORKDIR /work
|
|
|
|
# Rootless podman maps the host user into the container, so the image must not
|
|
# assume a fixed uid. Keep world-writable toolchain dirs and let the caller
|
|
# pass --user.
|
|
RUN chmod -R a+rwX ${ANDROID_HOME}
|
|
|
|
CMD ["/bin/bash"]
|