Skip to content

Ship an unsigned preview Nocturne.app and DMG - #67

Merged
Okonai merged 7 commits into
mainfrom
release/unsigned-preview-dmg
Oct 27, 2026
Merged

Okonai merged 7 commits into
mainfrom
release/unsigned-preview-dmg

Conversation

@Okonai

@Okonai Okonai commented Oct 27, 2026 •

Copy link
Copy Markdown
Contributor

Ships a downloadable unsigned preview of Nocturne as one standalone Nocturne.app + DMG. No Apple Developer ID yet; the signed/notarized DMG stays tracked in #32.

What this does

  • One app, starts its own engine (A). The download is a single Nocturne.app: the JUCE UI binary and the engine binaries (nocturne, nocturne-supervisor, nocturne-ctl, nocturne-desk) share one Contents/MacOS. On an interactive launch against a loopback host the UI ensures a local engine via a new EngineLauncher: it reuses one already answering on 127.0.0.1:7400, otherwise spawns the bundled nocturne-supervisor beside it (found by bundle path, not the build dir), and on quit stops only the engine it started. The UI stays a loopback-only protocol client otherwise — no new listeners; a loopback client needs no pairing token, so the trust model is untouched. The show machine's LaunchAgent path (tools/autostart.sh) is unchanged; a dev build with two separate bundles is a no-op.
  • Ad-hoc signed, hardened runtime. tools/make-bundle.sh assembles the bundle and signs every nested binary then the bundle inside-out (codesign --force --options runtime --timestamp=none -s -) with tools/nocturne.entitlements (audio input, network client + server). codesign --verify --deep --strict passes; spctl -a -vv rejects it on purpose (not notarized).
  • Icon. Placeholder original art (crescent moon + wordmark) in assets/icon/ as SVG → 1024 PNG → .icns (make-icns.sh), wired into the merged Info.plist with CFBundleIdentifier, CFBundleShortVersionString (from the CMake project version), LSMinimumSystemVersion.
  • DMG. tools/make-dmg.sh → Nocturne-<version>-unsigned.dmg (app + Applications symlink + FIRST LAUNCH.txt) and a .sha256.
  • CI. .github/workflows/release.yml builds the universal bundle on a v* tag, makes the DMG and attaches it to a draft release. Tag/dispatch only — never a required PR check.
  • Docs. README Download section; docs/install.md (unsigned preview, open on macOS 15+ via Open Anyway and macOS 14 via right-click → Open, the xattr -dr com.apple.quarantine alternative and what it does, checksum verify, signed/notarized later Release workflow: signed, notarized, stapled universal DMG on a tag #32). ADR 0015 records the decision and amends ADR 0010 for this preview only.

Tests

  • New tests/ui/engine_launcher_test.cpp: reuse, spawn-once, stop-only-ours, failed spawn (4 cases).
  • Full UI suite green locally: 150 passed, 0 failed.
  • Universal slices confirmed (lipo -archs: x86_64 arm64), entitlements + hardened runtime confirmed, bundled engine runs offscreen and serves ws://…:7400.

Tested on

  • Build + all local tests on Apple Silicon (M2-class, macOS 26.2). CI tests arm64 only.
  • Not verified on a clean machine / second Mac. Step 7 (download the draft DMG via a browser so quarantine is set, install, Open Anyway, confirm engine+supervisor start and a show plays, capture Gatekeeper dialogs) is left for a human — see docs/install.md for the exact steps.

Ask-first items (confirmed in advance)

  • New control surface: UI spawns a local engine (loopback only). Approved: option (A).
  • Release/CI workflow added. Approved: v* tag, draft only, not required on PRs.

View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

The standalone app is one Nocturne.app: the UI binary and the engine binaries
share Contents/MacOS. On an interactive launch against a loopback host the UI
now ensures a local engine through EngineLauncher: it reuses one that already
answers on 127.0.0.1:7400, otherwise spawns the nocturne-supervisor beside it,
and on quit stops only the engine it started. It finds the supervisor by the
running executable's bundle path, so a dev build with separate bundles (no
supervisor next to the UI) is a no-op. The UI stays a loopback-only protocol
client otherwise; the show machine's LaunchAgent path is untouched.

tests/ui/engine_launcher_test.cpp covers reuse, spawn-once, stop-only-ours and
a failed spawn.

Signed-off-by: Okonai <6979365+Okonai@users.noreply.github.com>
tools/make-bundle.sh assembles one Nocturne.app from the built UI and engine
bundles: the engine binaries move in beside the UI binary, the placeholder icon
and the merged Info.plist (bundle id com.azylume.nocturne, version from the
project, LSMinimumSystemVersion 14.0, audio usage strings) go in, then it
ad-hoc signs every nested binary and the bundle inside-out with the hardened
runtime and tools/nocturne.entitlements (audio input, network client and
server), so Apple Silicon will run the arm64 slice. It verifies with
--deep --strict and records that spctl rejects it, since this build is unsigned
to Gatekeeper on purpose (#32 is the signed, notarised one).

tools/make-dmg.sh packs that app into Nocturne-<version>-unsigned.dmg with an
Applications symlink and a FIRST LAUNCH.txt, and writes a SHA-256 file.

The placeholder icon is original art (a crescent moon over the wordmark);
assets/icon/make-icns.sh rebuilds the .icns from assets/icon/nocturne.svg.

Signed-off-by: Okonai <6979365+Okonai@users.noreply.github.com>
A v* tag (or a manual run) now builds the universal bundle, assembles the
standalone Nocturne.app, makes the DMG and checksum, and attaches them to a
DRAFT GitHub Release (release.yml). It is tag- and dispatch-only, never a
required check on pull requests; CI stays the gate.

README gains a Download section; docs/install.md explains that this is an
unsigned preview, how to open it on macOS 15+ (Open Anyway) and macOS 14
(right-click > Open), the xattr quarantine alternative and what it does, how to
verify the checksum, and that signed and notarised comes later (#32).
docs/reference.md and ADR 0015 record the standalone bundle and that the UI
starts its own loopback engine; ADR 0015 amends ADR 0010 for this preview only.

Signed-off-by: Okonai <6979365+Okonai@users.noreply.github.com>
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Oct 27, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-10-27T09:33:03.102466Z 83c0451 PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 83c0451555

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/ui/client/engine_launcher.cpp Outdated
if (!fs::exists(supervisor, ec))
return 0; // Not a single-bundle build (e.g. a dev build with separate bundles); do not spawn.
const std::string path = supervisor.string();
char* argv[] = {const_cast<char*>(path.c_str()), nullptr};

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Pass the selected port to the spawned supervisor

When the UI is started with the supported --port N option and nothing is listening on that local port, this invocation starts the supervisor without arguments, so it launches the engine on its default port 7400 while the session connects to N. The standalone UI stays disconnected and may start an unrelated engine on 7400; pass the requested port through as supervisor engine arguments.

Useful? React with 👍 / 👎.

Comment thread src/ui/app/main.cpp
// A dev build with separate bundles has no supervisor next to the UI, so this is a no-op there.
if (interactive && isLoopbackHost(host)) {
launcher_ = std::make_unique<azl::ui::EngineLauncher>(azl::ui::defaultEngineHooks(uint16_t(port)));
launcher_->ensureRunning();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the shared engine alive for other UI instances

When a second interactive UI launches while the first one's local engine is answering, this launcher reuses it without recording ownership. If the first UI then quits, its shutdown terminates the supervisor and engine even though the second UI is still connected; that UI retries the dead connection but never calls ensureRunning() again. Since Application::moreThanOneInstanceAllowed() permits this case, coordinate engine lifetime across instances or have surviving clients relaunch it.

Useful? React with 👍 / 👎.

CMakeLists defaults to RelWithDebInfo, so the release job now configures with
-DCMAKE_BUILD_TYPE=Release. The two third-party actions (softprops/action-gh-release,
hendrikmuhs/ccache-action) are pinned to a commit SHA, since this job has
contents: write and a moved tag must not change what runs.

Signed-off-by: Okonai <6979365+Okonai@users.noreply.github.com>
Review follow-ups for the standalone preview:

- The UI-spawned engine now listens on 127.0.0.1 alone (engine --loopback,
  bound through Network.framework's local endpoint; supervisor --desk-loopback),
  so a downloaded preview never exposes its control port or venue desk on the
  venue or cafe network. Remote control stays the explicit ctl/pairing path.
- The launcher passes the UI's --port through to the engine, so a non-default
  port reaches the engine the UI then connects to, not a stray one on 7400.
- EngineLauncher.ensureRunning() is idempotent and probe-guarded: it never
  spawns a second engine while one is up, and a UI that reused another
  instance's engine adopts and restarts it if that instance quits and stops it
  (EngineKeepalive, after ~10 s disconnected). tests/ui/engine_launcher_test.cpp
  covers the adopt case.
- The UI shows "connecting…" for a few seconds while a freshly spawned engine
  comes up instead of flashing its connection errno.
- docs/install.md documents stopping a leftover engine (pkill nocturne-supervisor).

Signed-off-by: Okonai <6979365+Okonai@users.noreply.github.com>
Change of plan for the preview launcher (#67): on a machine where the UI runs,
the UI no longer spawns nocturne-supervisor. It spawns the nocturne engine
directly (--loopback --port) and is the watchdog itself: it restarts an engine
that dies at most twice in 60 s, then gives up and shows the reason in the
transport bar. A heartbeat pipe (NOCTURNE_HEARTBEAT_FD, as the supervisor uses)
ties the engine's life to the UI's, so quitting the UI, even a force-quit, stops
the engine it started; the engine is not given NOCTURNE_SUPERVISED, so the UI is
its only watchdog. It still reuses an engine already on 127.0.0.1 and never
stops one it did not start. The supervisor stays the show-machine path
(autostart.sh / the setup script, #66), never started by the UI.

The restart budget, reuse and adopt logic are hooks-based and unit-tested
without a live engine (tests/ui/engine_launcher_test.cpp).

The supervisor is now stoppable on demand as well as by SIGTERM: a new
{"op":"stop"} machine action (nocturne-ctl machine stop) makes the engine quit
with a stop exit code (restart::kStop), and the supervisor then stops the engine
and exits without restarting it. Stop is refused under the Show lock and is a
no-op on an engine with no supervisor (a UI-spawned one). MachineActions gains
the Stop callback; tests/machine_actions_test.cpp covers reply, the no-supervisor
error and the lock refusal.

ADR 0015, docs/reference.md and docs/install.md updated to match.

Signed-off-by: Okonai <6979365+Okonai@users.noreply.github.com>
The usage-text golden test (tests/engine_config_test.cpp) compares kEngineUsage
byte for byte; the earlier --loopback flag added two lines that CI's engine-0
shard caught. Regenerated from nocturne --help; the only change is the
--loopback entry.

Signed-off-by: Okonai <6979365+Okonai@users.noreply.github.com>
@Okonai
Okonai merged commit 3e6a543 into main Oct 27, 2026
7 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant