#!/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 Override build directory Signing options: --identity "" Codesign identity (e.g. "Developer ID Application: Your Name (TEAMID)") --entitlements Optional entitlements plist for codesign Notarization options (recommended): --notary-profile notarytool keychain profile name (created via `xcrun notarytool store-credentials ...`) --skip-notarize Skip notarization Outputs: - Signed app: /strawberry.app - Zip for notarization: /strawberry-notarize.zip - DMG (optional): /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 \"\" --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