LumenDocumentation
Website
Browse documentation

Build and verify

Set up the toolchain, build and sign Lumen, run checks, and maintain the documentation.

On this page
Working from source

File references name paths in the local checkout. A public repository link will be added when available. Preparation commands are separate from normal app operation.

This page explains how to build, sign, test, and document Lumen from source. It's for contributors who change the app, its packages, or its documentation. To build and open the signed app for the first time, see Getting started.

Before you begin#

  • Use an Apple Silicon Mac with macOS 14 or later.
  • Install an Xcode toolchain with Swift 6 or later.
  • Before you change product behavior, read AGENTS.md, the architecture overview, and the preserved design system.

Workspace#

Open the root Package.swift in Xcode or another Swift-aware editor. The root package builds Lumen, LumenWorker, and LumenEvaluate. It depends on the local LocalGemmaCore and LocalAppUI packages and has no remote Swift package dependencies.

When the codebase knowledge graph is available, use it for code discovery. Treat the original design kit as reference material, and don't rewrite its prototypes to imply native support.

Build and sign#

To build and sign the app, run:

sh
./scripts/build-app.sh

The default configuration is debug. For a release-optimized local bundle, run ./scripts/build-app.sh release. Both configurations create build/Lumen.app, embed LumenWorker, copy notices, generate the icon, sign the helper and app, and verify the bundle. Wait for signing to finish before you inspect the UI.

Note: swift build compiles the code but doesn't assemble the signed app. swift run Lumen runs without the app bundle's intended sandbox and uses a different storage location, so validate permissions and privacy with the signed bundle.

Choose a signing identity#

Signing uses the single usable Apple application certificate in your keychain. If you have several, set LUMEN_SIGNING_IDENTITY to the hash of the certificate you want. To list certificate hashes, run security find-identity -v -p codesigning.

Keep using the same identity. macOS associates microphone and screen/audio permissions with its designated requirement. If you switch from an earlier ad-hoc signature, you approve permissions once for the new identity. The build never resets or grants permissions.

If no application certificate is available, the build warns and falls back to ad-hoc signing. To select ad-hoc signing explicitly, for example in CI, set LUMEN_SIGNING_IDENTITY=-. An ad-hoc signature is tied to one build, so permission grants don't survive executable changes.

Local signing doesn't use a timestamp service. Developer ID builds get the hardened runtime and a secure timestamp; see Build a small app that downloads its models.

Important: Don't add credentials or certificates to the repository.

Stage an update#

To stage an update while the installed app is in use, set LUMEN_APP_DIR to a separate absolute bundle path. A staged build doesn't register itself with Launch Services. Replace and register the normal bundle only after its running process has quit and its recording or indexing work is safe to interrupt.

Verify signatures#

To check a signed bundle, run:

sh
bash scripts/check-signing.sh build/Lumen.app

The check rejects hash-bound identities. To check that two certificate-signed builds recognize each other, pass both bundle paths. For certificate builds, the signing helper (scripts/sign-app.sh) runs the single-build check automatically.

scripts/test-signing.py signs two different synthetic executables and checks their requirements in both directions. It doesn't launch either fixture or request microphone or screen access. On machines without a signing identity, the certificate test is skipped.

Run correctness checks#

To run the correctness checks, run:

sh
./scripts/check.sh

The script does the following:

  • Runs the core package tests and root app tests.
  • Generates synthetic fixtures.
  • Proves that a loopback socket connection is denied under sandbox-exec.
  • Runs five retrieval queries through the worker and SQLite with networking blocked.

The current baseline contains these tests:

SuiteTests
Core package32
Root app10, including an opt-in real-model test that's skipped without prepared models
Python reference-adapter contract4
Notes-evidence validation3
Signing2

Run the real-model test separately with the prepared bundle. Counts change over time, so rely on the command output, and update the validation record when behavior changes.

Note: Synthetic runtime and audio fixtures and compatible signing requirements don't establish live capture or model quality.

After correctness changes, also run ./scripts/build-app.sh release. The release build catches bundle assembly and signing failures that compilation alone can't. For real-runtime measurements, see Evaluation, and keep quality claims separate from protocol tests.

Prepare and evaluate local models#

To prepare and evaluate the local AI bundle, follow the bundled local models guide. Explicit preparation uses pinned standalone Python and dependencies, revision-pinned safetensors snapshots, and LFS SHA-256 verification. The app build copies the prepared bundle and signs its native libraries and sandbox-inheriting Python interpreter. The app downloads nothing at startup, and models only when the person chooses to.

Build a small app that downloads its models#

Developer builds bundle every model, so the app is about 6.8 GB. For distribution from the website, the app leaves the weights out, and people download them once from Hugging Face when they choose Get started or Download in Settings › General. Everything that runs, including the Python runtime, stays in the signed app, so setup never needs system Python, Homebrew, uv, Xcode, ffmpeg, or git. The design is in model downloads.

  1. Prepare the local AI bundle as usual (build/local-ai/bundle).

  2. Build the small app. The build generates the download manifest from the pinned repositories and revisions in the model catalog and the search catalog: one https://huggingface.co/<repo>/resolve/<revision>/<file> address, size, and SHA-256 per file, re-checked against the tools' lumen-tools.json. A few small files that Hugging Face doesn't host, such as provenance notes and a license text, are embedded in the manifest. The manifest goes into Resources/ModelPacks/, and its SHA-256 is pinned in Info.plist as LumenModelManifestSHA256. Nothing is uploaded.

    sh
    LUMEN_APP_DIR="$PWD/build/model-packs/Lumen.app" LUMEN_MODEL_DOWNLOADS=1 bash scripts/build-app.sh release

    To check every address against Hugging Face's own checksums first (needs a network connection), run python3 scripts/make-model-manifest.py build/local-ai/bundle build/model-packs/manifest.json --check-remote, then build with LUMEN_MODEL_MANIFEST set to that file.

  3. Package it as a compressed disk image (ULMO, LZMA, by default; set LUMEN_DMG_FORMAT=ULFO for LZFSE, which is larger but opens faster):

    sh
    bash scripts/make-dmg.sh build/model-packs/Lumen.app

    With a Developer ID Application certificate and LUMEN_NOTARY_PROFILE set to an xcrun notarytool store-credentials profile, the script signs, notarizes, staples, and checks the image with Gatekeeper. scripts/sign-app.sh already signs Developer ID builds with the hardened runtime and a secure timestamp; to rehearse that with another certificate, set LUMEN_HARDENED_RUNTIME=1. Without both, the result is named …-unsigned-test.dmg, its volume says Unsigned test, and a note inside says it isn't notarized. Never publish a test image.

How it fits together:

  • LumenModelDownloader.xpc is the only Lumen code with the network client entitlement. It has its own sandbox, accepts only huggingface.co addresses (and Hugging Face's own storage on redirect), sends no cookies or identifiers, and writes only to file handles the app opens in its own container. scripts/check-signing.sh fails if any other code has a network entitlement.
  • Each file downloads with a plain GET. There is no resume: an interrupted file is fetched again from the start. Each file is verified against its SHA-256, refetched once if it doesn't match, then the download stops. A pack moves into Models/ in the app's data folder only when all of its files pass, with rollback.
  • Before the download, setup and Settings show a storage bar: used space, Lumen's share, and what's free, measured with volumeAvailableCapacityForImportantUsage. Get started stays unavailable until there's room for the download plus 1 GB.
  • A build whose BundledAI/Tools/*/models folders hold weights counts those packs as installed, so developer builds work as before.
  • scripts/test-pack-server.py serves small synthetic files at Hugging Face–shaped paths over loopback for the end-to-end test.

Check that nothing comes from the developer's Mac#

With a prepared bundle, scripts/check.sh also runs:

  • scripts/check-clean-machine.py: every tool verifies its files and runs a tiny real inference with env -i, PATH=/usr/bin:/bin, a temporary home, cache variables (HF_HOME, TRANSFORMERS_CACHE, TORCH_HOME, NUMBA_CACHE_DIR, XDG_CACHE_HOME) in a temporary container, and a sandbox that denies the network and any read of system or Xcode Python, Homebrew, /usr/local, ~/.cache, and ~/.local. Python tools report every module and library they loaded; anything outside the bundle, /System, or /usr/lib fails. Set LUMEN_SKIP_CLEAN_MACHINE=1 to skip it.
  • scripts/check-linkage.py: every Mach-O may link only to /System, /usr/lib, or @rpath, @executable_path, and @loader_path. The app build removes leftover run paths (from wheel builds, Homebrew, and Xcode) before signing.

The app gives every helper the same cache variables, inside its container.

Verify the native UI#

Inspect the signed app against a small synthetic fixture folder. Check the following:

  • First-run setup, when applicable
  • Keyword search and source pages
  • Empty and error states
  • Relaunch persistence
  • The sample label
  • Timestamp navigation
  • Markdown preview and save

When a change affects them, also check light and dark appearance, larger text, focus, and keyboard navigation.

Important: Don't grant microphone or system-audio access, or record a room, as an implicit automated check.

Real capture, permission denial and revocation, call routing, VoiceOver, long meetings, and target hardware need deliberate validation. Record exactly which paths you exercised and which you didn't.

Check the documentation#

You need Python 3 only for the documentation checker. To run it:

sh
python3 scripts/check-docs.py

The checker has no third-party dependencies. It checks local Markdown and HTML links, Markdown heading anchors, image and file targets, and that fenced blocks are closed. It covers root documents, docs, GitHub Markdown templates, runtime Markdown, and the Evaluation README. It doesn't fetch external URLs or validate archived design-kit pages. Run it after you edit navigation or move a guide.

Documentation guidelines#

  • Use exact visible UI labels and synthetic examples.
  • Put capability limits next to the workflow they affect.
  • Capture screenshots from the signed native app with fixture data. Each caption must distinguish samples from actual processing. For details, see the documentation assets guide.

Continuous integration#

The prepared GitHub workflow runs the documentation checks, the correctness script, and a signed release build on macOS. It won't run on a hosted runner until the repository is published. Until then, local checks are the evidence recorded in this folder.

Changes that need extra care#

These changes need the following evidence:

ChangeRequired evidence
Storage, retention, recoveryInput survives failure or cancellation; save ordering and scope remain correct
Grants or helper launchParent owns bookmarks; inherited helper can read only intended active scopes
Runtime integrationChecksums, provenance/license, schema validation, cancellation, network-denied execution, measured quality
SearchExact names/numbers, missing evidence, replacement/pruning, source/page validity
Product copyImplemented vs planned remains explicit; no unmeasured language/hardware claim

What's next#

By · Documentation updated

Found a problem? Start with troubleshooting.

Search documentation

Start typing to find a guide.

Documentation search runs in your browser. Queries are not sent to a search service.