Contents

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-studio runs 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.ts resolves 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 is resources/ beside it (../Resources on macOS). REELWRIGHT_RESOURCES overrides both, for tests.
  • Rendering. GodotEngine stops going through tools/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-studio beside 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 an override.cfg beside 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 an override.cfg that 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.
  • Studio's daemon. Studio starts reelwright serve --show <dir> beside it when it is installed, and node core/src/cli/main.ts serve from a checkout, as tools/studio.sh does now.
  • Tools. adapters/tools.ts looks in the bundled tools/ before PATH (paths.ts's bundledToolsDir: tools/ beside the core; REELWRIGHT_TOOLS for 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-everything and then only what the core runs ffmpeg for, listed once in the script's USES by 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 in tools/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 tag v<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 version prints 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.json once 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.
  • 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.sh does 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:

  1. build the core's single executable, and export Studio with the pinned Godot and its export templates;
  2. fetch and check the pinned tools, and build Rhubarb where it has no release;
  3. assemble the folder above and package it;
  4. 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 validate and compile on the demo show;
    • a still rendered through the exported Studio;
    • Studio launched with a show, which starts its daemon and answers session.hello;
  5. 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:

  1. One resource root in paths.ts, and reelwright version with the product version. (implementation) VERSION at the repository's root, which core/package.json matches.
  2. Rendering without bash: GodotEngine starts Godot itself; the render tests pass with the script gone from the render path. (implementation) core/src/render/godot-launch.ts, and reelwright engine <command>, which tools/engine-cli.sh now runs; REELWRIGHT_ENGINE names an exported engine.
  3. 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) and tools/release/smoke.ts, which runs it with an empty PATH and checks its Lighthouse timeline is byte-identical to the checkout's.
  4. Studio exported for Linux, rendering a still for the packaged core. (implementation) engine/export_presets.cfg has a preset per platform, each with the custom feature reelwright_studio, under which the project is called Reelwright Studio and keeps its own user folder; config/version is the product version, which the macOS and Windows metadata take. engine/cli/cli.gd opens 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 the reelwright beside its executable. The core, as one executable, starts the Studio beside it (render/godot-launch.ts's exportedEngine): reelwright-studio, on Windows reelwright-studio.console.exe (the console wrapper, whose output it reads), on macOS Reelwright Studio, not Reelwright, 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 --studio puts 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 match engine/tests/goldens on lavapipe. Studio launched from that folder opens the welcome screen, and with -- --show <dir> it starts the reelwright serve beside it and connects.
  5. The Linux package (AppImage and tarball) with its tools, and its smoke test. (implementation, the tarball) tools/release/fetch-tools.ts fetches the pinned ffmpeg and Rhubarb, checks their SHA-256 and lays them out as tools/ffmpeg/{bin,lib} and tools/rhubarb/{rhubarb,res} (Linux's lib/ 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 in tools/release/licenses/, or the build fails); build-studio Godot's and its components', from the engine CLI's new licenses command; fetch-tools ffmpeg's, where its source is, and Rhubarb's. tools/release/package.ts puts the parts in Reelwright-<version>-linux-x86_64/ with one LICENSES/ and a README, and packs it as .tar.xz (172 MB; 484 MB unpacked) with SHA256SUMS. smoke.ts --package unpacks 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 (see m5-writers-room.md, synth 1.2). (implementation, the AppImage) package.ts also makes Reelwright-<version>-x86_64.AppImage (206 MB) from the same folder, with the appimagetool and type 2 runtime pinned in tools/toolchain.json (--runtime-file, so nothing is fetched while it builds, and APPIMAGE_EXTRACT_AND_RUN, so the machine that builds it needs no FUSE). Its AppRun opens Studio (reelwright-studio -- "$@", so --show <dir> works); cli first, or a link to it named reelwright, runs the command line. The desktop entry and icon are the reel from the site, engine/app/icon.svg, which is also the project's config/icon that the macOS and Windows exports turn into theirs; Godot draws the AppImage's 256-pixel PNG. smoke.ts --package <x.AppImage> runs cli version through the AppImage itself, then smokes its files. Launched from the mounted image, Studio opens a show and runs the reelwright serve inside the mount, whose first open of a show without caches spends about 40 s on lip sync before the overview fills in.
  6. The release workflow for all three platforms. (implementation, the parts) Every part builds for any platform from any host. build-core takes --platform and injects the blob into the Node tools/toolchain.json pins 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 by tools/release/macho.ts, which writes what lipo -create writes (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 runs tools/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 --templates installs only the six export templates a release uses, out of Godot's pinned 1.3 GB archive. (implementation, Windows) package.ts --platform windows makes the same folder, Reelwright-<version>-windows-x86_64.zip, and …-setup.exe with NSIS 3 (tools/release/windows/reelwright.nsi; makensis runs on Linux too). The installer needs no administrator: it installs into %LOCALAPPDATA%\Programs\Reelwright unless 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_image in the toolchain, tools/release/windows-image.ts); smoke.ts checks 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 macos puts 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 with codesign --verify --deep --strict, and makes Reelwright-<version>-macos-universal.dmg with 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 and tools/release/codesign.ts checks 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.ts then 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.ts runs on all three: Windows' environment (a profile of its own, SystemRoot), any package (a folder or app, .tar.xz, .AppImage, .zip, -setup.exe installed, smoked, then uninstalled and checked gone, or .dmg mounted and copied out), universal programs and a strict signature for an app, a timeout on every command, and --no-render for a machine that cannot draw.
  7. The update check, the problem report, and the downloads page on reelwright.diy. (implementation, the problem report) reelwright doctor checks 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 (only tools/uv/install.sh fetched 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) RwUpdateCheck asks reelwright.diy/releases/latest.json (site/releases/latest.json, kept equal to VERSION by 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.ts checks the built packages against SHA256SUMS and 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.ts puts 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).