# 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 \ # 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"]