Files
wz-phone/scripts/build-windows-cloud.sh
Siavash Sameni 7949266e11
Some checks failed
Mirror to GitHub / mirror (push) Failing after 38s
Build Release Binaries / build-amd64 (push) Failing after 3m59s
windows: docker + hcloud build scripts for cross-compile
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.
2026-04-10 12:35:02 +04:00

392 lines
14 KiB
Bash
Executable File

#!/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