diff --git a/build-production-mac.sh b/build-production-mac.sh new file mode 100644 index 0000000..cce2aea --- /dev/null +++ b/build-production-mac.sh @@ -0,0 +1,296 @@ +#!/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 "$@"