From 4a1c165295ab1d082e17d9a57f661761f990e328 Mon Sep 17 00:00:00 2001 From: David Helkowski Date: Thu, 22 Jan 2026 15:53:09 +0900 Subject: [PATCH] Update .gitignore to exclude build output directories while tracking build tooling scripts in /build_tools/ --- .gitignore | 6 + build_tools/README.md | 66 +++++++++ build_tools/macos/build_app.sh | 155 ++++++++++++++++++++ build_tools/macos/build_sign_notarize.sh | 172 +++++++++++++++++++++++ build_tools/macos/install_brew_deps.sh | 119 ++++++++++++++++ 5 files changed, 518 insertions(+) create mode 100644 build_tools/README.md create mode 100755 build_tools/macos/build_app.sh create mode 100755 build_tools/macos/build_sign_notarize.sh create mode 100755 build_tools/macos/install_brew_deps.sh diff --git a/.gitignore b/.gitignore index e2cb7544a..ca2c45072 100644 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,9 @@ /dist/scripts/maketarball.sh /debian/changelog _codeql_detected_source_root + +# Build output (keep build tooling scripts in /build_tools/ tracked) +/cmake-build*/ +/build*/ +!/build_tools/ +!/build_tools/** diff --git a/build_tools/README.md b/build_tools/README.md new file mode 100644 index 000000000..b8ce50f1b --- /dev/null +++ b/build_tools/README.md @@ -0,0 +1,66 @@ +# Build helper scripts + +This `build_tools/` directory contains **helper scripts and notes** for building Strawberry. + +- It is **not** intended to be your CMake build output directory. +- Recommended CMake build output directories: `cmake-build/`, `build-release/`, etc. + +## macOS + +- Install dependencies via Homebrew: + +```bash +./build_tools/macos/install_brew_deps.sh +``` + +- Build Strawberry: + +```bash +./build_tools/macos/build_app.sh --release +open ./cmake-build-macos-release/strawberry.app +``` + +## macOS signing + notarization (Developer ID distribution) + +This repo includes `build_tools/macos/build_sign_notarize.sh` to automate: + +- build → (optional deploy) → codesign → notarize → staple → verify + +### One-time setup (Apple Developer) + +- **Install certificates**: + - In the Apple Developer portal, create (or download) a **Developer ID Application** certificate. + - Install it into your login keychain (Xcode can manage this via **Xcode → Settings → Accounts**). + +- **Provisioning profiles**: + - For **Developer ID distribution (outside the Mac App Store)**, you typically **do not need a provisioning profile**. + - You *do* need profiles if you are building a **Mac App Store**-signed app (not what this repo’s scripts target). + +- **Notarization credentials**: + - Create a `notarytool` keychain profile (recommended) so you don’t have to pass secrets on the command line: + +```bash +# NOTE: is a positional argument (not a flag). +# Pick any name you want, e.g. "strawberry-notary". +xcrun notarytool store-credentials "" \ + --apple-id "" \ + --team-id "" \ + --password "" +``` + +### Listing what’s installed locally + +Run with no args to list local signing identities + notarytool profiles: + +```bash +./build_tools/macos/build_sign_notarize.sh +``` + +### Build + sign + notarize + +```bash +./build_tools/macos/build_sign_notarize.sh --run --release --clean --deploy \ + --identity "Developer ID Application: Your Name (TEAMID)" \ + --notary-profile "" +``` + diff --git a/build_tools/macos/build_app.sh b/build_tools/macos/build_app.sh new file mode 100755 index 000000000..fde58a336 --- /dev/null +++ b/build_tools/macos/build_app.sh @@ -0,0 +1,155 @@ +#!/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_app.sh [--debug|--release] [--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 + --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_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 ;; + --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_clean" -eq 1 ]]; then + echo "==> [$(ts)] Cleaning build dir" + rm -rf "$build_dir" +fi + +mkdir -p "$build_dir" + +# 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}" + +echo "==> [$(ts)] Configuring (CMAKE_PREFIX_PATH=${cmake_prefix_path})" +cmake_extra_args=() + +# 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 + +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"} + +echo "==> [$(ts)] 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 + + echo "==> [$(ts)] Running: deploy" + cmake --build "$build_dir" --target deploy +fi + +if [[ "$do_dmg" -eq 1 ]]; then + echo "==> [$(ts)] Running: dmg" + cmake --build "$build_dir" --target dmg +fi + +echo "==> [$(ts)] Done" +echo "Built app:" +echo " ${build_dir}/strawberry.app" + diff --git a/build_tools/macos/build_sign_notarize.sh b/build_tools/macos/build_sign_notarize.sh new file mode 100755 index 000000000..bb0d13914 --- /dev/null +++ b/build_tools/macos/build_sign_notarize.sh @@ -0,0 +1,172 @@ +#!/usr/bin/env bash +set -euo pipefail + +ts() { date +"%H:%M:%S"; } + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd -- "${script_dir}/../.." && pwd)" + +usage() { + cat <<'EOF' +Usage: + ./build_tools/macos/build_sign_notarize.sh # list local signing identities + notary profiles + ./build_tools/macos/build_sign_notarize.sh --run [options] # build, sign, notarize, staple + +Common options: + --run Perform build/sign/notarize (otherwise list identities/profiles) + --release | --debug Build config (default: Release) + --clean Clean build dir before build + --deploy Run CMake 'deploy' target before signing (recommended for distributing) + --build-dir Override build directory + +Signing options: + --identity "" Codesign identity (e.g. "Developer ID Application: Your Name (TEAMID)") + --entitlements Optional entitlements plist for codesign + +Notarization options (recommended): + --notary-profile notarytool keychain profile name (created via `xcrun notarytool store-credentials ...`) + --skip-notarize Skip notarization + +Outputs: + - Signed app: /strawberry.app + - Zip for notarization: /strawberry-notarize.zip + +Notes: + - This script is intended for Developer ID distribution (outside Mac App Store). + - If you want Sparkle updates, you'll typically ship a notarized .zip + an appcast feed. +EOF +} + +list_identities_and_profiles() { + echo "==> [$(ts)] macOS code signing identities (Keychain)" + security find-identity -p codesigning -v || true + + echo + echo "==> [$(ts)] notarytool profiles (keychain profiles)" + xcrun notarytool list-profiles 2>/dev/null || echo "(none; create one with: xcrun notarytool store-credentials ...)" + + echo + echo "==> [$(ts)] Provisioning profiles (macOS)" + 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 +} + +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 + +do_run=0 +config="Release" +do_clean=0 +do_deploy=0 +build_dir="" +identity="" +entitlements="" +notary_profile="" +skip_notarize=0 + +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 ;; + --deploy) do_deploy=1; shift ;; + --build-dir) build_dir="${2:-}"; shift 2 ;; + --identity) identity="${2:-}"; shift 2 ;; + --entitlements) entitlements="${2:-}"; shift 2 ;; + --notary-profile) notary_profile="${2:-}"; shift 2 ;; + --skip-notarize) skip_notarize=1; shift ;; + -h|--help) usage; exit 0 ;; + *) echo "Unknown arg: $1" >&2; usage; exit 2 ;; + esac +done + +if [[ "$do_run" -eq 0 ]]; then + usage + echo + list_identities_and_profiles + exit 0 +fi + +if [[ -z "$build_dir" ]]; then + lc_config="$(echo "$config" | tr '[:upper:]' '[:lower:]')" + build_dir="${repo_root}/cmake-build-macos-${lc_config}" +fi + +app_path="${build_dir}/strawberry.app" +bin_path="${app_path}/Contents/MacOS/strawberry" +zip_path="${build_dir}/strawberry-notarize.zip" + +if [[ -z "$identity" ]]; then + echo "Error: Missing --identity (Developer ID Application identity)." >&2 + exit 2 +fi + +if [[ "$skip_notarize" -eq 0 && -z "$notary_profile" ]]; then + echo "Error: Missing --notary-profile (or pass --skip-notarize)." >&2 + exit 2 +fi + +echo "==> [$(ts)] Building Strawberry" +build_args=( "--release" ) +if [[ "$config" == "Debug" ]]; then build_args=( "--debug" ); fi +if [[ "$do_clean" -eq 1 ]]; then build_args+=( "--clean" ); fi +if [[ -n "$build_dir" ]]; then build_args+=( "--build-dir" "$build_dir" ); fi +if [[ "$do_deploy" -eq 1 ]]; then build_args+=( "--deploy" ); fi + +"${repo_root}/build_tools/macos/build_app.sh" "${build_args[@]}" + +if [[ ! -x "$bin_path" ]]; then + echo "Error: built app not found at: $app_path" >&2 + exit 1 +fi + +echo "==> [$(ts)] Codesigning (hardened runtime)" +codesign_args=( --force --timestamp --options runtime --sign "$identity" ) +if [[ -n "$entitlements" ]]; then + codesign_args+=( --entitlements "$entitlements" ) +fi + +find "$app_path" -type f \( -name "*.dylib" -o -name "*.so" -o -perm -111 \) -print0 | while IFS= read -r -d '' f; do + codesign "${codesign_args[@]}" "$f" >/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)] Creating zip for notarization" +rm -f "$zip_path" +ditto -c -k --sequesterRsrc --keepParent "$app_path" "$zip_path" + +if [[ "$skip_notarize" -eq 0 ]]; then + echo "==> [$(ts)] Notarizing" + xcrun notarytool submit "$zip_path" --keychain-profile "$notary_profile" --wait + + echo "==> [$(ts)] Stapling" + xcrun stapler staple "$app_path" +fi + +echo "==> [$(ts)] Gatekeeper assessment" +spctl -a -vv --type execute "$app_path" || true + +echo +echo "Done." +echo "App: $app_path" +echo "Zip: $zip_path" + diff --git a/build_tools/macos/install_brew_deps.sh b/build_tools/macos/install_brew_deps.sh new file mode 100755 index 000000000..504f05308 --- /dev/null +++ b/build_tools/macos/install_brew_deps.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +set -euo pipefail + +script_dir="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" +repo_root="$(cd -- "${script_dir}/../.." && pwd)" + +ts() { date +"%H:%M:%S"; } + +run_with_heartbeat() { + local desc="$1" + shift + + local start now elapsed hb_pid + start="$(date +%s)" + + echo "==> [$(ts)] ${desc}" + + # Heartbeat: print elapsed time periodically in case the underlying command is quiet + ( + while true; do + sleep 20 + now="$(date +%s)" + elapsed="$((now - start))" + echo " [$(ts)] ... still working (${elapsed}s elapsed) ..." + done + ) & + hb_pid="$!" + + set +e + "$@" + local rc=$? + set -e + + kill "$hb_pid" >/dev/null 2>&1 || true + wait "$hb_pid" >/dev/null 2>&1 || true + + 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)" +} + +if [[ "$(uname -s)" != "Darwin" ]]; then + echo "Error: This script is for macOS only." >&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 + +# Homebrew taps are git clones; local formula changes must be committed to be visible. +if command -v git >/dev/null 2>&1 && git -C "$repo_root" rev-parse --is-inside-work-tree >/dev/null 2>&1; then + if git -C "$repo_root" status --porcelain Formula/ | grep -q .; then + echo "Error: You have uncommitted changes under Formula/." >&2 + echo "Homebrew taps are git clones, so uncommitted formulae won't be visible to 'brew tap'." >&2 + echo "Commit your changes, then re-run this script." >&2 + exit 1 + fi +fi + +# Optional: disable auto-update for faster, more predictable runs. +export HOMEBREW_NO_AUTO_UPDATE="${HOMEBREW_NO_AUTO_UPDATE:-1}" + +cd "$repo_root" + +echo "==> [$(ts)] Using repo: $repo_root" + +# Strawberry includes local Homebrew formulae under Formula/. +# Homebrew requires formulae to be in a tap; we tap this repo via file:// and then +# update the tap clone to the latest commit (without untapping, since Homebrew may +# refuse to untap when formulae from this tap are installed). +run_with_heartbeat "Ensuring local tap exists: strawberry/local" bash -lc \ + "brew tap | grep -q '^strawberry/local$' || brew tap strawberry/local 'file://$repo_root' >/dev/null" + +run_with_heartbeat "Refreshing strawberry/local tap clone" bash -lc ' + tap_repo="$(brew --repo strawberry/local)" + cd "$tap_repo" + # Make sure the remote points at the current local repo path. + git remote set-url origin "file://'"$repo_root"'" + git fetch -q origin + default_ref="$(git symbolic-ref -q --short refs/remotes/origin/HEAD || true)" + if [ -z "$default_ref" ]; then + default_ref="origin/master" + fi + git reset --hard -q "$default_ref" +' + +for f in kdsingleapplication-qt6 qtsparkle-qt6 sparkle-framework libgpod; do + if ! brew info "strawberry/local/${f}" >/dev/null 2>&1; then + echo "Error: Missing formula strawberry/local/${f} in the tapped repo." >&2 + echo "If you recently added/changed formulae, ensure they are committed, then refresh the tap:" >&2 + echo " git -C \"$(brew --repo strawberry/local)\" pull --ff-only" >&2 + exit 1 + fi +done + +run_with_heartbeat "Installing dependencies from Brewfile" \ + brew bundle install --file "$repo_root/Brewfile" --verbose + +cat <