#!/usr/bin/env bash set -euo pipefail # Guard: this script must be executed with bash (not sourced into zsh, not run via sh). if [[ -z "${BASH_VERSION:-}" ]]; then echo "Error: this script must be run with bash (it uses bash arrays)." >&2 echo "Run:" >&2 echo " bash ./build_tools/macos/build_mas_pkg.sh --run ..." >&2 exit 2 fi ts() { date +"%H:%M:%S"; } lower() { echo "$1" | tr '[:upper:]' '[:lower:]'; } script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" repo_root="$(cd -- "${script_dir}/../.." && pwd)" prepare_login_keychain_for_signing() { # Some setups require explicitly granting Apple tooling access to the private key(s) # (otherwise productbuild/codesign can fail with authorization errors like: # CSSM Exception: -60008 Unable to obtain authorization for this operation # or "User interaction is not allowed"). # # This function is optional and only runs if a keychain password is provided. local keychain_path="$HOME/Library/Keychains/login.keychain-db" local pw="${1:-}" if [[ -z "$pw" ]]; then echo "==> [$(ts)] Note: no keychain password provided; skipping keychain access-control preparation." echo " If productbuild later fails with -60008 authorization errors, fix it with either:" echo " - Keychain Access → login → My Certificates → select the *private key* under the Installer cert → Get Info → Access Control → allow productbuild" echo " - OR (CLI): security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k \"$keychain_path\"" return 0 fi echo "==> [$(ts)] Preparing login keychain for signing (unlock + key partition list)" # Unlock so Security/Authorization can use keys without prompting. security unlock-keychain -p "$pw" "$keychain_path" >/dev/null 2>&1 || true # Allow Apple tools (codesign/productbuild) to access the private key without GUI prompts. # This is the standard fix used for non-interactive signing. security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$pw" "$keychain_path" >/dev/null 2>&1 || true } ensure_keychain_search_list() { # codesign builds the cert chain using the user keychain search list. # If the list is missing the System keychain, you can get: # "unable to build chain to self-signed root" + errSecInternalComponent local login_kc="$HOME/Library/Keychains/login.keychain-db" local system_kc="/Library/Keychains/System.keychain" local roots_kc="/System/Library/Keychains/SystemRootCertificates.keychain" local current current="$(security list-keychains -d user 2>/dev/null | tr -d '"' | tr -d ' ' || true)" if echo "$current" | grep -Fq "$system_kc"; then return 0 fi echo "==> [$(ts)] Note: adding System keychains to the user keychain search list (fixes common codesign chain errors)" echo " (This changes the user keychain search list; run 'security list-keychains -d user' to view.)" security list-keychains -d user -s "$login_kc" "$system_kc" "$roots_kc" >/dev/null 2>&1 || true } diagnose_chain_failure() { echo "==> [$(ts)] Codesign failed. Common causes on macOS:" echo " - System keychains not in the user keychain search list" echo " - Missing/invalid WWDR intermediate certificate" echo " - Keychain/key access issues" echo echo "==> [$(ts)] Keychain search list:" security list-keychains -d user || true echo echo "==> [$(ts)] Checking for Apple WWDR intermediate in System keychain:" security find-certificate -a -c "Apple Worldwide Developer Relations" /Library/Keychains/System.keychain 2>/dev/null | head -n 5 || true echo echo "==> [$(ts)] If WWDR is missing, install the current Apple WWDR intermediate certificate (via Xcode or Apple Developer portal)." echo "==> [$(ts)] Then re-run this script." } preflight_identity() { local what="$1" local policy="$2" local identity="$3" # NOTE: security expects "-p " as *two* args; do not pass "-p codesigning" as one string. if ! security find-identity -p "$policy" -v 2>/dev/null | grep -Fq "$identity"; then echo "Error: ${what} identity not found/usable in Keychain: $identity" >&2 echo "Run: ./build_tools/macos/check_signing_identities.sh" >&2 exit 2 fi } usage() { cat <<'EOF' Usage: ./build_tools/macos/build_mas_pkg.sh --run [options] What it does: - Builds Strawberry in Mac App Store mode (BUILD_FOR_MAC_APP_STORE=ON) - Runs deploy (macdeployqt + bundling) so the app bundle is self-contained - Embeds a Mac App Store provisioning profile into the app bundle - Codesigns the app with an Apple Distribution identity + entitlements - Builds a signed .pkg suitable for uploading to App Store Connect Required options: --run --codesign-identity "" (e.g. "Apple Distribution: Dry Ark LLC (TEAMID)") --installer-identity "" (e.g. "3rd Party Mac Developer Installer: Dry Ark LLC (TEAMID)") --provisionprofile Path to a *Mac App Store* provisioning profile (*.provisionprofile) Optional: --release | --debug Build config (default: Release) --clean Clean build dir before build --build-dir Override build directory --entitlements Codesign entitlements (default: dist/macos/entitlements.mas.plist) --bundle-id Override CFBundleIdentifier (default: com.dryark.strawberry) --pkg-out Output .pkg path (default: /strawberry-mas.pkg) --keychain-password OPTIONAL: unlock/login keychain + set key partition list for Apple tools (alternative: set env var STRAWBERRY_KEYCHAIN_PASSWORD) Examples: # Tip: if your keychain password contains characters like ! or $, prefer the env var or single quotes. STRAWBERRY_KEYCHAIN_PASSWORD='your-login-keychain-password' \ ./build_tools/macos/build_mas_pkg.sh --run --release --clean \ --codesign-identity "Apple Distribution: Your Name (TEAMID)" \ --installer-identity "3rd Party Mac Developer Installer: Your Name (TEAMID)" \ --provisionprofile "$HOME/Library/MobileDevice/Provisioning Profiles/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX.provisionprofile" ./build_tools/macos/build_mas_pkg.sh --run --release --clean \ --codesign-identity "Apple Distribution: Your Name (TEAMID)" \ --installer-identity "3rd Party Mac Developer Installer: Your Name (TEAMID)" \ --provisionprofile "$HOME/Library/MobileDevice/Provisioning Profiles/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX.provisionprofile" Notes: - Mac App Store submissions do NOT use Developer ID notarization. - You must create a Mac App Store provisioning profile for your App ID in Apple Developer. EOF } if [[ "$(uname -s)" != "Darwin" ]]; then echo "Error: This script is for macOS only." >&2 exit 1 fi do_run=0 config="Release" do_clean=0 build_dir="" codesign_identity="" installer_identity="" provisionprofile="" entitlements="" bundle_id="com.dryark.strawberry" pkg_out="" keychain_password="${STRAWBERRY_KEYCHAIN_PASSWORD:-}" 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 ;; --build-dir) build_dir="${2:-}"; shift 2 ;; --codesign-identity) codesign_identity="${2:-}"; shift 2 ;; --installer-identity) installer_identity="${2:-}"; shift 2 ;; --provisionprofile) provisionprofile="${2:-}"; shift 2 ;; --entitlements) entitlements="${2:-}"; shift 2 ;; --bundle-id) bundle_id="${2:-}"; shift 2 ;; --pkg-out) pkg_out="${2:-}"; shift 2 ;; --keychain-password) keychain_password="${2:-}"; shift 2 ;; -h|--help) usage; exit 0 ;; *) echo "Unknown arg: $1" >&2; usage; exit 2 ;; esac done if [[ "$do_run" -eq 0 ]]; then usage echo echo "==> [$(ts)] Tip: list available signing identities:" echo " security find-identity -p codesigning -v" echo " security find-identity -p basic -v" exit 0 fi if [[ -z "$codesign_identity" ]]; then echo "Error: missing --codesign-identity" >&2 exit 2 fi if [[ -z "$installer_identity" ]]; then echo "Error: missing --installer-identity" >&2 exit 2 fi if [[ -z "$provisionprofile" || ! -f "$provisionprofile" ]]; then echo "Error: missing/invalid --provisionprofile: $provisionprofile" >&2 exit 2 fi if [[ -z "$entitlements" ]]; then entitlements="${repo_root}/dist/macos/entitlements.mas.plist" fi if [[ ! -f "$entitlements" ]]; then echo "Error: entitlements file not found: $entitlements" >&2 exit 2 fi if [[ -z "$build_dir" ]]; then build_dir="${repo_root}/cmake-build-macos-$(lower "$config")-mas" fi if [[ -z "$pkg_out" ]]; then pkg_out="${build_dir}/strawberry-mas.pkg" fi echo "==> [$(ts)] Repo: ${repo_root}" echo "==> [$(ts)] Build dir: ${build_dir}" echo "==> [$(ts)] Config: ${config}" echo "==> [$(ts)] Bundle ID: ${bundle_id}" echo "==> [$(ts)] Entitlements: ${entitlements}" echo "==> [$(ts)] Provisioning profile: ${provisionprofile}" echo "==> [$(ts)] Output pkg: ${pkg_out}" echo "==> [$(ts)] Building (Mac App Store mode)" build_args=( "--release" ) if [[ "$config" == "Debug" ]]; then build_args=( "--debug" ); fi if [[ "$do_clean" -eq 1 ]]; then build_args+=( "--clean" ); fi build_args+=( "--build-dir" "$build_dir" "--mas" "--deploy" ) # Provide bundle id via CMake cache variable. export MACOS_BUNDLE_ID="$bundle_id" "${repo_root}/build_tools/macos/build_app.sh" "${build_args[@]}" app_path="${build_dir}/strawberry.app" bin_path="${app_path}/Contents/MacOS/strawberry" if [[ ! -x "$bin_path" ]]; then echo "Error: built app not found at: $app_path" >&2 exit 1 fi echo "==> [$(ts)] Embedding provisioning profile" cp -f "$provisionprofile" "${app_path}/Contents/embedded.provisionprofile" ensure_keychain_search_list prepare_login_keychain_for_signing "$keychain_password" preflight_identity "codesign" "codesigning" "$codesign_identity" preflight_identity "installer" "basic" "$installer_identity" echo "==> [$(ts)] Codesigning app (Mac App Store)" codesign_args=( --force --timestamp --options runtime --sign "$codesign_identity" --entitlements "$entitlements" ) # Clean up any leftover codesign temp files from previous interrupted runs. 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. 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 # Only sign Mach-O binaries. if file -b "$f" | grep -q "Mach-O"; then codesign "${codesign_args[@]}" "$f" >/dev/null fi done 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 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 codesign "${codesign_args[@]}" "$app_path" >/dev/null echo "==> [$(ts)] Verifying codesign" codesign --verify --deep --strict --verbose=2 "$app_path" echo "==> [$(ts)] Building signed .pkg for App Store upload" rm -f "$pkg_out" >/dev/null 2>&1 || true if ! productbuild \ --component "$app_path" /Applications \ --sign "$installer_identity" \ "$pkg_out"; then echo "Error: productbuild failed while signing the .pkg." >&2 echo "Common cause: keychain/private-key authorization (e.g. CSSM -60008)." >&2 echo >&2 echo "Fix options:" >&2 echo "1) Keychain Access UI:" >&2 echo " - Keychain Access → login → My Certificates" >&2 echo " - Find: $installer_identity" >&2 echo " - Expand it and select the *private key* under it" >&2 echo " - Get Info → Access Control → allow /usr/bin/productbuild (optionally also allow /usr/bin/pkgbuild)" >&2 echo "2) CLI (recommended for repeatable builds):" >&2 echo " security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k \"$HOME/Library/Keychains/login.keychain-db\"" >&2 echo >&2 echo "Tip: you can also rerun this script with:" >&2 echo " --keychain-password " >&2 exit 1 fi echo "==> [$(ts)] Verifying pkg signature" pkgutil --check-signature "$pkg_out" || true echo echo "Done." echo "App: $app_path" echo "PKG: $pkg_out"