desktop: cross-compiled Windows NSIS installer lane (task 2015)
Desktop (Tauri) / Windows installer (cross-compiled) (push) Failing after 48s
Desktop (Tauri) / Tauri desktop (Linux) (push) Successful in 3m40s

Adds a `windows` job to desktop.yml on the new ci-tauri-win image, producing a
Windows -setup.exe without any Windows hardware. A Windows container can't run
on a Linux host, so cross-compilation is the only route: --runner cargo-xwin
supplies the MSVC CRT/SDK (pre-warmed into the image) and links with lld-link,
and makensis builds the installer.

NSIS only. .msi needs WiX v3, a Windows program — per Tauri, ".msi installers
can only be created on Windows". It comes back if a Windows node ever exists.

Kept as a separate job so a Windows-side failure can never block the Linux
artifacts, which are the primary product today. publish-release.sh now globs
the windows target root too; nullglob means each job uploads only what its own
workspace contains, and the release is created once and reused via the 409
path, so both jobs can publish to the same release safely.

No app code changes were needed. The AppImage self-integration UI already
gates on is_appimage (AccountView.vue:131, DesktopIntegrationPrompt.vue:22),
and $APPIMAGE is never set on Windows, so the OOBE prompt and Settings toggle
hide themselves.

Recorded plainly in ci-requirements.md that this is the weakest-verified lane
we have: Tauri calls Linux->Windows cross-compilation "not tested as much" and
a last resort, and a Linux runner cannot execute a Windows binary. Green means
it built. A real Windows machine check is mandatory before trusting a release,
and installers are unsigned until a certificate exists.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SreJkbxB4gx8pPsu8QbLPi
This commit is contained in:
2026-07-25 20:14:55 -04:00
co-authored by Claude Opus 5
parent dc68386d1a
commit ab961f13ce
3 changed files with 89 additions and 0 deletions
+57
View File
@@ -126,3 +126,60 @@ jobs:
env: env:
GITHUB_TOKEN: ${{ github.token }} GITHUB_TOKEN: ${{ github.token }}
run: bash desktop/packaging/publish-release.sh run: bash desktop/packaging/publish-release.sh
# Windows installer, CROSS-COMPILED from Linux — there is no Windows build host.
# A Windows container can't run on a Linux host (containers share the host
# kernel), so cross-compiling is the only route without Windows hardware:
# cargo-xwin + LLVM's lld-link + makensis are Linux programs that emit Windows
# PE output. That toolchain is why this needs its own image rather than ci-tauri.
#
# NSIS only. `.msi` needs WiX v3, which is a Windows program — Tauri: ".msi
# installers can only be created on Windows". It returns if a Windows node does.
#
# A separate job, so a Windows-side failure never blocks the Linux artifacts that
# are the primary product today. Tauri calls this path "not tested as much" and a
# last resort, and nothing here can LAUNCH a Windows binary — green means it
# built, not that it runs. A real-machine check stays mandatory before trusting it.
windows:
name: Windows installer (cross-compiled)
if: github.ref == 'refs/heads/dev' || github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')
runs-on: python-ci
container:
image: git.fabledsword.com/bvandeusen/ci-tauri-win:1.97
steps:
- uses: actions/checkout@v6
# Same reason as the Linux job: generate_context! embeds the built frontend
# at compile time, so it must exist before cargo runs.
- name: Build the shared frontend
run: npm ci && npm run build
working-directory: frontend
# --runner cargo-xwin swaps cargo for the cross-compiling driver (it supplies
# the MSVC CRT/SDK, pre-warmed into the image, and links with lld-link).
# Frontend already built above; skip the beforeBuildCommand rebuild.
- name: Tauri build (NSIS installer)
run: |
cargo tauri build \
--runner cargo-xwin \
--target x86_64-pc-windows-msvc \
--bundles nsis \
--config '{"build":{"beforeBuildCommand":""}}'
working-directory: desktop/src-tauri
- name: Upload installer
continue-on-error: true
uses: actions/upload-artifact@v3
with:
name: thoughtsync-windows
path: desktop/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe
if-no-files-found: warn
# Publishes to the SAME release as the Linux job. Safe to run twice: the
# script reuses an existing release (409) and nullglob means each job uploads
# only the bundles present in its own workspace.
- name: Publish release
if: startsWith(github.ref, 'refs/tags/v')
env:
GITHUB_TOKEN: ${{ github.token }}
run: bash desktop/packaging/publish-release.sh
+24
View File
@@ -82,5 +82,29 @@ backend/frontend push.
`pacman -U`-tested here. That step logs `.PKGINFO` + the full file listing so `pacman -U`-tested here. That step logs `.PKGINFO` + the full file listing so
the package is auditable from the run log; a real Arch install is the operator's the package is auditable from the run log; a real Arch install is the operator's
confirm. confirm.
### Windows lane — second job, second image
`desktop.yml` also runs a `windows` job that cross-compiles the NSIS installer.
- **Image:** `git.fabledsword.com/bvandeusen/ci-tauri-win:1.97` (Rust + Node +
`cargo-xwin` + LLVM/`lld` + NSIS). A separate image from `ci-tauri` per
CI-Runner's `docs/process.md` fork rule — the MSVC CRT/SDK cache alone is >1 GB.
Its pins are held in lockstep with `ci-tauri`; bump them together, since both
lanes compile the same source.
- **Why cross-compile:** there is no Windows build host, and a Windows container
cannot run on a Linux host (containers share the host kernel). `cargo-xwin`,
`lld-link` and `makensis` are Linux programs that emit Windows PE output.
- **NSIS only.** `.msi` requires WiX v3, a Windows program — per Tauri, "`.msi`
installers can only be created on Windows."
- **Separate job on purpose:** a Windows failure must not block the Linux
artifacts, which are the primary product today.
- **Weakest verification of any lane.** Tauri documents this path as "not as
straight forward as compiling on Windows directly and is not tested as much",
to be used "only as a last resort" — and a Linux runner cannot execute a
Windows binary. Green means it *built*. A real Windows machine check is
mandatory before trusting a release.
- **Unsigned.** Installers will trip SmartScreen until a code-signing
certificate exists; that is a purchasing decision, not a CI one.
- No Postgres lane (unchanged): the desktop app's local store + sync behavior is - No Postgres lane (unchanged): the desktop app's local store + sync behavior is
verified on the operator's machine, not in CI. verified on the operator's machine, not in CI.
+8
View File
@@ -17,6 +17,7 @@
# desktop/src-tauri/target/release/bundle/appimage/*.AppImage (de-bundled) # desktop/src-tauri/target/release/bundle/appimage/*.AppImage (de-bundled)
# desktop/src-tauri/target/release/bundle/deb/*.deb # desktop/src-tauri/target/release/bundle/deb/*.deb
# desktop/src-tauri/target/release/bundle/arch/*.pkg.tar.* (prebuilt pacman) # desktop/src-tauri/target/release/bundle/arch/*.pkg.tar.* (prebuilt pacman)
# desktop/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/nsis/*.exe
# #
# Instance-agnostic: server + repo come from the runner's github.* context # Instance-agnostic: server + repo come from the runner's github.* context
# (Forgejo populates them for compatibility), so nothing is hardcoded to one host. # (Forgejo populates them for compatibility), so nothing is hardcoded to one host.
@@ -37,13 +38,20 @@ AUTH=(-H "Authorization: token $GITHUB_TOKEN")
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)"
BUNDLE_ROOT="$REPO_ROOT/desktop/src-tauri/target/release/bundle" BUNDLE_ROOT="$REPO_ROOT/desktop/src-tauri/target/release/bundle"
# Cross-compiled Windows output lands under the target triple, not the host root.
WIN_BUNDLE_ROOT="$REPO_ROOT/desktop/src-tauri/target/x86_64-pc-windows-msvc/release/bundle"
# --- collect the assets to upload ------------------------------------------- # --- collect the assets to upload -------------------------------------------
shopt -s nullglob shopt -s nullglob
# nullglob (set above) drops the patterns that didn't match, which is what lets the
# Linux job and the Windows job each run this script against the SAME release and
# upload only what they actually built — they run in separate workspaces, so neither
# can see the other's bundles. The release is created once and reused (409 path).
ASSETS=( ASSETS=(
"$BUNDLE_ROOT"/appimage/*.AppImage "$BUNDLE_ROOT"/appimage/*.AppImage
"$BUNDLE_ROOT"/deb/*.deb "$BUNDLE_ROOT"/deb/*.deb
"$BUNDLE_ROOT"/arch/*.pkg.tar.* "$BUNDLE_ROOT"/arch/*.pkg.tar.*
"$WIN_BUNDLE_ROOT"/nsis/*.exe
) )
if [ ${#ASSETS[@]} -eq 0 ]; then if [ ${#ASSETS[@]} -eq 0 ]; then
echo "ERROR: no bundles under $BUNDLE_ROOT — did the tauri build run?" >&2 echo "ERROR: no bundles under $BUNDLE_ROOT — did the tauri build run?" >&2