Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,8 @@ Maple is a **Tauri-based AI chat application** that runs on desktop (macOS, Linu
## Environment
The user typically runs `nix develop` (using `flake.nix`) before starting Claude. This means you're usually in a Nix shell with all required tools (bun, cargo, rustc, etc.) already available.

`frontend/.env.local` must exist for any local build (mac/linux/windows). CI sets these values via GitHub env, but local devs need the file or Vite bakes empty strings into the bundle and the app launches to a silent white-screen. Copy `frontend/.env.local.template` to `frontend/.env.local` if it's missing. On Windows, `scripts/setup-windows.ps1` creates it automatically. See `docs/windows-build.md` for the Windows dev workflow.

## Build & Development Commands
Use justfile for all commands (`just --list` to see all):

Expand Down
32 changes: 32 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,16 @@ sudo apt install libwebkit2gtk-4.1-dev \
librsvg2-dev
```

### Windows
A bootstrap script handles VS Build Tools, Rust, Node, and `.env.local`.
Run from an elevated PowerShell in the repo root:
```powershell
powershell -ExecutionPolicy Bypass -File scripts/setup-windows.ps1
```
See [docs/windows-build.md](docs/windows-build.md) for end-to-end VM
setup, the `just windows-build` / `just windows-dev` workflow, and the
ARM64/x64 cross-build matrix.

4. Add required Rust targets for universal macOS builds:
```bash
rustup target add aarch64-apple-darwin x86_64-apple-darwin
Expand Down Expand Up @@ -244,6 +254,28 @@ The GitHub Actions workflow will automatically:

If there's a new version of the enclave pushed to staging or prod, append the new PCR0 value to the `pcr0Values` or `pcr0DevValues` arrays in `frontend/src/app.tsx`.

## Windows Development

After running `scripts/setup-windows.ps1` once, both build and dev are
single-command `just` recipes:

```powershell
# Native ARM64 build (default):
just windows-build

# Tauri dev server (Vite hot-reload):
just windows-dev

# Cross-build to x64 from an ARM64 host:
just windows-build arm64_amd64
```

The recipes wrap `vcvarsall.bat` so you don't need a Developer PowerShell
open, point `CARGO_TARGET_DIR` off the source tree (Parallels-safe), and
apply the `tauri.windows.conf.json` overlay (swaps `bun` for `npm` since
bun has no Windows ARM64 binary). Full guide:
[docs/windows-build.md](docs/windows-build.md).

## iOS Development

Run in emulator:
Expand Down
162 changes: 162 additions & 0 deletions docs/windows-build.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# Windows development

End-to-end guide for building and running Maple on Windows 10/11 (x64 or
ARM64). Goal: clone → setup → first build in ~30 minutes on a clean VM.

> Most ARM64 dev was done on Apple Silicon Parallels VMs (Win11 ARM64).
> Snapdragon X laptops use the same toolchain. Native x64 is also
> supported.

## Prerequisites

A fresh Win10/11 install needs three things: the right Visual Studio Build
Tools components, a Rust toolchain pinned to the same version CI uses, and
a `frontend/.env.local` with the backend URL. The bootstrap script handles
all of it.

```powershell
# In repo root, from an elevated PowerShell (Administrator):
powershell -ExecutionPolicy Bypass -File scripts/setup-windows.ps1
```

The script is idempotent — safe to re-run if something fails mid-way or you
want to verify state.

What it installs:

- **VS Build Tools 2022** with the exact components the Rust/Tauri chain
needs: `VC.Tools.x86.x64`, `VC.Tools.ARM64`, `VC.Llvm.Clang`,
`Windows11SDK.22621`. The Clang component is the one most likely to be
forgotten; `ring` 0.17 on `aarch64-pc-windows-msvc` needs clang for
ARM64 assembly and `cl.exe` alone can't compile it.
- **Node.js LTS** via winget (`bun` has no Windows ARM64 binary; the
`tauri.windows.conf.json` overlay swaps `bun` for `npm` automatically).
- **rustup** + Rust toolchain pinned to **1.95.0** (matches the CI pin in
`.github/workflows/desktop-build.yml`). Override with
`-RustToolchain 1.96.0` if you need a different version.
- **VC++ 2015+ Redistributable** for both x64 and ARM64 (rollup's native
module links against the ARM64 redist on ARM hosts).
- **LLVM/Clang** standalone as a backstop for the VS Clang component.
- **Git for Windows** — `git.exe` for the clone, plus Git Bash
(`bash.exe` + the bundled `curl` / `sha256sum` / `unzip` / `cygpath`
unix tools that `scripts/tauri-windows.ps1` and the ONNX Runtime helper
both rely on).
- **just** — the recipe runner for `just windows-build` / `just windows-dev`.
- A `frontend/.env.local` template pointing at the production enclave
(see [.env.local handling](#envlocal-handling) below).

After setup, **open a new PowerShell** so PATH picks up the new tools.
Sanity check:

```powershell
rustc --version # rustc 1.95.0
node --version # v22.x.x or later (LTS)
cargo --version # cargo 1.95.0
```

## First build

The build dance (vcvarsall + cargo target dir + ORT setup) is wrapped in
two `just` recipes that hide all of it:

```powershell
# Native ARM64 build (default; produces an ARM64 NSIS installer):
just windows-build

# Cross-build to x64 from an ARM64 host:
just windows-build arm64_amd64

# Native x64 build:
just windows-build x64
```

The recipe resolves to `scripts/tauri-windows.ps1`, which:

1. Locates `vcvarsall.bat` via `vswhere`.
2. Sets `CARGO_TARGET_DIR` to `%USERPROFILE%\maple-cargo-target` — off the
source tree so Parallels shared-folder writes don't corrupt cargo
metadata. Override with `-CargoTargetDir`.
3. Runs `frontend/src-tauri/scripts/provide-windows-onnxruntime.sh`
through Git Bash to fetch + SHA-verify ONNX Runtime, then imports the
`ORT_LIB_LOCATION` / `ORT_SKIP_DOWNLOAD` / `ORT_DYLIB_PATH` env vars.
Mirrors how `desktop-build.yml` feeds those into `$GITHUB_ENV`. Skip
with `-SkipOrt` to fall back to the `ort` crate's auto-download.
4. Chains `vcvarsall.bat <arch>` and the `tauri build` invocation in a
single `cmd /c` so the MSVC env survives into the build.
5. Applies the `tauri.windows.conf.json` overlay so `npm` replaces `bun`
in `beforeBuildCommand` / `beforeDevCommand`.

The built `.exe` lands under `%USERPROFILE%\maple-cargo-target\release\bundle\nsis\`.

## Iteration loop

Use `just windows-dev` for the hot-reload loop — same arch handling, runs
`tauri dev` instead of `tauri build`. Vite reloads frontend changes
instantly; Rust changes trigger an incremental cargo rebuild.

```powershell
just windows-dev
```

Major iteration unlock vs. rebuilding the full installer every change.

## `.env.local` handling

Vite reads `frontend/.env.local` at **build time** and bakes the values
into the bundle. A missing file produces a silent white-screen on launch
(captured for ~30 min of devtools debugging during PR 1 manual smoke).

The bootstrap script creates `frontend/.env.local` automatically if absent.
For non-Windows hosts (or if you want a non-prod backend), copy the
template:

```bash
cp frontend/.env.local.template frontend/.env.local
# edit if pointing at a non-prod backend
```

> Reminder: Vite bakes env vars at build time. **Restart the dev server
> after editing `.env.local`** or values won't propagate.

## Gotchas

| Gotcha | Why it bites | Fix |
|---|---|---|
| `bun` doesn't ship a Windows ARM64 binary | `beforeBuildCommand: "bun run build"` fails on ARM hosts | `tauri.windows.conf.json` overlay swaps in `npm` (auto-applied by `just windows-*`) |
| `ring` 0.17 build fails on `aarch64-pc-windows-msvc` with `cl.exe` errors | needs clang for ARM64 asm; `cl.exe` can't compile it | Install `VC.Llvm.Clang` component (setup script does it) |
| `aws-lc-sys` build fails | same root cause | same fix |
| Cargo target dir corruption on Parallels VM | shared-folder writes don't play with cargo metadata | `tauri-windows.ps1` points `CARGO_TARGET_DIR` at `%USERPROFILE%\maple-cargo-target` |
| Silent white-screen on launch | empty / missing `frontend/.env.local` | Copy `frontend/.env.local.template` |
| `just` errors with `Error parsing line: 'VITE_...'` at line index 0 | UTF-8 BOM at start of `frontend/.env.local` (PS 5.1's `Out-File` / `Set-Content` / `>` redirect and old Notepad all add one by default) | Re-run `scripts/setup-windows.ps1` — it strips the BOM in place; or save the file in VS Code (no BOM by default), or in PS 7+ use `Set-Content -Encoding utf8NoBOM` |
| `vcvarsall.bat arm64` vs `arm64_amd64` | wrong arch produces wrong-bitness binary that fails to load | Use `just windows-build arm64` for native, `arm64_amd64` to cross to x64 |
| `winget install --quiet --override` swallows exit codes for VS BuildTools modify | bootstrapper reports success even when components didn't land | Setup script uses `--passive` and verifies on disk via `vswhere` after install |

## CI vs local parity

Local `just windows-build` and the CI `build-windows` job
(`.github/workflows/desktop-build.yml` for master push,
`desktop-pr-build.yml` for PRs) share these pins:

- Rust **1.95.0** (`-RustToolchain` param in setup script;
`dtolnay/rust-toolchain` action with `toolchain: 1.95.0` in CI)
- ONNX Runtime **1.22.0** with SHA-verified archive + DLL fetch via
`frontend/src-tauri/scripts/provide-windows-onnxruntime.sh` (same
helper for both)
- Tauri config: identical `tauri.conf.json` + Windows overlay applied via
`--config`

CI builds an unsigned NSIS x64 installer. Local builds default to ARM64
native; pass `x64` for x64-native parity with CI artifacts.

## Code signing

Out of scope for now. Tracked under PR 7 (MPLR-goufxxvn) — Authenticode
signing requires certificate provisioning and is gated on a code-signing
cert being purchased.

## Native ARM64 shipping decision

CI currently produces x64 artifacts only. ARM64 hosts run those under
Microsoft's x64 emulation (works, slower than native). Whether to ship a
native ARM64 build alongside x64 is tracked as a spike under
MPLR-xhfehqft.
18 changes: 18 additions & 0 deletions frontend/.env.local.template
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this points to a local backend which I don't expect us to really be running on a windows vm? to validate and test the windows build I think it probably makes sense to point to a prod/staging environment. but this file definitely feels like a duplicate, what if we point to /.env.example but in the windows setup we overwrite the value to be a real env?

Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
# Copy this file to frontend/.env.local before running `just dev`, `just
# desktop-build`, `just windows-dev`, etc. The values below point at the
# production enclave -- override the URLs for a non-prod backend.
#
# Vite bakes these values into the bundle at BUILD time, not runtime --
# rerun the dev server or rebuild after editing. A missing or empty
# VITE_OPEN_SECRET_API_URL produces a silent white-screen on launch.

VITE_OPEN_SECRET_API_URL=https://enclave.trymaple.ai

# Public OpenSecret project id. Optional; the app uses this value by default.
VITE_CLIENT_ID=ba5a14b5-d915-47b1-b7b1-afda52bc5fc6

# Override if pointing at a non-prod billing backend.
# VITE_MAPLE_BILLING_API_URL=https://billing.opensecret.cloud

# Force a specific model for dev. Leave commented to use the app's default.
# VITE_DEV_MODEL_OVERRIDE=gpt-4o
4 changes: 4 additions & 0 deletions frontend/src-tauri/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -10,5 +10,9 @@
# ONNX Runtime Linux (downloaded for bundling)
/onnxruntime-linux/

# ONNX Runtime Windows (downloaded for bundling; arch-suffixed by
# provide-windows-onnxruntime.sh so x64 + arm64 can coexist).
/onnxruntime-windows-*/

# Generated cargo config for iOS builds
/.cargo/
24 changes: 24 additions & 0 deletions frontend/src-tauri/scripts/onnxruntime-pins.sh
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,30 @@ onnxruntime_windows_x64_dll_sha256_for_version() {
esac
}

onnxruntime_windows_arm64_archive_sha256_for_version() {
case "$1" in
1.22.0)
printf '%s\n' "7008f7ff82f8e7de563a22f2b590e08e706a1289eba606b93de2b56edfb1e04b"
;;
*)
echo "No pinned Windows arm64 ONNX Runtime archive SHA-256 for version '$1'." >&2
return 1
;;
esac
}

onnxruntime_windows_arm64_dll_sha256_for_version() {
case "$1" in
1.22.0)
printf '%s\n' "79281671a386ed1baab9dbdbb09fe55f99577011472e9526cf9d0b468bb6bcc7"
;;
*)
echo "No pinned Windows arm64 ONNX Runtime DLL SHA-256 for version '$1'." >&2
return 1
;;
esac
}

onnxruntime_ios_commit_for_version() {
case "$1" in
1.22.2)
Expand Down
23 changes: 18 additions & 5 deletions frontend/src-tauri/scripts/provide-windows-onnxruntime.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,30 @@
set -euo pipefail

ORT_VERSION="${ORT_VERSION:-1.22.0}"
# Default x64 so CI (windows-latest, x64-only) keeps working unchanged.
# tauri-windows.ps1 sets this to 'arm64' when building for aarch64 hosts.
ORT_TARGET_ARCH="${ORT_TARGET_ARCH:-x64}"
case "${ORT_TARGET_ARCH}" in
x64|arm64) ;;
*)
echo "Unsupported ORT_TARGET_ARCH '${ORT_TARGET_ARCH}'. Use 'x64' or 'arm64'." >&2
exit 1
;;
esac

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "${SCRIPT_DIR}/onnxruntime-pins.sh"

TAURI_DIR="$(cd "${SCRIPT_DIR}/.." && pwd)"
ORT_ROOT="${TAURI_DIR}/onnxruntime-windows"
ORT_DIR="${ORT_ROOT}/onnxruntime-win-x64-${ORT_VERSION}"
ORT_ARCHIVE="onnxruntime-win-x64-${ORT_VERSION}.zip"
# Arch-suffixed root so a dev who toggles between native arm64 and arm64_amd64
# cross-builds doesn't re-download every switch (and so both can coexist).
ORT_ROOT="${TAURI_DIR}/onnxruntime-windows-${ORT_TARGET_ARCH}"
ORT_DIR="${ORT_ROOT}/onnxruntime-win-${ORT_TARGET_ARCH}-${ORT_VERSION}"
ORT_ARCHIVE="onnxruntime-win-${ORT_TARGET_ARCH}-${ORT_VERSION}.zip"
ORT_URL="https://github.com/microsoft/onnxruntime/releases/download/v${ORT_VERSION}/${ORT_ARCHIVE}"
ORT_DLL="${ORT_DIR}/lib/onnxruntime.dll"
ORT_ARCHIVE_SHA256="$(onnxruntime_windows_x64_archive_sha256_for_version "${ORT_VERSION}")"
ORT_DLL_SHA256="$(onnxruntime_windows_x64_dll_sha256_for_version "${ORT_VERSION}")"
ORT_ARCHIVE_SHA256="$(onnxruntime_windows_${ORT_TARGET_ARCH}_archive_sha256_for_version "${ORT_VERSION}")"
ORT_DLL_SHA256="$(onnxruntime_windows_${ORT_TARGET_ARCH}_dll_sha256_for_version "${ORT_VERSION}")"

sha256_file() {
local path="$1"
Expand Down
7 changes: 7 additions & 0 deletions frontend/src-tauri/tauri.windows.conf.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"$schema": "../node_modules/@tauri-apps/cli/config.schema.json",
"build": {
"beforeDevCommand": "npm run dev",
"beforeBuildCommand": "npm run build"
}
}
18 changes: 18 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
set dotenv-path := "frontend/.env.local"
set dotenv-required := false

# On Windows, default to cmd.exe instead of `sh` (which isn't on PATH unless
# Git Bash's usr/bin is added). Recipes guarded by [windows] are single
# powershell invocations -- cmd /c is enough and avoids a PS-inside-PS hop.
set windows-shell := ["cmd.exe", "/c"]

# List available commands
default:
@just --list
Expand Down Expand Up @@ -75,6 +80,19 @@ desktop-build-no-cc:
desktop-build-debug-no-cc:
cd frontend && unset CC && bun tauri build --debug

# Build Tauri desktop release for Windows. Wraps vcvarsall.bat + ONNX Runtime
# setup so you don't need a Developer Shell open. Pass arch (default arm64
# native; arm64_amd64 for x64 cross-build from ARM host; x64 for native x64).
[windows]
windows-build arch="arm64":
powershell -ExecutionPolicy Bypass -File scripts/tauri-windows.ps1 -Command build -Arch {{arch}}

# Tauri dev server for Windows (Vite hot-reload). Same vcvars/arch handling
# as windows-build.
[windows]
windows-dev arch="arm64":
powershell -ExecutionPolicy Bypass -File scripts/tauri-windows.ps1 -Command dev -Arch {{arch}}

# Format Rust code
rust-fmt:
cd frontend/src-tauri && cargo fmt
Expand Down
Loading