From 1493b89bd361329f7ab6badbbcd2794ce36f09ff Mon Sep 17 00:00:00 2001 From: TerrifiedBug Date: Thu, 27 Aug 2026 20:48:34 +0100 Subject: [PATCH] fix: sign Sparkle's Autoupdate so notarization passes Apple rejected v0.1.0 with three errors, both architectures, all on Sparkle.framework/Versions/B/Autoupdate: not signed with a valid Developer ID certificate, no secure timestamp, no hardened runtime. Autoupdate is a bare Mach-O, so it matched neither *.xpc nor *.app and the inside-out loop skipped it, leaving the ad-hoc signature that package_app.sh applies. Sign bare Mach-O helpers beside the bundles, pruning nested bundles whose executables are signed with their bundle. notarytool also exits 0 on an Invalid submission, so the script walked on to stapler and died with "Record not found", which says nothing about the cause. Read the status and print the notary log instead. Verified: every Mach-O in the bundle now reports team 92X3ACDPD2, flags=0x10000(runtime) and a timestamp, and the real notary service returns Accepted in 23s. --- Scripts/build-release.sh | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/Scripts/build-release.sh b/Scripts/build-release.sh index 976fd7f..2a26e73 100755 --- a/Scripts/build-release.sh +++ b/Scripts/build-release.sh @@ -59,10 +59,23 @@ sign() { SPARKLE="$APP/Contents/Frameworks/Sparkle.framework" if [[ -d "$SPARKLE" ]]; then step "Codesigning Sparkle (inside-out, no --deep)" - # XPC services and helper apps first, then the framework itself. + # Nested bundles first: XPCServices/*.xpc and Updater.app. while IFS= read -r -d '' nested; do sign "$nested" done < <(find "$SPARKLE" \( -name '*.xpc' -o -name '*.app' \) -print0) + # Then bare Mach-O helpers beside them, i.e. Versions/B/Autoupdate. These + # match neither *.xpc nor *.app, so they used to keep the ad-hoc signature + # package_app.sh applies, and notarization rejected the whole archive with + # "Autoupdate: The binary is not signed with a valid Developer ID + # certificate", no secure timestamp and no hardened runtime. Nested bundles + # are pruned: their main executables are signed with their bundle above. + while IFS= read -r -d '' helper; do + # A bare `grep -q ... && sign` would make the loop exit non-zero whenever + # the last file is not Mach-O, and set -e would kill the script. + if [[ "$(basename "$helper")" != "Sparkle" ]] && file -b "$helper" | grep -q 'Mach-O'; then + sign "$helper" + fi + done < <(find "$SPARKLE" \( -name '*.xpc' -o -name '*.app' \) -prune -o -type f -print0) sign "$SPARKLE" fi @@ -76,8 +89,23 @@ if [[ "${SKIP_NOTARIZE:-}" == "1" ]]; then else step "Notarizing" ditto -c -k --keepParent "$APP" "$DIST/notarize.zip" - xcrun notarytool submit "$DIST/notarize.zip" --keychain-profile "$NOTARY_PROFILE" --wait + # notarytool exits 0 even when the submission comes back Invalid, so the + # status has to be read out. Without this the script walks on to stapler and + # dies there with "Record not found", which says nothing about the cause. + SUBMISSION="$(xcrun notarytool submit "$DIST/notarize.zip" \ + --keychain-profile "$NOTARY_PROFILE" --wait --output-format json)" rm -f "$DIST/notarize.zip" + echo "$SUBMISSION" + NOTARY_STATUS="$(sed -n 's/.*"status" *: *"\([^"]*\)".*/\1/p' <<<"$SUBMISSION" | tail -1)" + if [[ "$NOTARY_STATUS" != "Accepted" ]]; then + echo "Notarization returned $NOTARY_STATUS, not Accepted." >&2 + SUBMISSION_ID="$(sed -n 's/.*"id" *: *"\([^"]*\)".*/\1/p' <<<"$SUBMISSION" | head -1)" + if [[ -n "$SUBMISSION_ID" ]]; then + echo "Notary log for $SUBMISSION_ID:" >&2 + xcrun notarytool log "$SUBMISSION_ID" --keychain-profile "$NOTARY_PROFILE" >&2 || true + fi + exit 1 + fi step "Stapling" xcrun stapler staple "$APP"