164 lines
10 KiB
Bash
Executable File
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"
|