diff --git a/docker/Dockerfile.android b/docker/Dockerfile.android new file mode 100644 index 0000000..3362c9c --- /dev/null +++ b/docker/Dockerfile.android @@ -0,0 +1,37 @@ +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 +ENV ANDROID_SDK_PLATFORM=34 +ENV ANDROID_SDK_BUILD_TOOLS=34.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..3aec145 --- /dev/null +++ b/docker/README.md @@ -0,0 +1,121 @@ +# PalladiumWallet — Reproducible Builds via Docker + +All three distribution targets (Windows, Linux, Android) are built inside +Docker containers so the output is identical regardless of what is installed +on the host machine. + +**Prerequisites:** Docker Engine (or Docker Desktop) running on the host. +No .NET SDK, JDK, or Android SDK required on the host. + +--- + +## Quick start + +```bash +# From the repository root (or the docker/ folder — both work): +./docker/build.sh +``` + +Running without arguments shows an interactive menu. Pick a target or `all`. + +--- + +## CLI usage + +``` +./docker/build.sh [TARGET] [--rebuild] + +Targets: + windows Win x64 single-file executable + linux Linux x64 self-contained tarball + android Android APK (debug-signed) + all All three targets + +Options: + --rebuild Force rebuild of Docker images (needed after Dockerfile changes) +``` + +### Examples + +```bash +# Build everything +./docker/build.sh all + +# Windows only +./docker/build.sh windows + +# Android, forcing a fresh Docker image +./docker/build.sh android --rebuild +``` + +--- + +## Output + +All artifacts land in `dist/` at the repository root: + +| Target | Path | +|---------|--------------------------------------------------| +| Windows | `dist/windows/PalladiumWallet-{ver}-win-x64.exe` | +| Linux | `dist/linux/PalladiumWallet-{ver}-linux-x64.tar.gz` | +| Android | `dist/android/PalladiumWallet-{ver}.apk` | + +The version is read automatically from `src/App/PalladiumWallet.App.csproj`. + +--- + +## Docker images + +| Image name | Dockerfile | Used for | Approx. size | +|---------------------|-----------------------|--------------------|--------------| +| `plm-build-desktop` | `Dockerfile.desktop` | windows + linux | ~1.5 GB | +| `plm-build-android` | `Dockerfile.android` | android | ~5 GB | + +Images are built automatically on first use and reused on subsequent runs. +Use `--rebuild` only after you modify a Dockerfile. + +### NuGet package cache + +A Docker named volume `plm-nuget-cache` is created on first run and reused +for all subsequent builds, so NuGet packages are not re-downloaded every time. + +To inspect or remove it: + +```bash +docker volume inspect plm-nuget-cache +docker volume rm plm-nuget-cache # forces full re-download on next build +``` + +--- + +## Source isolation + +The repository is mounted **read-only** inside every container. The build +commands work on a copy at `/tmp/build`, so `bin/`, `obj/`, and other +generated files never appear in your working tree. + +--- + +## Android notes + +The `plm-build-android` image bakes in the entire Android SDK and the +`dotnet android workload` (~5 GB total). The first `docker build` for this +image can take 10–20 minutes depending on network speed. + +The resulting APK is **debug-signed** (suitable for sideloading during +development). For Play Store distribution, release signing must be configured +separately. + +If the `commandlinetools` URL in `Dockerfile.android` becomes stale (Google +rotates the build number), update the URL to the latest +[Android command-line tools](https://developer.android.com/studio#command-tools) +and run `./docker/build.sh android --rebuild`. + +--- + +## Linux AppImage (future) + +The Linux target currently produces a self-contained binary tarball. Once a +`pupnet.conf` is added to the repository, the `build_linux` function in +`build.sh` can be extended to call `dotnet pupnet --runtime linux-x64` inside +the same `plm-build-desktop` image to produce an AppImage. diff --git a/docker/build.sh b/docker/build.sh new file mode 100755 index 0000000..d46cf59 --- /dev/null +++ b/docker/build.sh @@ -0,0 +1,197 @@ +#!/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} + " +} + +# ── 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 \ + --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 \ + --self-contained \ + -o /tmp/linux-out + tar -czf \"/output/PalladiumWallet-${VERSION}-linux-x64.tar.gz\" \ + -C /tmp/linux-out ." \ + "${DIST_DIR}/linux" + ok "Linux → dist/linux/PalladiumWallet-${VERSION}-linux-x64.tar.gz" +} + +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}/"