2026-07-02 10:45:37 +02:00
#!/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 <<EOF
$(bold "Usage:") $(basename "$0") [TARGET] [OPTIONS]
$(bold "Targets:")
windows Win x64 single-file executable → dist/windows/
linux Linux x64 single-file binary → dist/linux/
2026-07-02 21:54:15 +02:00
android Android APK (release-signed) → dist/android/
2026-07-02 10:45:37 +02:00
all All three targets
$(bold "Options:")
--rebuild Force rebuild of Docker images (e.g. after Dockerfile change)
-h, --help Show this help
$(bold "Examples:")
$(basename "$0") # interactive menu
$(basename "$0") all # build everything
$(basename "$0") windows # Windows only
$(basename "$0") android --rebuild
EOF
}
# ── Argument parsing ──────────────────────────────────────────────────────────
REBUILD = false
TARGET = ""
for arg in " $@ " ; do
case " $arg " in
windows| linux| android| all) TARGET = " $arg " ; ;
--rebuild) REBUILD = true ; ;
-h| --help) usage; exit 0 ; ;
*) err " Unknown argument: $arg " ; usage; exit 1 ; ;
esac
done
# ── Interactive menu if no target given ───────────────────────────────────────
if [ [ -z " $TARGET " ] ] ; then
bold "PalladiumWallet — reproducible build"
echo ""
PS3 = "Select target: "
options = ( "windows" "linux" "android" "all" "quit" )
select opt in " ${ options [@] } " ; do
case " $opt " in
windows| linux| android| all) TARGET = " $opt " ; break ; ;
quit) echo "Aborted." ; exit 0 ; ;
*) echo "Invalid choice, try again." ; ;
esac
done
echo ""
fi
# ── Preflight checks ──────────────────────────────────────────────────────────
command -v docker & >/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 '(?<=<Version>)[^<]+' " $CSPROJ " ) || die " Cannot extract <Version> 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).
2026-07-02 21:54:15 +02:00
# 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 = ( )
2026-07-02 10:45:37 +02:00
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 " \
2026-07-02 21:54:15 +02:00
" ${ EXTRA_DOCKER_ARGS [@] } " \
2026-07-02 10:45:37 +02:00
" $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
2026-07-02 21:54:15 +02:00
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)."
2026-07-02 10:45:37 +02:00
info "Building Android APK …"
2026-07-02 21:54:15 +02:00
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 <Version> (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 } "
)
2026-07-02 10:45:37 +02:00
run_build " $IMAGE_ANDROID " \
" JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 \
dotnet build src/App.Android \
-c Release \
-t:SignAndroidPackage \
2026-07-02 21:54:15 +02:00
-p:AndroidSdkDirectory=\${ANDROID_HOME} \
-p:ApplicationVersion= ${ VERSION_CODE } \
2026-07-09 08:45:12 +02:00
-p:AndroidKeyStore=true \
2026-07-02 21:54:15 +02:00
-p:AndroidSigningKeyStore=/keystore/release.keystore \
-p:AndroidSigningKeyAlias=palladiumwallet \
-p:AndroidSigningStorePass=\${ANDROID_KS_PASS} \
-p:AndroidSigningKeyPass=\${ANDROID_KEY_PASS}
2026-07-02 10:45:37 +02:00
cp src/App.Android/bin/Release/net10.0-android/*-Signed.apk \
\"/output/PalladiumWallet- ${ VERSION } .apk\" " \
" ${ DIST_DIR } /android "
2026-07-02 21:54:15 +02:00
EXTRA_DOCKER_ARGS = ( )
2026-07-02 10:45:37 +02:00
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 } / "