diff --git a/Makefile b/Makefile index 147eff8..14c0aa1 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,9 @@ APP = build/Trapps.app BINARY = build/trapps SOURCES = $(wildcard Sources/trapps/*.swift) VERSION = $(shell /usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' Support/Info.plist) -ZIP = build/Trapps-$(VERSION).zip +DMG = build/Trapps-$(VERSION).dmg +DMG_STAGING = build/dmg +VOLNAME = Trapps CASK_TMPL = packaging/trapps.cask.tmpl CASK = build/trapps.rb MACOS_TARGET = apple-macos13.0 @@ -68,6 +70,18 @@ define SIGN_SPARKLE codesign --force $(2) --sign "$(1)" "$(APP)/Contents/Frameworks/Sparkle.framework" endef +# Pack the signed .app into a compressed, read-only .dmg with a drag-to-Applications +# shortcut. ditto (not cp) preserves the code signature. Removes any prior staging +# dir and dmg first so nothing stale leaks in. +define MAKE_DMG + rm -rf $(DMG_STAGING) "$(DMG)" + mkdir -p $(DMG_STAGING) + ditto $(APP) "$(DMG_STAGING)/Trapps.app" + ln -s /Applications "$(DMG_STAGING)/Applications" + hdiutil create -volname "$(VOLNAME)" -srcfolder $(DMG_STAGING) -ov -format UDZO "$(DMG)" + rm -rf $(DMG_STAGING) +endef + # Prefer a real signing identity so the Accessibility grant survives rebuilds; # fall back to ad-hoc. Override with: make IDENTITY="Apple Development: ..." IDENTITY ?= $(shell security find-identity -v -p codesigning 2>/dev/null \ @@ -137,7 +151,7 @@ run: bundle # --- Distribution --- # Universal binary, hardened runtime, Developer ID signature. `make release` -# then `make notarize` produces a Gatekeeper-clean zip in build/. +# then `make notarize` produces a Gatekeeper-clean dmg in build/. build/trapps-arm64: $(SOURCES) $(SPARKLE_FW) mkdir -p build @@ -152,7 +166,8 @@ release: build/trapps-arm64 build/trapps-x86_64 build/AppIcon.icns echo "error: no 'Developer ID Application' identity in the keychain."; \ echo "Notarization requires one (Apple Developer Program membership)."; \ exit 1; } - rm -rf $(APP) $(ZIP) + rm -rf $(APP) + rm -f build/Trapps-*.zip build/Trapps-*.dmg mkdir -p $(APP)/Contents/MacOS lipo -create -output $(APP)/Contents/MacOS/trapps build/trapps-arm64 build/trapps-x86_64 cp Support/Info.plist $(APP)/Contents/Info.plist @@ -160,21 +175,24 @@ release: build/trapps-arm64 build/trapps-x86_64 build/AppIcon.icns $(EMBED_SPARKLE) $(call SIGN_SPARKLE,$(RELEASE_IDENTITY),--options runtime --timestamp) codesign --force --options runtime --timestamp --sign "$(RELEASE_IDENTITY)" $(APP) - ditto -c -k --keepParent $(APP) $(ZIP) - @echo "Built $(ZIP); next: make notarize" + $(MAKE_DMG) + @echo "Built $(DMG); next: make notarize" +# Submitting the dmg notarizes the app inside it too (Apple registers the app's +# code hash), so one round-trip covers both. Staple the ticket to the dmg so it +# opens cleanly offline. The app itself is notarized but not stapled - offline +# first launch is a non-issue for a brew-installed, network-downloaded tool. notarize: - xcrun notarytool submit $(ZIP) $(NOTARY_AUTH) --wait - xcrun stapler staple $(APP) - ditto -c -k --keepParent $(APP) $(ZIP) - @echo "Notarized, stapled, and re-zipped $(ZIP)" + xcrun notarytool submit $(DMG) $(NOTARY_AUTH) --wait + xcrun stapler staple $(DMG) + @echo "Notarized + stapled dmg: $(DMG)" # Render the Homebrew cask from the template, filling in the current version and -# the sha256 of the built zip. Run after `make release && make notarize`; the +# the sha256 of the built dmg. Run after `make release && make notarize`; the # resulting build/trapps.rb goes into the homebrew-tap repo. See packaging/README.md. cask: $(CASK_TMPL) - @test -f $(ZIP) || { echo "error: $(ZIP) not found - run 'make release && make notarize' first."; exit 1; } - @sha=$$(shasum -a 256 $(ZIP) | awk '{print $$1}'); \ + @test -f $(DMG) || { echo "error: $(DMG) not found - run 'make release && make notarize' first."; exit 1; } + @sha=$$(shasum -a 256 $(DMG) | awk '{print $$1}'); \ sed -e 's/@@VERSION@@/$(VERSION)/' -e "s/@@SHA256@@/$$sha/" $(CASK_TMPL) > $(CASK); \ echo "Rendered $(CASK) (version $(VERSION), sha256 $$sha)" @@ -186,13 +204,13 @@ sparkle-keys: $(SPARKLE_FW) # Regenerate the appcast for the current version. Pulls the existing feed from the # pinned '$(APPCAST_TAG)' release, appends this version (enclosure -> this version's -# release dmg/zip), signs everything with your Keychain EdDSA key, and writes +# release dmg), signs everything with your Keychain EdDSA key, and writes # build/appcast.xml. Run after `make release && make notarize`, then publish with: # gh release upload $(APPCAST_TAG) build/appcast.xml --clobber appcast: $(SPARKLE_FW) - @test -f $(ZIP) || { echo "error: $(ZIP) not found - run 'make release && make notarize' first."; exit 1; } + @test -f $(DMG) || { echo "error: $(DMG) not found - run 'make release && make notarize' first."; exit 1; } @rm -rf build/appcast && mkdir -p build/appcast - @cp $(ZIP) build/appcast/ + @cp $(DMG) build/appcast/ @curl -fsSL -o build/appcast/appcast.xml "$(APPCAST_URL)" \ && echo "merged existing feed" || echo "no existing feed - starting fresh" $(SPARKLE_BIN)/generate_appcast \ diff --git a/README.md b/README.md index d805097..bf9deca 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ A single macOS menu bar icon that reveals every menu bar item ("tray app") from ## Install -Download the latest `Trapps-x.y.z.zip` from [Releases](https://github.com/GregTheGreek/trapps/releases), unzip, and drag `Trapps.app` to `/Applications`. Releases are universal (Apple Silicon + Intel), Developer ID-signed, and notarized. Requires macOS 13+. +Download the latest `Trapps-x.y.z.dmg` from [Releases](https://github.com/GregTheGreek/trapps/releases), open it, and drag `Trapps.app` to `/Applications`. Releases are universal (Apple Silicon + Intel), Developer ID-signed, and notarized. Requires macOS 13+. Or build from source - see below. @@ -52,13 +52,13 @@ The Makefile signs with your Apple Development / Developer ID certificate if one Versioning is automated with [release-please](https://github.com/googleapis/release-please) (`.github/workflows/release-please.yml`); signing and notarization are done locally so the Developer ID private key never leaves the machine. 1. Conventional commits merged to `main` accumulate into an auto-maintained release PR. Merging it bumps the version in `Support/Info.plist` (via the `x-release-please-version` markers) and `CHANGELOG.md`, tags `vX.Y.Z`, and publishes the GitHub release (without assets). -2. Locally, on `main` at that tag, build the signed + notarized zip, attach it, then refresh the Sparkle appcast so existing installs see the update: +2. Locally, on `main` at that tag, build the signed + notarized dmg, attach it (the exact versioned file, not a glob - a wildcard picks up stale artifacts from earlier builds), then refresh the Sparkle appcast so existing installs see the update: ```sh git pull V=$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' Support/Info.plist) make release && make notarize - gh release upload "v$V" "build/Trapps-$V.zip" + gh release upload "v$V" "build/Trapps-$V.dmg" make appcast # writes build/appcast.xml (signed) gh release upload appcast build/appcast.xml --clobber diff --git a/packaging/README.md b/packaging/README.md index 2d9c10b..dc1a3cf 100644 --- a/packaging/README.md +++ b/packaging/README.md @@ -4,7 +4,7 @@ Trapps ships as a [Homebrew cask](https://docs.brew.sh/Cask-Cookbook) from a personal tap so users can `brew install --cask gregthegreek/tap/trapps`. `trapps.cask.tmpl` is the cask with `@@VERSION@@` / `@@SHA256@@` placeholders. -`make cask` renders it against the freshly built, notarized zip. +`make cask` renders it against the freshly built, notarized dmg. ## Publishing a release to the tap @@ -12,12 +12,12 @@ The tap is a separate repo named `GregTheGreek/homebrew-tap` (the `homebrew-` prefix is what makes `brew tap gregthegreek/tap` resolve). Its casks live under `Casks/`. -Prerequisite: a real notarized zip must already be attached to the GitHub +Prerequisite: a real notarized dmg must already be attached to the GitHub release (see the "Releasing" section in the top-level README) - the cask's `sha256` is computed from it, and `brew install` downloads it from there. ```sh -make release && make notarize # produces build/Trapps-.zip +make release && make notarize # produces build/Trapps-.dmg make cask # renders build/trapps.rb (version + sha256 filled in) # One time, if the tap does not exist yet: diff --git a/packaging/trapps.cask.tmpl b/packaging/trapps.cask.tmpl index 733d48d..6ac1d26 100644 --- a/packaging/trapps.cask.tmpl +++ b/packaging/trapps.cask.tmpl @@ -2,7 +2,7 @@ cask "trapps" do version "@@VERSION@@" sha256 "@@SHA256@@" - url "https://github.com/GregTheGreek/trapps/releases/download/v#{version}/Trapps-#{version}.zip" + url "https://github.com/GregTheGreek/trapps/releases/download/v#{version}/Trapps-#{version}.dmg" name "Trapps" desc "Menu bar app revealing every menu bar item, including ones behind the notch" homepage "https://github.com/GregTheGreek/trapps"