Skip to content

Latest commit

 

History

History
132 lines (115 loc) · 7.99 KB

File metadata and controls

132 lines (115 loc) · 7.99 KB

Distribution: Developer ID + notarization (not the Mac App Store)

Skyformac is distributed as a signed, notarized .dmg via GitHub Releases and a Homebrew Cask — not through the Mac App Store. That decision (made explicitly, not by default) sidesteps the two hardest blockers a Store submission would have required: App Sandbox (would need a Documents- folder migration for existing users, and the ZWO camera SDK's behavior under sandboxing is untested) and any question of GPLv3's compatibility with the Store's own terms. See docs/app-store-readiness.md for that earlier investigation — kept for reference, not being pursued.

License stays GPLv3 throughout; direct distribution outside the Store has no license- compatibility concern the way Store distribution might.

One-time setup (do this once, not per release)

  1. Enroll in the Apple Developer Program ($99/year) at developer.apple.com if not already enrolled.
  2. Create a "Developer ID Application" certificate — Xcode → Settings → Accounts → your Apple ID → Manage Certificates → "+" → "Developer ID Application". This installs the certificate (and its private key) into your login keychain; scripts/release.sh signs with whatever certificate of this type it finds there.
  3. Find your Team ID — developer.apple.com → Account → Membership Details. You'll pass this as SKYFORMAC_TEAM_ID every time you run a release build (see below); it's not committed anywhere in the repo.
  4. Store notarytool credentials once, so scripts/release.sh doesn't need them passed in every time:
    xcrun notarytool store-credentials "skyformac-notarize" \
      --apple-id "your-apple-id@example.com" \
      --team-id "YOUR_TEAM_ID" \
      --password "an-app-specific-password"
    
    The password is an app-specific password (appleid.apple.com → Sign-In and Security → App-Specific Passwords), not your real Apple ID password. This stores credentials under the keychain profile name skyformac-notarize, which the script uses by default (override with SKYFORMAC_NOTARY_PROFILE if you name it differently).

Cutting a release

  1. Bump MARKETING_VERSION in the Xcode project (skyformac target's Build Settings, both Debug and Release configurations) to the new version number.
  2. Update CHANGELOG.md — move the [Unreleased] content under a new dated version heading.
  3. Build, sign, and notarize:
    export SKYFORMAC_TEAM_ID=YOUR_TEAM_ID
    make release
    
    This runs scripts/release.sh, which:
    • Archives a Release build (xcodebuild archive)
    • Exports it signed with your Developer ID Application certificate
    • Verifies the signature (codesign --verify, spctl --assess)
    • Packages it as build/release/Skyformac-<version>.dmg (the .app plus an /Applications symlink for drag-install)
    • Submits it to Apple for notarization and waits for the result (notarytool submit --wait)
    • Staples the notarization ticket to the .dmg (stapler staple) so Gatekeeper can verify it offline, without a network round-trip to Apple at first launch
    • Runs a final Gatekeeper check (spctl -a -t open)
  4. Tag the release and push:
    git tag v<version>
    git push origin v<version>
    
  5. Create the GitHub Release for that tag (gh release create v<version> build/release/Skyformac-<version>.dmg --title "..." --notes-file ..., or via the web UI) and attach the .dmg as a release asset.
  6. Update the Homebrew Cask (Casks/skyformac.rb) with the new version and the .dmg's real SHA-256 (shasum -a 256 build/release/Skyformac-<version>.dmg), then copy that same file into the tap repo (see below) and push it there too.

Ad-hoc manual releases (no Developer ID certificate available)

Every release through v0.5.2 was actually built this way, not via scripts/release.sh above — there's no Developer ID Application certificate or notarytool profile set up. The app is still usable (ad-hoc signed, matching what "Fix Gatekeeper Warning.command" is for), but building this way instead of via xcodebuild archive hits two real gotchas worth knowing about:

  • Strip the XCTest/Testing frameworks before signing/packaging. xcodebuild build -scheme skyformac builds every target in the shared scheme, and the skyformac app target ends up with Testing.framework, XCTest.framework, XCUIAutomation.framework, and several more Apple testing frameworks embedded in Contents/Frameworks — dead weight the main executable never links against (confirmed via otool -L — none of them show up), roughly doubling the shipped app's size. Remove everything in Contents/Frameworks except libASICamera2.dylib before signing.
  • Use zip, not ditto -c -k, to build the .zip asset. ditto's zip archiver scatters AppleDouble resource-fork sidecar files (._Info.plist, ._skyformac, ...) inside the app bundle's own directory tree, not just alongside it — extracting that zip elsewhere and running codesign --verify on the result fails with "a sealed resource is missing or invalid" (Gatekeeper would refuse to launch it too), because those extra files aren't in the bundle's sealed resource manifest. hdiutil create -srcfolder for the .dmg doesn't have this problem — only the zip path does. Confirmed by actually extracting the built zip and running codesign --verify --deep --strict against the result, not just checking the pre-zip .app.
  • The .zip needs Fix Gatekeeper Warning.command copied in too, same as the .dmg — it's easy to package just the .app alone by mistake (this shipped broken in the v0.5.0–v0.5.1 .zip release assets: no script at all, so the README's own zip instructions had nothing to run).

Roughly:

xcodebuild build -scheme skyformac -configuration Release -destination 'platform=macOS' \
  CONFIGURATION_BUILD_DIR="$PWD/build/release-app"
APP="build/release-app/skyformac.app"
rm -rf "$APP/Contents/PlugIns"
# Strip everything except libASICamera2.dylib from Contents/Frameworks (see above)
codesign --force --deep --sign - "$APP"
codesign --verify --deep --strict "$APP"

mkdir -p build/zip-staging && cp -R "$APP" build/zip-staging/ && cp "scripts/Fix Gatekeeper Warning.command" build/zip-staging/
(cd build/zip-staging && zip -r -X -y "../release-assets/skyformac-v<version>-macOS.zip" skyformac.app "Fix Gatekeeper Warning.command")

mkdir -p build/dmg-staging && cp -R "$APP" build/dmg-staging/ && ln -s /Applications build/dmg-staging/Applications && cp "scripts/Fix Gatekeeper Warning.command" build/dmg-staging/
hdiutil create -volname "Skyformac <version>" -srcfolder build/dmg-staging -ov -format UDZO build/release-assets/skyformac-v<version>-macOS.dmg

Before uploading either asset, extract it fresh into a scratch directory and run codesign --verify --deep --strict on the result — checking the pre-packaging .app isn't enough, since packaging itself is exactly what can break the seal.

Homebrew Cask

Casks/skyformac.rb in this repo is kept in sync with the live formula, not read by Homebrew directly — the actual tap Homebrew installs from is giulioroggero/homebrew-skyformac. After updating the version/SHA-256 here, copy the same file to that repo's own Casks/skyformac.rb and push it there; brew tap giulioroggero/skyformac / brew install --cask skyformac won't see a new version until that copy lands.

What scripts/release.sh deliberately does NOT do

Signing/notarization credentials never leave your machine — there's no GitHub Actions automation for this (a deliberate choice: storing a code-signing certificate and notarization password as CI secrets is a real security tradeoff, and this project has a comfortable "release when ready, on the maintainer's own machine" cadence that doesn't need it). CI (.github/workflows/ci.yml) only ever builds a debug, ad-hoc-signed build for running the test suite — it has nothing to do with producing a real release artifact.