#!/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)" current_cmd_pid="" current_hb_pid="" kill_tree() { local pid="$1" [[ -z "${pid}" ]] && return 0 # Recurse into children first (best-effort). local child for child in $(pgrep -P "$pid" 2>/dev/null || true); do kill_tree "$child" done kill -TERM "$pid" >/dev/null 2>&1 || true } cleanup() { # Never fail cleanup on errors. set +e if [[ -n "${current_hb_pid}" ]]; then kill "${current_hb_pid}" >/dev/null 2>&1 || true wait "${current_hb_pid}" >/dev/null 2>&1 || true current_hb_pid="" fi if [[ -n "${current_cmd_pid}" ]]; then # If still running, terminate process tree. kill -0 "${current_cmd_pid}" >/dev/null 2>&1 && kill_tree "${current_cmd_pid}" current_cmd_pid="" fi } trap 'cleanup; exit 130' INT TERM trap 'cleanup' EXIT run_with_heartbeat() { local desc="$1" shift local start now elapsed hb_pid start="$(date +%s)" echo "==> [$(ts)] ${desc}" # Run the command in the background so we can reliably clean it up on Ctrl-C. set +e "$@" & local cmd_pid=$! set -e current_cmd_pid="$cmd_pid" ( while kill -0 "$cmd_pid" >/dev/null 2>&1; do sleep 20 now="$(date +%s)" elapsed="$((now - start))" echo " [$(ts)] ... still working (${elapsed}s elapsed) ..." done ) & hb_pid="$!" current_hb_pid="$hb_pid" set +e wait "$cmd_pid" local rc=$? set -e # Clear globals before stopping heartbeat to avoid cleanup double-kill. current_cmd_pid="" kill "$hb_pid" >/dev/null 2>&1 || true wait "$hb_pid" >/dev/null 2>&1 || true current_hb_pid="" now="$(date +%s)" elapsed="$((now - start))" if [[ $rc -ne 0 ]]; then echo "Error: '${desc}' failed after ${elapsed}s (exit $rc)." >&2 return "$rc" fi echo "==> [$(ts)] Done: ${desc} (${elapsed}s)" } usage() { cat <<'EOF' Usage: ./build_tools/macos/build_app.sh [--debug|--release] [--mas] [--deploy] [--dmg] [--clean] [--build-dir ] What it does: - Configures and builds Strawberry with CMake + Ninja - Optional: runs CMake targets 'deploy' (bundle deps) and 'dmg' (create DMG) Options: --release Release build (default) --debug Debug build --mas Build for Mac App Store (BUILD_FOR_MAC_APP_STORE=ON). Disables Sparkle/QtSparkle and any localhost OAuth redirect listener. --deploy Run: cmake --build --target deploy --dmg Run: cmake --build --target dmg (implies --deploy) --clean Delete the build dir before configuring --build-dir Override build directory (default: /cmake-build-macos-) -h, --help Show help EOF } config="Release" do_mas=0 do_deploy=0 do_dmg=0 do_clean=0 build_dir="" while [[ $# -gt 0 ]]; do case "$1" in --release) config="Release"; shift ;; --debug) config="Debug"; shift ;; --mas) do_mas=1; shift ;; --deploy) do_deploy=1; shift ;; --dmg) do_dmg=1; do_deploy=1; shift ;; --clean) do_clean=1; shift ;; --build-dir) build_dir="${2:-}"; shift 2 ;; -h|--help) usage; exit 0 ;; *) echo "Unknown arg: $1" >&2; usage; exit 2 ;; esac done if [[ "$(uname -s)" != "Darwin" ]]; then echo "Error: This script is for macOS only." >&2 exit 1 fi if ! command -v xcode-select >/dev/null 2>&1 || ! xcode-select -p >/dev/null 2>&1; then echo "Error: Xcode Command Line Tools not found." >&2 echo "Install them first: xcode-select --install" >&2 exit 1 fi if ! command -v brew >/dev/null 2>&1; then echo "Error: Homebrew ('brew') not found in PATH." >&2 echo "Install Homebrew first: https://brew.sh/" >&2 exit 1 fi if ! command -v cmake >/dev/null 2>&1; then echo "Error: cmake not found. Did you run ./build_tools/macos/install_brew_deps.sh ?" >&2 exit 1 fi if ! command -v ninja >/dev/null 2>&1; then echo "Error: ninja not found. Did you run ./build_tools/macos/install_brew_deps.sh ?" >&2 exit 1 fi brew_prefix="$(brew --prefix)" qt_prefix="$(brew --prefix qt)" icu_prefix="$(brew --prefix icu4c || true)" if [[ -z "$build_dir" ]]; then build_dir="${repo_root}/cmake-build-macos-$(lower "$config")" fi echo "==> [$(ts)] Repo: ${repo_root}" echo "==> [$(ts)] Build dir: ${build_dir}" echo "==> [$(ts)] Config: ${config}" if [[ "$do_mas" -eq 1 ]]; then echo "==> [$(ts)] MAS: enabled (BUILD_FOR_MAC_APP_STORE=ON)" fi if [[ "$do_clean" -eq 1 ]]; then echo "==> [$(ts)] Cleaning build dir" # macOS 26+ can apply provenance metadata that blocks deletion even when permissions look normal. # Clear common xattrs and immutable flags before deleting. xattr -dr com.apple.provenance "$build_dir" >/dev/null 2>&1 || true xattr -dr com.apple.quarantine "$build_dir" >/dev/null 2>&1 || true chflags -R nouchg,noschg "$build_dir" >/dev/null 2>&1 || true rm -rf "$build_dir" || { echo "Error: failed to remove build dir: $build_dir" >&2 echo "This is usually due to macOS provenance/flags. Try:" >&2 echo " xattr -dr com.apple.provenance \"$build_dir\"" >&2 echo " chflags -R nouchg,noschg \"$build_dir\"" >&2 echo " rm -rf \"$build_dir\"" >&2 exit 1 } fi mkdir -p "$build_dir" # If you've run a previously-built app directly from the build directory, macOS can apply provenance # metadata that makes the bundle effectively immutable (even when permissions look normal). # That breaks CMake because it needs to update strawberry.app/Contents/Info.plist during configure/build. app_bundle="${build_dir}/strawberry.app" if [[ -d "${app_bundle}/Contents" ]]; then # Try to clear provenance/quarantine metadata first (best effort). xattr -dr com.apple.provenance "${app_bundle}" >/dev/null 2>&1 || true xattr -dr com.apple.quarantine "${app_bundle}" >/dev/null 2>&1 || true # If the bundle is still not writable, remove it so CMake can recreate it. if ! ( : > "${app_bundle}/Contents/.cmake_write_test" ) 2>/dev/null; then echo "==> [$(ts)] Existing ${app_bundle} is not writable (likely macOS provenance). Removing it." rm -rf "${app_bundle}" else rm -f "${app_bundle}/Contents/.cmake_write_test" >/dev/null 2>&1 || true fi fi # Make pkg-config more reliable with Homebrew. export PKG_CONFIG_PATH="${brew_prefix}/lib/pkgconfig:${brew_prefix}/share/pkgconfig:${PKG_CONFIG_PATH:-}" # For dist/CMakeLists.txt Info.plist minimum version logic. export MACOSX_DEPLOYMENT_TARGET="${MACOSX_DEPLOYMENT_TARGET:-12.0}" cmake_prefix_path="${qt_prefix};${brew_prefix}" cmake_extra_args=() # Mac App Store build mode if [[ "$do_mas" -eq 1 ]]; then cmake_extra_args+=("-DBUILD_FOR_MAC_APP_STORE=ON") fi # Optional: override Sparkle update feed / key for your own published builds. # Example: # export SPARKLE_FEED_URL="https://example.com/appcast.xml" # export SPARKLE_PUBLIC_ED25519_KEY="base64==" if [[ -n "${SPARKLE_FEED_URL:-}" ]]; then cmake_extra_args+=("-DSPARKLE_FEED_URL=${SPARKLE_FEED_URL}") fi if [[ -n "${SPARKLE_PUBLIC_ED25519_KEY:-}" ]]; then cmake_extra_args+=("-DSPARKLE_PUBLIC_ED25519_KEY=${SPARKLE_PUBLIC_ED25519_KEY}") fi run_with_heartbeat "Configuring (CMAKE_PREFIX_PATH=${cmake_prefix_path})" \ cmake -S "$repo_root" -B "$build_dir" -G Ninja \ -DCMAKE_BUILD_TYPE="$config" \ -DCMAKE_PREFIX_PATH="$cmake_prefix_path" \ -DCMAKE_FRAMEWORK_PATH="${brew_prefix}/Frameworks;${brew_prefix}/opt/sparkle-framework/Frameworks" \ -DOPTIONAL_COMPONENTS_MISSING_DEPS_ARE_FATAL=OFF \ ${cmake_extra_args+"${cmake_extra_args[@]}"} \ ${icu_prefix:+-DICU_ROOT="$icu_prefix"} run_with_heartbeat "Building" \ cmake --build "$build_dir" --parallel if [[ "$do_deploy" -eq 1 ]]; then echo "==> [$(ts)] Preparing env for 'deploy' target (GIO/GStreamer)" export GIO_EXTRA_MODULES="${brew_prefix}/lib/gio/modules" export GST_PLUGIN_SCANNER="${brew_prefix}/opt/gstreamer/libexec/gstreamer-1.0/gst-plugin-scanner" export GST_PLUGIN_PATH="${brew_prefix}/lib/gstreamer-1.0" # Optional, but helps dist/macos/macgstcopy.sh bundle libsoup which GStreamer loads dynamically. libsoup_prefix="$(brew --prefix libsoup 2>/dev/null || true)" if [[ -n "${libsoup_prefix}" ]]; then libsoup_dylib="$(ls -1 "${libsoup_prefix}"/lib/libsoup-*.dylib 2>/dev/null | head -n 1 || true)" if [[ -n "${libsoup_dylib}" ]]; then export LIBSOUP_LIBRARY_PATH="${libsoup_dylib}" fi fi run_with_heartbeat "Running: deploy" \ cmake --build "$build_dir" --target deploy fi if [[ "$do_dmg" -eq 1 ]]; then run_with_heartbeat "Running: dmg" \ cmake --build "$build_dir" --target dmg fi echo "==> [$(ts)] Done" echo "Built app:" echo " ${build_dir}/strawberry.app"