Files
spacetris/build-production-mac.sh
2025-11-30 14:32:52 +01:00

314 lines
8.8 KiB
Bash

#!/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
PACKAGE_RUNTIME_DIR=""
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/"
local app_name="${APP_BUNDLE_PATH##*/}"
PACKAGE_BINARY="${app_name}/Contents/MacOS/${PROJECT_NAME}"
PACKAGE_RUNTIME_DIR="$PACKAGE_DIR/${app_name}/Contents/MacOS"
else
log INFO "Copying executable..."
cp "$EXECUTABLE_PATH" "$PACKAGE_DIR/${PROJECT_NAME}"
chmod +x "$PACKAGE_DIR/${PROJECT_NAME}"
PACKAGE_BINARY="${PROJECT_NAME}"
PACKAGE_RUNTIME_DIR="$PACKAGE_DIR"
fi
log OK "Binary ready (${PACKAGE_BINARY})"
}
copy_assets() {
if [[ -n ${APP_BUNDLE_PATH:-} ]]; then
log INFO "Assets already bundled inside the .app; skipping external copy."
return
fi
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")
local copied_names=()
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")
local seen=0
for existing in "${copied_names[@]}"; do
if [[ "$existing" == "$name" ]]; then
seen=1
break
fi
done
if (( !seen )); then
cp "$dylib" "$PACKAGE_RUNTIME_DIR/"
copied_names+=("$name")
log OK "Copied $name"
fi
done < <(find "$dir" -maxdepth 1 -type f -name '*.dylib' -print0)
fi
done
done
done
if [[ ${#copied_names[@]} -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 > "$PACKAGE_DIR/Launch-Tetris.command" <<EOF
#!/usr/bin/env bash
cd "\$(dirname \"\$0\")"
${launch_command}
EOF
chmod +x "$PACKAGE_DIR/Launch-Tetris.command"
cat > "$PACKAGE_DIR/README-mac.txt" <<EOF
Tetris SDL3 Game - macOS Release $VERSION
=========================================
Requirements:
- macOS 12 Monterey or newer (Apple Silicon or Intel)
- GPU with Metal support
Installation:
1. Unzip the archive anywhere (e.g., ~/Games/Tetris).
2. Ensure the executable bit stays set (script already does this).
3. Double-click Launch-Tetris.command or run the binary/app manually from Terminal or Finder.
Files:
- tetris / Launch-Tetris.command (start the game)
- assets/ (art, audio, fonts)
- *.dylib from SDL3 and related dependencies
- FreeSans.ttf font
Enjoy!
EOF
log OK "Created README and launcher"
}
validate_package() {
log INFO "Validating package contents..."
local missing=()
local required=("$PACKAGE_BINARY")
if [[ -z ${APP_BUNDLE_PATH:-} ]]; then
required+=(assets FreeSans.ttf)
fi
for required in "${required[@]}"; do
if [[ ! -e "$PACKAGE_DIR/$required" ]]; then
missing+=("$required")
fi
done
if (( ${#missing[@]} > 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 "$@"