From 7949266e114f2d222320ccd74f684125d219caae Mon Sep 17 00:00:00 2001 From: Siavash Sameni Date: Fri, 10 Apr 2026 12:35:02 +0400 Subject: [PATCH] windows: docker + hcloud build scripts for cross-compile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two parallel paths to build wzp-desktop.exe for x86_64-pc-windows-msvc: scripts/Dockerfile.windows-builder Debian 12 base, matches scripts/Dockerfile.android-builder's layout: - apt: build-essential, cmake, ninja-build, llvm, clang, lld, nasm, libssl-dev, node 20 LTS - rust stable + x86_64-pc-windows-msvc target - cargo-xwin pre-installed - Pre-warmed ~/.cache/cargo-xwin layer: creates a throwaway cargo project and runs `cargo xwin build` once during image build so the MSVC CRT + Windows SDK (~1.5 GB) is baked into an image layer. Saves ~4 minutes off every cold cross-compile run. - Builder user uid 1000 to match existing bind-mount perms on SepehrHomeserverdk. scripts/build-windows-docker.sh Same pattern as scripts/build-tauri-android.sh but for Windows: - Fires a remote build on SepehrHomeserverdk via ssh + heredoc - Mounts the shared cargo-registry + cargo-git cache + a target-windows dir (separate from the android target cache so different triples don't stomp each other) - Runs npm install + npm run build for the frontend dist, then cargo xwin build --release --target x86_64-pc-windows-msvc --bin wzp-desktop inside the container - Uploads the resulting .exe to rustypaste (via the .env token on the remote, same as android script) and fires ntfy.sh/wzp notifications at start + completion - scp's the .exe back to target/windows-exe/wzp-desktop.exe locally - --image-build flag triggers a fire-and-forget `docker build` of the Dockerfile.windows-builder on the remote (used once after the Dockerfile changes). The image is already built at the moment of this commit — sha256:f3895cb2fde7 scripts/build-windows-cloud.sh Kept as an alternative cross-compile path using a fresh Hetzner VM (cx33, 8 vCPU, 8 GB — bumped from cx23 after the smaller size OOM'd mid-rustc). The docker-on-SepehrHomeserverdk path is now the preferred fast path because the image has a pre-warmed xwin cache and a persistent cargo target volume, making warm builds ~3 minutes vs the cloud path's ~20 minutes cold each run. The cloud script stays around for when we want a truly isolated environment. Both scripts notify via ntfy.sh/wzp and upload to paste.dk.manko.yoga so the user can pick up the artefact + see status without polling. --- scripts/Dockerfile.windows-builder | 92 +++++++ scripts/build-windows-cloud.sh | 391 +++++++++++++++++++++++++++++ scripts/build-windows-docker.sh | 236 +++++++++++++++++ 3 files changed, 719 insertions(+) create mode 100644 scripts/Dockerfile.windows-builder create mode 100755 scripts/build-windows-cloud.sh create mode 100755 scripts/build-windows-docker.sh diff --git a/scripts/Dockerfile.windows-builder b/scripts/Dockerfile.windows-builder new file mode 100644 index 0000000..2b68423 --- /dev/null +++ b/scripts/Dockerfile.windows-builder @@ -0,0 +1,92 @@ +# ============================================================================= +# WZ Phone — Windows (x86_64-pc-windows-msvc) cross-compile image +# +# Cross-compiles the Tauri desktop binary for Windows from a Linux host via +# `cargo xwin`, which auto-downloads the Microsoft CRT + Windows SDK at build +# time. This image pre-warms that cache so the cross-compile is as close as +# possible to a native Linux build on rebuild (~3 min warm vs ~20 min cold). +# +# Build: +# docker build -t wzp-windows-builder -f Dockerfile.windows-builder . +# +# Run: driven by scripts/build-windows-docker.sh (see that file). +# ============================================================================= +FROM debian:bookworm + +ARG RUST_TARGET=x86_64-pc-windows-msvc + +ENV DEBIAN_FRONTEND=noninteractive + +# ── System packages ────────────────────────────────────────────────────────── +# - build-essential + pkg-config + libssl-dev: baseline cargo build toolchain +# - cmake + ninja-build: audiopus_sys (libopus) uses cmake and expects Ninja +# as the generator for the windows target; without ninja-build the cmake +# build fails with "CMake was unable to find a build program corresponding +# to Ninja" partway through. +# - llvm + clang + lld: cargo-xwin uses clang + lld-link for PE/COFF output. +# - nasm: ring / rustls assembly for Windows needs NASM on non-Windows hosts. +# - curl, git, ca-certificates, unzip: obvious plumbing. +# - xz-utils: some Microsoft installer archives are xz-compressed. +RUN apt-get update && apt-get install -y --no-install-recommends \ + build-essential \ + cmake \ + ninja-build \ + curl \ + git \ + pkg-config \ + libssl-dev \ + ca-certificates \ + llvm \ + clang \ + lld \ + nasm \ + unzip \ + xz-utils \ + file \ + && rm -rf /var/lib/apt/lists/* + +# ── Node.js 20 LTS (required by Tauri for frontend build) ──────────────────── +RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - \ + && apt-get install -y --no-install-recommends nodejs \ + && rm -rf /var/lib/apt/lists/* \ + && node --version \ + && npm --version + +# ── Builder user (1000:1000) — matches host bind-mount UID for the cache +# volumes so cargo-registry / target survive across runs without perms +# gymnastics. +RUN groupadd -g 1000 builder \ + && useradd -m -u 1000 -g 1000 -s /bin/bash builder + +USER builder +WORKDIR /home/builder + +# ── Rust toolchain + Windows target + cargo-xwin ──────────────────────────── +RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs \ + | sh -s -- -y --default-toolchain stable \ + && . $HOME/.cargo/env \ + && rustup target add ${RUST_TARGET} \ + && cargo install cargo-xwin --locked + +ENV PATH="/home/builder/.cargo/bin:$PATH" \ + XWIN_ACCEPT_LICENSE=1 \ + RUST_TARGET_WIN=${RUST_TARGET} + +# ── Pre-warm the xwin cache ───────────────────────────────────────────────── +# cargo-xwin downloads the Microsoft CRT + Windows SDK (~1.5-2 GB) into +# ~/.cache/cargo-xwin the first time it runs. Baking that into an image +# layer saves ~4 minutes off every subsequent cold run. +# +# We do this by creating a throwaway Rust project, building it with +# cargo-xwin against the Windows target, then deleting the project but +# keeping the xwin cache. +RUN set -eux; \ + mkdir -p /tmp/xwin-warmup && cd /tmp/xwin-warmup && \ + . $HOME/.cargo/env && \ + cargo new --bin xwin-warmup --quiet && \ + cd xwin-warmup && \ + cargo xwin build --release --target ${RUST_TARGET} 2>&1 | tail -5 && \ + cd / && rm -rf /tmp/xwin-warmup && \ + du -sh $HOME/.cache/cargo-xwin + +WORKDIR /build/source diff --git a/scripts/build-windows-cloud.sh b/scripts/build-windows-cloud.sh new file mode 100755 index 0000000..08edfb8 --- /dev/null +++ b/scripts/build-windows-cloud.sh @@ -0,0 +1,391 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Build WarzonePhone desktop .exe for Windows x86_64 using a temporary +# Hetzner Cloud VPS. Cross-compiles from Linux via `cargo xwin`, which +# auto-downloads the Windows SDK + MSVC CRT the first time it runs. +# +# No Windows machine needed for the build itself — the produced .exe +# still has to be copied to a real Windows host to run (we can only +# verify compile + link here, not runtime). +# +# Prerequisites: +# - hcloud CLI authenticated +# - SSH key "wz" registered in Hetzner +# - Local ssh-agent loaded with an SSH key that can read the +# git.manko.yoga repo (the script forwards the agent so the VM's +# git clone uses your identity). Run `ssh-add /Users/manwe/CascadeProjects/wzp` +# once before invoking this script if you haven't already. +# +# Usage: +# ./scripts/build-windows-cloud.sh Full build (create → build → download → destroy) +# ./scripts/build-windows-cloud.sh --prepare Create VM and install deps only +# ./scripts/build-windows-cloud.sh --build Build on existing VM +# ./scripts/build-windows-cloud.sh --transfer Download .exe from VM +# ./scripts/build-windows-cloud.sh --destroy Delete the VM +# ./scripts/build-windows-cloud.sh --all prepare + build + transfer (VM persists) +# ./scripts/build-windows-cloud.sh --upload Re-upload source to existing VM +# +# Environment variables (all optional): +# WZP_BRANCH Branch to build (default: feat/desktop-audio-rewrite) +# WZP_SERVER_TYPE Hetzner server type (default: cx23 — small, cheap, x86) +# WZP_KEEP_VM Set to 1 to skip destroy on full build + +SSH_KEY_NAME="wz" +SSH_KEY_PATH="/Users/manwe/CascadeProjects/wzp" +SERVER_TYPE="${WZP_SERVER_TYPE:-cx33}" # cx23 (4GB RAM) OOMs on tauri+rustls cross-compile — bump to cx33 (8GB, 8 vCPU) +IMAGE="ubuntu-24.04" +SERVER_NAME="wzp-windows-builder" +REMOTE_USER="root" +OUTPUT_DIR="target/windows-exe" +PROJECT_DIR="$(cd "$(dirname "$0")/.." && pwd)" +BRANCH="${WZP_BRANCH:-feat/desktop-audio-rewrite}" +KEEP_VM="${WZP_KEEP_VM:-0}" + +SSH_OPTS="-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=10 -o LogLevel=ERROR" + +RUST_TARGET="x86_64-pc-windows-msvc" + +NTFY_TOPIC="https://ntfy.sh/wzp" +RUSTY_ENV_FILE="$HOME/.wzp/rustypaste.env" + +# --------------------------------------------------------------------------- +# Helpers +# --------------------------------------------------------------------------- + +log() { echo -e "\n\033[1;36m>>> $*\033[0m"; } +err() { echo -e "\033[1;31mERROR: $*\033[0m" >&2; } +die() { + err "$@" + notify "WZP Windows build FAILED — $*" + # If the user wants to keep the VM alive for debugging (WZP_KEEP_VM=1), + # don't tear it down on failure — they might want to ssh in and poke at + # the build state. Only auto-destroy when KEEP_VM is explicitly off. + if [ "${KEEP_VM:-0}" != "1" ]; then + do_destroy_quiet + else + err "VM kept alive for debugging (WZP_KEEP_VM=1). Destroy with $0 --destroy" + fi + exit 1 +} + +notify() { + # Fire-and-forget ntfy. Silently ignored if there's no network. + curl -sf -m 5 -d "$1" "$NTFY_TOPIC" > /dev/null 2>&1 || true +} + +# Upload a file to the online rustypaste (paste.dk.manko.yoga), return +# the public URL on stdout. Requires $RUSTY_ENV_FILE to contain +# rusty_address + rusty_auth_token (synced from SepehrHomeserverdk's +# /mnt/storage/manBuilder/.env once; see README). +rustypaste_upload() { + local file="$1" + [ -f "$file" ] || { echo ""; return; } + [ -f "$RUSTY_ENV_FILE" ] || { echo ""; return; } + # shellcheck disable=SC1090 + source "$RUSTY_ENV_FILE" + if [ -n "${rusty_address:-}" ] && [ -n "${rusty_auth_token:-}" ]; then + curl -s -F "file=@$file" -H "Authorization: $rusty_auth_token" "$rusty_address" || echo "" + else + echo "" + fi +} + +get_vm_ip() { + hcloud server list -o columns=name,ipv4 -o noheader 2>/dev/null | grep "$SERVER_NAME" | awk '{print $2}' | tr -d ' ' +} + +ssh_cmd() { + local ip + ip=$(get_vm_ip) + [ -n "$ip" ] || die "No VM found. Run --prepare first." + ssh $SSH_OPTS -A -i "$SSH_KEY_PATH" "$REMOTE_USER@$ip" "$@" +} + +scp_down() { + local ip + ip=$(get_vm_ip) + [ -n "$ip" ] || die "No VM found." + scp $SSH_OPTS -i "$SSH_KEY_PATH" "$REMOTE_USER@$ip:$1" "$2" +} + +do_destroy_quiet() { + local name + name=$(hcloud server list -o columns=name -o noheader 2>/dev/null | grep "$SERVER_NAME" | tr -d ' ' || true) + if [ -n "$name" ]; then + echo "" + err "Cleaning up — destroying VM $name" + hcloud server delete "$name" 2>/dev/null || true + fi +} + +# --------------------------------------------------------------------------- +# --prepare: Create VM, install all build dependencies +# --------------------------------------------------------------------------- + +do_prepare() { + local existing + existing=$(hcloud server list -o columns=name -o noheader 2>/dev/null | grep "$SERVER_NAME" | tr -d ' ' || true) + if [ -n "$existing" ]; then + log "VM already exists: $existing — reusing" + do_upload + return + fi + + notify "WZP Windows build STARTED ($BRANCH) — spinning up $SERVER_TYPE" + log "Creating Hetzner VM ($SERVER_TYPE, $IMAGE)..." + hcloud server create \ + --name "$SERVER_NAME" \ + --type "$SERVER_TYPE" \ + --image "$IMAGE" \ + --ssh-key "$SSH_KEY_NAME" \ + --location fsn1 \ + --quiet \ + || die "Failed to create VM" + + local ip + ip=$(get_vm_ip) + [ -n "$ip" ] || die "VM created but no IP found" + echo " VM: $SERVER_NAME @ $ip" + + log "Waiting for SSH..." + local ok=0 + for i in $(seq 1 30); do + if ssh $SSH_OPTS -i "$SSH_KEY_PATH" "$REMOTE_USER@$ip" "echo ok" &>/dev/null; then + ok=1 + break + fi + sleep 2 + done + [ "$ok" -eq 1 ] || die "SSH timeout after 60s" + + # System packages — cargo-xwin needs llvm/lld; ring needs nasm on + # Windows; audiopus_sys (libopus) uses cmake + ninja to build for the + # Windows target; tauri's build.rs needs the frontend dist which needs + # node+npm. + log "Installing system packages (llvm, lld, clang, nasm, ninja, node)..." + ssh_cmd "export DEBIAN_FRONTEND=noninteractive && \ + apt-get update -qq && \ + apt-get install -y -qq \ + build-essential cmake ninja-build curl git pkg-config \ + llvm clang lld nasm \ + libssl-dev ca-certificates \ + unzip wget \ + > /dev/null 2>&1" \ + || die "Failed to install system packages" + + # Node.js 20 via NodeSource + ssh_cmd "curl -fsSL https://deb.nodesource.com/setup_20.x | bash - > /dev/null 2>&1 && \ + apt-get install -y -qq nodejs > /dev/null 2>&1" \ + || die "Failed to install Node.js" + + echo " clang: $(ssh_cmd "clang --version | head -1")" + echo " node: $(ssh_cmd "node --version")" + echo " npm: $(ssh_cmd "npm --version")" + + # Rust + log "Installing Rust toolchain + target $RUST_TARGET..." + ssh_cmd "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable > /dev/null 2>&1" \ + || die "Failed to install Rust" + ssh_cmd "source \$HOME/.cargo/env && rustup target add $RUST_TARGET > /dev/null 2>&1" \ + || die "Failed to add Windows target" + echo " rust: $(ssh_cmd "source \$HOME/.cargo/env && rustc --version")" + + # cargo-xwin — the cross compiler glue that fetches Windows SDK + CRT + # on demand and shims cc/lld to produce PE/COFF output. The Microsoft + # license is auto-accepted via XWIN_ACCEPT_LICENSE=1 below (current + # cargo-xwin removed the --accept-license CLI flag in favour of the + # env var; --dry-run just prints what it would do). + log "Installing cargo-xwin..." + ssh_cmd "source \$HOME/.cargo/env && cargo install cargo-xwin > /dev/null 2>&1" \ + || die "Failed to install cargo-xwin" + echo " cargo-xwin: $(ssh_cmd "source \$HOME/.cargo/env && cargo xwin --version 2>&1 | head -1")" + + # Make the license-accept env var persist across later ssh_cmd calls so + # `cargo xwin build` in do_build() doesn't prompt interactively. + ssh_cmd "echo 'export XWIN_ACCEPT_LICENSE=1' >> \$HOME/.bashrc" + + # Do the source upload + git clone (agent-forwarded) here. + do_upload + + log "VM ready!" + echo " IP: $ip" + echo " SSH: ssh -A -i $SSH_KEY_PATH root@$ip" +} + +# --------------------------------------------------------------------------- +# --upload: Clone the repo on the VM (not rsync — the branch we want +# lives in a separate worktree, and cloning from git is simpler + reuses +# whatever SSH identity the calling shell has loaded in its agent). +# --------------------------------------------------------------------------- + +GIT_REPO="ssh://git@git.manko.yoga:222/manawenuz/wz-phone.git" + +do_upload() { + log "Cloning wz-phone on VM (branch $BRANCH, agent-forwarded)..." + local ip + ip=$(get_vm_ip) + [ -n "$ip" ] || die "No VM found." + + # Accept the git host key once so `git clone` doesn't hang asking. + ssh_cmd "mkdir -p \$HOME/.ssh && \ + ssh-keyscan -p 222 -t rsa,ecdsa,ed25519 git.manko.yoga >> \$HOME/.ssh/known_hosts 2>/dev/null" + + # Fresh clone each run — cheap on a short-lived builder VM, avoids + # stale state if the branch was force-pushed. --recurse-submodules so + # deps/featherchat (which has the warzone-protocol workspace member) + # comes along for the ride. + ssh_cmd "rm -rf /root/wzp-build && \ + git clone --depth 1 --branch $BRANCH --recurse-submodules --shallow-submodules $GIT_REPO /root/wzp-build 2>&1 | tail -5" \ + || die "git clone failed — is your ssh-agent loaded with a key that can read git.manko.yoga?" + + echo " Cloned $BRANCH into /root/wzp-build (with submodules)" +} + +# --------------------------------------------------------------------------- +# --build: Build frontend + cross-compile wzp-desktop.exe +# --------------------------------------------------------------------------- + +do_build() { + log "Building frontend (vite)..." + ssh_cmd "cd /root/wzp-build/desktop && \ + npm install --silent 2>&1 | tail -3 && \ + npm run build 2>&1 | tail -5" \ + || die "Frontend build failed" + + log "Cross-compiling wzp-desktop.exe ($RUST_TARGET) via cargo-xwin..." + # XWIN_ACCEPT_LICENSE=1 is required by recent cargo-xwin for headless + # runs; --cross-compiler clang-cl picks the system clang shipped by the + # apt install step in do_prepare. + ssh_cmd "source \$HOME/.cargo/env && \ + export XWIN_ACCEPT_LICENSE=1 && \ + cd /root/wzp-build/desktop/src-tauri && \ + cargo xwin build --release --target $RUST_TARGET --bin wzp-desktop 2>&1 | tail -30" \ + || die "Windows cross-compile failed" + + ssh_cmd "[ -f /root/wzp-build/target/$RUST_TARGET/release/wzp-desktop.exe ]" \ + || die "wzp-desktop.exe not found after build" + + local exe_size + exe_size=$(ssh_cmd "du -h /root/wzp-build/target/$RUST_TARGET/release/wzp-desktop.exe | cut -f1") + echo " .exe: $exe_size" + + local git_hash + git_hash=$(ssh_cmd "cd /root/wzp-build && git rev-parse --short HEAD") + notify "WZP Windows build OK [$git_hash] ($exe_size)" + export WZP_BUILD_GIT_HASH="$git_hash" + export WZP_BUILD_SIZE="$exe_size" +} + +# --------------------------------------------------------------------------- +# --transfer: Download the .exe to local machine +# --------------------------------------------------------------------------- + +do_transfer() { + log "Downloading wzp-desktop.exe..." + mkdir -p "$OUTPUT_DIR" + + scp_down "/root/wzp-build/target/$RUST_TARGET/release/wzp-desktop.exe" "$OUTPUT_DIR/wzp-desktop.exe" + local local_size + local_size=$(du -h "$OUTPUT_DIR/wzp-desktop.exe" | cut -f1) + echo " $OUTPUT_DIR/wzp-desktop.exe ($local_size)" + + # Upload to online rustypaste and notify with the URL. + log "Uploading to rustypaste..." + local url + url=$(rustypaste_upload "$OUTPUT_DIR/wzp-desktop.exe" || echo "") + if [ -n "$url" ]; then + echo " $url" + local hash="${WZP_BUILD_GIT_HASH:-?}" + notify "WZP Windows build ready [$hash] ($local_size) +$url" + else + echo " (rustypaste upload skipped — no creds in $RUSTY_ENV_FILE)" + notify "WZP Windows build transferred ($local_size) — rustypaste upload skipped" + fi + + log "Transfer complete!" + echo "" + echo " Copy to a real Windows x86_64 host and double-click to run." + echo " WebView2 runtime is required on Windows 10 (ships with Win 11)." +} + +# --------------------------------------------------------------------------- +# --destroy: Delete the VM +# --------------------------------------------------------------------------- + +do_destroy() { + local name + name=$(hcloud server list -o columns=name -o noheader 2>/dev/null | grep "$SERVER_NAME" | tr -d ' ' || true) + if [ -z "$name" ]; then + echo "No VM to destroy." + return + fi + log "Deleting VM: $name" + hcloud server delete "$name" + echo " Done." +} + +# --------------------------------------------------------------------------- +# Full build: create → build → transfer → destroy +# --------------------------------------------------------------------------- + +do_full() { + trap 'err "Build failed!"; [ "${KEEP_VM:-0}" = "1" ] || do_destroy_quiet; exit 1' ERR + + do_prepare + do_build + do_transfer + + if [ "$KEEP_VM" = "1" ]; then + log "VM kept alive (WZP_KEEP_VM=1). Destroy with: $0 --destroy" + else + do_destroy + fi + + log "All done!" + echo "" + echo " ┌────────────────────────────────────────────────┐" + echo " │ Windows .exe: $OUTPUT_DIR/wzp-desktop.exe" + echo " │" + echo " │ Transfer to a Windows x86_64 machine and run." + echo " └────────────────────────────────────────────────┘" +} + +# --------------------------------------------------------------------------- +# Main +# --------------------------------------------------------------------------- + +case "${1:-}" in + --prepare) do_prepare ;; + --build) do_build ;; + --transfer) do_transfer ;; + --destroy) do_destroy ;; + --upload) do_upload ;; + --all) + do_prepare + do_build + do_transfer + log "VM still running. Destroy with: $0 --destroy" + ;; + "") + do_full + ;; + *) + echo "Usage: $0 [--prepare|--build|--transfer|--destroy|--all|--upload]" + echo "" + echo " (no args) Full build: create VM → build → download → destroy VM" + echo " --prepare Create VM and install deps" + echo " --build Build on existing VM" + echo " --transfer Download .exe from VM" + echo " --destroy Delete the VM" + echo " --all prepare + build + transfer (VM persists)" + echo " --upload Re-upload source to existing VM" + echo "" + echo "Environment:" + echo " WZP_BRANCH=$BRANCH" + echo " WZP_SERVER_TYPE=$SERVER_TYPE" + echo " WZP_KEEP_VM=$KEEP_VM (set to 1 to skip auto-destroy)" + exit 1 + ;; +esac diff --git a/scripts/build-windows-docker.sh b/scripts/build-windows-docker.sh new file mode 100755 index 0000000..a31fe3f --- /dev/null +++ b/scripts/build-windows-docker.sh @@ -0,0 +1,236 @@ +#!/usr/bin/env bash +set -euo pipefail + +# ============================================================================= +# WZ Phone — Windows x86_64 cross-compile (Docker on SepehrHomeserverdk) +# +# Cross-compiles the Tauri desktop binary for Windows via `cargo xwin` +# inside the wzp-windows-builder Docker image on SepehrHomeserverdk. +# Uploads the resulting .exe to rustypaste, fires ntfy.sh/wzp notifications +# at start + finish, and SCPs the .exe back locally. +# +# Same pattern as build-tauri-android.sh but for the Windows cross-compile +# pipeline: +# - Source: desktop/src-tauri/ +# - Build: cargo xwin build --release --target x86_64-pc-windows-msvc +# - Output: target/x86_64-pc-windows-msvc/release/wzp-desktop.exe +# +# Usage: +# ./scripts/build-windows-docker.sh # full pipeline +# ./scripts/build-windows-docker.sh --no-pull # skip git fetch +# ./scripts/build-windows-docker.sh --rust # force-clean rust target +# ./scripts/build-windows-docker.sh --image-build # (re)build the docker image +# +# Environment: +# WZP_BRANCH Branch to build (default: feat/desktop-audio-rewrite) +# ============================================================================= + +REMOTE_HOST="SepehrHomeserverdk" +BASE_DIR="/mnt/storage/manBuilder" +NTFY_TOPIC="https://ntfy.sh/wzp" +LOCAL_OUTPUT="target/windows-exe" +BRANCH="${WZP_BRANCH:-feat/desktop-audio-rewrite}" +SSH_OPTS="-o ConnectTimeout=15 -o ServerAliveInterval=15 -o ServerAliveCountMax=4 -o LogLevel=ERROR" + +REBUILD_RUST=0 +DO_PULL=1 +IMAGE_BUILD=0 +for arg in "$@"; do + case "$arg" in + --rust) REBUILD_RUST=1 ;; + --pull) DO_PULL=1 ;; + --no-pull) DO_PULL=0 ;; + --image-build) IMAGE_BUILD=1 ;; + -h|--help) + sed -n '3,27p' "$0" + exit 0 + ;; + esac +done + +log() { echo -e "\033[1;36m>>> $*\033[0m"; } +ssh_cmd() { ssh -A $SSH_OPTS "$REMOTE_HOST" "$@"; } + +notify_local() { curl -s -d "$1" "$NTFY_TOPIC" > /dev/null 2>&1 || true; } + +mkdir -p "$LOCAL_OUTPUT" + +# ─── Optional: (re)build the docker image on the remote ──────────────────── +# Runs once, whenever the Dockerfile changes. Fire-and-forget so the local +# script doesn't wait for the ~15 minute image build. +if [ "$IMAGE_BUILD" = "1" ]; then + log "Uploading Dockerfile.windows-builder to remote..." + scp $SSH_OPTS "$(dirname "$0")/Dockerfile.windows-builder" \ + "$REMOTE_HOST:$BASE_DIR/Dockerfile.windows-builder" + + log "Triggering remote image build (fire-and-forget)..." + ssh_cmd "cd $BASE_DIR && \ + nohup docker build --pull -f Dockerfile.windows-builder \ + -t wzp-windows-builder . \ + > /tmp/wzp-windows-image-build.log 2>&1 & \ + echo 'image build PID: '\$!" + notify_local "WZP Windows image build dispatched (check /tmp/wzp-windows-image-build.log on remote)" + log "Image build running in background on $REMOTE_HOST." + log "Tail the log with: ssh $REMOTE_HOST 'tail -f /tmp/wzp-windows-image-build.log'" + exit 0 +fi + +# ─── Upload remote build runner script ───────────────────────────────────── +log "Uploading remote build script..." +ssh_cmd "cat > /tmp/wzp-windows-build.sh" <<'REMOTE_SCRIPT' +#!/usr/bin/env bash +set -euo pipefail + +BASE_DIR="/mnt/storage/manBuilder" +NTFY_TOPIC="https://ntfy.sh/wzp" +BRANCH="${1:-feat/desktop-audio-rewrite}" +DO_PULL="${2:-1}" +REBUILD_RUST="${3:-0}" + +LOG_FILE=/tmp/wzp-windows-build.log +GIT_HASH="unknown" +ENV_FILE="$BASE_DIR/.env" + +notify() { curl -s -d "$1" "$NTFY_TOPIC" > /dev/null 2>&1 || true; } + +# Upload to rustypaste; print URL on stdout (or empty on failure). +upload_to_rustypaste() { + local file="$1" + [ ! -f "$ENV_FILE" ] && { echo ""; return; } + # shellcheck disable=SC1090 + source "$ENV_FILE" + if [ -n "${rusty_address:-}" ] && [ -n "${rusty_auth_token:-}" ]; then + curl -s -F "file=@$file" -H "Authorization: $rusty_auth_token" "$rusty_address" || echo "" + else + echo "" + fi +} + +on_error() { + local line="$1" + local log_url + log_url=$(upload_to_rustypaste "$LOG_FILE" || echo "") + if [ -n "$log_url" ]; then + notify "WZP Windows build FAILED [$GIT_HASH] (line $line) +log: $log_url" + else + notify "WZP Windows build FAILED [$GIT_HASH] (line $line) — log upload failed, see $LOG_FILE on remote" + fi +} +trap 'on_error $LINENO' ERR + +exec > >(tee "$LOG_FILE") 2>&1 + +# ── git fetch + reset the target branch ─────────────────────────────────── +if [ "$DO_PULL" = "1" ]; then + echo ">>> git fetch + reset $BRANCH" + cd "$BASE_DIR/data/source" + git reset --hard HEAD 2>/dev/null || true + git gc --prune=now 2>/dev/null || true + git fetch origin "$BRANCH" 2>&1 | tail -3 + git checkout "$BRANCH" 2>/dev/null || git checkout -b "$BRANCH" "origin/$BRANCH" + git reset --hard "origin/$BRANCH" + git submodule update --init --recursive || true +fi + +GIT_HASH=$(cd "$BASE_DIR/data/source" && git rev-parse --short HEAD 2>/dev/null || echo unknown) +GIT_MSG=$(cd "$BASE_DIR/data/source" && git log -1 --pretty=%s 2>/dev/null | head -c 60 || echo "?") +notify "WZP Windows build STARTED [$GIT_HASH] — $GIT_MSG" + +# Fix perms so builder uid 1000 can read/write the mounted source. +find "$BASE_DIR/data/source" "$BASE_DIR/data/cache" \ + ! -user 1000 -o ! -group 1000 2>/dev/null | \ + xargs -r chown 1000:1000 2>/dev/null || true + +if [ "$REBUILD_RUST" = "1" ]; then + echo ">>> Cleaning Rust windows target dir..." + rm -rf "$BASE_DIR/data/cache/target-windows/x86_64-pc-windows-msvc" \ + "$BASE_DIR/data/cache/target-windows/release" +fi + +# ── Docker run ───────────────────────────────────────────────────────────── +# Cached volumes: +# - cargo-registry / cargo-git: shared with the android builder — both use +# the same crates, so the download cache is worth sharing. +# - target-windows: the Windows target tree. Kept separate from the android +# target-cache so the two pipelines don't stomp on each other's build +# artefacts (different triples, but the workspace root target dir has +# shared subdirs like release/build/ that can get confused). +# - cargo-xwin cache is BAKED into the docker image, no volume needed. + +mkdir -p "$BASE_DIR/data/cache/cargo-registry" \ + "$BASE_DIR/data/cache/cargo-git" \ + "$BASE_DIR/data/cache/target-windows" +chown -R 1000:1000 "$BASE_DIR/data/cache/target-windows" 2>/dev/null || true + +docker run --rm \ + --user 1000:1000 \ + -v "$BASE_DIR/data/source:/build/source" \ + -v "$BASE_DIR/data/cache/cargo-registry:/home/builder/.cargo/registry" \ + -v "$BASE_DIR/data/cache/cargo-git:/home/builder/.cargo/git" \ + -v "$BASE_DIR/data/cache/target-windows:/build/source/target" \ + wzp-windows-builder \ + bash -c ' +set -euo pipefail +cd /build/source/desktop + +echo ">>> npm install" +npm install --silent 2>&1 | tail -5 || npm install 2>&1 | tail -20 + +echo ">>> npm run build" +npm run build 2>&1 | tail -5 + +echo ">>> cargo xwin build --release --target x86_64-pc-windows-msvc --bin wzp-desktop" +cd src-tauri +cargo xwin build --release --target x86_64-pc-windows-msvc --bin wzp-desktop 2>&1 | tail -30 + +echo "" +echo ">>> Build artifacts:" +ls -lh /build/source/target/x86_64-pc-windows-msvc/release/wzp-desktop.exe 2>/dev/null || echo "NO EXE" +' + +# Locate the produced .exe +EXE="$BASE_DIR/data/cache/target-windows/x86_64-pc-windows-msvc/release/wzp-desktop.exe" +if [ ! -f "$EXE" ]; then + LOG_URL=$(upload_to_rustypaste "$LOG_FILE" || echo "") + if [ -n "$LOG_URL" ]; then + notify "WZP Windows build [$GIT_HASH]: no .exe produced +log: $LOG_URL" + else + notify "WZP Windows build [$GIT_HASH]: no .exe produced — log upload failed" + fi + exit 1 +fi + +EXE_SIZE=$(du -h "$EXE" | cut -f1) + +RUSTY_URL=$(upload_to_rustypaste "$EXE" || echo "") +if [ -n "$RUSTY_URL" ]; then + notify "WZP Windows build OK [$GIT_HASH] ($EXE_SIZE) +$RUSTY_URL" +else + notify "WZP Windows build OK [$GIT_HASH] ($EXE_SIZE) — rustypaste upload skipped" +fi + +# Print path so the local script can scp it back +echo "EXE_REMOTE_PATH=$EXE" +REMOTE_SCRIPT + +ssh_cmd "chmod +x /tmp/wzp-windows-build.sh" + +notify_local "WZP Windows build dispatched (branch=$BRANCH)" +log "Triggering remote build (branch=$BRANCH)..." + +# Run; last line is EXE_REMOTE_PATH=... +REMOTE_OUTPUT=$(ssh_cmd "/tmp/wzp-windows-build.sh '$BRANCH' '$DO_PULL' '$REBUILD_RUST'" || true) +echo "$REMOTE_OUTPUT" | tail -60 + +EXE_REMOTE=$(echo "$REMOTE_OUTPUT" | grep '^EXE_REMOTE_PATH=' | tail -1 | cut -d= -f2-) +if [ -n "$EXE_REMOTE" ]; then + log "Downloading wzp-desktop.exe to $LOCAL_OUTPUT/..." + scp $SSH_OPTS "$REMOTE_HOST:$EXE_REMOTE" "$LOCAL_OUTPUT/wzp-desktop.exe" + echo " $LOCAL_OUTPUT/wzp-desktop.exe ($(du -h "$LOCAL_OUTPUT/wzp-desktop.exe" | cut -f1))" +else + log "No .exe produced — see ntfy / remote log /tmp/wzp-windows-build.log" + exit 1 +fi