278 lines
8.6 KiB
Bash
Executable File
278 lines
8.6 KiB
Bash
Executable File
#!/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 <path>]
|
|
|
|
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 <builddir> --target deploy
|
|
--dmg Run: cmake --build <builddir> --target dmg (implies --deploy)
|
|
--clean Delete the build dir before configuring
|
|
--build-dir Override build directory (default: <repo>/cmake-build-macos-<config>)
|
|
-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"
|
|
|