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
20 changes: 17 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,21 @@ rustup toolchain install nightly-2026-03-30 --component llvm-tools

**macOS:** Thuki is macOS-only. It uses NSPanel and Core Graphics APIs that are not available on other platforms.

**CMake:** required to build the bundled llama.cpp inference engine from source

```bash
brew install cmake
```

**Xcode:** full Xcode.app (from the App Store) is required, not just the Command Line Tools. The engine build needs `xcodebuild` to fetch the Metal toolchain that compiles its GPU shaders, and `xcodebuild` refuses to run when the active developer directory is the Command Line Tools. After installing Xcode, point `xcode-select` at it:

```bash
sudo xcode-select -s /Applications/Xcode.app
```

### Optional

No AI backend setup is required: Thuki bundles its own llama.cpp inference engine, and the dev/build scripts fetch the pinned `llama-server` sidecar automatically (see Development Setup below). Install these only if you want to develop against an alternative provider:
No AI backend setup is required: Thuki bundles its own llama.cpp inference engine, and the dev/build scripts build the pinned `llama-server` sidecar from source automatically (this uses the CMake and Xcode prerequisites above; see Development Setup below). Install these only if you want to develop against an alternative provider:

**Ollama:** to test the Ollama provider against a native install

Expand All @@ -66,9 +78,9 @@ No AI backend setup is required: Thuki bundles its own llama.cpp inference engin
bun install
```

3. **AI engine: nothing to set up**
3. **AI engine: built automatically**

Thuki bundles its own inference engine (llama.cpp's `llama-server`). On a fresh clone, the first `bun run dev` (or `build:backend` / `build:release`) automatically runs `bun run engine:ensure`, which downloads the pinned llama.cpp release, verifies its sha256, and installs the binary and its dylibs under `src-tauri/binaries/` (gitignored). This happens once; later runs are an instant no-op until the pin changes. You pick and download a starter model inside the app's onboarding flow.
Thuki bundles its own inference engine (llama.cpp's `llama-server`). On a fresh clone, the first `bun run dev` (or `build:backend` / `build:release`) automatically runs `bun run engine:ensure`, which clones the pinned llama.cpp tag, verifies the commit, then builds and installs the binary and its dylibs under `src-tauri/binaries/` (gitignored). That first build compiles llama.cpp from source, so it takes a while and needs the CMake and Xcode prerequisites above; later runs are an instant no-op until the pin changes. You pick and download a starter model inside the app's onboarding flow.

**Optional: develop against an alternative provider**

Expand All @@ -88,6 +100,8 @@ No AI backend setup is required: Thuki bundles its own llama.cpp inference engin

On first run, macOS will prompt for Accessibility permission. This is required for the global keyboard shortcut. Grant it once; it persists across restarts.

**Dev runs and permissions:** `bun run dev` launches a bare binary (`src-tauri/target/debug/thuki`), not an app bundle, so macOS attributes Accessibility and Screen Recording grants to the terminal app running the command (Terminal.app, iTerm, VS Code, etc.), not to Thuki. Grant both permissions to that terminal app. Switching terminal apps means re-granting.

---

## Running Tests
Expand Down
84 changes: 84 additions & 0 deletions scripts/ensure-llama-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,88 @@ async function verifyFrameworksList(needed: Set<string>): Promise<void> {
}
}

/**
* Outcome of a build-tool probe. `unknown` means the probe itself could not
* produce an answer (spawn threw, failed at the OS level for an unrelated
* reason, or returned no exit status).
*/
type ProbeResult = 'present' | 'missing' | 'unknown';

/**
* Probes a build tool by running a cheap version or lookup command.
*
* Every failure mode of the spawn is handled here: a probe must never throw out
* of preflight. Only a definitive answer is reported as `missing`; anything
* ambiguous is `unknown` so preflight stays out of the way and the existing
* build path fails as it does today. A permissive probe that misses a missing
* tool is an annoyance; a strict probe that trips on a working machine breaks
* every dev build and every release.
*
* @param command Tool to probe, resolved through `$PATH`.
* @param args Arguments for the probe invocation, kept cheap and side effect free.
* @returns Whether the tool is definitively present, definitively missing, or unknown.
*/
function probeTool(command: string, args: string[]): ProbeResult {
let result: ReturnType<typeof spawnSync>;
try {
result = spawnSync(command, args, { stdio: 'ignore' });
} catch {
return 'unknown';
}
if (result.error) {
const code = (result.error as NodeJS.ErrnoException).code;
// ENOENT (not on PATH) and EACCES (present but not executable) are the two
// definitive answers; any other spawn error says nothing about the tool.
return code === 'ENOENT' || code === 'EACCES' ? 'missing' : 'unknown';
}
if (typeof result.status !== 'number') {
return 'unknown';
}
return result.status === 0 ? 'present' : 'missing';
}

/**
* Checks the build prerequisites before any build work starts, and reports all
* of them at once so a contributor does not install one tool, re-run, and
* discover the next.
*
* Exits the process with an actionable message when a prerequisite is
* definitively missing; returns normally otherwise.
*/
function preflightBuildTools(): void {
const problems: string[] = [];

if (probeTool('cmake', ['--version']) === 'missing') {
problems.push(
' cmake is not installed. Install it with: brew install cmake',
' (see the prerequisites section of CONTRIBUTING.md)',
);
}

// The metal compiler probe is the source of truth: if the compiler is already
// there, the build needs nothing from Xcode.app and xcodebuild is irrelevant.
// xcodebuild only matters on the one path that uses it, the toolchain
// download in ensureMetalCompiler. `xcodebuild -version` is the same gate that
// download hits: it exits nonzero when the active developer directory is a
// Command Line Tools instance rather than Xcode.app.
if (
probeTool('xcrun', ['-sdk', 'macosx', '-f', 'metal']) === 'missing' &&
probeTool('xcodebuild', ['-version']) === 'missing'
) {
problems.push(
' the Metal shader compiler is missing and xcodebuild cannot download it,',
' because the active developer directory is a Command Line Tools instance.',
' Install Xcode from the App Store, then run:',
' sudo xcode-select -s /Applications/Xcode.app',
' (see the prerequisites section of CONTRIBUTING.md)',
);
}

if (problems.length > 0) {
fail(['missing build prerequisites:', ...problems].join('\n'));
}
}

// Ensures Apple's Metal shader compiler is available. GGML_METAL_EMBED_LIBRARY
// needs it to compile the shaders into the dylib at build time. It ships with
// Xcode <= 16; Xcode 26 split it into a separately downloadable component. The
Expand Down Expand Up @@ -318,6 +400,8 @@ if (await exists(binPath)) {
}
}

preflightBuildTools();

const workDir = await mkdtemp(join(tmpdir(), 'thuki-llama-'));
try {
const binDir = buildFromSource(workDir);
Expand Down