diff --git a/build_tools/README.md b/build_tools/README.md index db0ca476c..6c0e3da1c 100644 --- a/build_tools/README.md +++ b/build_tools/README.md @@ -77,3 +77,48 @@ This produces: --identity "Developer ID Application: Your Name (TEAMID)" \ --notary-profile "" ``` + +## macOS Mac App Store (MAS) build + signed PKG + +This repo includes `build_tools/macos/build_mas_pkg.sh` to automate: + +- build (MAS mode) → deploy (bundle deps) → embed provisioning profile → codesign → `productbuild` a signed `.pkg` + +### Requirements (Apple Developer) + +- An App Store Connect app record with bundle id **`com.dryark.strawberry`** (or your own). +- A **Mac App Store provisioning profile** for that App ID. +- Signing identities installed in your Keychain: + - **Apple Distribution** (for the `.app`) + - **3rd Party Mac Developer Installer** (for the `.pkg`) + +Tip: list what you have installed: + +```bash +security find-identity -p codesigning -v +security find-identity -p basic -v +ls -la "$HOME/Library/MobileDevice/Provisioning Profiles" | head -n 50 +``` + +### Manual setup guide (certificates, Keychain Access, profiles) + +See: `build_tools/macos/README_MAS.md` + +### Build the signed upload PKG + +```bash +./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" +``` + +Output: + +- `cmake-build-macos-release-mas/strawberry.app` +- `cmake-build-macos-release-mas/strawberry-mas.pkg` + +### Upload + submit for review + +- Upload the `.pkg` using Apple’s **Transporter** app (App Store Connect), or with `iTMSTransporter`. +- In App Store Connect, wait for processing, select the build, then **Submit for Review**. diff --git a/build_tools/macos/README_MAS.md b/build_tools/macos/README_MAS.md new file mode 100644 index 000000000..ef0066d86 --- /dev/null +++ b/build_tools/macos/README_MAS.md @@ -0,0 +1,138 @@ +# Mac App Store (MAS) submission guide (manual steps) + +This repo supports a **Mac App Store build mode** (`BUILD_FOR_MAC_APP_STORE=ON`) and includes scripts to build a signed upload `.pkg`. + +If you’re blocked because `security find-identity` only shows **Developer ID** and not **Apple Distribution / Installer**, follow the steps below. + +--- + +## Open Keychain Access (macOS “hidden” Utilities) + +Any of these work: + +- **Spotlight**: press `⌘ + Space` → type **Keychain Access** → Enter +- **Finder**: Applications → Utilities → **Keychain Access** +- **Terminal**: + +```bash +open -a "Keychain Access" +``` + +--- + +## The core issue: certificate exists but is not a usable identity + +If you see certificates like: + +- `Apple Distribution: ...` +- `3rd Party Mac Developer Installer: ...` + +but `security find-identity` does **not** list them, then the certificate is present but **the private key is missing** (or not paired / in the wrong keychain). + +You can confirm with: + +```bash +./build_tools/macos/check_signing_identities.sh +``` + +--- + +## Step 1 — Create the private keys on this Mac (CSR) + +1. Open **Keychain Access** +2. Menu: **Keychain Access → Certificate Assistant → Request a Certificate From a Certificate Authority…** +3. Fill: + - **User Email Address**: your Apple ID email + - **Common Name**: e.g. `Dry Ark LLC` (any label is fine) + - **CA Email Address**: leave blank + - Select: **Saved to disk** +4. Save the CSR (`.certSigningRequest`) somewhere safe + +This CSR step is what creates the **private key** locally in your login keychain. + +--- + +## Step 2 — Create + download the certificates (Apple Developer portal) + +In Apple Developer → **Certificates, Identifiers & Profiles** → **Certificates** → **+**: + +- Create **Apple Distribution** (use the CSR you just made) +- Create **Mac Installer Distribution** (or “3rd Party Mac Developer Installer”, wording varies) (use a CSR) + +Download the resulting `.cer` files. + +--- + +## Step 3 — Install certificates into your login keychain + +Double-click each downloaded `.cer` to install it. + +Then in **Keychain Access → login → My Certificates**: + +- Find **Apple Distribution: ...** and **expand it** + - You must see a **private key** under it. +- Find **... Installer ...** and expand it + - You must see a **private key** under it. + +If there’s no private key under the certificate, it will not be usable for signing on this Mac. + +--- + +## Step 4 — Verify identities from the CLI + +```bash +security find-identity -p codesigning -v +security find-identity -p basic -v +./build_tools/macos/check_signing_identities.sh +``` + +Expected: + +- `Apple Distribution: ...` shows up under **codesigning** +- `... Installer ...` shows up as an **installer identity** (used to sign upload `.pkg`) + +--- + +## Step 5 — Create + install the provisioning profile (Mac App Store) + +In Apple Developer → **Profiles** → **+**: + +- Platform: **macOS** +- Type: **Mac App Store** +- App ID: `com.dryark.strawberry` (or your own bundle id) +- Select the **Apple Distribution** certificate +- Generate + Download + +Install it by double-clicking it, or place it under: + +`~/Library/MobileDevice/Provisioning Profiles/` + +--- + +## Step 6 — Build the signed upload package (.pkg) + +This repo provides: + +- `build_tools/macos/build_mas_pkg.sh` (build → deploy → embed profile → sign → productbuild) + +Example: + +```bash +./build_tools/macos/build_mas_pkg.sh --run --release --clean \ + --codesign-identity "Apple Distribution: Dry Ark LLC (7628766FL2)" \ + --installer-identity "3rd Party Mac Developer Installer: Dry Ark LLC (7628766FL2)" \ + --provisionprofile "$HOME/Library/MobileDevice/Provisioning Profiles/XXXXXXXX-XXXX-XXXX-XXXX-XXXXXXXXXXXX.provisionprofile" +``` + +Outputs: + +- `cmake-build-macos-release-mas/strawberry.app` +- `cmake-build-macos-release-mas/strawberry-mas.pkg` + +--- + +## Step 7 — Upload + submit for review + +- Upload the `.pkg` using Apple’s **Transporter** app (App Store Connect). +- In App Store Connect, wait for processing, select the build, then **Submit for Review**. + diff --git a/build_tools/macos/build_mas_pkg.sh b/build_tools/macos/build_mas_pkg.sh new file mode 100755 index 000000000..00406657f --- /dev/null +++ b/build_tools/macos/build_mas_pkg.sh @@ -0,0 +1,205 @@ +#!/usr/bin/env bash +set -euo pipefail + +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)" + +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) + +Examples: + ./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="" + +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 ;; + -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" + +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 + +productbuild \ + --component "$app_path" /Applications \ + --sign "$installer_identity" \ + "$pkg_out" + +echo "==> [$(ts)] Verifying pkg signature" +pkgutil --check-signature "$pkg_out" || true + +echo +echo "Done." +echo "App: $app_path" +echo "PKG: $pkg_out" + diff --git a/build_tools/macos/check_signing_identities.sh b/build_tools/macos/check_signing_identities.sh new file mode 100755 index 000000000..323c5a0dd --- /dev/null +++ b/build_tools/macos/check_signing_identities.sh @@ -0,0 +1,146 @@ +#!/usr/bin/env bash +set -euo pipefail + +# macOS signing identity sanity check for: +# - Developer ID (outside Mac App Store) +# - Mac App Store (Apple Distribution + 3rd Party Mac Developer Installer) + +ts() { date +"%H:%M:%S"; } + +if [[ "$(uname -s)" != "Darwin" ]]; then + echo "Error: This script is for macOS only." >&2 + exit 1 +fi + +echo "==> [$(ts)] Strawberry macOS signing identity check" +echo "==> [$(ts)] Host: $(sw_vers -productName 2>/dev/null || true) $(sw_vers -productVersion 2>/dev/null || true)" +echo + +echo "==> [$(ts)] Keychains searched by 'security' (user)" +security list-keychains -d user || true +echo + +echo "==> [$(ts)] Valid code signing identities (must include private key)" +codesigning_out="$(security find-identity -p codesigning -v 2>&1 || true)" +echo "$codesigning_out" +echo + +echo "==> [$(ts)] Valid installer/pkg identities (must include private key)" +basic_out="$(security find-identity -p basic -v 2>&1 || true)" +echo "$basic_out" +echo + +echo "==> [$(ts)] Note" +cat <<'EOF' +- Apple uses multiple certificate types. The "basic" identity list can include certificates that are not usable + for signing a Mac App Store upload package. +- For App Store Connect uploads via .pkg, you typically need an *Installer* identity (e.g. "3rd Party Mac Developer Installer" + or "Mac Installer Distribution") and it must have a private key on this Mac. +EOF +echo + +list_cert_labels() { + local query="$1" + # Extract "labl" lines like: "labl"="Apple Distribution: ..." + security find-certificate -a -c "$query" 2>/dev/null \ + | sed -n 's/.*"labl"="\(.*\)".*/\1/p' \ + | sort -u +} + +check_label_in_identities() { + local label="$1" + local out="$2" + if echo "$out" | grep -Fq "$label"; then + echo "YES" + else + echo "NO" + fi +} + +check_label_in_installer_identities() { + local label="$1" + local out="$2" + # Only treat as installer-capable if the cert label itself is an installer cert. + case "$label" in + *Installer*|*installer*) ;; + *) echo "NO"; return 0 ;; + esac + if echo "$out" | grep -Fq "$label"; then + echo "YES" + else + echo "NO" + fi +} + +print_section() { + local title="$1" + shift + local queries=("$@") + + echo "==> [$(ts)] ${title}" + local any=0 + + local q + for q in "${queries[@]}"; do + local labels + labels="$(list_cert_labels "$q" || true)" + if [[ -z "$labels" ]]; then + continue + fi + any=1 + while IFS= read -r label; do + [[ -z "$label" ]] && continue + local in_codesign in_basic + in_codesign="$(check_label_in_identities "$label" "$codesigning_out")" + in_basic="$(check_label_in_installer_identities "$label" "$basic_out")" + printf -- "- %s\n" "$label" + printf -- " - codesigning identity: %s\n" "$in_codesign" + printf -- " - installer identity: %s\n" "$in_basic" + if [[ "$in_codesign" == "NO" && "$in_basic" == "NO" ]]; then + printf -- " - note: certificate exists, but it is NOT a usable identity on this Mac (almost always missing private key)\n" + fi + done <<<"$labels" + done + + if [[ "$any" -eq 0 ]]; then + echo "(no matching certificates found)" + fi + echo +} + +print_section "Expected for Developer ID (outside Mac App Store)" \ + "Developer ID Application" \ + "Developer ID Installer" + +print_section "Expected for Mac App Store submissions" \ + "Apple Distribution" \ + "Mac App Distribution" \ + "3rd Party Mac Developer Application" \ + "3rd Party Mac Developer Installer" \ + "Mac Installer Distribution" + +echo "==> [$(ts)] Quick interpretation" +cat <<'EOF' +- If a certificate label appears above, but both: + - codesigning identity: NO + - installer identity: NO + then the certificate is present but NOT usable for signing on this Mac. + The most common cause is: the private key is missing. + +Fix: +- Open Keychain Access → login → "My Certificates" +- Expand the certificate. You must see a private key underneath it. +- If there is no private key: + - Recreate the certificate on this Mac via Xcode (Accounts → Manage Certificates), OR + - Import a .p12 that includes the private key from the machine where it was created. +EOF +echo + +echo "==> [$(ts)] Provisioning profiles (Mac App Store builds require one)" +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 +