Distributing Scenes
This tutorial walks through the ways Luxar gets a finished scene in front of someone else: a standalone folder anyone with Python 3 can serve, a double-clickable native bundle with no Python dependency at all, and — when you would rather send a link than a folder — hosting the archive and opening it in the deployed viewer. We also cover how to share bundles across machines and operating systems without tripping macOS Gatekeeper.
What You Will Learn
How to export a Luxar scene as a self-contained folder anyone with Python 3 can run
How to produce double-clickable native bundles for macOS (.app) and Linux (portable folder)
How to share bundles across machines without tripping macOS Gatekeeper
How the embedded launcher’s lifecycle works (close window → graceful server shutdown)
When to use which option, including hosting the archive and sharing a viewer link
Prerequisites
A Luxar installation with the viewer built (
make build-viewer)For native bundles only: the launcher binaries built locally (
make install-go && make build-launchers) — see Build System Specification for the full toolchain setupA Zarr scene to export (see Tutorial 1: Creating Your First Scene to create one)
Folder export (zero deps for the recipient)
The default luxar export produces a self-contained folder with the
viewer, the zarr dataset, a tiny stdlib-only serve.py script, and
a README.txt:
luxar export my_scene.luxar.zarr -o my_export/
Output:
my_export/
viewer/ # Luxar viewer (HTML, JS, CSS, WASM)
data/ # Zarr dataset (copied as-is)
serve.py # Local HTTP server (Python 3 stdlib only)
README.txt # Quick-start instructions
The recipient runs:
cd my_export/
python serve.py
That starts a local server on a free port and opens the viewer in their
default browser. The only dependency is any Python 3 — no
pip install, no Node, no Rust. Add --open to launch a browser
on your end immediately:
luxar export my_scene.luxar.zarr -o my_export/ --open
Native bundles (no Python dependency at all)
For a recipient who shouldn’t have to install or even know about
Python, use --native. The output is a double-clickable bundle
wrapping a Go-compiled launcher around the viewer + data; the launcher
opens an embedded native WebView (WKWebView on macOS, WebKitGTK on
Linux) and serves the bundled zarr internally:
# macOS .app
luxar export my_scene.luxar.zarr -o out/ --native macos
# Linux portable folder for x86_64
luxar export my_scene.luxar.zarr -o out/ --native linux-amd64
# All three platforms at once, with a custom name
luxar export my_scene.luxar.zarr -o out/ \
--native macos,linux-amd64,linux-arm64 \
--name MyScene
Output structure (macOS):
out/
MyScene-README.txt # Sibling README (xattr -cr recovery, etc.)
MyScene.app/
Contents/
Info.plist # Bundle metadata + icon reference
MacOS/
launcher # Universal Mach-O (arm64 + amd64), +x
Resources/
AppIcon.icns # App icon
viewer/ # Luxar viewer
data/ # Zarr dataset
Output structure (Linux):
out/
MyScene-linux-amd64/
luxar-launcher # ELF, +x (needs webkit2gtk-4.1 runtime)
viewer/ # Luxar viewer
data/ # Zarr dataset
MyScene.png # Icon (FreeDesktop convention)
README.txt # Quick start + libwebkit2gtk dep note
Prerequisite: make build-launchers must have been run on a host
of the matching OS before luxar export --native ... can produce
that platform’s binary. CGO blocks pure cross-compilation, so a macOS
host cannot produce Linux binaries (and vice-versa) without a CGO
cross-toolchain. In practice you’d build each OS in CI.
Window lifecycle
The native launcher’s design philosophy is “close the window → close everything”:
Click the window’s red close button → the embedded WebView’s main loop exits → deferred
Destroy()runs → deferred HTTP serverShutdown()runs → the launcher process exits cleanly. No zombie WebKit child processes.Press Cmd+Q (macOS) or send the launcher SIGINT / SIGTERM → identical clean-shutdown path. The
LSUIElementflag is intentionally not set, so the app shows in the Dock and Cmd+Tab switcher.The launcher binds an ephemeral localhost port (no port collision), so multiple bundles can run side-by-side without interfering.
Browser fallback (headless smoke tests, or developers who want to
inspect with browser devtools): set the LUXAR_LAUNCHER_NO_WEBVIEW
environment variable:
LUXAR_LAUNCHER_NO_WEBVIEW=1 ./luxar-launcher
The launcher then opens the system default browser instead of an
embedded WebView; the local HTTP server still runs, you press
Ctrl+C to stop. This does not let the prebuilt Linux binary run
without libwebkit2gtk — WebKit is linked at build time, so the loader
aborts before the launcher can read this variable on a system missing the
webkit2gtk-4.1 runtime.
Looking ahead: signing and notarization
The xattr -cr workaround is fine for lab-internal sharing but not
for public distribution. The proper fix is an Apple Developer ID
signature (~$99/yr) plus the xcrun notarytool notarization step;
once notarized, macOS Gatekeeper accepts the bundle from any download
channel without warnings. The same idea applies on Windows
(Authenticode certificates) for when we add Windows support. None of
this changes the on-disk bundle format — signing and notarization
attach metadata to the existing .app, they don’t restructure it.
For now, xattr -cr covers all internal use cases. If your scene
needs to ship to people outside the lab, plan for the Developer ID
signing step.
When to use which
Use the folder export when:
The recipient has Python 3 (true on virtually any developer machine, any HPC node, any Linux distro)
You need the smallest possible artifact (the launcher binary adds ~6–11 MB; the folder is just viewer + data)
You want the recipient to be able to inspect / re-host with their own tooling (the
serve.pyis plain stdlib HTTP)
Use native bundles when:
The recipient should never see a terminal
You’re sharing with non-developers (collaborators, presentation audiences, paper reviewers)
You want a “real app” experience: Dock icon, Cmd+Q, native window controls