Files
strawberry/build_tools/macos/build_mas_pkg.sh
David Helkowski 8d49b87b7c Enhance macOS build tools with keychain management and troubleshooting guidance
This commit updates the `README_MAS.md` to include important notes on keychain trust settings and the installation of Apple intermediate certificates, addressing common codesigning issues. Additionally, the `build_mas_pkg.sh` script is enhanced with functions to prepare the login keychain for signing, diagnose chain failures, and provide clear error messages for authorization issues during the build process. These improvements aim to streamline the macOS build experience and assist developers in resolving keychain-related errors effectively.
2026-01-22 23:27:20 +09:00

325 lines
13 KiB
Bash
Executable File

#!/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 <password> \"$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 <policy>" 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 "<name>" (e.g. "Apple Distribution: Dry Ark LLC (TEAMID)")
--installer-identity "<name>" (e.g. "3rd Party Mac Developer Installer: Dry Ark LLC (TEAMID)")
--provisionprofile <path> Path to a *Mac App Store* provisioning profile (*.provisionprofile)
Optional:
--release | --debug Build config (default: Release)
--clean Clean build dir before build
--build-dir <path> Override build directory
--entitlements <plist> Codesign entitlements (default: dist/macos/entitlements.mas.plist)
--bundle-id <id> Override CFBundleIdentifier (default: com.dryark.strawberry)
--pkg-out <path> Output .pkg path (default: <build-dir>/strawberry-mas.pkg)
--keychain-password <pw> 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 <password> \"$HOME/Library/Keychains/login.keychain-db\"" >&2
echo >&2
echo "Tip: you can also rerun this script with:" >&2
echo " --keychain-password <login-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"