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.
325 lines
13 KiB
Bash
Executable File
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"
|
|
|