"A green swift build is a promise. The installed, verified bundle is the proof."
The Script Replaces Xcode's Build Phases
Without an Xcode project, nothing turns a SwiftPM binary into an app for you. Every family Mac app has a scripts/build-app.sh that does it, and they all follow the same order for the same reasons:
- Build release and ask SwiftPM where the product landed (
--show-bin-path). - Assemble
Spark.app/Contents/{MacOS,Resources}: the binary and the Info.plist, plus an icon when the app has one. - Stamp the marketing version from the product's version file into the Info.plist with
PlistBuddy, so one file decides the version. - Clear extended attributes (
xattr -cr). Finder metadata inside a bundle breaks code signing. - Sign the finished bundle — never before it is finished, because the signature seals every file.
- Verify:
codesign --verify --deep --strict, and check that the designated requirement is certificate-based, not a cdhash. - Stage, swap, verify again, roll back on failure: copy to a hidden staging name next to
/Applications, move the old app aside, move the new one in, verify the installed copy, restore the previous one if anything fails. - Register with LaunchServices (
lsregister -f) so the system sees the new build now.
Why Stage Instead of Copying Over
The kernel caches a signed executable's code signature per file (per inode). Overwriting a signed binary in place can leave the old signature cached against new bytes, which surfaces as a launch killed for an invalid signature. Writing a complete new bundle and renaming it into place gives the kernel a new file. The swap also means a failed install leaves the previous working app exactly where it was.
Two Details That Only Bite Once
- Command-line helpers need an Info.plist too. A tool that asks for a privacy permission — the family's calendar bridge reads EventKit — has no bundle to hold an Info.plist, so macOS denies the request without ever showing a dialog. Embed the plist in the binary with
-Xlinker -sectcreate -Xlinker __TEXT -Xlinker __info_plist -Xlinker Info.plist, then sign it. - Nested code is signed inside-out. Helpers, embedded executables and bundled dylibs are signed before the bundle that contains them. Apple's guidance is to sign each nested item explicitly rather than rely on
codesign --deepfor signing;--deepis fine for verifying. Bundling third-party dylibs and rewriting their load paths is a whole story of its own, told in/cwk-quests/ashen-reel-quest.