commit 23ed3fafc5f7ef9cf8591102c9a4a72199056e45
parent 42a70877f4bf7efde19cf20089f90cebe174d259
Author: Matthew Gantenbein <ganten1998@gmail.com>
Date: Wed, 20 May 2026 22:35:39 -0500
Add macOS .app build script + bundle icon
scripts/build-macos.sh builds the Rust release binary and assembles
Navi.app (Info.plist + CFBundleIcon + ad-hoc codesign), then packages
it as Navi-<version>-<arch>.zip via ditto. Supports ARCH=universal for
x86_64 + arm64. Output goes to dist/ which is now .gitignored.
README updated with the pre-built download instructions and the
build-from-source flow for the .app.
Co-authored-by: Cursor <cursoragent@cursor.com>
Diffstat:
4 files changed, 117 insertions(+), 3 deletions(-)
diff --git a/.gitignore b/.gitignore
@@ -1,6 +1,9 @@
# Rust build output
/target/
+# Packaging output
+/dist/
+
# Editor / OS
.DS_Store
.idea/
diff --git a/README.md b/README.md
@@ -8,7 +8,7 @@ Written in Rust on top of [egui](https://github.com/emilk/egui) + [glow](https:/
## Status
-- **macOS** — primary target, fully supported. Tested on Apple Silicon (M-series) at 60 / 120 / 240 Hz.
+- **macOS** — primary target, fully supported. Tested on Apple Silicon (M-series) at 60 / 120 / 240 Hz. A pre-built `Navi.app` ships with each release.
- **Linux** — builds cleanly; the macOS-only `CADisplayLink` pacer is gated, so a small amount of glue (using `SwapInterval::Wait(1)` or Wayland presentation-time / DRM vblank) is required before it renders. Everything else (egui, winit, glutin, sqlite, emacsclient discovery) is cross-platform.
- **Windows** — not supported. The `emacsclient` socket discovery is Unix-only and the macOS pacer would have to be replaced by DXGI waitable swap chains.
@@ -16,6 +16,12 @@ Written in Rust on top of [egui](https://github.com/emilk/egui) + [glow](https:/
## Quick start
+### Pre-built macOS app (Apple Silicon)
+
+Download `Navi-<version>-host.zip` from the [Releases](https://github.com/ganten7/navi/releases) page, unzip, and double-click `Navi.app`. The bundle is ad-hoc signed; on first launch macOS Gatekeeper may flag it — right-click → Open, or run `xattr -cr Navi.app` to clear the quarantine flag.
+
+### From source
+
```bash
git clone https://github.com/ganten7/navi.git
cd navi
@@ -23,13 +29,21 @@ cargo build --release
./target/release/navi
```
+To build the `.app` bundle yourself:
+
+```bash
+./scripts/build-macos.sh # arm64 (host)
+ARCH=universal ./scripts/build-macos.sh # arm64 + x86_64 universal
+open dist/Navi.app
+```
+
On first run, Navi auto-detects your `org-roam.db` and creates `~/.config/navi/config.json`. Subsequent launches start in well under a second.
---
## Requirements
-- **Rust 1.75+** (stable) — install via [rustup](https://rustup.rs/)
+- **Rust 1.75+** (stable) — install via [rustup](https://rustup.rs/) (source builds only)
- **org-roam v2** database (`nodes`, `files`, `links`, `tags`, `aliases`)
- **emacsclient** + a running Emacs server (`(server-start)` in your init) — for double-click-to-open
- A working OpenGL 3.3 context — built into macOS / standard on Linux
@@ -156,6 +170,8 @@ navi/ Binary: UI, rendering, event loop
src/painter.rs GraphPainter — grid, edges, nodes, labels
src/macos_display.rs macOS CADisplayLink + tier control
src/theme.rs Colour themes
+scripts/build-macos.sh Cargo build + Navi.app + zip for release
+assets/icon.icns App bundle icon
```
---
@@ -163,11 +179,14 @@ navi/ Binary: UI, rendering, event loop
## Building
```bash
-# Release (recommended)
+# Release (recommended for use)
cargo build --release
# Dev (slower at runtime, faster compile, includes debuginfo)
cargo build
+
+# macOS .app bundle + release zip
+./scripts/build-macos.sh
```
The release profile in the workspace has `lto = true` and `opt-level = 3`. Expect a 30–60 s clean release build on a modern laptop.
diff --git a/assets/icon.icns b/assets/icon.icns
Binary files differ.
diff --git a/scripts/build-macos.sh b/scripts/build-macos.sh
@@ -0,0 +1,92 @@
+#!/usr/bin/env bash
+# Build Navi.app for macOS and package it as Navi-<version>-<arch>.zip.
+#
+# Usage:
+# scripts/build-macos.sh # release build, host arch
+# ARCH=universal scripts/build-macos.sh # arm64 + x86_64 universal binary
+#
+# Output: dist/Navi.app and dist/Navi-<version>-<arch>.zip
+#
+# Requirements: Rust toolchain (rustup), Xcode CLT for codesign / ditto.
+# For ARCH=universal: rustup target add x86_64-apple-darwin aarch64-apple-darwin
+set -euo pipefail
+
+ROOT="$(cd "$(dirname "$0")/.." && pwd)"
+cd "$ROOT"
+
+ARCH="${ARCH:-host}"
+VERSION="$(grep '^version' navi/Cargo.toml | head -1 | sed -E 's/.*"([^"]+)".*/\1/')"
+APP="dist/Navi.app"
+ZIP="dist/Navi-${VERSION}-${ARCH}.zip"
+
+echo "==> Navi $VERSION arch=$ARCH"
+
+# ── Build ────────────────────────────────────────────────────────────────────
+case "$ARCH" in
+ host)
+ cargo build --release -p navi
+ BIN="target/release/navi"
+ ;;
+ universal)
+ rustup target add x86_64-apple-darwin aarch64-apple-darwin >/dev/null 2>&1 || true
+ cargo build --release -p navi --target x86_64-apple-darwin
+ cargo build --release -p navi --target aarch64-apple-darwin
+ mkdir -p target/universal/release
+ BIN="target/universal/release/navi"
+ lipo -create -output "$BIN" \
+ target/x86_64-apple-darwin/release/navi \
+ target/aarch64-apple-darwin/release/navi
+ ;;
+ *)
+ echo "unknown ARCH=$ARCH (expected host|universal)" >&2
+ exit 2
+ ;;
+esac
+
+# ── Bundle ───────────────────────────────────────────────────────────────────
+echo "==> Assembling $APP"
+rm -rf "$APP" "$ZIP"
+mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources"
+
+cp "$BIN" "$APP/Contents/MacOS/navi"
+chmod +x "$APP/Contents/MacOS/navi"
+strip -x "$APP/Contents/MacOS/navi" 2>/dev/null || true
+
+if [ -f assets/icon.icns ]; then
+ cp assets/icon.icns "$APP/Contents/Resources/AppIcon.icns"
+fi
+
+cat > "$APP/Contents/Info.plist" <<PLIST
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleName</key> <string>Navi</string>
+ <key>CFBundleDisplayName</key> <string>Navi</string>
+ <key>CFBundleExecutable</key> <string>navi</string>
+ <key>CFBundleIconFile</key> <string>AppIcon</string>
+ <key>CFBundleIdentifier</key> <string>com.navi.graph</string>
+ <key>CFBundleInfoDictionaryVersion</key> <string>6.0</string>
+ <key>CFBundlePackageType</key> <string>APPL</string>
+ <key>CFBundleShortVersionString</key> <string>${VERSION}</string>
+ <key>CFBundleVersion</key> <string>${VERSION}</string>
+ <key>LSMinimumSystemVersion</key> <string>11.0</string>
+ <key>NSHighResolutionCapable</key> <true/>
+ <key>LSApplicationCategoryType</key> <string>public.app-category.utilities</string>
+ <key>NSPrincipalClass</key> <string>NSApplication</string>
+</dict>
+</plist>
+PLIST
+
+# Ad-hoc codesign so macOS lets the user open it (still flagged by Gatekeeper
+# on first launch — instruct users to right-click → Open or `xattr -cr Navi.app`).
+codesign --force --deep --sign - "$APP" >/dev/null 2>&1 || \
+ echo "warn: codesign failed (continuing, app will trip Gatekeeper)"
+
+# ── Package ──────────────────────────────────────────────────────────────────
+echo "==> Packaging $ZIP"
+ditto -c -k --sequesterRsrc --keepParent "$APP" "$ZIP"
+
+du -h "$BIN" "$APP/Contents/MacOS/navi" "$ZIP" | sed 's/^/ /'
+echo "==> Done. Open with: open $APP"
+echo "==> Upload to release: gh release upload v${VERSION} $ZIP --clobber"