Files

164 lines
10 KiB
Bash
Executable File

#!/bin/bash
# Also honor the Bash requirement if launched explicitly as `zsh scripts/release.sh`.
if [ -z "${BASH_VERSION:-}" ]; then
case "${ZSH_EVAL_CONTEXT:-}" in
*:file) printf 'Run this script with: bash ./scripts/release.sh\n' >&2;return 1 ;;
*) exec /bin/bash "$0" "$@" ;;
esac
fi
set -euo pipefail
cd "$(dirname "$0")/.."
usage() {
cat <<'EOF'
Usage: ./scripts/release.sh [--signed-only | --unsigned] [--] [xcodebuild arguments]
Default: Developer ID sign, notarize and staple the app and branded DMG, then
require successful Gatekeeper checks before publishing the DMG.
--signed-only: Build a Developer ID signed DMG without notarization.
--unsigned: Build an unsigned preview DMG.
Only the current DMG is kept in dist; reports stay under build-release/release-verification.
KJMIX_SIGNING_IDENTITY: Developer ID Application certificate name or SHA-1.
Auto-selects when all matching certificates have one name.
KJMIX_NOTARY_PROFILE: notarytool Keychain profile (default: KJMixPro).
Save credentials once with ./scripts/setup-notarization.sh in Terminal.
EOF
}
release_mode=notarized
case "${1:-}" in
--help|-h) usage; exit 0 ;;
--signed-only) release_mode=signed;shift ;;
--unsigned) release_mode=unsigned;shift ;;
esac
if [[ "${1:-}" == -- ]]; then shift; fi
release_work="$(mktemp -d "${TMPDIR:-/tmp}/kjmix-release.XXXXXX")"
release_mount="$release_work/mounted"
release_mounted=false
release_completed=false
release_reports=""
cleanup() {
if [[ "$release_completed" != true && -n "$release_reports" && -d "$release_reports" ]]; then printf 'FAILED or interrupted. This attempt is NOT a verified release.\n' > "$release_reports/STATUS.txt";fi
if [[ "$release_mounted" == true ]]; then /usr/bin/hdiutil detach "$release_mount" -quiet || true; fi
/bin/rm -rf "$release_work"
}
trap cleanup EXIT
fail() { printf 'Release failed: %s\n' "$*" >&2;exit 1; }
release_identity="${KJMIX_SIGNING_IDENTITY:-}"
release_profile="${KJMIX_NOTARY_PROFILE:-KJMixPro}"
if [[ "$release_mode" != unsigned ]]; then
release_identities="$(/usr/bin/security find-identity -v -p codesigning)"
if [[ -z "$release_identity" ]]; then
release_names="$(printf '%s\n' "$release_identities" | /usr/bin/sed -n 's/.*"\(Developer ID Application:.*\)"/\1/p' | /usr/bin/sort -u)"
[[ -n "$release_names" ]] || fail 'No Developer ID Application certificate with a private key is available. Use --unsigned only for a local preview.'
[[ "$(printf '%s\n' "$release_names" | /usr/bin/wc -l | /usr/bin/tr -d ' ')" == 1 ]] || fail 'Multiple Developer ID identities found. Set KJMIX_SIGNING_IDENTITY to the intended certificate SHA-1.'
release_identity="$(printf '%s\n' "$release_identities" | /usr/bin/awk '/"Developer ID Application:/ {print $2;exit}')"
fi
printf 'Signing identity: %s\n' "$release_identity"
fi
if [[ "$release_mode" == notarized ]]; then
if ! xcrun notarytool history --keychain-profile "$release_profile" --output-format json > "$release_work/profile-check.json" 2> "$release_work/profile-error.txt"; then
/bin/cat "$release_work/profile-error.txt" >&2
fail "Notarization profile '$release_profile' is unavailable or Apple could not validate it. Run ./scripts/setup-notarization.sh, or explicitly use --signed-only for an unnotarized build."
fi
fi
xcodebuild -project "KJMix Pro.xcodeproj" -scheme "KJMix Pro" -configuration Release -destination 'platform=macOS' -derivedDataPath "$PWD/build-release" CODE_SIGNING_ALLOWED=NO build "$@"
release_built="$PWD/build-release/Build/Products/Release/KJMix Pro.app"
[[ -d "$release_built" ]] || fail 'Release app was not produced.'
release_version="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$release_built/Contents/Info.plist")"
[[ "$release_version" =~ ^[A-Za-z0-9._-]+$ ]] || fail 'Invalid release version.'
release_payload="$release_work/payload"
[[ ! -L "$PWD/dist" ]] || fail 'dist must be a real directory, not a symbolic link.'
/bin/mkdir -p "$release_payload" dist
release_app="$release_payload/KJMix Pro.app"
/usr/bin/ditto --noextattr --norsrc "$release_built" "$release_app"
release_icon="$release_app/Contents/Resources/AppIcon.icns"
[[ -s "$release_icon" ]] || fail 'AppIcon.icns is missing.'
# The app icon is already in its bundle; use the same approved artwork for the volume.
/bin/cp "$release_icon" "$release_payload/.VolumeIcon.icns"
/bin/ln -s /Applications "$release_payload/Applications"
release_suffix=macOS
if [[ "$release_mode" == unsigned ]]; then release_suffix=unsigned-macOS;fi
if [[ "$release_mode" == signed ]]; then release_suffix=signed-unnotarized-macOS;fi
release_name="KJMixPro-$release_version-$release_suffix"
release_reports="$PWD/build-release/release-verification/$release_name"
/bin/mkdir -p "$release_reports"
printf 'Preparing release. NOT yet verified.\n' > "$release_reports/STATUS.txt"
notarize() {
local artifact="$1" label="$2" report="$release_reports/$2-notary.json" submission notary_status
printf 'Submitting %s to Apple notarization…\n' "$label"
if ! xcrun notarytool submit "$artifact" --keychain-profile "$release_profile" --wait --timeout 30m --output-format json > "$report"; then
printf 'Notarization did not complete. Inspect %s; Apple may still be processing the submission.\n' "$report" >&2
return 1
fi
notary_status="$(/usr/bin/plutil -extract status raw -o - "$report")"
submission="$(/usr/bin/plutil -extract id raw -o - "$report")"
if [[ "$notary_status" != Accepted ]]; then
xcrun notarytool log "$submission" --keychain-profile "$release_profile" "$release_reports/$label-notary-log.json" || true
fail "Apple did not accept $label (status: $notary_status). See $release_reports."
fi
}
if [[ "$release_mode" != unsigned ]]; then
/usr/bin/codesign --force --options runtime --timestamp --sign "$release_identity" "$release_app"
/usr/bin/codesign --verify --deep --strict --verbose=2 "$release_app" 2> "$release_reports/app-signature.txt"
/usr/bin/codesign --display --verbose=4 "$release_app" 2> "$release_reports/app-signing-details.txt"
# A development/ad-hoc certificate must never be presented as a distribution identity.
/usr/bin/awk '/^Authority=Developer ID Application:/ {ok=1} END {exit !ok}' "$release_reports/app-signing-details.txt" || fail 'The app was not signed with Developer ID Application.'
fi
if [[ "$release_mode" == notarized ]]; then
/usr/bin/ditto -c -k --sequesterRsrc --keepParent "$release_app" "$release_work/app-for-notarization.zip"
notarize "$release_work/app-for-notarization.zip" app
xcrun stapler staple "$release_app" > "$release_reports/app-staple.txt" 2>&1
xcrun stapler validate "$release_app" >> "$release_reports/app-staple.txt" 2>&1
/usr/sbin/spctl --assess --type execute --verbose=4 "$release_app" > "$release_reports/app-gatekeeper.txt" 2>&1
if [[ -x /usr/bin/syspolicy_check ]]; then /usr/bin/syspolicy_check distribution "$release_app" > "$release_reports/app-distribution.txt" 2>&1;fi
fi
# Configure the custom volume icon on a writable image, then compress for distribution.
/usr/bin/hdiutil create -quiet -srcfolder "$release_payload" -volname 'KJMix Pro' -fs HFS+ -format UDRW "$release_work/writable.dmg"
/bin/mkdir -p "$release_mount"
/usr/bin/hdiutil attach -quiet -readwrite -nobrowse -noautoopen -mountpoint "$release_mount" "$release_work/writable.dmg"
release_mounted=true
xcrun SetFile -a C "$release_mount"
/usr/bin/hdiutil detach -quiet "$release_mount"
release_mounted=false
release_dmg="$release_work/$release_name.dmg"
/usr/bin/hdiutil convert -quiet "$release_work/writable.dmg" -format UDZO -imagekey zlib-level=9 -o "$release_dmg"
# Set the DMG file's Finder icon before signing the image.
xcrun swift scripts/set-dmg-icon.swift "$release_icon" "$release_dmg"
if [[ "$release_mode" != unsigned ]]; then
/usr/bin/codesign --force --timestamp --sign "$release_identity" "$release_dmg"
/usr/bin/codesign --verify --strict --verbose=2 "$release_dmg" 2> "$release_reports/dmg-signature.txt"
fi
if [[ "$release_mode" == notarized ]]; then
notarize "$release_dmg" dmg
xcrun stapler staple "$release_dmg" > "$release_reports/dmg-staple.txt" 2>&1
xcrun stapler validate "$release_dmg" >> "$release_reports/dmg-staple.txt" 2>&1
/usr/sbin/spctl --assess --type open --context context:primary-signature --verbose=4 "$release_dmg" > "$release_reports/dmg-gatekeeper.txt" 2>&1
fi
/usr/bin/hdiutil verify "$release_dmg" > "$release_reports/dmg-integrity.txt" 2>&1
# Inspect the final compressed image, not only the source staging folder.
/usr/bin/hdiutil attach -quiet -readonly -nobrowse -noautoopen -mountpoint "$release_mount" "$release_dmg"
release_mounted=true
[[ -s "$release_mount/.VolumeIcon.icns" && -L "$release_mount/Applications" && -d "$release_mount/KJMix Pro.app" ]] || fail 'DMG app, Applications shortcut or volume icon is missing.'
[[ "$(/usr/bin/readlink "$release_mount/Applications")" == /Applications ]] || fail 'Applications shortcut target is incorrect.'
if [[ "$release_mode" != unsigned ]]; then /usr/bin/codesign --verify --deep --strict "$release_mount/KJMix Pro.app" 2> "$release_reports/mounted-app-signature.txt";fi
if [[ "$release_mode" == notarized ]]; then
xcrun stapler validate "$release_mount/KJMix Pro.app" > "$release_reports/mounted-app-staple.txt" 2>&1
/usr/sbin/spctl --assess --type execute --verbose=4 "$release_mount/KJMix Pro.app" > "$release_reports/mounted-app-gatekeeper.txt" 2>&1
fi
/usr/bin/hdiutil detach -quiet "$release_mount"
release_mounted=false
/bin/mv -f "$release_dmg" "dist/$release_name.dmg"
# Preserve the previous distribution until the new image has passed every check.
# Nothing other than the current DMG belongs in this generated output directory.
/usr/bin/find "$PWD/dist" -mindepth 1 -maxdepth 1 ! -name "$release_name.dmg" -exec /bin/rm -rf -- {} +
/usr/bin/shasum -a 256 "dist/$release_name.dmg" > "$release_reports/SHA256.txt"
if [[ "$release_mode" == notarized ]]; then
printf 'Developer ID signed; Apple notarization accepted; app and DMG tickets stapled; Gatekeeper checks passed.\n' | /usr/bin/tee "$release_reports/STATUS.txt"
else
printf '%s build only. NOT notarized or Gatekeeper verified.\n' "$release_mode" | /usr/bin/tee "$release_reports/STATUS.txt"
fi
release_completed=true
printf 'Created dist/%s.dmg\nVerification reports: %s\n' "$release_name" "$release_reports"