Skip to content

Latest commit

 

History

History
542 lines (401 loc) · 22.8 KB

DESKTOP_DEVELOPMENT.md

File metadata and controls

542 lines (401 loc) · 22.8 KB

Desktop development and local packaging

English | 简体中文

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.

How Desktop is built

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.

Prerequisites

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/desktop

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

Fast checks before opening the app

Frontend:

npm --prefix apps/desktop run typecheck
npm --prefix apps/desktop test
npm --prefix apps/desktop run build

Tauri/Rust:

cargo check --locked --manifest-path apps/desktop/src-tauri/Cargo.toml --all-targets
cargo test --locked --manifest-path apps/desktop/src-tauri/Cargo.toml

On 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=1

This tests the native notification and cleanup boundary, not an actual system reboot. Qualify installed autostart separately when doing a real Windows reboot.

Build the local WebCodex runtime

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

macOS/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-runner

The 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 data directory

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.

Run Desktop from source

After building the dogfood runtime:

npm --prefix apps/desktop run tauri -- dev

Tauri starts Vite through the configured beforeDevCommand. The debug Desktop then resolves the runtime in this order:

  1. bundled runtime resources, when present;
  2. WEBCODEX_DESKTOP_BIN_DIR;
  3. the repository target/dogfood directory.

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.

Linux source preview against an existing Server

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

Direct 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-desktop

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

Build a Windows installer locally

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

By 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 -AllowDirty

This 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 -Smoke

The manual steps below are equivalent diagnostic stages when you need to inspect one part of the pipeline.

1. Use a clean committed source state

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 HEAD

Commit the source you intend to package before continuing. Generated/ignored build output does not need to be committed.

2. Build the exact dogfood runtime

Run the PowerShell runtime-build block above from the repository root. Keep $source, $version, and $builtAt in the same shell.

3. Stage runtime resources

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.

4. Build NSIS

$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

5. Smoke the installer

$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 $platform

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

Build a macOS DMG locally

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

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

1. Use a clean committed source state

git status --short
git rev-parse HEAD

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

2. Select the native platform

case "$(uname -m)" in
  arm64)  platform="darwin-arm64" ;;
  x86_64) platform="darwin-x64" ;;
  *) echo "Unsupported macOS architecture: $(uname -m)" >&2; exit 1 ;;
esac

3. Stage runtime resources

stage="${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.

4. Build the DMG

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"

5. Smoke signing, architecture, and runtime identity

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 adhoc

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

Local package versus an official release

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.

Common failures

bundled_runtime_missing / binary_missing

For source development, rebuild all three root binaries:

cargo build --locked --profile dogfood -p webcodex -p webcodex-cli -p webcodex-runner

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

binary_version_mismatch

The CLI, Server, and Runner came from different builds. Rebuild all three from one checkout and do not mix files from separate target directories.

Staging reports an unexpected identity or dirty=true

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.

macOS architecture mismatch

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.

Windows installer builds but Desktop cannot find runtime

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.

UI works in Vite but fails in Desktop

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.

Authoritative automation

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.