This guide covers Desktop source development on Linux, Windows, and macOS, plus local packaging. For unified NSIS, .pkg, and .deb release/acceptance status, see Unified installation. The Windows NSIS / macOS DMG helpers below remain local compatibility packaging workflows.
For ordinary installation, use Desktop installation. For day-to-day product use, see Using Desktop. Formal public release publication is a separate maintainer workflow described by the release checklist.
WebCodex Desktop is a Tauri 2 application, but the distributable is not only the Tauri UI shell. A complete Desktop bundle contains three source-matched native WebCodex executables:
webcodex
webcodex-server
webcodex-runner
On Windows they are .exe files. On macOS they are native Mach-O executables.
There are therefore two different development paths:
| Goal | Runtime source | Packaging |
|---|---|---|
| Run Desktop from source | target/dogfood by default, or WEBCODEX_DESKTOP_BIN_DIR |
No installer required |
| Build an installable package | Runtime is first verified and staged into Tauri resources | Windows NSIS or macOS DMG |
A plain tauri build that skips the WebCodex runtime staging step can produce a
Tauri bundle that compiles but is not a valid WebCodex Desktop distribution.
All platforms need:
- Git;
- the stable Rust toolchain used by the repository;
- Node.js 22 and npm;
- the repository checkout;
- enough disk space for the root Rust build and the separate Desktop Tauri build.
Install both JavaScript dependency sets once per checkout/update. Desktop uses
shared UI source from frontend/src/ui, so its typecheck and build need the
frontend packages as well:
npm ci --prefix frontend
npm ci --prefix apps/desktopWindows development also needs a native MSVC/Windows SDK build environment and a working WebView2 runtime. Build the x64 package on an x64 Windows host and the ARM64 package on a native Windows ARM64 host.
macOS development needs Xcode Command Line Tools. Build darwin-arm64 on Apple
Silicon and darwin-x64 on an Intel Mac. The staging helper deliberately rejects
a host/binary architecture mismatch.
Frontend:
npm --prefix apps/desktop run typecheck
npm --prefix apps/desktop test
npm --prefix apps/desktop run buildTauri/Rust:
cargo check --locked --manifest-path apps/desktop/src-tauri/Cargo.toml --all-targets
cargo test --locked --manifest-path apps/desktop/src-tauri/Cargo.tomlOn Windows, process-tree/lifecycle work may also need the ignored real-process suite documented in Testing.
Windows session shutdown uses a hidden native top-level window, including tray
startup without opening the UI. The Desktop owner receives shutdown before its
ordinary children, accepts queries without stopping connections, and performs
cleanup only for a confirmed WM_ENDSESSION. All exposure stdin leases close
together, while the shared Server remains available for admitted work to drain.
Concurrent Tauri/native exit notifications wait for the same cleanup attempt;
lock acquisition, process cleanup and operation settlement share a four-second
Windows budget. Work that cannot settle retains the tunnel restart fence.
Managed tunnel children observe parent stdin EOF throughout preparation and
startup, including the first control-plane poll. Startup cancellation retains
the task owner until its shutdown result is observed, so an idle connection can
retire its fence without first becoming ready. Persistent daemon mode continues
to ignore stdin EOF.
Abrupt termination or power loss still requires uncertainty resolution; reboot
alone is never permission to remove a fence or replay work.
Desktop retains best-effort lifecycle metadata under its effective data root in
desktop-lifecycle-v1/current.jsonl and previous.jsonl (256 KiB each). Settings'
Configuration and data inventory lists both locations. Records contain a Desktop
session UUID/PID, timestamp, typed exit/shutdown events, and canonical owned-process
snapshots with generation, phase, PID and exit code. Activity messages, command
lines, child stdout/stderr and credentials remain outside these files.
A bounded background queue keeps disk I/O off the session-shutdown path. Each
written record is synced; rotation retains only two files. Disk failures disable
journaling without changing runtime ownership or recovery. Queue overflow is
reported as dropped_before on the next accepted record. Recent events can be
missing after abrupt termination, and an absent exit event does not prove a crash.
A partial last line is preserved and separated from subsequent records. These
files are diagnostic evidence only: they never authorize restart, remove tunnel
fences, or replay work.
The focused native regression sends session messages only to a disposable fixture window and checks four real child leases plus shared Server ordering:
cargo test --locked --manifest-path apps/desktop/src-tauri/Cargo.toml --lib desktop_real_process_windows_session_shutdown_drains_all_profiles -- --ignored --test-threads=1
cargo test --locked -p webcodex --lib regular_tunnel_real_process_parent_eof_during_local_probe -- --ignored --test-threads=1This tests the native notification and cleanup boundary, not an actual system reboot. Qualify installed autostart separately when doing a real Windows reboot.
Desktop debug builds look for the three binaries in the repository's
target/dogfood directory. Build all three from the same checkout.
PowerShell:
$source = (git rev-parse HEAD).Trim()
$short = (git rev-parse --short=12 HEAD).Trim()
$version = (node -e "console.log(require('./npm/webcodex/package.json').version)").Trim()
$builtAt = [Int64]((git show -s --format=%ct HEAD).Trim())
$env:WEBCODEX_BUILT_AT = "$builtAt"
cargo build --locked --profile dogfood -p webcodex -p webcodex-cli -p webcodex-runnermacOS/Linux-style shell:
source_sha="$(git rev-parse HEAD)"
short_source="$(git rev-parse --short=12 HEAD)"
version="$(node -p "require('./npm/webcodex/package.json').version")"
built_at="$(git show -s --format=%ct HEAD)"
export WEBCODEX_BUILT_AT="$built_at"
cargo build --locked --profile dogfood -p webcodex -p webcodex-cli -p webcodex-runnerThe Desktop runtime resolver requires the CLI, Server, and Runner to report the same version and Git commit.
For an unusual local layout, set WEBCODEX_DESKTOP_BIN_DIR to another directory
containing all three source-matched binaries. This override is a debug/development
path. Installed non-debug Desktop builds require their bundled runtime resources.
Desktop normally starts from Tauri's per-user app-local-data directory. On Windows, Desktop resolves the ancestors of the final Desktop-owned data-root component to their physical filesystem location before creating any missing tail directories. This supports redirected Windows profiles whose C:\\Users\\<user> path is an NTFS Junction while keeping one effective root for Desktop state, secrets, runtime/local, connection state, provider/coding-agent state, updates, diagnostics, and reset/recovery flows. An existing final WebCodex data-root component is not canonicalized through a Junction/symlink; such a reparse point is rejected instead so the credential-path safety boundary remains visible.
For operator recovery or debugging, WEBCODEX_DESKTOP_DATA_DIR overrides the Tauri location. The value must be an absolute path. Windows applies the same physical-resolution rule to the override; Linux and macOS keep the supplied absolute path without new symlink canonicalization semantics. This override does not relax CLI credential-path checks: the bundled CLI still validates the final effective path and refuses unsafe credential-directory redirection.
After building the dogfood runtime:
npm --prefix apps/desktop run tauri -- devTauri starts Vite through the configured beforeDevCommand. The debug Desktop
then resolves the runtime in this order:
- bundled runtime resources, when present;
WEBCODEX_DESKTOP_BIN_DIR;- the repository
target/dogfooddirectory.
For frontend-only iteration, npm --prefix apps/desktop run dev starts Vite, but
that is not a complete Desktop runtime and does not replace a Tauri run when the
change depends on native commands, process lifecycle, tray/menu behavior, file
dialogs, autostart, or bundled runtime behavior.
A native source preview uses the real Desktop backend and embedded frontend assets. It does not install a .deb, migrate service ownership, or establish reboot/installer acceptance. Debian/Ubuntu builds need build-essential, pkg-config, libssl-dev, libgtk-3-dev, libwebkit2gtk-4.1-dev, and libayatana-appindicator3-dev, as well as the Rust/Node prerequisites above. Run the following from the repository root after installing both npm dependency sets:
cargo build --locked --profile dogfood -p webcodex -p webcodex-cli -p webcodex-runner
npm run build --prefix apps/desktop
cargo build --locked --profile dogfood \
--manifest-path apps/desktop/src-tauri/Cargo.toml --features tauri/custom-protocolDirect Cargo builds need tauri/custom-protocol to embed and load the built UI; the Tauri CLI normally selects it for packaged builds. These paths assume the default Cargo target directories. Verify --build-info-json on all four executables before deployment: source SHA, version, architecture, and dirty status must match the intended candidate. Build timestamps may differ. Local dogfood is not published build-provenance verification.
If Server and Runner are already managed independently, connect Desktop as a viewer. On a machine with no saved Environment, import an existing user API credential, using the Server's actual reachable URL and a protected credential file:
target/dogfood/webcodex environment configure \
--join http://127.0.0.1:8080 --no-project \
--token-file /private/path/webcodex-user-token --bin-dir "$PWD/target/dogfood"
WEBCODEX_DESKTOP_BIN_DIR="$PWD/target/dogfood" \
apps/desktop/src-tauri/target/dogfood/webcodex-desktopLaunch from the logged-in graphical session. An existing Environment is checked for conflicts; this procedure is not an instruction to overwrite it. Viewer setup creates no Runner identity and does not install, stop, or take over existing services. Local Runner · Not configured refers to this Desktop environment, while Projects can show authorized work on independently running local or remote Runners. Open SERVER_URL/runtime for the same Server's browser console.
To upgrade independently managed Server/Runner binaries, first identify their real service owner, check active work, save the previous binaries/configuration and a consistent Server data snapshot, then use that owner's lifecycle controls. Verify the deployed build and recovery of the same Runner identities/project registrations. Keep existing Tunnel configuration with its owner. This manual source deployment is separate from Core migration and installer upgrade; see the recorded Linux dogfood evidence.
The current Windows distribution format is a per-user NSIS installer. The official project build is unsigned.
For normal local dogfood, use the complete helper from the repository root:
.\scripts\build_desktop_windows_local.ps1By default it requires a clean committed worktree. It installs both shared frontend
and Desktop npm dependencies, builds the three dogfood runtime binaries, stages the
verified runtime, reuses target\desktop-local-tauri\ as the Tauri compilation
cache, creates the unsigned NSIS installer, and writes the installer plus SHA-256 file
under target\desktop-local-dist\.
To build an installer from uncommitted work for local testing, opt in explicitly:
.\scripts\build_desktop_windows_local.ps1 -AllowDirtyThis does not disguise the source state: bundled runtime identity remains
dirty=true, staging verifies that exact identity, and the output filename contains
dirty-<commit>. Such an installer is local dogfood evidence only and must not be
published as a release artifact.
The helper deliberately does not install the package by default, so it is safe to use on a daily dogfood machine that already has WebCodex Desktop installed. To run the destructive native install/uninstall smoke as well, use a disposable VM/test user or a Windows user with no existing WebCodex Desktop installation:
.\scripts\build_desktop_windows_local.ps1 -SmokeThe manual steps below are equivalent diagnostic stages when you need to inspect one part of the pipeline.
The manual flow below describes the clean path. The Desktop staging helper verifies the
explicit expected dirty state; ordinary CI/release and this manual clean path expect
dirty=false in every embedded runtime binary. Use the one-command -AllowDirty
path above when intentionally packaging uncommitted local work.
git status --short
git rev-parse HEADCommit the source you intend to package before continuing. Generated/ignored build output does not need to be committed.
Run the PowerShell runtime-build block above from the repository root. Keep
$source, $version, and $builtAt in the same shell.
Use a fresh output path; the staging helper refuses to overwrite an existing directory.
$stage = Join-Path $env:TEMP "webcodex-desktop-bundle-$PID-$([DateTimeOffset]::UtcNow.ToUnixTimeMilliseconds())"
& .\scripts\prepare_desktop_bundle.ps1 `
-BinDir "target\dogfood" `
-Version $version `
-SourceSha $source `
-BuiltAt $builtAt `
-OutputDir $stage
$config = Join-Path $stage "tauri.bundle.conf.json"The helper verifies all three executables, copies them byte-for-byte into a
generated webcodex-runtime resource tree, and creates the Tauri config overlay.
$targetDir = Join-Path $PWD "target\desktop-local-tauri"
$env:CARGO_TARGET_DIR = $targetDir
$bundleDir = Join-Path $targetDir "release\bundle\nsis"
if (Test-Path -LiteralPath $bundleDir) {
Remove-Item -LiteralPath $bundleDir -Recurse -Force
}
Push-Location apps\desktop
try {
node node_modules/@tauri-apps/cli/tauri.js build --bundles nsis --config $config --ci --no-sign -- --locked
if ($LASTEXITCODE -ne 0) { throw "Tauri NSIS build failed" }
} finally {
Pop-Location
}The installer is under:
target\desktop-local-tauri\release\bundle\nsis\
Use the native host that matches the package:
| Host | Package value used by smoke/release evidence |
|---|---|
| Windows x64 | win32-x64 |
| Windows ARM64 | win32-arm64 |
$bundleDir = Join-Path $targetDir "release\bundle\nsis"
$installers = @(Get-ChildItem -LiteralPath $bundleDir -Filter "*-setup.exe" -File)
if ($installers.Count -ne 1) {
throw "Expected exactly one NSIS installer, found $($installers.Count)"
}
$platform = if ($env:PROCESSOR_ARCHITECTURE -eq "ARM64") {
"win32-arm64"
} elseif ($env:PROCESSOR_ARCHITECTURE -eq "AMD64") {
"win32-x64"
} else {
throw "Unsupported Windows architecture: $env:PROCESSOR_ARCHITECTURE"
}
& .\scripts\desktop_install_windows_smoke.ps1 `
-Installer $installers[0].FullName `
-Version $version `
-SourceSha $source `
-BuiltAt $builtAt `
-Platform $platformThe smoke script exercises the native installer and validates the bundled runtime identity. It deliberately refuses to run when WebCodex Desktop is already installed for the current Windows user, and it uninstalls the temporary package after the check. Use a disposable VM/test user or another host without an existing Desktop installation rather than disturbing a daily-use installation.
macOS signing has three explicit modes: self-signed is the current persistent fallback for public releases and long-lived dogfood; developer-id uses Developer ID Application plus notarization/stapling when Apple credentials are available; adhoc is only for disposable CI/local verification and promises no TCC upgrade continuity. Keep the same self-signed certificate and private key across builds and upgrades. Recreating a certificate with the same name changes identity. Both Desktop (dev.webcodex.desktop) and bundled Runner (dev.webcodex.runner) use certificate-anchored designated requirements without binary cdhash. Final app/DMG verification runs after Tauri nested signing. This does not preserve grants from historical ad-hoc builds automatically: users may need to reauthorize the new persistent identity once. Self-signed distribution is not Apple notarized; use System Settings → Privacy & Security → Open Anyway if Gatekeeper requires it, without disabling Gatekeeper globally.
The manual ad-hoc recipe below is disposable verification only. For long-lived builds use the default self-signed local helper below. See release signing configuration.
For the normal local path, the repository already provides a complete helper:
bash scripts/build_desktop_macos_local.shIt requires a clean worktree, installs both shared frontend and Desktop npm dependencies, builds the dogfood runtime, stages it, creates the native self-signed DMG with the existing persistent certificate, runs the macOS smoke validation, and writes the final file under target/desktop-local-dist/. The steps below are a disposable ad-hoc manual flow for diagnosing a build.
git status --short
git rev-parse HEADCommit the source you intend to package. Then run the macOS runtime-build block
above and keep source_sha, version, and built_at in the same shell.
case "$(uname -m)" in
arm64) platform="darwin-arm64" ;;
x86_64) platform="darwin-x64" ;;
*) echo "Unsupported macOS architecture: $(uname -m)" >&2; exit 1 ;;
esacstage="${TMPDIR:-/tmp}/webcodex-desktop-bundle-$$-$(date +%s)"
test ! -e "$stage"
python3 scripts/prepare_desktop_bundle_macos.py \
--bin-dir target/dogfood \
--version "$version" \
--source-sha "$source_sha" \
--built-at "$built_at" \
--platform "$platform" \
--signing-mode adhoc \
--output-dir "$stage"
config="$stage/tauri.bundle.conf.json"
metadata="$stage/desktop-bundle.json"The helper verifies source identity, SHA-256, and Mach-O architecture before creating the Tauri resource overlay.
export APPLE_SIGNING_IDENTITY="-"
export CARGO_TARGET_DIR="$PWD/target/desktop-local-tauri-$$"
(
cd apps/desktop
npm exec tauri -- build \
--bundles dmg \
--config "$config" \
--ci -- --locked
)Find and require the single generated DMG:
set -- "$CARGO_TARGET_DIR"/release/bundle/dmg/*.dmg
if [ "$#" -ne 1 ] || [ ! -f "$1" ]; then
echo "Expected exactly one DMG" >&2
exit 1
fi
dmg="$1"bash scripts/desktop_install_macos_smoke.sh \
--dmg "$dmg" \
--version "$version" \
--source-sha "$source_sha" \
--built-at "$built_at" \
--platform "$platform" \
--stage-metadata "$metadata" \
--signing-mode adhocBecause these builds are ad-hoc signed and not notarized, Gatekeeper may block a newly downloaded/copied local DMG on first launch. Use System Settings → Privacy & Security → Open Anyway when appropriate. Do not disable Gatekeeper globally.
A successful local EXE/DMG is development evidence, not publication authority.
For normal contribution work you do not need to:
- create or push a Git tag;
- create a GitHub Release;
- publish npm;
- run the full six-platform release matrix;
- use release-operator publication commands.
Official WebCodex release candidates are built from an immutable tagged source by the reviewed native workflow. That flow packages:
- Windows x64 Desktop;
- Windows ARM64 Desktop;
- macOS Intel Desktop;
- macOS Apple Silicon Desktop;
- the six standalone runtime platforms.
See .github/workflows/release-build.yml and Release Checklist for the formal release contract. Do not upload a local dogfood installer as an official WebCodex release artifact.
For source development, rebuild all three root binaries:
cargo build --locked --profile dogfood -p webcodex -p webcodex-cli -p webcodex-runnerOr point WEBCODEX_DESKTOP_BIN_DIR at the directory containing the matching
three binaries.
For an installed non-debug Desktop, reinstall a matching packaged Desktop build; do not manually copy random binaries into application resources.
The CLI, Server, and Runner came from different builds. Rebuild all three from
one checkout and do not mix files from separate target directories.
The packaging helpers intentionally require reproducible committed input. Commit
the source, set WEBCODEX_BUILT_AT from git show -s --format=%ct HEAD, and
rebuild all three runtime binaries before staging again.
prepare_desktop_bundle_macos.py is native-only. Do not stage darwin-x64 on an
Apple Silicon host or darwin-arm64 on Intel. Use the matching native Mac, as the
project's CI/release matrix does.
Rebuild through prepare_desktop_bundle.ps1 and pass its generated
tauri.bundle.conf.json to Tauri. A raw NSIS build without this overlay is not a
complete WebCodex Desktop package.
Reproduce with the full Tauri development command. Browser/Vite-only preview does not provide Tauri IPC, native process lifecycle, tray/menu APIs, autostart, or bundled runtime resources.
When this guide and implementation ever disagree, treat the current scripts and native CI as the mechanical source of truth and update the guide in the same change:
- ordinary CI;
- extended native validation;
- release candidate build;
scripts/prepare_desktop_bundle.ps1;scripts/build_desktop_windows_local.ps1;scripts/build_desktop_macos_local.sh;scripts/prepare_desktop_bundle_macos.py;scripts/desktop_install_windows_smoke.ps1;scripts/desktop_install_macos_smoke.sh.