Files
strawberry/build_tools/macos/build_sign_notarize.sh
David Helkowski 3d10414a88 Refactor notarization process and enhance build scripts for macOS
This commit updates the build_sign_notarize.sh script to improve the notarization process by introducing a conditional stapling option. It also cleans up temporary files and clears macOS provenance metadata to prevent issues during builds. The Dmg.cmake script is modified to remove the reliance on environment variables for codesigning, streamlining the build process. Additionally, the build_app.sh script is enhanced with heartbeat logging for long-running commands and improved cleanup procedures for build directories.
2026-01-22 18:37:02 +09:00

310 lines
11 KiB
Bash
Executable File

#!/usr/bin/env bash
set -euo pipefail
ts() { date +"%H:%M:%S"; }
script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)"
repo_root="$(cd -- "${script_dir}/../.." && pwd)"
usage() {
cat <<'EOF'
Usage:
./build_tools/macos/build_sign_notarize.sh # list local signing identities + notary profiles
./build_tools/macos/build_sign_notarize.sh --run [options] # build, sign, notarize, staple
Common options:
--run Perform build/sign/notarize (otherwise list identities/profiles)
--release | --debug Build config (default: Release)
--clean Clean build dir before build
--deploy Run CMake 'deploy' target before signing (default: on)
--no-deploy Do not run 'deploy' (not recommended for distribution)
--dmg Build a DMG after app notarization, then notarize+staple the DMG too
--build-dir <path> Override build directory
Signing options:
--identity "<name>" Codesign identity (e.g. "Developer ID Application: Your Name (TEAMID)")
--entitlements <plist> Optional entitlements plist for codesign
Notarization options (recommended):
--notary-profile <name> notarytool keychain profile name (created via `xcrun notarytool store-credentials <name> ...`)
--skip-notarize Skip notarization
Outputs:
- Signed app: <build-dir>/strawberry.app
- Zip for notarization: <build-dir>/strawberry-notarize.zip
- DMG (optional): <build-dir>/strawberry-*.dmg
Notes:
- This script is intended for Developer ID distribution (outside Mac App Store).
- If you want Sparkle updates, you'll typically ship a notarized .zip + an appcast feed.
EOF
}
list_identities_and_profiles() {
echo "==> [$(ts)] macOS code signing identities (Keychain)"
security find-identity -p codesigning -v || true
echo
echo "==> [$(ts)] notarytool credential profiles"
echo "Note: this Xcode notarytool version does not provide a 'list-profiles' command."
echo "If you forgot the profile name you created, check Keychain Access or re-run:"
echo " xcrun notarytool store-credentials \"<profile-name>\" --apple-id \"you@example.com\" --team-id \"TEAMID\""
echo
echo "==> [$(ts)] Provisioning profiles (macOS)"
prof_dir="${HOME}/Library/MobileDevice/Provisioning Profiles"
if [[ -d "${prof_dir}" ]]; then
ls -la "${prof_dir}" | head -n 50
else
echo "(none found at '${prof_dir}')"
fi
}
if [[ "$(uname -s)" != "Darwin" ]]; then
echo "Error: This script is for macOS only." >&2
exit 1
fi
if ! command -v xcode-select >/dev/null 2>&1 || ! xcode-select -p >/dev/null 2>&1; then
echo "Error: Xcode Command Line Tools not found." >&2
echo "Install them first: xcode-select --install" >&2
exit 1
fi
do_run=0
config="Release"
do_clean=0
do_deploy=1
do_dmg=0
build_dir=""
identity=""
entitlements=""
notary_profile=""
skip_notarize=0
while [[ $# -gt 0 ]]; do
case "$1" in
--run) do_run=1; shift ;;
--release) config="Release"; shift ;;
--debug) config="Debug"; shift ;;
--clean) do_clean=1; shift ;;
--deploy) do_deploy=1; shift ;;
--no-deploy) do_deploy=0; shift ;;
--dmg) do_dmg=1; shift ;;
--build-dir) build_dir="${2:-}"; shift 2 ;;
--identity) identity="${2:-}"; shift 2 ;;
--entitlements) entitlements="${2:-}"; shift 2 ;;
--notary-profile) notary_profile="${2:-}"; shift 2 ;;
--skip-notarize) skip_notarize=1; shift ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown arg: $1" >&2; usage; exit 2 ;;
esac
done
if [[ "$do_run" -eq 0 ]]; then
usage
echo
list_identities_and_profiles
exit 0
fi
if [[ -z "$build_dir" ]]; then
lc_config="$(echo "$config" | tr '[:upper:]' '[:lower:]')"
build_dir="${repo_root}/cmake-build-macos-${lc_config}"
fi
app_path="${build_dir}/strawberry.app"
bin_path="${app_path}/Contents/MacOS/strawberry"
zip_path="${build_dir}/strawberry-notarize.zip"
dmg_path=""
notarize_and_maybe_staple() {
local file_path="$1"
local label="$2"
local do_staple="${3:-1}"
echo "==> [$(ts)] Notarizing ${label}"
local out
out="$(mktemp -t notarytool-submit.XXXXXX)"
xcrun notarytool submit "$file_path" --keychain-profile "$notary_profile" --wait --output-format plist --no-progress >"$out"
local submit_id submit_status
submit_id="$(/usr/bin/plutil -extract id raw -o - "$out" 2>/dev/null || true)"
submit_status="$(/usr/bin/plutil -extract status raw -o - "$out" 2>/dev/null || true)"
rm -f "$out" || true
if [[ -z "$submit_id" ]]; then
echo "Error: could not parse notarization submission id for ${label}." >&2
exit 1
fi
echo "==> [$(ts)] Notary submission id: $submit_id"
echo "==> [$(ts)] Notary status: $submit_status"
if [[ "$submit_status" != "Accepted" ]]; then
echo "Error: notarization failed for ${label} with status '$submit_status'. Fetching log..." >&2
xcrun notarytool log "$submit_id" --keychain-profile "$notary_profile" || true
exit 1
fi
if [[ "$do_staple" -eq 1 ]]; then
echo "==> [$(ts)] Stapling ${label}"
xcrun stapler staple "$file_path"
fi
}
if [[ -z "$identity" ]]; then
echo "Error: Missing --identity (Developer ID Application identity)." >&2
exit 2
fi
if [[ "$skip_notarize" -eq 0 && -z "$notary_profile" ]]; then
echo "Error: Missing --notary-profile (or pass --skip-notarize)." >&2
exit 2
fi
echo "==> [$(ts)] Building Strawberry"
build_args=( "--release" )
if [[ "$config" == "Debug" ]]; then build_args=( "--debug" ); fi
if [[ "$do_clean" -eq 1 ]]; then build_args+=( "--clean" ); fi
if [[ -n "$build_dir" ]]; then build_args+=( "--build-dir" "$build_dir" ); fi
if [[ "$do_deploy" -eq 1 ]]; then build_args+=( "--deploy" ); fi
"${repo_root}/build_tools/macos/build_app.sh" "${build_args[@]}"
if [[ ! -x "$bin_path" ]]; then
echo "Error: built app not found at: $app_path" >&2
exit 1
fi
echo "==> [$(ts)] Codesigning (hardened runtime)"
codesign_args=( --force --timestamp --options runtime --sign "$identity" )
if [[ -n "$entitlements" ]]; then
codesign_args+=( --entitlements "$entitlements" )
fi
# Clean up any leftover codesign temp files from previous interrupted runs.
# codesign may create *.cstemp alongside binaries while updating signatures.
find "$app_path" -name "*.cstemp" -print0 2>/dev/null | while IFS= read -r -d '' f; do
rm -f "$f" || true
done
# Clear macOS provenance/quarantine metadata which can interfere with modifying files in-place.
xattr -dr com.apple.provenance "$app_path" >/dev/null 2>&1 || true
xattr -dr com.apple.quarantine "$app_path" >/dev/null 2>&1 || true
# Sign nested code first, then frameworks, then the main app bundle.
#
# Important: do NOT codesign individual files *inside* a .framework bundle (e.g. Sparkle.framework/Sparkle),
# because codesign expects frameworks to be signed as bundles and may error with
# "bundle format is ambiguous (could be app or framework)".
# 1) Sign dylibs and standalone executables that are NOT inside a .framework/.app/.xpc bundle.
find "$app_path" -type f \( -name "*.dylib" -o -name "*.so" -o -perm -111 \) \
! -name "*.cstemp" \
! -path "*/Contents/Frameworks/*.framework/*" \
! -path "*/Contents/Frameworks/*.app/*" \
! -path "*/Contents/Frameworks/*.xpc/*" \
! -path "*/Contents/PlugIns/*.framework/*" \
! -path "*/Contents/PlugIns/*.app/*" \
! -path "*/Contents/PlugIns/*.xpc/*" \
-print0 | while IFS= read -r -d '' f; do
codesign "${codesign_args[@]}" "$f" >/dev/null
done
# 2) Sign nested helper apps and XPC services (Sparkle ships these inside its framework).
find "$app_path" -type d \( -name "*.xpc" -o -name "*.app" \) -print0 2>/dev/null | while IFS= read -r -d '' b; do
codesign "${codesign_args[@]}" "$b" >/dev/null
done
# 2b) Sparkle.framework contains a standalone helper executable "Autoupdate" under Versions/* that is
# not inside an .app or .xpc bundle. Notarization requires it be signed with Developer ID + timestamp.
sparkle_fw="$app_path/Contents/Frameworks/Sparkle.framework"
if [[ -d "$sparkle_fw" ]]; then
find "$sparkle_fw/Versions" -type f -perm -111 \
! -name "*.cstemp" \
! -path "*/_CodeSignature/*" \
-print0 2>/dev/null | while IFS= read -r -d '' f; do
codesign "${codesign_args[@]}" "$f" >/dev/null
done
fi
# 3) Sign frameworks as bundles.
find "$app_path/Contents/Frameworks" "$app_path/Contents/PlugIns" -type d -name "*.framework" -print0 2>/dev/null | while IFS= read -r -d '' fw; do
codesign "${codesign_args[@]}" "$fw" >/dev/null
done
# 4) Finally sign the main app.
codesign "${codesign_args[@]}" "$app_path" >/dev/null
echo "==> [$(ts)] Verifying codesign"
codesign --verify --deep --strict --verbose=2 "$app_path"
echo "==> [$(ts)] Creating zip for notarization"
rm -f "$zip_path"
ditto -c -k --sequesterRsrc --keepParent "$app_path" "$zip_path"
if [[ "$skip_notarize" -eq 0 ]]; then
# ZIP archives cannot be stapled; notarization is still useful for distribution and Sparkle.
notarize_and_maybe_staple "$zip_path" "ZIP" 0
echo "==> [$(ts)] Stapling app"
xcrun stapler staple "$app_path"
fi
if [[ "$do_dmg" -eq 1 ]]; then
echo "==> [$(ts)] Building DMG (from already-signed app; no redeploy)"
if ! command -v create-dmg >/dev/null 2>&1; then
echo "Error: create-dmg not found. Install it with Homebrew (it's in Brewfile):" >&2
echo " ./build_tools/macos/install_brew_deps.sh" >&2
exit 1
fi
# Build a versioned DMG name using Info.plist (falls back to Strawberry version constant).
plist="${app_path}/Contents/Info.plist"
bundle_version="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleVersion' "$plist" 2>/dev/null || true)"
if [[ -z "${bundle_version}" ]]; then
bundle_version="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$plist" 2>/dev/null || true)"
fi
if [[ -z "${bundle_version}" ]]; then
bundle_version="unknown"
fi
arch="$(uname -m)"
dmg_path="${build_dir}/strawberry-${bundle_version}-${arch}.dmg"
rm -f "$dmg_path"
(
cd "$build_dir"
create-dmg \
--volname strawberry \
--background "${repo_root}/dist/macos/dmg_background.png" \
--app-drop-link 450 218 \
--icon strawberry.app 150 218 \
--window-size 600 450 \
"$(basename "$dmg_path")" \
strawberry.app
)
if [[ -z "$dmg_path" ]]; then
echo "Error: DMG was not created in $build_dir" >&2
exit 1
fi
echo "==> [$(ts)] Codesigning DMG"
codesign --force --timestamp --sign "$identity" "$dmg_path"
if [[ "$skip_notarize" -eq 0 ]]; then
notarize_and_maybe_staple "$dmg_path" "DMG" 1
fi
fi
echo "==> [$(ts)] Gatekeeper assessment"
spctl -a -vv --type execute "$app_path" || true
echo
echo "Done."
echo "App: $app_path"
echo "Zip: $zip_path"
if [[ -n "${dmg_path}" ]]; then
echo "DMG: $dmg_path"
fi