Skip to content

Release builds

This page mirrors implementation notes maintained in CLAUDE.md. Update both when changing this subsystem.

Cue ships as a signed, notarized .app / DMG (macOS) and a PyInstaller-built .exe + Inno Setup installer (Windows). The macOS path is the more involved of the two due to hardened runtime + Apple notary requirements.

macOS — Nuitka + Developer ID + notarization

Single command:

bash scripts/build_nuitka.sh

What that script does:

  1. Activates the conda cue env (uv-built env is for dev; the build env is conda for ABI consistency between Python, PyGObject, GStreamer, and the GStreamer plugins). See scripts/environment.yml.
  2. Compiles via Nuitka into a .app bundle.
  3. Bundles dist-info from the vendored owa packages so importlib.metadata.entry_points() discovers the plugin registrations.
  4. Resolves GStreamer.framework from the system path (not bundled — too large; users install GStreamer system-wide). A small libiconv compatibility shim is built so PyGObject finds the GNU iconv* symbol set.
  5. Bundles the llama-server binary under Cue.app/Contents/MacOS/bin/llama-b<tag>/ for on-device inference (downloaded automatically by the script if missing).
  6. Codesigns every Mach-O binary serially with the Developer ID Application certificate, hardened runtime, embedded entitlements (scripts/release/entitlements.plist), and a secure timestamp. Retries on transient timestamp-server hiccups.
  7. Notarizes the .app via xcrun notarytool submit --wait using a keychain profile (configured via xcrun notarytool store-credentials).
  8. Staples the notary ticket onto the .app.
  9. Builds a DMG, signs it with the same Developer ID identity, submits to the notary, staples.

Artifacts land in dist/Cue.app and dist/Cue-<version>.dmg.

Hardened runtime entitlements

scripts/release/entitlements.plist:

  • com.apple.security.cs.disable-library-validation — required because GStreamer.framework dylibs are not signed by our team-id.
  • com.apple.security.cs.allow-dyld-environment-variablesrun.py re-execs itself after setting DYLD_LIBRARY_PATH / GST_PLUGIN_PATH / GI_TYPELIB_PATH to point at the system GStreamer.framework. Hardened runtime strips these env vars unless this entitlement is granted.
  • com.apple.security.cs.allow-jit — required because GStreamer's orc SIMD JIT compiler calls pthread_jit_write_protect_np to toggle W^X memory.

Apple Developer prerequisites

  • Apple Developer Program membership.
  • Developer ID Application certificate installed in the login keychain (verify with security find-identity -v -p codesigning).
  • xcrun notarytool store-credentials cue-notarize run once with Apple ID + app-specific password + Team ID.
  • security set-key-partition-list -S apple-tool:,apple: run once on the cert so codesign doesn't prompt for keychain access on every binary.

The build script auto-detects the Developer ID identity via security find-identity and falls back to ad-hoc signing (--sign -) when none is present — useful for local-only smoke testing without distribution.

Selftests

The frozen build supports selftests for catching missing native libraries before users hit them:

Cue.app/Contents/MacOS/run --selftest=llama_server_health
Cue.app/Contents/MacOS/run --selftest=llama_server_import

The CI step runs these post-build.

Windows — PyInstaller + Inno Setup

python scripts/build_installer.py

Or step-by-step:

scripts\build_windows.bat       # PyInstaller .exe
python scripts/build_installer.py  # Inno Setup .msi

PyInstaller produces a one-folder Cue/ distribution that includes the bundled llama-server.exe under Cue\bin\llama-b<tag>\. Inno Setup (scripts/release/cue_setup.iss) wraps it into a Windows installer that registers the app, sets up the Start Menu shortcut, and creates the data dir on first run.

Code signing on Windows is currently TBD — the installer can be signed via SignTool with an Authenticode certificate, but the default build skips this step.

Bundled llama-server

The on-device digest backend uses an external multimodal inference binary, not llama-cpp-python. See On-device vision for the rationale.

The build matrix:

  • macOS arm64 / x64llama-<tag>-bin-macos-{arm64,x64}.tar.gz release artifact from ggml-org/llama.cpp.
  • Windows x64llama-<tag>-bin-win-cpu-x64.zip release artifact.

scripts/build_nuitka.sh and scripts/build_installer.py download + verify + extract these binaries into the bundle. The pinned release tag is updated by editing the manifest in scripts/.

CI integration

.github/workflows/ci.yml runs ruff lint + format on every push. Application release builds are not run in CI (they need keychain access and an Apple Developer cert that isn't in CI).

.github/workflows/docs.yml is a separate workflow:

  • On every PR + main push, runs uv pip install -e ".[docs]" and uv run --no-sync mkdocs build --strict. Catches broken nav, missing anchors, and mkdocstrings import regressions before the deploy.
  • On main pushes, additionally invokes cloudflare/wrangler-action@v3 with the CLOUDFLARE_API_TOKEN + CLOUDFLARE_ACCOUNT_ID repo secrets to push the built site/ to the cue Cloudflare Pages project (pages deploy site --project-name=cue --branch=main).
  • Submodule fetch is intentionally off in this workflow — docs build doesn't need ocap or owa, and Cloudflare's git-connect flow couldn't read our private vendor submodules anyway. That's the whole reason deploys go through GitHub Actions instead of Cloudflare's Git integration.

Public docs URL: https://cue-aif.pages.dev/.

Version

Single source of truth: pyproject.tomlversion. Also update _VERSION in main.py and the README footer.

See also