#!/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). # Optional extra `docker run` args (e.g. keystore mount, signing passwords via # -e) can be set by the caller in the EXTRA_DOCKER_ARGS array beforehand. EXTRA_DOCKER_ARGS=() 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" \ "${EXTRA_DOCKER_ARGS[@]}" \ "$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 local keystore="${SCRIPT_DIR}/keystore/release.keystore" [[ -f "$keystore" ]] || die "No release keystore found at docker/keystore/release.keystore. Run ./docker/keystore/generate-keystore.sh once first (see docker/keystore/README.md)." info "Building Android APK …" read -r -s -p "Keystore password: " ANDROID_KS_PASS echo read -r -s -p "Key password (press Enter to reuse the keystore password): " ANDROID_KEY_PASS echo ANDROID_KEY_PASS="${ANDROID_KEY_PASS:-$ANDROID_KS_PASS}" # versionCode derived from (MAJOR.MINOR.PATCH, pre-release suffix # stripped) so it always increases with releases — required for some # installers to accept an in-place update even when the signature matches. local semver="${VERSION%%-*}" IFS='.' read -r VMAJOR VMINOR VPATCH <<< "$semver" local VERSION_CODE=$(( ${VMAJOR:-0} * 10000 + ${VMINOR:-0} * 100 + ${VPATCH:-0} )) info "versionCode: ${VERSION_CODE}" EXTRA_DOCKER_ARGS=( --volume "${keystore}:/keystore/release.keystore:ro" --env "ANDROID_KS_PASS=${ANDROID_KS_PASS}" --env "ANDROID_KEY_PASS=${ANDROID_KEY_PASS}" ) 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} \ -p:ApplicationVersion=${VERSION_CODE} \ -p:AndroidKeyStore=true \ -p:AndroidSigningKeyStore=/keystore/release.keystore \ -p:AndroidSigningKeyAlias=palladiumwallet \ -p:AndroidSigningStorePass=\${ANDROID_KS_PASS} \ -p:AndroidSigningKeyPass=\${ANDROID_KEY_PASS} cp src/App.Android/bin/Release/net10.0-android/*-Signed.apk \ \"/output/PalladiumWallet-${VERSION}.apk\"" \ "${DIST_DIR}/android" EXTRA_DOCKER_ARGS=() 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}/"