Iterion ships an optional desktop wrapper built with Wails v2. The desktop binary embeds the studio SPA and the Iterion HTTP server inside a native window via WebKit (Linux/macOS) or WebView2 (Windows).
This document captures the build pipeline for all four supported platforms and the rationale behind the structural choices that aren’t obvious from the sources alone.
//go:build desktop. The default go test ./... does not require Wails.Assets = nil, so Wails delegates to the custom handler in
asset_proxy.go. That handler
serves SPA/static paths from the GUI binary’s embedded
pkg/server.StaticFS and reverse-proxies only /api/* HTTP requests to
the selected loopback pkg/server (the default per-project headless
daemon, or the in-process fallback). The studio is embedded via
//go:embed all:static — never copied into the Wails frontend dir.desktop:package:linux:<arch> for AppImage.-skipbindingsWails CLI insists on running go build from the directory holding wails.json,
and it generates JS bindings against the main package in that directory. Since
our Go main lives in cmd/iterion-desktop/, the file is co-located there. The
Taskfile uses a per-task dir: cmd/iterion-desktop so the working directory is
always correct.
We pass two Wails flags that aren’t part of the default scaffold:
-skipbindings: the studio consumes bindings via the window.go.main.App.*
globals injected by the Wails runtime, not via the static JS shims Wails
generates. Skipping bindings generation removes a redundant codegen step.-s: skip Wails’ scaffold frontend build. There is no frontend to
build under cmd/iterion-desktop/; the supported repo entry points are the
task desktop:* targets, which run task studio:build first and then pass
Wails’ short -s flag. The SPA is embedded into pkg/server/static, and
the handler in asset_proxy.go
serves the index and static chunks directly from that GUI embed, while
/api/* is the only traffic reverse-proxied to the selected loopback
server.-tags desktop,webkit2_41 is required on Linux to opt into the modern WebKit2
GTK 4.1 ABI. Without webkit2_41 Wails would target webkit2gtk-4.0, which
Debian trixie / Ubuntu 24.04 no longer ship.
Wails build = native compile = system dev headers. The default path uses
the host package manager (apt) — that’s what the devcontainer and CI runners
use. devbox install links only the runtime output of gtk3/webkitgtk, so
their headers and .pc files (the -dev outputs) are absent and a plain
devbox run -- go build -tags desktop,webkit2_41 fails at pkg-config.
If you can’t use apt/sudo (a restricted shell, a bare Nix box), you don’t have to — nix can provide the headers; see Alternative: nix-provided headers below.
sudo apt-get install -y \
libgtk-3-dev libwebkit2gtk-4.1-dev libsoup-3.0-dev \
fuse libfuse2t64 dpkg-dev patchelf desktop-file-utils \
gtk-update-icon-cache librsvg2-common xvfb
These packages are wired into .devcontainer/devcontainer.json’s
postCreateCommand, and into the Linux deps step of
.github/workflows/desktop-release.yml.
Xcode Command Line Tools provide everything Wails needs (WebKit lives in the
SDK, no extra brew formulas required). The CI runs macos-14 (Apple
Silicon) and produces a single darwin/universal .app via Wails’
-platform darwin/universal (lipo’d amd64 + arm64).
The CI runs on windows-latest. Wails uses WebView2 which is bundled with
modern Windows; the build needs nsis only for the installer step (-nsis
flag in the Taskfile target). On the runner this is auto-installed via the
[Wails CLI] download.
nixpkgs does ship the gtk3/webkitgtk headers — devbox just doesn’t realise
the -dev outputs. You can realise them yourself from the same nixpkgs the
devbox flake pins (fetched from cache.nixos.org, not compiled) and point
pkg-config at them, with no apt and no sudo. The wrapper does it for you:
# compile / vet / test the desktop package (no GUI needed):
scripts/desktop/nix-pkgconfig-env.sh go test -tags desktop,webkit2_41 ./cmd/iterion-desktop/
# a RUNNABLE GUI binary additionally needs the `production` tag — Wails' Linux
# app impl is gated behind `production` (or `dev`); without it the binary starts
# and prints "Wails applications will not build without the correct build tags"
# (it fell back to app_default_unix.go). `wails build` injects this tag for you;
# a plain `go build` must pass it:
scripts/desktop/nix-pkgconfig-env.sh \
go build -tags desktop,webkit2_41,production -o ./iterion-desktop ./cmd/iterion-desktop/
Two non-obvious things the script handles (and that trip up a hand-rolled attempt):
PKG_CONFIG_PATH — it reads a
target-specific PKG_CONFIG_PATH_<arch>_unknown_linux_gnu and overwrites the
plain one. The script exports the target variable.Requires:, so the path must
include every -dev output’s pkgconfig dir in the closure, not just
gtk’s and webkit’s. The script derives that from nix-store -qR.This is enough to compile, vet, and run the Go tests for the desktop
package (including the cloud-connection code and its integration tests).
Producing an installable bundle (.deb/AppImage) still wants the apt tooling
(dpkg-dev, patchelf, linuxdeploy, …); the nix path covers the Go build,
not the packaging.
⚠ The nix path is for COMPILING/TESTING, not for a binary you RUN on a non-NixOS host. A binary linked against nix gtk/webkit also pulls nix’s Mesa/
libEGL/libGL(via the bakedRUNPATH), and nix’slibEGLcannot find a non-NixOS system’s GPU drivers (it looks under the nix store, not/usr/lib/.../dri). At runtime it aborts withCould not create default EGL display: EGL_BAD_PARAMETERand the window is black (only the native GTK menu bar paints). A runnable / distributable Linux GUI must link the SYSTEM webkit + libEGL so it can reach the host’s GPU drivers. On a host that already has the apt-devpackages + a Go 1.26 toolchain, build OUTSIDE devbox so noNIX_LDFLAGS-rpathis baked:env -i HOME="$HOME" PATH=/usr/local/go/bin:/usr/bin:/bin \ CGO_ENABLED=1 CC=/usr/bin/gcc CXX=/usr/bin/g++ \ PKG_CONFIG=/usr/bin/pkg-config \ PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/share/pkgconfig \ go build -tags desktop,webkit2_41,production \ -o build/bin/iterion-desktop-linux-amd64 ./cmd/iterion-desktop/Verify it is clean with
ldd build/bin/iterion-desktop-linux-amd64in a plain shell: no/nix/storepaths,libEGL/libwebkit2gtk-4.1resolving under/lib/x86_64-linux-gnu. (The canonical Taskfile targetdesktop:build:linux:amd64does the equivalent via the system pkg-config;env -ioutside devbox is the fallback when you can’t easily get a clean PATH.)
The codebase is wired for both devbox run -- workflows and host-native
workflows. Devbox sets up Go + Node + Task; apt provides webkit/gtk dev
headers. The Wails CLI is installed on demand into $GOPATH/bin.
# 1. Install Wails CLI (one-off, per dev environment)
devbox run -- task desktop:install-tools
# 2. Build studio + desktop binary
export PATH="$(go env GOPATH)/bin:$HOME/.local/bin:$PATH"
devbox run -- task desktop:build:linux:amd64
# → build/bin/iterion-desktop-linux-amd64 (~46 MB)
# 3. Optional: smoke-test under Xvfb
xvfb-run -a -s '-screen 0 1280x800x24' \
timeout 8s build/bin/iterion-desktop-linux-amd64
# Expected: "Editor server listening on http://localhost:<port>"
The AppImage build needs the AppImage tooling (~20 MB):
mkdir -p "$HOME/.local/bin"
curl -fsSL -o "$HOME/.local/bin/linuxdeploy" \
https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage
curl -fsSL -o "$HOME/.local/bin/linuxdeploy-plugin-gtk" \
https://raw.githubusercontent.com/linuxdeploy/linuxdeploy-plugin-gtk/master/linuxdeploy-plugin-gtk.sh
chmod +x "$HOME/.local/bin/"linuxdeploy*
export PATH="$HOME/.local/bin:$PATH"
devbox run -- task desktop:package:linux:amd64
# → ./iterion-desktop-linux-amd64.AppImage (~110 MB, includes WebKit)
The script (scripts/desktop/build-appimage.sh)
runs linuxdeploy with --appimage-extract-and-run, so it works inside
containers and CI runners that lack /dev/fuse.
devbox’s Nix-wrapped pkg-config rewrites PKG_CONFIG_PATH to its own Nix store
(which has no webkitgtk dev outputs). When invoking Wails directly inside
devbox run --, prepend the system pkg-config to PATH so the wrapper is
bypassed:
export PATH="/usr/bin:$PATH"
export PKG_CONFIG=/usr/bin/pkg-config
export PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/share/pkgconfig
The Taskfile entries already invoke wails through the system PATH; this hint
only matters for ad-hoc wails build runs.
.deb build (local iteration only)For tight inner loops where you want a real .deb to dpkg -i but don’t care
about minification or the AppImage:
devbox run -- task desktop:package:linux:amd64:deb:fast
sudo dpkg -i build/bin/iterion-desktop-linux-amd64.deb
Cold build runs in ~50–60s (vs 90–150s for the prod task), warm rebuild
~10–20s. The .deb file path and contents layout are identical to the prod
build — only the bundle inside changes.
Tradeoffs vs desktop:package:linux:amd64:deb:
.deb itself is also bigger).Version: field is stamped <semver>+fast.<commit> — visible in
dpkg -l iterion-desktop so you can’t confuse it with a prod build.:deb task produces one as a side-effect;
the fast variant skips it because build-deb.sh doesn’t consume it).After running any :fast task, pkg/server/static/ contains the unminified
bundle. Before producing release artefacts, wipe it and rebuild:
rm -rf pkg/server/static
devbox run -- task studio:build
DO NOT distribute a fast-built .deb. The CI release path remains the source
of truth for shipped artefacts.
Wails relies on platform-specific WebView toolchains and CGO, so cross-builds generally don’t work. Each target must be built on a matching host:
| Target | Build host |
|---|---|
linux/amd64 |
any Linux x86_64 (devcontainer OK) |
linux/arm64 |
Linux aarch64 (CI uses ubuntu-24.04-arm) |
darwin/universal |
macOS — single .app for Intel + Apple Silicon (CI uses macos-14; lipo via -platform darwin/universal) |
windows/amd64 |
Windows x64 (CI uses windows-latest) |
windows/arm64 |
Windows x64 cross to arm64 (Wails handles this) |
.github/workflows/desktop-release.yml)actions/setup-go + actions/setup-node.go install wails@latest and go install task@latest; both end up in
$GOPATH/bin, which is appended to GITHUB_PATH.apt-get install of the headers + AppImage tooling listed
above; linuxdeploy and linuxdeploy-plugin-gtk fetched from
github.com/linuxdeploy.task studio:build (single source of truth — same task as local dev).task desktop:build:<os>:<arch> per matrix entry.readelf smoke-check on the AppImage to confirm the runtime header arch.task desktop:build:linux:amd64 succeeds locally.build/bin/iterion-desktop-linux-amd64 opens a window, loads the
editor, can run a .bot workflow end-to-end.task desktop:package:linux:amd64 runs on a
freshly-installed Debian or Ubuntu without devbox installed.v* tag → desktop-release.yml job is green for all six matrix
entries.