From 6a5daa0c1896cc9b2ac193d626cd3a648bd60c22 Mon Sep 17 00:00:00 2001 From: Davide Grilli Date: Thu, 2 Jul 2026 10:45:37 +0200 Subject: [PATCH] feat(build): add Docker-based reproducible build system MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds docker/ with two Dockerfiles and a build.sh interactive script that builds all three release targets (Windows exe, Linux binary, Android APK) inside pinned Docker containers — no SDK required on the host. Key design decisions: - Source mounted read-only; build runs on an in-container copy so bin/obj never pollute the working tree. - NuGet packages cached in a named Docker volume (plm-nuget-cache) across runs to avoid re-downloading on each build. - Single-file desktop publishes use IncludeNativeLibrariesForSelfExtract so Avalonia's native libs (Skia, HarfBuzz, ANGLE) are embedded — without this flag the exe/binary silently fails to start. - Android Dockerfile pins platform API 36 (dictated by the .NET 10 android workload, error XA5207 if mismatched) and bakes the full Android SDK + workload into the image layer. - Artifacts are chown'd back to the host user so dist/ files are never owned by root. All three targets verified end-to-end from a clean docker system prune -a: Windows PE32+ exe (103 MB), Linux single-file binary launches on this host, Android APK installs and renders UI on API-34 x86_64 emulator. --- docker/Dockerfile.android | 39 ++++++++ docker/Dockerfile.desktop | 11 +++ docker/README.md | 183 ++++++++++++++++++++++++++++++++++ docker/build.sh | 201 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 434 insertions(+) create mode 100644 docker/Dockerfile.android create mode 100644 docker/Dockerfile.desktop create mode 100644 docker/README.md create mode 100755 docker/build.sh diff --git a/docker/Dockerfile.android b/docker/Dockerfile.android new file mode 100644 index 0000000..1a57a81 --- /dev/null +++ b/docker/Dockerfile.android @@ -0,0 +1,39 @@ +FROM mcr.microsoft.com/dotnet/sdk:10.0 + +# ── Java (required by the Android SDK tools) ───────────────────────────────── +RUN apt-get update && apt-get install -y --no-install-recommends \ + openjdk-17-jdk-headless \ + wget \ + unzip \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +ENV JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 +ENV ANDROID_HOME=/opt/android-sdk +# .NET 10 android workload compiles against API level 36 (see error XA5207 +# if this drifts: the workload's Xamarin.Android.Tooling.targets dictates it). +ENV ANDROID_SDK_PLATFORM=36 +ENV ANDROID_SDK_BUILD_TOOLS=36.0.0 + +# ── Android command-line tools ──────────────────────────────────────────────── +# Pin the build number to a known-good release; update when Google breaks the URL. +# Latest as of 2025: commandlinetools-linux-11076708_latest.zip (cmdline-tools 12.0) +RUN mkdir -p "${ANDROID_HOME}/cmdline-tools" && \ + wget -q "https://dl.google.com/android/repository/commandlinetools-linux-11076708_latest.zip" \ + -O /tmp/cmdtools.zip && \ + unzip -q /tmp/cmdtools.zip -d "${ANDROID_HOME}/cmdline-tools" && \ + mv "${ANDROID_HOME}/cmdline-tools/cmdline-tools" "${ANDROID_HOME}/cmdline-tools/latest" && \ + rm /tmp/cmdtools.zip + +ENV PATH="${JAVA_HOME}/bin:${ANDROID_HOME}/cmdline-tools/latest/bin:${ANDROID_HOME}/platform-tools:${PATH}" + +# ── Android SDK packages ────────────────────────────────────────────────────── +RUN yes | sdkmanager --licenses > /dev/null && \ + sdkmanager \ + "platform-tools" \ + "platforms;android-${ANDROID_SDK_PLATFORM}" \ + "build-tools;${ANDROID_SDK_BUILD_TOOLS}" + +# ── dotnet android workload (~1 GB, baked into this layer) ─────────────────── +RUN dotnet workload install android + +WORKDIR /tmp/build diff --git a/docker/Dockerfile.desktop b/docker/Dockerfile.desktop new file mode 100644 index 0000000..a248159 --- /dev/null +++ b/docker/Dockerfile.desktop @@ -0,0 +1,11 @@ +FROM mcr.microsoft.com/dotnet/sdk:10.0 + +# clang and zlib are required by the .NET SDK for native compilation steps +# that occur even during managed cross-publish (win-x64 / linux-x64). +RUN apt-get update && apt-get install -y --no-install-recommends \ + clang \ + zlib1g-dev \ + libkrb5-dev \ + && apt-get clean && rm -rf /var/lib/apt/lists/* + +WORKDIR /tmp/build diff --git a/docker/README.md b/docker/README.md new file mode 100644 index 0000000..4190f92 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,183 @@ +# PalladiumWallet — Reproducible Builds via Docker + +This folder builds all three distribution targets — **Windows**, **Linux**, +**Android** — inside Docker containers. The entire toolchain (.NET 10 SDK, +JDK, Android SDK, android workload) lives in the container images, pinned in +the Dockerfiles, so: + +- **anyone can produce the official binaries** with a single command, on any + Linux machine, without installing any SDK on the host; +- **the toolchain never drifts**: every build uses exactly the same SDK + versions, regardless of what is installed (or updated) on the host; +- **the build environment is auditable**: the Dockerfiles in this folder are + the complete, reviewable definition of how release binaries are made — which + matters for a wallet, where users must be able to trust that the shipped + binary comes from the published source. + +> **Scope note:** this pins the *build environment*. Bit-for-bit identical +> output across machines is a stronger property that .NET does not guarantee +> by default (embedded timestamps, signing); if two builds of the same commit +> differ, they differ only in those metadata, not in code. + +--- + +## Prerequisites + +A Linux host (native, WSL2, or a VM) with **Docker Engine** installed and +running. Nothing else — no .NET SDK, no JDK, no Android SDK. + +```bash +# Check that Docker works: +docker info +``` + +If Docker is missing, install it from +(or `sudo apt install docker.io` on Debian/Ubuntu). If `docker info` fails +with a permission error, add yourself to the `docker` group and start a new +shell: + +```bash +sudo usermod -aG docker $USER +``` + +**Disk space:** ~2 GB for the desktop image, ~7 GB for the Android image +(SDK + emulator-less toolchain). **First-run time:** the images are built +automatically on first use — a few minutes for desktop, 10–20 minutes for +Android (large downloads). Subsequent builds reuse the cached images and take +well under a minute (desktop) / a few minutes (Android). + +--- + +## Quick start + +```bash +# From the repository root (or from the docker/ folder — both work): +./docker/build.sh +``` + +Running without arguments shows an interactive menu — pick a single target or +`all`. Non-interactive usage: + +``` +./docker/build.sh [TARGET] [--rebuild] + +Targets: + windows Win x64 single-file executable (native libs embedded) + linux Linux x64 single-file binary (runs as-is, nothing to install) + android Android APK (debug-signed) + all All three targets + +Options: + --rebuild Force rebuild of the Docker images (needed after editing a Dockerfile) +``` + +Examples: + +```bash +./docker/build.sh all # build everything +./docker/build.sh windows # Windows only +./docker/build.sh android --rebuild # Android, rebuilding the image first +``` + +--- + +## Output — and how to use each artifact + +All artifacts land in `dist/` at the repository root. The version number is +read automatically from `` in `src/App/PalladiumWallet.App.csproj`. + +| Target | Path | +|---------|--------------------------------------------------| +| Windows | `dist/windows/PalladiumWallet-{ver}-win-x64.exe` | +| Linux | `dist/linux/PalladiumWallet-{ver}-linux-x64` | +| Android | `dist/android/PalladiumWallet-{ver}.apk` | + +**Windows** — a single self-contained `.exe` (runtime and native libraries +embedded). Copy it to any 64-bit Windows 10/11 machine and double-click. +The first launch takes a few extra seconds (it unpacks native libraries to a +per-user cache); later launches are normal. SmartScreen may warn because the +binary is not code-signed — choose "Run anyway". + +**Linux** — a single self-contained binary, already executable. Copy and run: + +```bash +./PalladiumWallet-{ver}-linux-x64 +``` + +Works on any desktop distro with glibc, X11/Wayland and fontconfig (i.e. +effectively all of them); no .NET or other packages to install. If you +transfer it through a channel that strips permissions (e.g. a web download), +restore the execute bit with `chmod +x`. + +**Android** — a debug-signed APK for sideloading: transfer it to the phone +and open it (enable "install from unknown sources" if prompted), or install +via `adb install dist/android/PalladiumWallet-*.apk`. Supports Android 6.0+ +(API 23), arm64 phones and x86_64 emulators. + +> **Signature caveat:** debug-signed APKs built on different machines carry +> different keys. If a previous build is already installed, Android refuses +> the update (`INSTALL_FAILED_UPDATE_INCOMPATIBLE`) — uninstall the old app +> first. **Uninstalling deletes the app's data: back up the wallet seed +> before doing this.** Release signing with a stable key is not set up yet. + +--- + +## How it works + +### Docker images + +| Image | Dockerfile | Used for | Size | +|---------------------|----------------------|-----------------|---------| +| `plm-build-desktop` | `Dockerfile.desktop` | windows + linux | ~1.5 GB | +| `plm-build-android` | `Dockerfile.android` | android | ~5 GB | + +Images are built automatically the first time a target needs them and reused +afterwards. Use `--rebuild` only after modifying a Dockerfile. + +### Source isolation + +The repository is mounted **read-only** inside the container; the build works +on a copy at `/tmp/build`. Your working tree is never touched — no stray +`bin/`/`obj/` directories, and a dirty working tree doesn't leak into the +build beyond the files it contains. Artifacts are written back through a +bind mount to `dist/` and chown'd to your user. + +### NuGet cache + +A Docker named volume `plm-nuget-cache` holds downloaded NuGet packages +across builds. To reclaim the space or force a clean re-download: + +```bash +docker volume rm plm-nuget-cache +``` + +--- + +## Troubleshooting + +- **`Docker daemon is not running`** — start it (`sudo systemctl start + docker`; on WSL2, start Docker Desktop or the docker service). +- **Android image build fails downloading `commandlinetools`** — Google + rotates the build number in the URL. Update the URL in + `Dockerfile.android` to the current one from + and rerun with + `--rebuild`. +- **`error XA5207: Could not find android.jar for API level N`** — the .NET + android workload moved to a newer API level. Bump + `ANDROID_SDK_PLATFORM` (and `ANDROID_SDK_BUILD_TOOLS`) in + `Dockerfile.android` to the level the error names, then `--rebuild`. +- **APK won't install over an existing app** — signature mismatch between + debug keys; see the signature caveat above. +- **Everything is broken / start from scratch** — + `docker system prune -a && docker volume rm plm-nuget-cache`, then rerun + the script (images and packages are re-downloaded). + +--- + +## Linux AppImage (future) + +The Linux target currently produces a single-file self-contained binary. +Once a `pupnet.conf` is added to the repository, the `build_linux` function +in `build.sh` can be extended to call PupNet Deploy inside the same +`plm-build-desktop` image to also produce an AppImage with desktop +integration (icon, menu entry). diff --git a/docker/build.sh b/docker/build.sh new file mode 100755 index 0000000..bd51c0d --- /dev/null +++ b/docker/build.sh @@ -0,0 +1,201 @@ +#!/usr/bin/env bash +# Reproducible build script for PalladiumWallet using Docker. +# Run from anywhere; paths are resolved relative to this script. +set -euo pipefail + +# ── Paths ───────────────────────────────────────────────────────────────────── +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_ROOT="$(cd "${SCRIPT_DIR}/.." && pwd)" +DIST_DIR="${PROJECT_ROOT}/dist" + +# ── Docker image names ──────────────────────────────────────────────────────── +IMAGE_DESKTOP="plm-build-desktop" +IMAGE_ANDROID="plm-build-android" + +# Named volume for NuGet package cache (shared across all builds, survives reboots). +NUGET_VOLUME="plm-nuget-cache" + +# ── Helpers ─────────────────────────────────────────────────────────────────── +bold() { printf '\033[1m%s\033[0m\n' "$*"; } +info() { printf '\033[1;34m>>>\033[0m %s\n' "$*"; } +ok() { printf '\033[1;32m✓\033[0m %s\n' "$*"; } +err() { printf '\033[1;31m✗\033[0m %s\n' "$*" >&2; } +die() { err "$*"; exit 1; } + +usage() { + cat </dev/null || die "Docker is not installed or not in PATH." +docker info &>/dev/null || die "Docker daemon is not running." + +# ── Read project version from csproj ───────────────────────────────────────── +CSPROJ="${PROJECT_ROOT}/src/App/PalladiumWallet.App.csproj" +[[ -f "$CSPROJ" ]] || die "Cannot find $CSPROJ — run this script from the repo." +VERSION=$(grep -oP '(?<=)[^<]+' "$CSPROJ") || die "Cannot extract from $CSPROJ." +info "Version: ${VERSION}" + +# ── Ensure NuGet volume exists ──────────────────────────────────────────────── +docker volume inspect "$NUGET_VOLUME" &>/dev/null || \ + docker volume create "$NUGET_VOLUME" > /dev/null + +# ── Image builders ──────────────────────────────────────────────────────────── +ensure_desktop_image() { + if [[ "$REBUILD" == true ]] || ! docker image inspect "$IMAGE_DESKTOP" &>/dev/null; then + info "Building Docker image: ${IMAGE_DESKTOP}" + docker build \ + --tag "$IMAGE_DESKTOP" \ + --file "${SCRIPT_DIR}/Dockerfile.desktop" \ + "${SCRIPT_DIR}" + fi +} + +ensure_android_image() { + if [[ "$REBUILD" == true ]] || ! docker image inspect "$IMAGE_ANDROID" &>/dev/null; then + info "Building Docker image: ${IMAGE_ANDROID}" + docker build \ + --tag "$IMAGE_ANDROID" \ + --file "${SCRIPT_DIR}/Dockerfile.android" \ + "${SCRIPT_DIR}" + fi +} + +# ── Common docker run wrapper ───────────────────────────────────────────────── +# $1 = image name, $2 = inline bash commands to execute inside the container. +# Source is copied to /tmp/build inside the container so bin/obj never pollute +# the repo. Output is written to /output (→ host DIST_DIR sub-folder). +run_build() { + local image="$1" + local commands="$2" + local out_dir="$3" + + mkdir -p "$out_dir" + + docker run --rm \ + --volume "${PROJECT_ROOT}:/src:ro" \ + --volume "${out_dir}:/output" \ + --volume "${NUGET_VOLUME}:/root/.nuget/packages" \ + "$image" \ + bash -euo pipefail -c " + cp -r /src/. /tmp/build + cd /tmp/build + ${commands} + chown -R $(id -u):$(id -g) /output + " +} + +# ── Per-target build functions ──────────────────────────────────────────────── +build_windows() { + ensure_desktop_image + info "Building Windows x64 …" + run_build "$IMAGE_DESKTOP" \ + "dotnet publish src/App.Desktop \ + -r win-x64 \ + -c Release \ + -p:PublishSingleFile=true \ + -p:IncludeNativeLibrariesForSelfExtract=true \ + --self-contained \ + -o /tmp/win-out + cp /tmp/win-out/PalladiumWallet.exe \ + \"/output/PalladiumWallet-${VERSION}-win-x64.exe\"" \ + "${DIST_DIR}/windows" + ok "Windows → dist/windows/PalladiumWallet-${VERSION}-win-x64.exe" +} + +build_linux() { + ensure_desktop_image + info "Building Linux x64 …" + run_build "$IMAGE_DESKTOP" \ + "dotnet publish src/App.Desktop \ + -r linux-x64 \ + -c Release \ + -p:PublishSingleFile=true \ + -p:IncludeNativeLibrariesForSelfExtract=true \ + --self-contained \ + -o /tmp/linux-out + install -m 755 /tmp/linux-out/PalladiumWallet \ + \"/output/PalladiumWallet-${VERSION}-linux-x64\"" \ + "${DIST_DIR}/linux" + ok "Linux → dist/linux/PalladiumWallet-${VERSION}-linux-x64" +} + +build_android() { + ensure_android_image + info "Building Android APK …" + run_build "$IMAGE_ANDROID" \ + "JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 \ + dotnet build src/App.Android \ + -c Release \ + -t:SignAndroidPackage \ + -p:AndroidSdkDirectory=\${ANDROID_HOME} + cp src/App.Android/bin/Release/net10.0-android/*-Signed.apk \ + \"/output/PalladiumWallet-${VERSION}.apk\"" \ + "${DIST_DIR}/android" + ok "Android → dist/android/PalladiumWallet-${VERSION}.apk" +} + +# ── Dispatch ────────────────────────────────────────────────────────────────── +START=$(date +%s) + +case "$TARGET" in + windows) build_windows ;; + linux) build_linux ;; + android) build_android ;; + all) + build_windows + build_linux + build_android + ;; +esac + +ELAPSED=$(( $(date +%s) - START )) +bold "" +bold "Done in ${ELAPSED}s. Artifacts in: ${DIST_DIR}/"