Build and test / Desktop (Linux) (push) Failing after 1h18m38s
Build and test / Layer separation (push) Successful in 48s
🐳 Android image / Build and push (push) Successful in 9m41s
Build and test / android-image (push) Successful in 9m41s
Traceability / Requirement traces (push) Successful in 48s
Build and test / Android (aarch64) (push) Successful in 21m54s
VS Code's rust-analyzer extension ships a server newer than 1.92.0 supports and prompts, on every window, to add one. Listing the component in rust-toolchain.toml makes rustup supply the server matching the pin — the same thing the pin buys everywhere else. The cost lands outside the editor, which is why this is four files rather than one. rustup reconciles that component list against the installed toolchain on the first cargo call in the work tree and downloads what is missing, inside whatever job happens to be running. An unasked-for fetch in the middle of a build step is nobody's line item and hard to find in a log. So every environment that builds this repo names it too: baked into the Android image, and in the install step of each CI job that rolls its own toolchain. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
198 lines
10 KiB
Docker
198 lines
10 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 \
|
|
# git-lfs: the segmentation model is in LFS, and a plain `git` leaves a
|
|
# 133-byte pointer where 11 MB of weights should be. Without this the
|
|
# CI fetch step dies on `git: 'lfs' is not a git command` and the build
|
|
# then panics in dr-segment's build script -- a clear message about a
|
|
# missing `git lfs pull` that no amount of pulling would have fixed,
|
|
# because the client was never here to run.
|
|
git-lfs \
|
|
# file: the API-level check reads the Android version out of the linked
|
|
# .so's ELF notes with it. Its absence hid behind the missing model for
|
|
# as long as the build panicked before ever reaching that line.
|
|
file \
|
|
# 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.
|
|
#
|
|
# The component list must be a superset of rust-toolchain.toml's. rustup
|
|
# reconciles that file against the installed toolchain on the first cargo
|
|
# invocation in the work tree, and silently downloads whatever is missing —
|
|
# inside the CI job, after this image has already been pulled. Anything the
|
|
# repo asks for therefore belongs here, or the image is not the whole
|
|
# environment it claims to be. rust-analyzer is in that list for the editor's
|
|
# sake; here it costs a layer and buys a job that fetches nothing.
|
|
# ---------------------------------------------------------------------------
|
|
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 rust-analyzer \
|
|
&& 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"]
|