M11 Release: an installable Studio for macOS, Windows and Linux
Reelwright ships first as an installable, versioned, supported desktop edition, and only then as a web service (the owner's order, 2026-10-02; ADR-0011). This note says what an installed Reelwright is, how its parts find each other without a source checkout, and the order it is built in.
Read with ADR-0011 (one UI, two hosts), ADR-0009 (voices and lip sync) and
docs/design/m1-engine.md (the render job).
What gets installed
One folder per platform, the same inside:
Reelwright/ (macOS: Reelwright Studio.app/Contents/MacOS and …/Resources)
reelwright-studio[.exe] Studio and the renderer: the engine project, exported
(macOS: Reelwright Studio; Windows: and its .console.exe)
reelwright[.exe] the core: CLI, daemon and MCP server, one executable
resources/
VERSION the product version, e.g. 0.11.0
schemas/v1/ the document schemas
library/ rig templates, gesture and part libraries
examples/marigold/ the demo show, read-only
toolchain.json pinned tool versions and checksums
tools/ rhubarb, ffmpeg, ffprobe for this platform
LICENSES/ every bundled component's licence
On macOS the folder is the app, and code and data are kept apart, as code signing wants:
Contents/MacOS holds Reelwright Studio, reelwright beside it and tools/ (the
programs); Contents/Resources holds what resources/ holds, LICENSES/, and Rhubarb's
models, which tools/rhubarb/res links to.
- Studio and the renderer are one export. The engine project's main scene is the
engine CLI (
res://cli/cli.tscn). Exported,reelwright-studioruns a render job when given one (--headless -- --job <job.json> --out <dir>) and opens Studio when it is not, so the renderer that previews and the renderer that renders are the same code, as they are today. Its settings live in its own user folder,Reelwright(Linux~/.local/share/Reelwright, macOS~/Library/Application Support/Reelwright, Windows%APPDATA%\Reelwright). - The core is a single-file executable (Node's single executable application): the core bundled into one script and injected into the Node binary. Nobody installs Node, Godot or Python.
- Shows live wherever the user keeps them. The installed folder is read-only. Caches
(voice takes, previews, timelines) stay in each show's
.reelwright/, and user-wide downloads (the Kokoro model, uv) in the user cache (REELWRIGHT_CACHE, default~/.cache/reelwright,~/Library/Caches/Reelwright,%LOCALAPPDATA%\Reelwright\Cache).
How the parts find each other
- Resources.
core/src/paths.tsresolves everything it reads (schemas, the library, the demo show, the toolchain) from one resource root. From a checkout that is the repository, as today. In the single executable it isresources/beside it (../Resourceson macOS).REELWRIGHT_RESOURCESoverrides both, for tests. - Rendering.
GodotEnginestops going throughtools/engine-cli.sh, a bash script that cannot run on Windows. It starts Godot itself:- from a checkout,
godot --headless --path engine -- …, after the same import check the script makes; - installed, the
reelwright-studiobeside it, whose pack holds the engine project. (0.3.1) Godot opens its main window whatever it is asked to do, and an exported engine takes that window's settings only from its pack, its command line and anoverride.cfgbeside the executable it was started as. Studio and the renderer being one program, the core starts the renderer through a second name for it in the user cache (<cache>/engine/<key>/: a hard link, or a copy where none can be made; on macOS an app of symbolic links) beside anoverride.cfgthat asks for no boot splash, no focus and a 1×1 borderless window at the screen's far corner. Its frames are drawn off screen anyway.
- from a checkout,
- Studio's daemon. Studio starts
reelwright serve --show <dir>beside it when it is installed, andnode core/src/cli/main.ts servefrom a checkout, astools/studio.shdoes now. - Tools.
adapters/tools.tslooks in the bundledtools/beforePATH(paths.ts'sbundledToolsDir:tools/beside the core;REELWRIGHT_TOOLSfor tests). The environment overrides (FFMPEG,FFPROBE,RHUBARB,UV) still win.
The tools that come with it
- ffmpeg and ffprobe, GPL builds with libx264, bundled as separate executables. They
are GPL; Reelwright calls them as programs, and ships their licence and where their source
is in
LICENSES/. Linux carries a lean build of its own (tools/release/build-ffmpeg.ts): FFmpeg 9.0.2 from its release tarball with--disable-everythingand then only what the core runs ffmpeg for, listed once in the script'sUSESby who needs it, linked statically with x264, FreeType, HarfBuzz and Brotli (the storyboard's captions, in the WOFF2 Inter) and zlib, every source pinned by SHA-256. ffprobe is configured apart with only the reading side. 14 MB for both programs, which load no library at all, against BtbN's 193 MB of libraries; they are built once into the user cache, in about two minutes. Windows uses BtbN's build of FFmpeg's 9.0 release branch with shared libraries (one copy for both programs), from a month-end autobuild, which BtbN keeps, pinned by SHA-256 intools/toolchain.json. Not jellyfin-ffmpeg, which links libfdk-aac into a GPL build. BtbN builds nothing for macOS, so the Mac carries Martin Riedl's static GPL builds of the 9.0.2 release (ffmpeg.martin-riedl.de: one per architecture, signed with his Developer ID, made by his public build scripts, linking only macOS's own libraries), pinned by the SHA-256 published beside each, and joined into universal binaries. The lean build is written to be pointed at Windows (mingw-w64) and macOS later; their CI cannot be watched from the dev box yet. - Rhubarb Lip Sync 1.14.0: the release binaries for Linux x86_64 and Windows. On macOS
its release is Intel-only and crashes under Rosetta (ADR-0009), so the release builds it
from its pinned commit for arm64 and x86_64 at once, one universal binary
(
tools/rhubarb/install.sh --universal, on the Mac runner). - Voices. Kokoro runs under uv and downloads a 330 MB model, so it is not in the
installer. The first time a show is voiced, Reelwright fetches the pinned uv and the
model into the user cache, checks their checksums, and says so. Until then lines are
timed from their text (
--estimate-takes), so everything but the voices works offline.
Versions
- One product version, semantic, in
resources/VERSION. Studio, the core and the release share it, and a release is a git tagv<version>. It stays 0.x until the owner calls a release 1.0. - Documents keep their own schema versions (EpisodeIR 1.26, Timeline 1.x, …), all under
schemas/v1.reelwright versionprints the product version, and each release's notes list the schema versions it writes. A release reads every older minor version of each document and writes the newest; reading one changes nothing on disk until something is saved. - Each release has a changelog section (Keep a Changelog), cut from
[Unreleased]when it is tagged.
Updates and support
- Update check. Studio asks
https://reelwright.diy/releases/latest.jsononce a day, can be switched off, and only says that a newer release is out and where its notes are. It never downloads or installs anything by itself. - Supported platforms:
- macOS 13.5 or later (Node 24's floor), Apple silicon and Intel, as one universal
.dmg; - Windows 10 and 11 (x64), as an installer and a
.zip; - Linux x86_64 (glibc 2.35 or later), as an AppImage and a
.tar.xz. - Each release is tested on each of them.
- macOS 13.5 or later (Node 24's floor), Apple silicon and Intel, as one universal
- Report a problem.
reelwright doctor --report <file>(and the same from Studio's menu) writes one zip: the versions, the platform, the tool checks, the last logs, and the show's document hashes. It never includes API keys, voice takes or the show's text. - Signing. The macOS app is signed with a Developer ID and notarized, and the Windows
installer is signed. Both need the owner's credentials. Until then releases are marked
unsigned, and the install notes say how to open them. For Windows the seam is one setting:
REELWRIGHT_WINDOWS_SIGN, a command that signs one program in place with Authenticode, run on every program before the .zip and the installer are made and on the installer and its uninstaller as NSIS writes them (tools/release/windows/sign.shdoes it from a .pfx with osslsigncode). Unsigned, the first run meets SmartScreen: "Windows protected your PC", publisher unknown, More info and Run anyway.
Built and checked by CI
.github/workflows/release.yml runs on a v* tag, on Linux, macOS and Windows runners:
- build the core's single executable, and export Studio with the pinned Godot and its export templates;
- fetch and check the pinned tools, and build Rhubarb where it has no release;
- assemble the folder above and package it;
- smoke-test the package, not the checkout. In a clean temporary home, with no Node,
Godot or repository on
PATH, it checks five things:reelwright version;reelwright validateandcompileon the demo show;- a still rendered through the exported Studio;
- Studio launched with a show, which starts its daemon and answers
session.hello;
- sign it when the secrets are present, and publish it on reelwright.diy with its SHA-256 and the release notes.
(implementation) Linux builds everything any host can, because one pinned Godot exports
Studio for every platform the same and the Windows core is the Windows Node with the blob in
it: job linux (ubuntu-24.04) builds the Linux and Windows packages and Studio's macOS app,
and smokes the .tar.xz and the AppImage. A Mac must sign, make the disk image and build
Rhubarb, so job macos (macos-15, Apple silicon) builds the Mac's package from that app and
smokes it. windows (windows-2022) smokes the installer, then the .zip; macos-intel
(macos-15-intel) smokes the disk image on Intel with --no-render, since GitHub's Intel Macs
have no GPU that Godot can draw with. A last job collects every package with one
SHA256SUMS into the run's artifact Reelwright-<version>. Publishing stays a person's
step, so the workflow holds no secrets, and there is nothing to sign with yet. The smoke
checks a fifth thing, with no display needed: Studio started headless on the demo show
starts its daemon and opens the show through it (session.hello), which it logs.
The order it is built in
Each step ends with something run and watched, on Linux first:
- One resource root in
paths.ts, andreelwright versionwith the product version. (implementation)VERSIONat the repository's root, whichcore/package.jsonmatches. - Rendering without bash:
GodotEnginestarts Godot itself; the render tests pass with the script gone from the render path. (implementation)core/src/render/godot-launch.ts, andreelwright engine <command>, whichtools/engine-cli.shnow runs;REELWRIGHT_ENGINEnames an exported engine. - The core as one executable on Linux, run from an empty directory: version,
validate, compile. (implementation)
tools/release/build-core.ts(esbuild, then postject into a copy of Node) andtools/release/smoke.ts, which runs it with an empty PATH and checks its Lighthouse timeline is byte-identical to the checkout's. - Studio exported for Linux, rendering a still for the packaged core.
(implementation)
engine/export_presets.cfghas a preset per platform, each with the custom featurereelwright_studio, under which the project is called Reelwright Studio and keeps its own user folder;config/versionis the product version, which the macOS and Windows metadata take.engine/cli/cli.gdopens Studio's scene when that feature is on and no command was given (none, or only Studio's own--show), and Studio's default daemon is then thereelwrightbeside its executable. The core, as one executable, starts the Studio beside it (render/godot-launch.ts'sexportedEngine):reelwright-studio, on Windowsreelwright-studio.console.exe(the console wrapper, whose output it reads), on macOSReelwright Studio, notReelwright, which on a case-insensitive disk would be the core's own name.tools/release/build-studio.ts --platform <p>exports any platform from any host; macOS needs the project's ETC2/ASTC import for arm64 (every texture is lossless, so no desktop pixel changes) and is signed ad hoc by Godot's own signer. Adding the core and its resources to the app breaks that signature, so the package (step 6) signs the whole app again.tools/release/smoke.ts --studioputs a built core and Studio in one folder and, with GODOT naming nothing and no Godot on PATH, renders the Lighthouse's nine golden stills through the core, which matchengine/tests/goldenson lavapipe. Studio launched from that folder opens the welcome screen, and with-- --show <dir>it starts thereelwright servebeside it and connects. - The Linux package (AppImage and tarball) with its tools, and its smoke test.
(implementation, the tarball)
tools/release/fetch-tools.tsfetches the pinned ffmpeg and Rhubarb, checks their SHA-256 and lays them out astools/ffmpeg/{bin,lib}andtools/rhubarb/{rhubarb,res}(Linux'slib/went with BtbN's build: its lean ffmpeg is static, and the package 31% smaller). Each part writes its licences: build-core Reelwright's, Node's and every bundled npm package's, read from esbuild's metafile (a package that ships none needs its licence kept intools/release/licenses/, or the build fails); build-studio Godot's and its components', from the engine CLI's newlicensescommand; fetch-tools ffmpeg's, where its source is, and Rhubarb's.tools/release/package.tsputs the parts inReelwright-<version>-linux-x86_64/with oneLICENSES/and a README, and packs it as.tar.xz(172 MB; 484 MB unpacked) withSHA256SUMS.smoke.ts --packageunpacks it and, with FFMPEG, FFPROBE and RHUBARB cleared and no Node or Godot on PATH, also compiles the demo with its recorded takes and renders, mixes and encodes two seconds of it to H.264 and AAC, read back by the package's own ffprobe. Packaging found that recipe sounds failed on any ffmpeg from 8.0 (seem5-writers-room.md, synth 1.2). (implementation, the AppImage)package.tsalso makesReelwright-<version>-x86_64.AppImage(206 MB) from the same folder, with the appimagetool and type 2 runtime pinned intools/toolchain.json(--runtime-file, so nothing is fetched while it builds, andAPPIMAGE_EXTRACT_AND_RUN, so the machine that builds it needs no FUSE). Its AppRun opens Studio (reelwright-studio -- "$@", so--show <dir>works);clifirst, or a link to it namedreelwright, runs the command line. The desktop entry and icon are the reel from the site,engine/app/icon.svg, which is also the project'sconfig/iconthat the macOS and Windows exports turn into theirs; Godot draws the AppImage's 256-pixel PNG.smoke.ts --package <x.AppImage>runscli versionthrough the AppImage itself, then smokes its files. Launched from the mounted image, Studio opens a show and runs thereelwright serveinside the mount, whose first open of a show without caches spends about 40 s on lip sync before the overview fills in. - The release workflow for all three platforms.
(implementation, the parts) Every part builds for any platform from any host. build-core
takes
--platformand injects the blob into the Nodetools/toolchain.jsonpins for it (node.release, 24.21.0, from nodejs.org with its SHA-256), never the Node that runs it, which must still be that version because it writes the blob. Windows' Node has its Authenticode signature cut off first, since the injection would leave it invalid. macOS gets both Nodes injected and joined into one universal binary bytools/release/macho.ts, which writes whatlipo -createwrites (byte for byte, at the same alignment) so a Linux machine can build and check it; only a Mac signs it. fetch-tools takes Windows' Rhubarb from its release zip and, for macOS, makes ffmpeg and ffprobe universal the same way and runstools/rhubarb/install.sh --universal(Rhubarb built for arm64 and x86_64 at once; a Mac only). The export's minimum macOS is 13.5, Node 24's.tools/godot/install.sh --templatesinstalls only the six export templates a release uses, out of Godot's pinned 1.3 GB archive. (implementation, Windows)package.ts --platform windowsmakes the same folder,Reelwright-<version>-windows-x86_64.zip, and…-setup.exewith NSIS 3 (tools/release/windows/reelwright.nsi; makensis runs on Linux too). The installer needs no administrator: it installs into%LOCALAPPDATA%\Programs\Reelwrightunless told otherwise, adds Reelwright Studio to the Start menu, a desktop shortcut if ticked, and the folder to the user's PATH unless unticked (path.ps1, which edits the registry value whole, where NSIS's own strings stop at 1024 characters), and registers "Uninstall Reelwright.exe" with Settings > Apps. Over an older install, and when uninstalling, it removes only what the package holds, never the rest of a folder someone chose. 176 MB, against 229 MB for the zip. On this Linux machine, under Wine, the Windows core prints its version, validates the demo, and compiles it with its own ffprobe.exe and rhubarb.exe to the same timeline bytes as the Linux checkout; the installer installs and the uninstaller removes, both silently. Every release builds Windows here, beside Linux and macOS (the lead's release-build.sh), with makensis and osslsigncode from Ubuntu's packages and a portable Wine 11.0, each pinned (windows_imagein the toolchain,tools/release/windows-image.ts);smoke.tschecks the installer and the .zip on Linux (their layout, every program's PE header, Studio's version, the signature or its absence, the core's code against the Linux core's) and runs them by Wine: installed, the core's checks, uninstalled. (implementation, macOS)package.ts --platform macosputs the core in Studio's app (layout above), signs every program and then the app ad hoc (keeping the microphone entitlement Godot gave Studio), checks it withcodesign --verify --deep --strict, and makesReelwright-<version>-macos-universal.dmgwith an Applications link. Unsigned by a Developer ID, so the first launch needs System Settings' Open Anyway (or removing the quarantine flag). (implementation, macOS on Linux) The release's Mac package is built on the same Linux machine as Linux's, so every release ships both at once: the core and Studio already cross-built; Rhubarb, which has no release for Apple silicon, is cross-compiled from its pinned source with Zig (no Apple SDK), and on Linux gives the same bytes as Rhubarb's own release on every recorded take; rcodesign signs the app ad hoc andtools/release/codesign.tschecks every page hash, binding and sealed file as macOS would; mkfs.hfsplus and Mozilla's libdmg-hfsplus make the disk image (lzma), read back and compared file by file.smoke.tsthen checks the image on Linux with 7-Zip: layout, Info.plist, universal programs, the signature, the core's code (the same blob as the Linux core's, byte for byte), and every core check on the app's own resources. What only a Mac proves (mounting, the first launch, running Studio and the Zig-built Rhubarb) the release workflow's Mac jobs exercise on the tag, with a Mac-built package. (implementation, the smoke)smoke.tsruns on all three: Windows' environment (a profile of its own,SystemRoot), any package (a folder or app,.tar.xz,.AppImage,.zip,-setup.exeinstalled, smoked, then uninstalled and checked gone, or.dmgmounted and copied out), universal programs and a strict signature for an app, a timeout on every command, and--no-renderfor a machine that cannot draw. - The update check, the problem report, and the downloads page on reelwright.diy.
(implementation, the problem report)
reelwright doctorchecks the install, resources, engine, ffmpeg (with libx264), ffprobe, Rhubarb, voices and the cache's free space, each with how to fix it, and--report <file.zip>writes them with the system, the settings, the show's document hashes, its validation codes and its last render results into one zip, with no key, take, transcript or show text. The package smoke runs it. Voicing, which the doctor found impossible in a release (onlytools/uv/install.shfetched uv), now fetches the pinned uv into the cache the first time a line is voiced, and keeps Kokoro's Python environment in the cache too, since an installed project is read-only: the release, from an empty home, voices the salvage-claim episode's 12 lines in under a minute plus the model's download. (implementation, the update check)RwUpdateCheckasksreelwright.diy/releases/latest.json(site/releases/latest.json, kept equal toVERSIONby a test) at most once a day while the welcome screen's switch is on, and a newer version shows on the welcome screen and in the rail with a link to its notes; it never downloads anything. Studio's "Report a problem…" (welcome screen and the rail's show menu) saves the same zip through the core its daemon runs. A first-time user has no show, so the welcome screen's "Try the demo show" makes a copy of the Marigold the release carries (reelwright init <dir> --demo) in Documents/Reelwright (Reelwright in the home folder where there is no Documents folder), opens it and says where it is: a show with episodes to watch and change at once. (implementation) "New show…" makes a show of the person's own (reelwright init <dir> --blank --title <title> [--logline] [--setting] [--style]): its own theme from their words, Reelwright's built-in styles (engine/addons/reelwright/library/styles/) and its starting patterns (cartoon people, a room and the open air,engine/addons/reelwright/library/blank/), nothing of the Marigold, and no cast, sets or episodes, which Studio's New character, New set and New episode then make with no model. The Marigold's kit (--starter) and a copy of the demo (--demo) are the other two starting points, each said by name. (implementation, the downloads page) The repository is private, and reelwright.diy is a static site, so the packages live on the web service's volume and it serves them to anyone at/releases/<version>/<file>(docs/design/m12-web-service.md), as downloads that resume.tools/site/releases.tschecks the built packages againstSHA256SUMSand records each one's size, SHA-256 and that URL for reelwright.diy/download, the release feed and Studio's update check. 0.1.0, the first release, is out for Linux (2026-10-04). From 0.9.0 it records Linux's and the Mac's folders together, and latest.json and the feed name every package with its SHA-256;tools/release/publish.tsputs each platform on the service (staged while the gate runs, then moved in with one merged SHA256SUMS; a different file under a published name is refused).