Files
vnidrop/apple/RELEASE-MACOS.md
cdricms cc194f6a7b feat(apple): add direct-download macOS channel (notarized DMG + Sparkle + Homebrew)
Add a second macOS shipping channel alongside the App Store build:

- New VniDropDirect target (Release-Direct config) sharing VniDrop's sources via
  an AppBase target template; links Sparkle behind the DIRECT_DISTRIBUTION flag so
  the App Store binary never bundles a self-updater. arm64-only (core is arm64).
- Sparkle updater (SparkleUpdater.swift) + "Check for Updates" menu, compiled only
  under DIRECT_DISTRIBUTION; Info.plist SUFeedURL points at the GitHub Release
  /latest/download/appcast.xml, non-sandboxed entitlements for Developer ID.
- build-dmg.sh (archive → Developer ID export → DMG → sign → notarize → staple),
  generate-appcast.sh, and ExportOptions-DeveloperID.plist.
- apple-release.yml: on tag v*.*.*, build/notarize the DMG, publish the GitHub
  Release with appcast, and push the Homebrew cask to sudosylabs/homebrew-vnidrop.
  apple.yml gains a PR compile-check of the direct target.
- CFBundleVersion is stamped at build time as a UTC YYMMDD.HHMM timestamp for both
  channels, replacing the hand-maintained build number.
- Docs (RELEASE-MACOS.md, README), cask template + tap README, localized
  updates_check string, Makefile targets, gitignore for dist/ artifacts.
2026-07-27 14:31:36 +02:00

5.5 KiB

Releasing VniDrop for macOS

VniDrop ships on macOS through two independent channels:

Channel Target / config Signing Updates
Mac App Store / TestFlight VniDrop / Release Apple Distribution App Store
Direct download (.dmg) VniDropDirect / Release-Direct Developer ID + notarization Sparkle

The direct build adds the Sparkle auto-updater, gated behind the DIRECT_DISTRIBUTION compile flag so the App Store binary never links or ships a self-updater (which Apple forbids). Because Swift Package Manager links products per target, the isolation is a dedicated VniDropDirect target — not just a build configuration.

This document covers the direct-download channel. The App Store channel goes through Xcode Organizer / App Store Connect as before (see RELEASE-TESTFLIGHT.fr.md).

Versioning

Both channels use Apple's two-field convention, shown as MARKETING_VERSION (build):

  • CFBundleShortVersionString = MARKETING_VERSION — the human X.Y.Z version (from the git tag for a release). This is what users, the DMG filename, the GitHub Release, and the Homebrew cask all use.
  • CFBundleVersion = a UTC YYMMDD.HHMM timestamp, stamped into the built Info.plist by the "Stamp build number" build phase (apple/project.yml). It fires for every build — Xcode Organizer archive and CLI alike — so each build is monotonic and self-describing. Sparkle compares this field to order updates, and App Store Connect requires each upload's build to exceed the previous one; a timestamp satisfies both automatically. build-dmg.sh pins one timestamp per archive (via VNIDROP_BUILD) so the app, DMG, and appcast agree.

No build number is maintained by hand.


One-time setup

1. Developer ID Application certificate

Direct distribution requires a Developer ID Application certificate (distinct from the Apple Distribution cert used for the App Store). In Xcode ▸ Settings ▸ Accounts ▸ Manage Certificates ▸ +Developer ID Application. Confirm it's in the keychain:

security find-identity -v -p codesigning | grep "Developer ID Application"

2. Notarization credentials (App Store Connect API key)

Create an API key at App Store Connect ▸ Users and Access ▸ Integrations ▸ Keys (role: Developer). Download the AuthKey_XXXX.p8, and note the Key ID and Issuer ID. Store a local notarytool profile:

xcrun notarytool store-credentials vnidrop-notary \
  --key /path/to/AuthKey_XXXX.p8 \
  --key-id <KEY_ID> \
  --issuer <ISSUER_ID>

3. Sparkle EdDSA signing key

Generate the update-signing key once (private key stored in the login keychain):

# Sparkle's generate_keys, from the downloaded Sparkle release (bin/generate_keys)
./bin/generate_keys

It prints the public key. Put it in apple/VniDrop/Resources/Info.plist as SUPublicEDKey, replacing REPLACE_WITH_SPARKLE_ED_PUBLIC_KEY. Export the private key for CI:

./bin/generate_keys -x sparkle_ed_private_key   # writes the private key file

4. Homebrew tap repo

Create an empty GitHub repo sudosylabs/homebrew-vnidrop (a Casks/ folder is enough). The release workflow pushes Casks/vnidrop.rb there. Users install:

brew install --cask sudosylabs/vnidrop/vnidrop

5. CI secrets

Add these to the sudosylabs/vnidrop repo (Settings ▸ Secrets ▸ Actions):

Secret Value
DEVELOPER_ID_CERT_P12 base64 of the exported Developer ID .p12
DEVELOPER_ID_CERT_PASSWORD password for that .p12
NOTARY_API_KEY base64 of AuthKey_XXXX.p8
NOTARY_KEY_ID App Store Connect key ID
NOTARY_ISSUER App Store Connect issuer ID
SPARKLE_ED_PRIVATE_KEY contents of the exported Sparkle private key file
HOMEBREW_TAP_TOKEN PAT with write access to homebrew-vnidrop

SUFeedURL in Info.plist points at https://github.com/sudosylabs/vnidrop/releases/latest/download/appcast.xml. GitHub's /releases/latest/download/ path always redirects to the newest non-prerelease release's asset, so no GitHub Pages or repo commits are needed.


Per-release flow

  1. Bump MARKETING_VERSION (and CURRENT_PROJECT_VERSION) in apple/project.yml if needed, commit to master.

  2. Tag and push:

    git tag v0.2.0
    git push origin v0.2.0
    
  3. .github/workflows/apple-release.yml then:

    • builds the Rust core (release), regenerates the project,
    • archives + exports the VniDropDirect target (Developer ID, hardened runtime),
    • builds, signs, notarizes and staples VniDrop-<version>.dmg,
    • runs generate_appcast to produce appcast.xml (enclosure → the release DMG),
    • creates the GitHub Release with both assets, and
    • renders + pushes the Homebrew cask to the tap.

Existing installs pick up the new version automatically via Sparkle (feed → latest release's appcast.xml); brew upgrade respects auto_updates true.


Building a DMG locally

# Signed DMG (skips notarization unless NOTARY_PROFILE is set):
make build-apple-dmg VERSION=0.2.0

# Full signed + notarized DMG:
NOTARY_PROFILE=vnidrop-notary make build-apple-dmg VERSION=0.2.0

Output: apple/dist/VniDrop-<version>.dmg. See apple/scripts/build-dmg.sh for the environment variables (DEVELOPER_ID_APP, DEVELOPMENT_TEAM, NOTARY_PROFILE). Generate the appcast with apple/scripts/generate-appcast.sh <version>.

To compile-check the direct target without signing:

make build-apple-macos-direct