macOS: packaging/macos/package.sh (run by launcher.yml) builds Horizon.app (Info.plist, bundle id dev.horizon.launcher, macOS 11+, the .icns), signs it ad hoc, and makes the two release assets: horizon-launcher-macos-arm64.dmg, the installer (the app and an Applications link over a background, laid out by dmgbuild without a GUI), and the .zip the launcher updates from. A launcher inside the app replaces the whole bundle: copied beside it with ditto, swapped in by renames, restarted; a bare binary, as older launchers are, still finds its binary in the zip. A macOS test zips, unpacks, swaps and re-verifies a signed bundle. Signing for real is a MACOS_SIGN_IDENTITY away (packaging/macos/README.md, with what Gatekeeper asks meanwhile). Linux: release builds keep horizon-launcher.desktop (with the binary's current path), horizon.desktop (hidden, for the game window's icon) and the hicolor icons under $XDG_DATA_HOME, without root; `horizon-launcher desktop-entry --remove` takes them out and stops it, `desktop-entry` puts them back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The macOS launcher: Horizon.app in a .dmg
package.sh BINARY VERSION OUTDIR (run by .github/workflows/launcher.yml on macOS) makes:
Horizon.app:Contents/MacOS/horizon-launcher,Contents/Info.plist(this folder's, bundle iddev.horizon.launcher, macOS 11 or later),Contents/Resources/horizon.icns(packaging/icons, fromcargo run -p xtask -- icons).horizon-launcher-macos-arm64.dmg: the installer. Opening it shows the app and a link to Applications overbackground.png/background@2x.png(drawn frombackground.html); the player drags one onto the other. Built by dmgbuild, which writes the Finder layout without a GUI session.horizon-launcher-macos-arm64.zip: the same app, zipped withditto. The launcher updates itself from this: inside Horizon.app it replaces the whole bundle (copied beside it, then swapped in by renaming, then restarted); a barehorizon-launcherbinary, as launchers before the app shipped, finds the binary inside the zip and replaces only itself.
Signing and Gatekeeper
The app is signed ad hoc (codesign --sign -): no Apple account is needed, the bundle is
consistent and runs on Apple Silicon, but Gatekeeper doesn't know the developer. The first time
a player opens it after downloading:
- macOS 14 and earlier: right-click (or Control-click) Horizon in Applications, choose Open, then Open in the dialog.
- macOS 15 and later: open it once (it is refused), then System Settings › Privacy & Security, Open Anyway next to "Horizon was blocked", and confirm.
After that it opens normally, and its own updates don't ask again (they aren't downloaded by a browser, so they carry no quarantine flag).
To sign and notarize for real (Ludovic's Apple Developer account): import the "Developer ID
Application" certificate into the runner's keychain, set MACOS_SIGN_IDENTITY to its name for
package.sh (it then signs with the hardened runtime and a timestamp), and after it, notarize and
staple the disk image:
xcrun notarytool submit dist/horizon-launcher-macos-arm64.dmg --keychain-profile horizon --wait
xcrun stapler staple dist/horizon-launcher-macos-arm64.dmg