Skip to content
Merged
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
15 changes: 15 additions & 0 deletions apps/macos/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# SwiftPM build products & local tooling
.build/
.swiftpm/
DerivedData/

# Packaging output (.app/.dmg/.zip — built by scripts/package-app.sh)
dist/

# Xcode
*.xcodeproj
*.xcworkspace
xcuserdata/

# macOS
.DS_Store
363 changes: 363 additions & 0 deletions apps/macos/DESIGN.md

Large diffs are not rendered by default.

116 changes: 116 additions & 0 deletions apps/macos/DISTRIBUTION.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# Lattice Studio — Distribution Guide

## Building a distributable .app

Run the packaging script from the repo root:

```bash
cd lattice/
./apps/macos/scripts/package-app.sh
```

This will:
1. Run `swift build -c release` to produce the Swift instrument panel.
2. Build all 6 Rust engine binaries with `cargo build --release`.
3. Assemble `apps/macos/dist/LatticeStudio.app` with the engine binaries inside
`Contents/Resources/bin/` so the app is fully self-contained.
4. Write `Contents/Info.plist` with bundle ID `ai.khive.lattice.studio`.
5. Ad-hoc codesign the bundle.
6. Produce `apps/macos/dist/LatticeStudio.dmg` and `LatticeStudio.zip`.

Options:

```bash
# Skip Swift rebuild (use existing .build/release/LatticeStudio)
./apps/macos/scripts/package-app.sh --skip-build

# Skip Cargo rebuild (use existing target/release/ binaries)
./apps/macos/scripts/package-app.sh --skip-cargo

# Custom output directory
./apps/macos/scripts/package-app.sh --out /path/to/out
```

## Installing

Drag `LatticeStudio.app` from the DMG to `/Applications`.

Requires macOS 14.0 (Sonoma) or later. The app is self-contained — no source
checkout, no Cargo, no Rust toolchain needed on the recipient machine.

## Gatekeeper caveat (ad-hoc signing)

The app is ad-hoc signed, which means macOS Gatekeeper will quarantine it on
first launch because it lacks a Developer ID certificate. Recipients must:

**Option 1 — right-click → Open:**
1. Right-click (or Ctrl-click) `LatticeStudio.app` in Finder.
2. Choose "Open" from the context menu.
3. Click "Open" in the dialog. macOS remembers the exception.

**Option 2 — remove the quarantine xattr:**
```bash
xattr -dr com.apple.quarantine /Applications/LatticeStudio.app
```

This is a one-time step. The app will open normally on subsequent launches.

## Upgrading to Developer ID signing (for App Store or notarized distribution)

When Ocean has a $99/year Apple Developer Program membership, replace the
ad-hoc step in `package-app.sh` with a full Developer ID workflow:

```bash
# 1. Replace the ad-hoc sign step with a Developer ID certificate
DEVELOPER_ID="Developer ID Application: Haiyang Li (TEAMID)"
codesign --force --deep --options runtime \
--entitlements apps/macos/LatticeStudio.entitlements \
--sign "$DEVELOPER_ID" \
"$BUNDLE"

# 2. Create a notarization credentials profile (one-time)
xcrun notarytool store-credentials "lattice-notary" \
--apple-id "lhydyxzh@gmail.com" \
--team-id "TEAMID" \
--password "@keychain:AC_PASSWORD"

# 3. Submit for notarization and wait
xcrun notarytool submit "$DMG" \
--keychain-profile "lattice-notary" \
--wait

# 4. Staple the notarization ticket to the DMG
xcrun stapler staple "$DMG"
```

After notarization, Gatekeeper accepts the app on any Mac without the
right-click-Open workaround. Distribution via direct download or any store
works without restrictions.

An entitlements file (`LatticeStudio.entitlements`) must grant at minimum:
```xml
<key>com.apple.security.cs.allow-jit</key><false/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key><false/>
```

The engine binaries (Rust) do not use JIT, so hardened runtime is compatible.

## Regenerating the app icon

```bash
uv run apps/macos/scripts/generate-icon.py
```

Outputs `apps/macos/Resources/LatticeStudio.icns`. The packaging script picks
it up automatically, or re-runs the generator if the file is missing.

## Binary resolution order (inside the .app)

The Swift bridge resolves engine binaries in this order:
1. `Contents/Resources/bin/<name>` — bundled binary (used from /Applications).
2. `LATTICE_BIN_DIR` env var — dev override.
3. `<repo root>/target/release/<name>` — running from source checkout.
4. `cargo run --release` — compile-on-demand fallback.

This means the same app binary works both installed from the DMG and invoked
during development inside the source tree.
25 changes: 25 additions & 0 deletions apps/macos/Package.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
// swift-tools-version: 6.0
import PackageDescription

// Lattice Studio — the instrument panel for the pure-Rust lattice engine.
// Driven entirely via lattice CLI subprocesses (line-delimited `@@lattice {json}`
// event protocol). No in-process ML, no Python, no ONNX — same posture as the engine.
let package = Package(
name: "LatticeStudio",
platforms: [
.macOS(.v14)
],
products: [
.executable(name: "LatticeStudio", targets: ["LatticeStudio"])
],
dependencies: [],
targets: [
.executableTarget(
name: "LatticeStudio",
path: "Sources/LatticeStudio",
swiftSettings: [
.swiftLanguageMode(.v5)
]
)
]
)
Binary file added apps/macos/Resources/LatticeStudio.icns
Binary file not shown.
43 changes: 43 additions & 0 deletions apps/macos/Sources/LatticeStudio/App/LatticeStudioApp.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import SwiftUI
import AppKit

final class AppDelegate: NSObject, NSApplicationDelegate {
// Weak back-reference injected by LatticeStudioApp so the delegate can reach the store.
// (NSApplicationDelegate is an ObjC protocol; we cannot pass the store in an init.)
weak var store: AppStore?

func applicationDidFinishLaunching(_ notification: Notification) {
NSApp.setActivationPolicy(.regular)
NSApp.activate(ignoringOtherApps: true)
}

/// Best-effort clean-quit: stop the active run so its trainer process exits normally
/// and its PID is deregistered before the app terminates. The startup reaper handles
/// the crash / force-quit case where this callback never fires.
func applicationWillTerminate(_ notification: Notification) {
store?.stopRun()
}

func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { true }
}

@main
struct LatticeStudioApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) private var appDelegate
@State private var store = AppStore()

var body: some Scene {
WindowGroup("Lattice", id: "main") {
ContentView(store: store)
.frame(minWidth: 1120, minHeight: 720)
.onAppear {
store.onAppear()
// Give the delegate a weak reference so applicationWillTerminate can
// stop the active run for a clean-quit. The reaper is the crash backstop.
appDelegate.store = store
}
}
.windowStyle(.titleBar)
.windowToolbarStyle(.unified(showsTitle: false))
}
}
Loading
Loading