874b6f20c2
Основа — Horizon (MIT). История апстрима не переносится: этот репозиторий ведёт свою.
90 lines
5.3 KiB
Markdown
90 lines
5.3 KiB
Markdown
# 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
|
||
id `dev.horizon.launcher`, macOS 11 or later), `Contents/Resources/horizon.icns`
|
||
(`packaging/icons`, from `cargo run -p xtask -- icons`).
|
||
- `horizon-launcher-macos-arm64.dmg`: the installer. Opening it shows the app and a link to
|
||
Applications over `background.png` / `background@2x.png` (drawn from `background.html`); the
|
||
player drags one onto the other. Built by [dmgbuild](https://github.com/dmgbuild/dmgbuild),
|
||
which writes the Finder layout without a GUI session.
|
||
- `horizon-launcher-macos-arm64.zip`: the same app, zipped with `ditto`. 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 bare `horizon-launcher` binary, as launchers before
|
||
the app shipped, finds the binary inside the zip and replaces only itself.
|
||
|
||
## Signing and notarization
|
||
|
||
Releases are signed with Ludovic's **Developer ID** ("Developer ID Application: Ludovic Marie
|
||
(YYJU63HSD4)"), with the hardened runtime and a secure timestamp, then **notarized** by Apple and
|
||
**stapled**, so Gatekeeper opens them on any Mac without a warning, offline too:
|
||
|
||
- the launcher (`package.sh`): Horizon.app is signed, notarized and stapled first, so the zip the
|
||
launcher updates itself from and the .dmg both hold it stapled; then the .dmg is signed,
|
||
notarized and stapled. The script ends with what Gatekeeper says of each, which must read
|
||
`accepted` and `source=Notarized Developer ID`.
|
||
- the game (`.github/workflows/release.yml`): `horizon-client`, `horizon-ctl` and
|
||
`horizon-prepare` are signed right after they are built (so the checks and the 1500-frame smoke
|
||
run use them as players will), and the zip is notarized. Programs outside a bundle can't hold a
|
||
ticket: Gatekeeper asks Apple online the first time each one runs.
|
||
|
||
No entitlements are needed. The launcher checks an update's signature before swapping the new
|
||
Horizon.app in, and again where it lands; when it doesn't hold, the old app is put back.
|
||
|
||
The switch is the repository variable `MACOS_SIGN_IDENTITY`: set to the identity's name, the
|
||
builds sign and notarize; empty (as in forks), they are signed **ad hoc** as before.
|
||
|
||
### The signing keychain on the Mac mini
|
||
|
||
The release runner (`ludovics-mac-mini-release`, label `horizon-release`, `MAC_RUNNER`) is a
|
||
LaunchAgent whose jobs run in their own security session, where the login keychain is locked.
|
||
So signing uses a keychain of its own, `~/Library/Keychains/horizon-signing.keychain-db`,
|
||
holding only the Developer ID identity, Apple's "Developer ID Certification Authority" and the
|
||
notarytool profile `horizon-notary` (the App Store Connect team key "App Store Connect API -
|
||
GitHub CI (DISPO)"). Its password is the item "Horizon signing keychain" in the Agents vault.
|
||
`keychain.sh unlock` (each job's first macOS step) reads it with the fleet's 1Password service
|
||
account, unlocks the keychain, checks the identity and the profile are there and hands both to
|
||
the later steps; `keychain.sh lock` ends the job. Nothing secret is in the repository or on
|
||
GitHub, and nothing is printed.
|
||
|
||
Setting it up again (a new Mac, a renewed certificate):
|
||
|
||
1. `packaging/macos/keychain.sh create`: the keychain, locked after two hours idle and on sleep.
|
||
2. In Keychain Access on the Mac (the login keychain, *My Certificates*), right-click
|
||
"Developer ID Application: Ludovic Marie (YYJU63HSD4)", *Export…*, save it as a .p12 with a
|
||
password; then in the Mac's Terminal `packaging/macos/keychain.sh import FILE.p12`. It asks for
|
||
that password, imports the identity and its intermediate, removes any other identity, lets
|
||
`codesign` use the key without a prompt and deletes the .p12. (The login keychain also holds
|
||
other people's Apple Development and Distribution identities: never those.)
|
||
3. `packaging/macos/keychain.sh notary`: the notarytool profile, from the App Store Connect key
|
||
(written to a private temporary file and deleted).
|
||
4. The repository variable `MACOS_SIGN_IDENTITY`.
|
||
|
||
The certificate and its intermediate expire on 1 February 2027: renew the certificate in the
|
||
Apple Developer account before then and repeat step 2.
|
||
|
||
Signing by hand, as the workflow does:
|
||
|
||
```sh
|
||
export MACOS_SIGN_IDENTITY="Developer ID Application: Ludovic Marie (YYJU63HSD4)"
|
||
export MACOS_SIGN_KEYCHAIN=~/Library/Keychains/horizon-signing.keychain-db MACOS_NOTARY_PROFILE=horizon-notary
|
||
packaging/macos/keychain.sh unlock
|
||
packaging/macos/package.sh target/release/horizon-launcher 0.0.0 out
|
||
packaging/macos/keychain.sh lock
|
||
```
|
||
|
||
### Gatekeeper with an ad hoc build
|
||
|
||
An ad hoc build (a fork's, or `package.sh` without `MACOS_SIGN_IDENTITY`) is consistent and runs
|
||
on Apple Silicon, but Gatekeeper doesn't know the developer. The first time it is opened 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).
|