From 27d934e726e4a8373f7db3ebdd05f806b8378d13 Mon Sep 17 00:00:00 2001 From: Fabio Lussi <198260595+itrycodinganditsbad@users.noreply.github.com> Date: Sun, 11 Oct 2026 22:09:39 +0200 Subject: [PATCH] macOS: build the DMG with hdiutil create so the app passes codesign --strict hdiutil makehybrid -hfs put a com.apple.FinderInfo on every file in the image, so the signed app inside the DMG (and any ditto copy of it) failed codesign --verify --deep --strict. Build the image with hdiutil create -srcfolder (retried, since create can hit "Resource busy" on CI), clear build-time xattrs before signing, and verify the app inside the finished DMG and a ditto copy of it with --strict. Ported from storytold/pdfcraft#823 and storytold/lightcraft#959. Fixes #176 Co-Authored-By: Claude Opus 5.5 --- packaging/macos/dmg/README.md | 4 +-- packaging/macos/package.sh | 55 ++++++++++++++++++++++++++++++----- 2 files changed, 50 insertions(+), 9 deletions(-) diff --git a/packaging/macos/dmg/README.md b/packaging/macos/dmg/README.md index 5bbc169..0cacbb7 100644 --- a/packaging/macos/dmg/README.md +++ b/packaging/macos/dmg/README.md @@ -2,8 +2,8 @@ What Finder shows when the DMG opens: a 660 × 400 pt background with the app icon and the `Applications` link side by side. `package.sh` copies these files into the image; nothing here is -generated at build time, so the DMG still builds with `hdiutil makehybrid` (no mounted device, no -Finder scripting on CI, nothing extra installed in the signing job). +generated at build time, so the DMG builds with a plain `hdiutil create -srcfolder` (no Finder +scripting on CI, nothing extra installed in the signing job). | File | What | |---|---| diff --git a/packaging/macos/package.sh b/packaging/macos/package.sh index a898e3d..8208aac 100755 --- a/packaging/macos/package.sh +++ b/packaging/macos/package.sh @@ -109,6 +109,10 @@ sed -e "s/@VERSION@/$VERSION/g" -e "s/@SHORT_VERSION@/$SHORT_VERSION/g" \ plutil -lint "$APP/Contents/Info.plist" printf 'APPL????' >"$APP/Contents/PkgInfo" +# Drop extended attributes picked up during the build (quarantine, FinderInfo) before signing: +# `codesign --verify --strict` rejects them, and nothing after signing may touch the bundle. +xattr -cr "$APP" + # Sign inside-out: nested code first, then the bundle itself (no --deep on the final signature). # Today the only nested code is the main executable; frameworks/helpers would be signed here too. sign --options runtime --entitlements "$HERE/entitlements.plist" "$APP/Contents/MacOS/SoundCraft" @@ -133,15 +137,52 @@ ln -s /Applications "$STAGE/Applications" mkdir -p "$STAGE/.background" cp "$HERE/dmg/background.tiff" "$STAGE/.background/background.tiff" cp "$HERE/dmg/dmg-layout.DS_Store" "$STAGE/.DS_Store" -rm -f "$DMG" "$WORK/raw.dmg" -# makehybrid + convert builds the image without attaching a device, unlike `create -srcfolder`, -# which is flaky on CI runners ("Resource busy") and hangs in sandboxed sessions. -# The volume name has no version: .DS_Store finds the background through an alias that includes it. -hdiutil makehybrid -hfs -hfs-volume-name "SoundCraft" -hfs-openfolder "$STAGE" -o "$WORK/raw.dmg" "$STAGE" -hdiutil convert "$WORK/raw.dmg" -format UDZO -imagekey zlib-level=9 -o "$DMG" -rm -f "$WORK/raw.dmg" +rm -f "$DMG" +# `hdiutil create -srcfolder` writes the files as they are. `makehybrid -hfs` gave every file a +# com.apple.FinderInfo, which `codesign --verify --strict` rejects as "Finder information, or +# similar detritus" on the app in the image and on a copy installed from it (#176). `create` +# attaches a device while it copies, which fails now and then on CI runners ("Resource busy"), so +# it gets a few tries. The volume name has no version: .DS_Store finds the background through an +# alias that includes it. +# Usage: hdiutil_retry ; tries five times with growing pauses. +hdiutil_retry() { + local what="$1" attempt + shift + for attempt in 1 2 3 4 5; do + if hdiutil "$@"; then + return 0 + fi + [ "$attempt" = 5 ] && { echo "hdiutil $what failed 5 times" >&2; return 1; } + warn "hdiutil $what failed (attempt $attempt of 5); retrying in $((attempt * 10))s" + sleep $((attempt * 10)) + done +} +hdiutil_retry create create -srcfolder "$STAGE" -volname "SoundCraft" -fs HFS+ -format UDZO -imagekey zlib-level=9 -ov "$DMG" sign "$DMG" codesign --verify --strict --verbose=2 "$DMG" +# The app users get must still pass --strict: inside the finished image and after a ditto copy out +# of it, so a valid container signature can't hide a damaged app (or a makehybrid-style regression). +MOUNT="$WORK/dmg-check" +COPY="$WORK/dmg-copy" +rm -rf "$MOUNT" "$COPY" +mkdir -p "$MOUNT" "$COPY" +hdiutil_retry attach attach -nobrowse -readonly -noautoopen -mountpoint "$MOUNT" "$DMG" >/dev/null +dmg_ok=1 +codesign --verify --strict --deep --verbose=2 "$MOUNT/SoundCraft.app" || dmg_ok=0 +if [ "$dmg_ok" = 1 ]; then + { ditto "$MOUNT/SoundCraft.app" "$COPY/SoundCraft.app" && + codesign --verify --strict --deep --verbose=2 "$COPY/SoundCraft.app"; } || dmg_ok=0 +fi +if [ "$dmg_ok" = 1 ] && [ "$NOTARIZE" = 1 ]; then + spctl --assess --type execute -vvv "$COPY/SoundCraft.app" || dmg_ok=0 +fi +[ "$dmg_ok" = 1 ] || xattr -lr "$MOUNT/SoundCraft.app" >&2 || true +hdiutil_retry detach detach "$MOUNT" >/dev/null || dmg_ok=0 +rm -rf "$COPY" +if [ "$dmg_ok" != 1 ]; then + echo "error: SoundCraft.app in $(basename "$DMG") fails strict verification" >&2 + exit 1 +fi if [ "$NOTARIZE" = 1 ]; then notarize "$DMG" xcrun stapler staple "$DMG"