#!/usr/bin/env bash set -euo pipefail # Build a universal (arm64+x86_64) Mac App Store upload package by: # - merging two already-deployed Strawberry.app bundles (arm64 + x86_64) using lipo # - embedding a Mac App Store provisioning profile # - codesigning with Apple Distribution (+ entitlements) # - producing a signed .pkg with productbuild (Installer identity) # # Intended workflow with two Macs: # 1) On Apple Silicon Mac: build+deploy MAS app bundle (unsigned) → copy strawberry.app somewhere # 2) On Intel Mac: build+deploy MAS app bundle (unsigned) → copy strawberry.app somewhere # 3) On the Mac that holds your signing keys (either one): run THIS script to merge+sign+package ts() { date +"%H:%M:%S"; } if [[ -z "${BASH_VERSION:-}" ]]; then echo "Error: this script must be run with bash." >&2 exit 2 fi if [[ "$(uname -s)" != "Darwin" ]]; then echo "Error: This script is for macOS only." >&2 exit 1 fi script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" repo_root="$(cd -- "${script_dir}/../.." && pwd)" ensure_keychain_search_list() { 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" security list-keychains -d user -s "$login_kc" "$system_kc" "$roots_kc" >/dev/null 2>&1 || true } prepare_login_keychain_for_signing() { local keychain_path="$HOME/Library/Keychains/login.keychain-db" local pw="${1:-}" if [[ -z "$pw" ]]; then return 0 fi echo "==> [$(ts)] Preparing login keychain for signing (unlock + key partition list)" security unlock-keychain -p "$pw" "$keychain_path" >/dev/null 2>&1 || true security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$pw" "$keychain_path" >/dev/null 2>&1 || true } preflight_identity() { local what="$1" local policy="$2" local identity="$3" 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 exit 2 fi } usage() { cat <<'EOF' Usage: ./build_tools/macos/build_mas_universal_pkg.sh --run [options] Required: --run --arm-app Path to arm64 Strawberry.app (already built+deployed, unsigned) --x86-app Path to x86_64 Strawberry.app (already built+deployed, unsigned) --codesign-identity "" Apple Distribution: ... --installer-identity "" 3rd Party Mac Developer Installer: ... --provisionprofile Mac App Store provisioning profile (*.provisionprofile) Optional: --out-dir Output directory (default: cmake-build-macos-release-mas-universal) --entitlements Codesign entitlements (default: dist/macos/entitlements.mas.plist) --pkg-out Output .pkg path (default: /strawberry-mas-universal.pkg) --bundle-id For display/logging only (does not rewrite Info.plist) --keychain-password Or set env var STRAWBERRY_KEYCHAIN_PASSWORD (quote it!) Notes: - This script does NOT build Strawberry. It merges two pre-built app bundles. - After lipo-merging, the app must be re-signed (this script does that). EOF } do_run=0 arm_app="" x86_app="" out_dir="" codesign_identity="" installer_identity="" provisionprofile="" entitlements="" pkg_out="" bundle_id="" keychain_password="${STRAWBERRY_KEYCHAIN_PASSWORD:-}" while [[ $# -gt 0 ]]; do case "$1" in --run) do_run=1; shift ;; --arm-app) arm_app="${2:-}"; shift 2 ;; --x86-app) x86_app="${2:-}"; shift 2 ;; --out-dir) out_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 ;; --pkg-out) pkg_out="${2:-}"; shift 2 ;; --bundle-id) bundle_id="${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 exit 0 fi if [[ -z "$arm_app" || ! -d "$arm_app" ]]; then echo "Error: missing/invalid --arm-app: $arm_app" >&2 exit 2 fi if [[ -z "$x86_app" || ! -d "$x86_app" ]]; then echo "Error: missing/invalid --x86-app: $x86_app" >&2 exit 2 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 "$out_dir" ]]; then out_dir="${repo_root}/cmake-build-macos-release-mas-universal" fi mkdir -p "$out_dir" universal_app="${out_dir}/strawberry.app" if [[ -e "$universal_app" ]]; then rm -rf "$universal_app" fi echo "==> [$(ts)] Repo: $repo_root" echo "==> [$(ts)] arm app: $arm_app" echo "==> [$(ts)] x86 app: $x86_app" echo "==> [$(ts)] out dir: $out_dir" if [[ -n "$bundle_id" ]]; then echo "==> [$(ts)] bundle id (expected): $bundle_id" fi echo "==> [$(ts)] Creating universal app bundle (lipo merge)" "${repo_root}/build_tools/macos/make_universal_app.sh" \ --arm-app "$arm_app" \ --x86-app "$x86_app" \ --out-app "$universal_app" \ --clean echo "==> [$(ts)] Embedding provisioning profile" cp -f "$provisionprofile" "${universal_app}/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 universal app (Mac App Store)" codesign_args=( --force --timestamp --options runtime --sign "$codesign_identity" --entitlements "$entitlements" ) # Clean up any leftover codesign temp files and xattrs. find "$universal_app" -name "*.cstemp" -print0 2>/dev/null | while IFS= read -r -d '' f; do rm -f "$f" || true; done xattr -dr com.apple.provenance "$universal_app" >/dev/null 2>&1 || true xattr -dr com.apple.quarantine "$universal_app" >/dev/null 2>&1 || true # Sign nested code first, then frameworks, then the main app bundle. find "$universal_app" -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 if /usr/bin/file -b "$f" | grep -q "Mach-O"; then codesign "${codesign_args[@]}" "$f" >/dev/null fi done find "$universal_app" -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 "$universal_app/Contents/Frameworks" "$universal_app/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[@]}" "$universal_app" >/dev/null echo "==> [$(ts)] Verifying codesign" codesign --verify --deep --strict --verbose=2 "$universal_app" if [[ -z "$pkg_out" ]]; then pkg_out="${out_dir}/strawberry-mas-universal.pkg" fi rm -f "$pkg_out" >/dev/null 2>&1 || true echo "==> [$(ts)] Building signed .pkg for App Store upload" productbuild \ --component "$universal_app" /Applications \ --sign "$installer_identity" \ "$pkg_out" echo "==> [$(ts)] Verifying pkg signature" pkgutil --check-signature "$pkg_out" || true echo echo "Done." echo "Universal app: $universal_app" echo "PKG: $pkg_out"