From 5d8549af2ed33cd4daeb58694cbf0f1ba23ca877 Mon Sep 17 00:00:00 2001 From: Garrett Mills Date: Fri, 16 Oct 2026 20:11:02 -0500 Subject: [PATCH] Windows installer: cross-build on Linux and publish to a downloads folder scripts/windows-publish.sh builds the NSIS setup.exe for x86_64-pc-windows-msvc through cargo-xwin, versioned like the RPM and APK, and copies it into a folder to serve: the newest three installers, a stable mtgc-latest-x64-setup.exe, and SHA256SUMS. --- README.md | 39 ++++++++++ package.json | 3 +- scripts/windows-publish.sh | 149 +++++++++++++++++++++++++++++++++++++ 3 files changed, 190 insertions(+), 1 deletion(-) create mode 100755 scripts/windows-publish.sh diff --git a/README.md b/README.md index eae2baa..96f66b1 100644 --- a/README.md +++ b/README.md @@ -138,6 +138,45 @@ From then on, `sudo dnf upgrade` picks up new releases. - The RPM is built against this machine's libraries, so it installs on this Fedora release and newer. +### Distributing to Windows + +The Windows installer is cross-built here on Linux and published to a plain +downloads folder you host. `scripts/windows-publish.sh` builds it through +[cargo-xwin](https://github.com/rust-cross/cargo-xwin) and needs, once: + +```bash +sudo dnf install mingw-nsis-base llvm lld clang +``` + +```bash +cargo install --locked cargo-xwin +``` + +```bash +rustup target add x86_64-pc-windows-msvc +``` + +Then, for each release: + +```bash +./scripts/windows-publish.sh ~/win-mtgc # or: npm run windows:publish -- ~/win-mtgc +``` + +On Windows, download and run the setup.exe. A newer one upgrades the +installed app in place. + +- **Serve the whole directory.** It holds the versioned installers + (`mtgc_0.1._x64-setup.exe`, the newest three), `mtgc-latest-x64-setup.exe` + as a stable link to the newest, and `SHA256SUMS`. +- Only the NSIS `setup.exe` can be built off Windows; an MSI needs WiX, which + runs only on Windows. The first build downloads the MSVC CRT and Windows SDK + into `~/.cache/cargo-xwin`. +- The installer isn't code-signed, so SmartScreen warns about an unknown + publisher the first time: **More info → Run anyway**. It fetches WebView2 if + the machine lacks it (Windows 10 and 11 normally ship it). +- Versions work as on Android: `0.1.`, see + [Distributing to phones](#distributing-to-phones-f-droid). + ## Using it - **Add Cards** — type a name (typos OK); ↑/↓ then Enter adds a copy. diff --git a/package.json b/package.json index 28b6f80..288a96a 100644 --- a/package.json +++ b/package.json @@ -11,7 +11,8 @@ "sync-server:build": "scripts/sync-server-image.sh build", "sync-server:push": "scripts/sync-server-image.sh push", "android:publish": "scripts/fdroid-publish.sh", - "rpm:publish": "scripts/rpm-publish.sh" + "rpm:publish": "scripts/rpm-publish.sh", + "windows:publish": "scripts/windows-publish.sh" }, "dependencies": { "@tauri-apps/api": "^2", diff --git a/scripts/windows-publish.sh b/scripts/windows-publish.sh new file mode 100755 index 0000000..455c315 --- /dev/null +++ b/scripts/windows-publish.sh @@ -0,0 +1,149 @@ +#!/usr/bin/env bash +# +# windows-publish.sh — cross-build the Windows installer and add it to a downloads folder. +# +# Usage: +# ./scripts/windows-publish.sh ~/win-mtgc +# +# Or through npm: `npm run windows:publish -- ~/win-mtgc`. +# +# Options: +# --allow-dirty publish uncommitted changes; the file name gets a -dirty suffix +# +# Environment: +# MTGC_WINDOWS_DIR downloads directory, if not given as an argument +# +# Builds on Linux for x86_64-pc-windows-msvc through cargo-xwin, which fetches +# the MSVC CRT and Windows SDK into ~/.cache/cargo-xwin the first time. Only +# the NSIS installer can be made off Windows (MSI needs WiX). The directory is +# public — serve it as is. Needs `sudo dnf install mingw-nsis-base llvm lld +# clang`, `cargo install --locked cargo-xwin`, `rustup target add +# x86_64-pc-windows-msvc`, and the desktop setup from ./scripts/devel-setup.sh. +# +set -euo pipefail + +usage() { + sed -n '3,22p' "$0" | sed 's/^# \{0,1\}//' + exit "${1:-0}" +} + +info() { printf '\033[1;34m==>\033[0m %s\n' "$*"; } +warn() { printf '\033[1;33m[warn]\033[0m %s\n' "$*"; } +err() { printf '\033[1;31m[err]\033[0m %s\n' "$*" >&2; } + +PACKAGE="mtgc" +TARGET="x86_64-pc-windows-msvc" +LATEST="$PACKAGE-latest-x64-setup.exe" +KEEP=3 # versions kept in the folder, so a bad release can be rolled back + +allow_dirty=false +dir="${MTGC_WINDOWS_DIR:-}" +while [ $# -gt 0 ]; do + case "$1" in + --allow-dirty) allow_dirty=true ;; + -h | --help) usage ;; + -*) usage 1 ;; + *) dir="$1" ;; + esac + shift +done +[ -n "$dir" ] || usage 1 +mkdir -p "$dir" +dir="$(cd "$dir" && pwd)" + +repo_root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" +cd "$repo_root" + +# --- 1. Tools --------------------------------------------------------------- + +for tool in makensis llvm-rc lld-link clang; do + if ! command -v "$tool" >/dev/null 2>&1; then + err "$tool not found. Install them: sudo dnf install mingw-nsis-base llvm lld clang" + exit 1 + fi +done +if ! command -v cargo-xwin >/dev/null 2>&1; then + err "cargo-xwin not found. Install it: cargo install --locked cargo-xwin" + exit 1 +fi +if command -v rustup >/dev/null 2>&1 && ! rustup target list --installed | grep -qxF "$TARGET"; then + err "the $TARGET Rust target is missing. Add it: rustup target add $TARGET" + exit 1 +fi + +# --- 2. Version ------------------------------------------------------------- +# +# The installer's own version stays numeric (NSIS insists); a dirty build is +# only marked in the file name. + +source scripts/lib/release-version.sh +file_version="$release_version" +if $release_dirty; then + if ! $allow_dirty; then + err "the working tree has uncommitted changes; commit them or pass --allow-dirty" + exit 1 + fi + file_version="$release_version-dirty" +fi + +# --- 3. Only ever move forward ---------------------------------------------- + +newest=0 +for exe in "$dir/${PACKAGE}"_*_x64-setup.exe; do + [ -e "$exe" ] || continue + patch="${exe##*/${PACKAGE}_}" + patch="${patch%_x64-setup.exe}" + patch="${patch%-dirty}" + patch="${patch##*.}" + [ "$patch" -gt "$newest" ] && newest="$patch" +done +if [ "$release_count" -le "$newest" ]; then + err "version $release_version is not newer than the folder's x.y.$newest; commit something first" + exit 1 +fi + +# --- 4. Build --------------------------------------------------------------- + +info "building $file_version for $TARGET" +outputs="target/$TARGET/release/bundle/nsis" +rm -rf "$outputs" +cargo tauri build --runner cargo-xwin --target "$TARGET" --bundles nsis --ci \ + --config "{\"version\":\"$release_version\"}" + +built="$(find "$outputs" -name '*-setup.exe' | head -1)" +if [ -z "$built" ]; then + err "the build produced no installer under $outputs" + exit 1 +fi + +# --- 5. Publish ------------------------------------------------------------- + +name="${PACKAGE}_${file_version}_x64-setup.exe" +cp "$built" "$dir/$name" +# A copy, not a symlink, so the stable link survives rsync and any static host. +cp "$built" "$dir/.$LATEST.tmp" +mv -f "$dir/.$LATEST.tmp" "$dir/$LATEST" + +# Drop all but the newest few versions. +mapfile -t old < <(find "$dir" -maxdepth 1 -name "${PACKAGE}_*_x64-setup.exe" | sort -V | head -n -"$KEEP") +if [ "${#old[@]}" -gt 0 ]; then + rm -f "${old[@]}" +fi + +(cd "$dir" && sha256sum "$LATEST" "${PACKAGE}"_*_x64-setup.exe >SHA256SUMS) + +# --- 6. How to use it ------------------------------------------------------- + +info "published $file_version" +cat <