iterion

Building the Iterion Desktop app

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.

Architecture rappel

wails.json location & -skipbindings

Wails 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:

-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.

System dependencies

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.

Debian / Ubuntu (devcontainer + GitHub Actions runners)

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.

macOS

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).

Windows

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.

Alternative: nix-provided headers (no apt / no sudo)

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):

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 baked RUNPATH), and nix’s libEGL cannot find a non-NixOS system’s GPU drivers (it looks under the nix store, not /usr/lib/.../dri). At runtime it aborts with Could not create default EGL display: EGL_BAD_PARAMETER and 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 -dev packages + a Go 1.26 toolchain, build OUTSIDE devbox so no NIX_LDFLAGS -rpath is 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-amd64 in a plain shell: no /nix/store paths, libEGL/libwebkit2gtk-4.1 resolving under /lib/x86_64-linux-gnu. (The canonical Taskfile target desktop:build:linux:amd64 does the equivalent via the system pkg-config; env -i outside devbox is the fallback when you can’t easily get a clean PATH.)

Building locally (Linux)

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>"

Packaging as AppImage

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.

Caveat: pkg-config inside devbox shell

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.

Fast .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:

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.

Cross-compiling

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)

CI flow (.github/workflows/desktop-release.yml)

  1. actions/setup-go + actions/setup-node.
  2. go install wails@latest and go install task@latest; both end up in $GOPATH/bin, which is appended to GITHUB_PATH.
  3. Linux only: apt-get install of the headers + AppImage tooling listed above; linuxdeploy and linuxdeploy-plugin-gtk fetched from github.com/linuxdeploy.
  4. task studio:build (single source of truth — same task as local dev).
  5. task desktop:build:<os>:<arch> per matrix entry.
  6. Per-package step (zip / nsis-portable / appimage).
  7. readelf smoke-check on the AppImage to confirm the runtime header arch.
  8. Ed25519 signing + manifest publication via scripts/desktop/sign-release.sh and scripts/desktop/generate-manifest.sh.

Smoke checklist before shipping a release