#!/usr/bin/env bash set -euo pipefail # macOS Production Build Script for the SDL3 Tetris project # Mirrors the Windows PowerShell workflow but uses common POSIX tooling so it # can be executed on macOS runners or local developer machines. PROJECT_NAME="tetris" BUILD_DIR="build-release" OUTPUT_DIR="dist" PACKAGE_DIR="" VERSION="$(date +"%Y.%m.%d")" CLEAN=0 PACKAGE_ONLY=0 print_usage() { cat <<'USAGE' Usage: ./build-production-mac.sh [options] Options: -c, --clean Remove existing build + dist folders before running -p, --package-only Skip compilation and only rebuild the distributable -o, --output DIR Customize output directory (default: dist) -h, --help Show this help text USAGE } log() { local level=$1; shift case "$level" in INFO) printf '\033[36m[INFO]\033[0m %s\n' "$*" ;; OK) printf '\033[32m[ OK ]\033[0m %s\n' "$*" ;; WARN) printf '\033[33m[WARN]\033[0m %s\n' "$*" ;; ERR) printf '\033[31m[ERR ]\033[0m %s\n' "$*" ;; *) printf '%s\n' "$*" ;; esac } require_macos() { if [[ $(uname) != "Darwin" ]]; then log ERR "This script is intended for macOS hosts." exit 1 fi } parse_args() { while [[ $# -gt 0 ]]; do case "$1" in -c|--clean) CLEAN=1; shift ;; -p|--package-only) PACKAGE_ONLY=1; shift ;; -o|--output) if [[ $# -lt 2 ]]; then log ERR "--output requires a directory argument" exit 1 fi OUTPUT_DIR="$2"; shift 2 ;; -h|--help) print_usage; exit 0 ;; *) log ERR "Unknown argument: $1" print_usage exit 1 ;; esac done } configure_paths() { PACKAGE_DIR="${OUTPUT_DIR}/TetrisGame-mac" } clean_previous() { if (( CLEAN )); then log INFO "Cleaning previous build artifacts..." rm -rf "$BUILD_DIR" "$OUTPUT_DIR" log OK "Previous artifacts removed" fi } configure_and_build() { if (( PACKAGE_ONLY )); then return fi log INFO "Configuring CMake (Release)..." cmake -S . -B "$BUILD_DIR" -DCMAKE_BUILD_TYPE=Release log OK "CMake configure complete" log INFO "Building Release target..." cmake --build "$BUILD_DIR" --config Release log OK "Build finished" } resolve_executable() { local candidates=( "$BUILD_DIR/Release/${PROJECT_NAME}" "$BUILD_DIR/${PROJECT_NAME}" "$BUILD_DIR/${PROJECT_NAME}.app/Contents/MacOS/${PROJECT_NAME}" ) for path in "${candidates[@]}"; do if [[ -x "$path" ]]; then EXECUTABLE_PATH="$path" break fi done if [[ -z ${EXECUTABLE_PATH:-} ]]; then log ERR "Unable to locate built executable." exit 1 fi if [[ "$EXECUTABLE_PATH" == *.app/Contents/MacOS/* ]]; then APP_BUNDLE_PATH="${EXECUTABLE_PATH%/Contents/MacOS/*}" else APP_BUNDLE_PATH="" fi log OK "Using executable: $EXECUTABLE_PATH" } prepare_package_dir() { log INFO "Creating package directory $PACKAGE_DIR ..." rm -rf "$PACKAGE_DIR" mkdir -p "$PACKAGE_DIR" } copy_binary_or_bundle() { if [[ -n ${APP_BUNDLE_PATH:-} ]]; then log INFO "Copying app bundle..." rsync -a "$APP_BUNDLE_PATH" "$PACKAGE_DIR/" PACKAGE_BINARY="${APP_BUNDLE_PATH##*/}/Contents/MacOS/${PROJECT_NAME}" else log INFO "Copying executable..." cp "$EXECUTABLE_PATH" "$PACKAGE_DIR/${PROJECT_NAME}" chmod +x "$PACKAGE_DIR/${PROJECT_NAME}" PACKAGE_BINARY="${PROJECT_NAME}" fi log OK "Binary ready (${PACKAGE_BINARY})" } copy_assets() { log INFO "Copying assets..." local folders=("assets" "fonts") for folder in "${folders[@]}"; do if [[ -d "$folder" ]]; then rsync -a "$folder" "$PACKAGE_DIR/" log OK "Copied $folder" fi done if [[ -f "FreeSans.ttf" ]]; then cp "FreeSans.ttf" "$PACKAGE_DIR/" log OK "Copied FreeSans.ttf" fi } copy_dependencies() { log INFO "Collecting dynamic libraries from vcpkg..." local triplets=("arm64-osx" "x64-osx" "universal-osx") local bases=("$BUILD_DIR/vcpkg_installed" "vcpkg_installed") declare -A copied=() for base in "${bases[@]}"; do for triplet in "${triplets[@]}"; do for sub in lib bin; do local dir="$base/$triplet/$sub" if [[ -d "$dir" ]]; then while IFS= read -r -d '' dylib; do local name=$(basename "$dylib") if [[ -z ${copied[$name]:-} ]]; then cp "$dylib" "$PACKAGE_DIR/" copied[$name]=1 log OK "Copied $name" fi done < <(find "$dir" -maxdepth 1 -type f -name '*.dylib' -print0) fi done done done if [[ ${#copied[@]} -eq 0 ]]; then log WARN "No .dylib files found; ensure vcpkg installed macOS triplet dependencies." fi } ensure_rpath() { local binary="$PACKAGE_DIR/$PACKAGE_BINARY" if [[ ! -f "$binary" ]]; then return fi if command -v otool >/dev/null 2>&1 && command -v install_name_tool >/dev/null 2>&1; then if ! otool -l "$binary" | grep -A2 LC_RPATH | grep -q '@executable_path'; then log INFO "Adding @executable_path rpath" install_name_tool -add_rpath "@executable_path" "$binary" fi fi } create_launchers() { local launch_command="./${PROJECT_NAME}" if [[ "$PACKAGE_BINARY" == *.app/Contents/MacOS/* ]]; then local app_dir="${PACKAGE_BINARY%%/Contents/*}" launch_command="open \"./${app_dir}\"" fi { cat <<'CMD' #!/usr/bin/env bash cd "$(dirname "$0")" CMD printf '%s\n' "${launch_command}" } > "$PACKAGE_DIR/Launch-Tetris.command" chmod +x "$PACKAGE_DIR/Launch-Tetris.command" cat > "$PACKAGE_DIR/README-mac.txt" < 0 )); then log WARN "Missing: ${missing[*]}" else log OK "Package looks complete" fi } create_zip() { mkdir -p "$OUTPUT_DIR" local zip_name="TetrisGame-mac-${VERSION}.zip" local zip_path="$OUTPUT_DIR/$zip_name" log INFO "Creating zip archive $zip_path ..." if command -v ditto >/dev/null 2>&1; then ditto -c -k --keepParent "$PACKAGE_DIR" "$zip_path" else (cd "$OUTPUT_DIR" && zip -r "$zip_name" "$(basename "$PACKAGE_DIR")") fi log OK "Zip created" } main() { parse_args "$@" require_macos configure_paths if [[ ! -f "CMakeLists.txt" ]]; then log ERR "Run from repository root (CMakeLists.txt missing)." exit 1 fi log INFO "======================================" log INFO " macOS Production Builder" log INFO "======================================" log INFO "Version: $VERSION" log INFO "Output: $OUTPUT_DIR" clean_previous configure_and_build resolve_executable prepare_package_dir copy_binary_or_bundle copy_assets copy_dependencies ensure_rpath create_launchers validate_package create_zip log INFO "Done. Package available at $PACKAGE_DIR" } main "$@"