From f1e5fa4ec3417a96668bb984f5ecd420c437021a Mon Sep 17 00:00:00 2001 From: Adrian Castro <22133246+castdrian@users.noreply.github.com> Date: Wed, 19 Aug 2026 20:07:47 +0200 Subject: [PATCH] =?UTF-8?q?build(mac):=20establish=20L=C3=96VE=2012=20Meta?= =?UTF-8?q?l=20parity?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 58 ++++++++++ .github/workflows/release.yml | 13 ++- Play-Mac.command | 31 +++--- README.md | 5 +- mobile/macos/APPLE_DEPENDENCIES_REF | 1 + mobile/macos/LOVE_SOURCE_REF | 1 + mobile/macos/LOVE_VERSION | 1 + mobile/macos/README.md | 19 ++++ scripts/build.sh | 84 ++++++++++---- scripts/build_love_macos.sh | 151 ++++++++++++++++++++++++++ scripts/run.sh | 26 ++++- scripts/setup.sh | 29 ++++- src/core/SaveData.lua | 2 +- tests/engine/launcher_stray_mount.lua | 2 +- 14 files changed, 372 insertions(+), 51 deletions(-) create mode 100644 mobile/macos/APPLE_DEPENDENCIES_REF create mode 100644 mobile/macos/LOVE_SOURCE_REF create mode 100644 mobile/macos/LOVE_VERSION create mode 100644 mobile/macos/README.md create mode 100755 scripts/build_love_macos.sh diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3bb5b18d..cce07b0a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,64 @@ permissions: contents: read jobs: + macos-changes: + name: detect macOS changes + runs-on: ubuntu-latest + outputs: + changed: ${{ steps.paths.outputs.changed }} + steps: + - uses: actions/checkout@v7 + with: + fetch-depth: 0 + - id: paths + env: + BASE_SHA: ${{ github.event.pull_request.base.sha || github.event.before }} + HEAD_SHA: ${{ github.sha }} + run: | + if [ -z "$BASE_SHA" ] || [ "$BASE_SHA" = "0000000000000000000000000000000000000000" ]; then + echo "changed=true" >> "$GITHUB_OUTPUT" + exit 0 + fi + if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -Eq '^(conf\.lua$|scripts/(build\.sh|build_love_macos\.sh|run\.sh|setup\.sh)$|Play-Mac\.command$|mobile/macos/|src/core/SaveData\.lua$|\.github/workflows/(ci|release)\.yml$)'; then + echo "changed=true" >> "$GITHUB_OUTPUT" + else + echo "changed=false" >> "$GITHUB_OUTPUT" + fi + + macos-build: + name: macOS LÖVE 12 build + needs: macos-changes + if: needs.macos-changes.outputs.changed == 'true' + runs-on: ${{ fromJSON(github.repository == 'bryanthaboi/gen1recomp' && '["self-hosted", "macOS"]' || '"macos-latest"') }} + steps: + - uses: actions/checkout@v7 + - name: Build LÖVE 12 macOS runtime + run: scripts/build_love_macos.sh --fetch + - name: Build and verify macOS app + env: + LOVE_APP: ${{ github.workspace }}/.bazinga/love12/love.app + MAC_STAGE_DIR: ${{ runner.temp }}/gen1recomp-mac-stage + run: | + set -euo pipefail + scripts/build.sh mac --no-notarize --identity - --version 0.0.0 + unzip -tqq 'dist/mac/gen1recomp++-macos.zip' + app="$MAC_STAGE_DIR/gen1recomp++.app" + [ -d "$app" ] + [ -x "$app/Contents/MacOS/gen1recomp++" ] + [ "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "$app/Contents/Info.plist")" = 'gen1recomp++' ] + [ "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleDisplayName' "$app/Contents/Info.plist")" = 'gen1recomp++' ] + [ "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "$app/Contents/Info.plist")" = 'com.theboisclub.gen1recompplusplus' ] + [ "$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$app/Contents/Info.plist")" = 0.0.0 ] + codesign --verify --deep --strict "$app" + otool -L "$app/Contents/Frameworks/love.framework/love" | grep -q '/Metal.framework/' + - name: Upload macOS build + uses: actions/upload-artifact@v7 + with: + name: gen1recomp++-macos + path: dist/mac/gen1recomp++-macos.zip + if-no-files-found: error + retention-days: 7 + ios-changes: name: detect iOS changes runs-on: ubuntu-latest diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index f7384709..34d6b0de 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -344,9 +344,16 @@ jobs: echo "Identities available to codesign:" security find-identity -v -p codesigning "$KEYCHAIN_PATH" + - name: Build LÖVE 12 macOS runtime + run: | + set -euo pipefail + scripts/build_love_macos.sh --fetch + - name: Build macOS + Windows + Linux env: GEN1TLS_DLL: ${{ github.workspace }}/dist/native/win-x64/gen1tls.dll + LOVE_APP: ${{ github.workspace }}/.bazinga/love12/love.app + MAC_STAGE_DIR: ${{ runner.temp }}/gen1recomp-mac-stage run: | set -euo pipefail # Sign in-build (identity auto-detected from the temp keychain); @@ -428,8 +435,8 @@ jobs: set -a; . "$ci_dir/notary.env"; set +a echo "::add-mask::$APPLE_APP_PASSWORD" - app=".bazinga/work/gen1recomp.app" - zip="dist/mac/gen1recomp-macos.zip" + app="$RUNNER_TEMP/gen1recomp-mac-stage/gen1recomp++.app" + zip="dist/mac/gen1recomp++-macos.zip" [ -d "$app" ] || { echo "::error::signed app not found at $app"; exit 1; } if [ -z "${APPLE_ID:-}" ] || [ -z "${APPLE_APP_PASSWORD:-}" ] || [ -z "${APPLE_TEAM_ID:-}" ]; then echo "::error::notary.env is missing APPLE_ID / APPLE_APP_PASSWORD / APPLE_TEAM_ID." @@ -474,7 +481,7 @@ jobs: outdir="dist/release" rm -rf "$outdir" mkdir -p "$outdir" - cp "dist/mac/gen1recomp-macos.zip" "$outdir/gen1recomp-${v}-macos.zip" + cp "dist/mac/gen1recomp++-macos.zip" "$outdir/gen1recomp++-${v}-macos.zip" cp "dist/win/gen1recomp-win64.zip" "$outdir/gen1recomp-${v}-windows.zip" cp "dist/linux/gen1recomp-linux.zip" "$outdir/gen1recomp-${v}-linux.zip" diff --git a/Play-Mac.command b/Play-Mac.command index 89c79ff1..8bae3121 100755 --- a/Play-Mac.command +++ b/Play-Mac.command @@ -27,9 +27,15 @@ ask() { # ask "question" -> yes by default printf '\n \033[1mPokémon Red - LÖVE2D port\033[0m\n\n' have_love() { - command -v love >/dev/null 2>&1 && return 0 - [ -x "/Applications/love.app/Contents/MacOS/love" ] && return 0 - [ -x "$HOME/Applications/love.app/Contents/MacOS/love" ] && return 0 + local app version + for app in ".bazinga/love12/love.app" "/Applications/love12.app" "$HOME/Applications/love12.app" \ + "/Applications/love.app" "$HOME/Applications/love.app"; do + [ -x "$app/Contents/MacOS/love" ] || continue + version="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$app/Contents/Info.plist" 2>/dev/null || true)" + printf '%s' "$version" | grep -Eq '^12(\.|$)' || continue + otool -L "$app/Contents/Frameworks/love.framework/love" 2>/dev/null \ + | grep -q '/Metal.framework/' && return 0 + done return 1 } @@ -57,22 +63,11 @@ if ! command -v python3 >/dev/null 2>&1; then fi fi -# ----------------------------------------------------------------- Homebrew -if ! have_love && ! command -v brew >/dev/null 2>&1 \ - && [ ! -x /opt/homebrew/bin/brew ] && [ ! -x /usr/local/bin/brew ]; then - warn "LÖVE (the game engine) is not installed; the easiest installer is Homebrew" - if ask "Install Homebrew now? (asks for your macOS password)"; then - /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" \ - || { err "Homebrew install failed"; pause_exit 1; } - else - warn "OK, download LÖVE 11.x yourself from https://love2d.org," - warn "drop love.app into /Applications, then run this again." - pause_exit 1 - fi +if ! have_love && ! command -v xcodebuild >/dev/null 2>&1; then + warn "LÖVE 12 is not installed and Xcode is required to build it for macOS." + warn "Install Xcode from the App Store, launch it once, then run this again." + pause_exit 1 fi -# make brew visible in THIS shell (fresh installs aren't on PATH yet) -[ -x /opt/homebrew/bin/brew ] && eval "$(/opt/homebrew/bin/brew shellenv)" -[ -x /usr/local/bin/brew ] && eval "$(/usr/local/bin/brew shellenv)" # ------------------------------------------------------------------- build echo diff --git a/README.md b/README.md index c7640938..bcb2f048 100644 --- a/README.md +++ b/README.md @@ -194,8 +194,9 @@ By default the game keeps your save, options, and the private ROM-derived data cache in your OS's normal per-user app data folder. To keep everything next to the game instead (handy for a USB stick or portable drive you carry between computers), drop an empty file named `portable.txt` next to the app -(next to `gen1recomp.app`/`.exe`, or next to `main.lua`/`conf.lua` when -running from source), then launch the game. Portable mode is desktop-only +(next to `gen1recomp++.app` on macOS or `gen1recomp.exe` on Windows, or next +to `main.lua`/`conf.lua` when running from source), then launch the game. +Portable mode is desktop-only (Windows, Linux, macOS); it has no effect on Android or iOS, where the app runs from a read-only package. diff --git a/mobile/macos/APPLE_DEPENDENCIES_REF b/mobile/macos/APPLE_DEPENDENCIES_REF new file mode 100644 index 00000000..e6f9dd11 --- /dev/null +++ b/mobile/macos/APPLE_DEPENDENCIES_REF @@ -0,0 +1 @@ +a5522634f1e581a1ebab73bf3ab4bd7a853b7a3e diff --git a/mobile/macos/LOVE_SOURCE_REF b/mobile/macos/LOVE_SOURCE_REF new file mode 100644 index 00000000..79521d4b --- /dev/null +++ b/mobile/macos/LOVE_SOURCE_REF @@ -0,0 +1 @@ +97d4465e8c81099f79696ea4b4bb8b1f9083bc1a diff --git a/mobile/macos/LOVE_VERSION b/mobile/macos/LOVE_VERSION new file mode 100644 index 00000000..d7213f31 --- /dev/null +++ b/mobile/macos/LOVE_VERSION @@ -0,0 +1 @@ +12.0 diff --git a/mobile/macos/README.md b/mobile/macos/README.md new file mode 100644 index 00000000..cc90f6df --- /dev/null +++ b/mobile/macos/README.md @@ -0,0 +1,19 @@ +# macOS build + +The desktop app uses the pinned LÖVE 12 runtime built from the LÖVE source +tree and the matching Apple dependency repository. The runtime enables Metal +and is fused into the branded `gen1recomp++.app` bundle. + +Build the runtime and desktop app from the repository root: + +```bash +scripts/build_love_macos.sh --fetch +LOVE_APP="$PWD/.bazinga/love12/love.app" scripts/build.sh mac --no-notarize --identity - +``` + +The source and dependency revisions are recorded in `LOVE_SOURCE_REF` and +`APPLE_DEPENDENCIES_REF`. Delete `.bazinga/love12/source` or use `--clean` +when changing those pins. + +The packaged executable is `gen1recomp++` inside `gen1recomp++.app`, and the +bundle declares LÖVE 12.0 compatibility. diff --git a/scripts/build.sh b/scripts/build.sh index c05de791..37d0dd7b 100755 --- a/scripts/build.sh +++ b/scripts/build.sh @@ -8,7 +8,7 @@ # [--notary-profile NAME] [--no-notarize] # [--release] # ios only: release config instead of debug # -# Output: dist/mac/gen1recomp-macos.zip +# Output: dist/mac/gen1recomp++-macos.zip # dist/win/gen1recomp-win64.zip # dist/linux/gen1recomp-linux.zip (fused x86_64 AppImage) # dist/android/debug/*.apk (full gradle output stays under @@ -27,14 +27,18 @@ ENTITLEMENTS="$ROOT/scripts/macos-entitlements.plist" APP_NAME="gen1recomp" BUNDLE_ID="com.theboisclub.pokemonred" +MAC_APP_NAME="gen1recomp++" +MAC_BUNDLE_ID="com.theboisclub.gen1recompplusplus" LOVE_VERSION="11.5" -LOVE_MAC_VERSION="12.0" +LOVE_MAC_VERSION="$(tr -d '[:space:]' < "$ROOT/mobile/macos/LOVE_VERSION" 2>/dev/null || echo 12.0)" +LOVE_MAC_APP="$HERE/love12/love.app" VERSION="$(git -C "$ROOT" rev-parse --short HEAD 2>/dev/null || echo dev)" VERSION_EXPLICIT=false IDENTITY="" TARGET="all" NOTARY_PROFILE="notary-profile" NOTARIZE=true +FETCH_MAC_RUNTIME=false IOS_RELEASE=false IOS_IPA=false @@ -42,6 +46,22 @@ say() { printf '\033[1;32m==>\033[0m %s\n' "$*"; } warn() { printf '\033[1;33mwarn:\033[0m %s\n' "$*" >&2; } fail() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; } +strip_bundle_metadata() { + local bundle="$1" + xattr -rc "$bundle" + xattr -rd com.apple.FinderInfo "$bundle" 2>/dev/null || true + xattr -rd 'com.apple.fileprovider.fpfs#P' "$bundle" 2>/dev/null || true +} + +valid_love12_app() { + local app="$1" version + [ -x "$app/Contents/MacOS/love" ] || return 1 + version="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$app/Contents/Info.plist" 2>/dev/null || true)" + printf '%s' "$version" | grep -Eq '^12(\.|$)' || return 1 + otool -L "$app/Contents/Frameworks/love.framework/love" 2>/dev/null \ + | grep -q '/Metal.framework/' +} + while [ $# -gt 0 ]; do case "$1" in mac|win|linux|android|ios|all) TARGET="$1" ;; @@ -49,6 +69,7 @@ while [ $# -gt 0 ]; do --identity) IDENTITY="$2"; shift ;; --notary-profile) NOTARY_PROFILE="$2"; shift ;; --no-notarize) NOTARIZE=false ;; + --fetch) FETCH_MAC_RUNTIME=true ;; --release) IOS_RELEASE=true ;; --ipa) IOS_IPA=true ;; *) fail "unknown argument: $1" ;; @@ -151,8 +172,21 @@ make_ico() { # $1 = output .ico path # --------------------------------------------------------------- macOS build_mac() { say "building macOS app" - local love_app="${LOVE_APP:-/Applications/love12.app}" - [ -d "$love_app" ] || fail "LÖVE.app not found at $love_app (install it or set LOVE_APP=/path/to/love.app)" + local love_app="${LOVE_APP:-}" + if [ -z "$love_app" ]; then + for candidate in "$LOVE_MAC_APP" "/Applications/love12.app" "$HOME/Applications/love12.app" \ + "/Applications/love.app" "$HOME/Applications/love.app"; do + if [ -d "$candidate" ] && valid_love12_app "$candidate"; then + love_app="$candidate" + break + fi + done + fi + if [ ! -d "$love_app" ] && [ -z "${LOVE_APP:-}" ] && $FETCH_MAC_RUNTIME; then + "$ROOT/scripts/build_love_macos.sh" --fetch + love_app="$LOVE_MAC_APP" + fi + [ -d "$love_app" ] || fail "LÖVE 12 Metal app not found; run scripts/build_love_macos.sh --fetch or set LOVE_APP=/path/to/love.app" local love_version love_version=$(/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$love_app/Contents/Info.plist" 2>/dev/null || true) printf '%s' "$love_version" | grep -Eq '^12(\.|$)' \ @@ -163,22 +197,30 @@ build_mac() { | grep -q '/Metal.framework/' \ || fail "macOS build requires a LÖVE runtime linked to Metal at $love_app" - local stage_dir - stage_dir="$(mktemp -d /tmp/gen1recomp-mac.XXXXXX)" - local out_app="$stage_dir/$APP_NAME.app" + local stage_dir="${MAC_STAGE_DIR:-${RUNNER_TEMP:-/tmp}/gen1recomp-mac-stage}" + local out_app="$stage_dir/$MAC_APP_NAME.app" + mkdir -p "$stage_dir" + rm -rf "$out_app" ditto --norsrc "$love_app" "$out_app" + local love_executable="$out_app/Contents/MacOS/love" + local app_executable="$out_app/Contents/MacOS/$MAC_APP_NAME" + [ -f "$love_executable" ] || fail "LÖVE app is missing Contents/MacOS/love" + mv "$love_executable" "$app_executable" + # drop any bundled placeholder .love and fuse ours in find "$out_app/Contents/Resources" -maxdepth 1 -name '*.love' -delete cp "$LOVE_FILE" "$out_app/Contents/Resources/game.love" local plist="$out_app/Contents/Info.plist" - /usr/libexec/PlistBuddy -c "Set :CFBundleName $APP_NAME" "$plist" 2>/dev/null \ - || /usr/libexec/PlistBuddy -c "Add :CFBundleName string $APP_NAME" "$plist" - /usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName $APP_NAME" "$plist" 2>/dev/null \ - || /usr/libexec/PlistBuddy -c "Add :CFBundleDisplayName string $APP_NAME" "$plist" - /usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier $BUNDLE_ID" "$plist" 2>/dev/null \ - || /usr/libexec/PlistBuddy -c "Add :CFBundleIdentifier string $BUNDLE_ID" "$plist" + /usr/libexec/PlistBuddy -c "Set :CFBundleName $MAC_APP_NAME" "$plist" 2>/dev/null \ + || /usr/libexec/PlistBuddy -c "Add :CFBundleName string $MAC_APP_NAME" "$plist" + /usr/libexec/PlistBuddy -c "Set :CFBundleDisplayName $MAC_APP_NAME" "$plist" 2>/dev/null \ + || /usr/libexec/PlistBuddy -c "Add :CFBundleDisplayName string $MAC_APP_NAME" "$plist" + /usr/libexec/PlistBuddy -c "Set :CFBundleExecutable $MAC_APP_NAME" "$plist" 2>/dev/null \ + || /usr/libexec/PlistBuddy -c "Add :CFBundleExecutable string $MAC_APP_NAME" "$plist" + /usr/libexec/PlistBuddy -c "Set :CFBundleIdentifier $MAC_BUNDLE_ID" "$plist" 2>/dev/null \ + || /usr/libexec/PlistBuddy -c "Add :CFBundleIdentifier string $MAC_BUNDLE_ID" "$plist" /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $VERSION" "$plist" 2>/dev/null \ || /usr/libexec/PlistBuddy -c "Add :CFBundleShortVersionString string $VERSION" "$plist" /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $VERSION" "$plist" 2>/dev/null \ @@ -200,7 +242,10 @@ build_mac() { /usr/libexec/PlistBuddy -c "Set :CFBundleIconFile OS X AppIcon" "$plist" 2>/dev/null \ || /usr/libexec/PlistBuddy -c "Add :CFBundleIconFile string 'OS X AppIcon'" "$plist" - xattr -rc "$out_app" + strip_bundle_metadata "$out_app" + + rm -rf "$WORK/$MAC_APP_NAME.app" + ln -s "$out_app" "$WORK/$MAC_APP_NAME.app" local id="$IDENTITY" if [ -z "$id" ]; then @@ -223,9 +268,9 @@ build_mac() { warn "keychain profile '$NOTARY_PROFILE' not found/working, skipping notarization." warn "set it up with: xcrun notarytool store-credentials \"$NOTARY_PROFILE\" --apple-id ... --team-id ... --password ..." else - local notarize_zip="$stage_dir/$APP_NAME-notarize.zip" + local notarize_zip="$stage_dir/$MAC_APP_NAME-notarize.zip" rm -f "$notarize_zip" - (cd "$stage_dir" && ditto -c -k --keepParent "$APP_NAME.app" "$notarize_zip") + (cd "$stage_dir" && ditto -c -k --keepParent "$MAC_APP_NAME.app" "$notarize_zip") say "submitting to Apple notary service (this can take a few minutes)" xcrun notarytool submit "$notarize_zip" --keychain-profile "$NOTARY_PROFILE" --wait say "stapling notarization ticket" @@ -234,12 +279,11 @@ build_mac() { fi fi - xattr -rc "$out_app" + strip_bundle_metadata "$out_app" - local zip_out="$DIST/mac/$APP_NAME-macos.zip" + local zip_out="$DIST/mac/$MAC_APP_NAME-macos.zip" rm -f "$zip_out" - (cd "$stage_dir" && ditto -c -k --norsrc --keepParent "$APP_NAME.app" "$zip_out") - rm -rf "$stage_dir" + (cd "$stage_dir" && ditto -c -k --norsrc --keepParent "$MAC_APP_NAME.app" "$zip_out") say "macOS build: $zip_out" } diff --git a/scripts/build_love_macos.sh b/scripts/build_love_macos.sh new file mode 100755 index 00000000..b48c6d8f --- /dev/null +++ b/scripts/build_love_macos.sh @@ -0,0 +1,151 @@ +#!/usr/bin/env bash + +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +MACOS_DIR="$ROOT/mobile/macos" +CACHE="$ROOT/.bazinga/love12" +SOURCE_DIR="${LOVE_SOURCE_DIR:-$CACHE/source}" +RUNTIME_APP="${LOVE_APP_OUTPUT:-$CACHE/love.app}" +BUILD_DIR="$CACHE/build" +LOVE_VERSION="$(tr -d '[:space:]' < "$MACOS_DIR/LOVE_VERSION")" +LOVE_SOURCE_REF="$(tr -d '[:space:]' < "$MACOS_DIR/LOVE_SOURCE_REF")" +APPLE_DEPENDENCIES_REF="$(tr -d '[:space:]' < "$MACOS_DIR/APPLE_DEPENDENCIES_REF")" +LOVE_SOURCE_REPO="${LOVE_SOURCE_REPO:-https://github.com/love2d/love.git}" +APPLE_DEPENDENCIES_REPO="${APPLE_DEPENDENCIES_REPO:-https://github.com/love2d/love-apple-dependencies.git}" + +FETCH=false +CLEAN=false + +say() { printf '\033[1;32m==>\033[0m %s\n' "$*"; } +fail() { printf '\033[1;31merror:\033[0m %s\n' "$*" >&2; exit 1; } + +strip_bundle_metadata() { + local bundle="$1" + xattr -rc "$bundle" + xattr -rd com.apple.FinderInfo "$bundle" 2>/dev/null || true + xattr -rd 'com.apple.fileprovider.fpfs#P' "$bundle" 2>/dev/null || true +} + +while [ $# -gt 0 ]; do + case "$1" in + --fetch) FETCH=true ;; + --clean) CLEAN=true ;; + -h|--help) + printf '%s\n' 'usage: scripts/build_love_macos.sh [--fetch] [--clean]' + exit 0 + ;; + *) fail "unknown argument: $1" ;; + esac + shift +done + +[ "$(uname -s)" = "Darwin" ] || fail "macOS LÖVE builds require Darwin" +command -v git >/dev/null 2>&1 || fail "git is required to fetch LÖVE sources" +command -v xcodebuild >/dev/null 2>&1 || fail "xcodebuild is required to build LÖVE for macOS" +command -v xattr >/dev/null 2>&1 || fail "xattr is required to normalize the runtime bundle" + +source_ready() { + [ -d "$SOURCE_DIR/platform/xcode/love.xcodeproj" ] \ + && [ -d "$SOURCE_DIR/platform/xcode/macosx/Frameworks/Lua.framework" ] \ + && [ -d "$SOURCE_DIR/platform/xcode/shared/Frameworks/SDL3.xcframework" ] +} + +source_is_pinned() { + [ "$(git -C "$SOURCE_DIR" rev-parse HEAD 2>/dev/null || true)" = "$LOVE_SOURCE_REF" ] \ + && [ -f "$SOURCE_DIR/.gen1recomp-apple-dependencies-ref" ] \ + && [ "$(tr -d '[:space:]' < "$SOURCE_DIR/.gen1recomp-apple-dependencies-ref")" = "$APPLE_DEPENDENCIES_REF" ] +} + +fetch_repo() { + local repo_url="$1" + local ref="$2" + local destination="$3" + mkdir -p "$destination" + git -C "$destination" init -q + git -C "$destination" remote add origin "$repo_url" + git -C "$destination" fetch --depth 1 origin "$ref" + git -C "$destination" checkout --detach -q FETCH_HEAD +} + +fetch_sources() { + local tmp + tmp="$(mktemp -d "$CACHE/fetch.XXXXXX")" + say "fetching LÖVE source $LOVE_SOURCE_REF" + fetch_repo "$LOVE_SOURCE_REPO" "$LOVE_SOURCE_REF" "$tmp/love" + say "fetching Apple dependencies $APPLE_DEPENDENCIES_REF" + fetch_repo "$APPLE_DEPENDENCIES_REPO" "$APPLE_DEPENDENCIES_REF" "$tmp/dependencies" + mkdir -p "$tmp/love/platform/xcode/macosx/Frameworks" "$tmp/love/platform/xcode/shared" + cp -R "$tmp/dependencies/macOS/Frameworks/." "$tmp/love/platform/xcode/macosx/Frameworks/" + cp -R "$tmp/dependencies/shared/." "$tmp/love/platform/xcode/shared/" + rm -rf "$SOURCE_DIR" + mkdir -p "$(dirname "$SOURCE_DIR")" + mv "$tmp/love" "$SOURCE_DIR" + printf '%s\n' "$APPLE_DEPENDENCIES_REF" > "$SOURCE_DIR/.gen1recomp-apple-dependencies-ref" + rm -rf "$tmp" + say "LÖVE source ready at $SOURCE_DIR" +} + +if $CLEAN; then + [ -z "${LOVE_SOURCE_DIR:-}" ] \ + || fail "--clean cannot be used with LOVE_SOURCE_DIR" + rm -rf "$SOURCE_DIR" "$BUILD_DIR" "$RUNTIME_APP" +fi + +if ! source_ready || ! source_is_pinned; then + if ! $FETCH; then + fail "pinned LÖVE 12 sources are missing at $SOURCE_DIR; run scripts/build_love_macos.sh --fetch" + fi + [ -z "${LOVE_SOURCE_DIR:-}" ] \ + || fail "LOVE_SOURCE_DIR is not the pinned LÖVE commit $LOVE_SOURCE_REF" + fetch_sources +fi + +PROJECT="$SOURCE_DIR/platform/xcode/love.xcodeproj" +rm -rf "$BUILD_DIR" "$RUNTIME_APP" +mkdir -p "$BUILD_DIR" +say "building LÖVE 12 macOS runtime" +xcodebuild \ + -quiet \ + -project "$PROJECT" \ + -target love-macosx \ + -configuration Release \ + -sdk macosx \ + SYMROOT="$BUILD_DIR" \ + OBJROOT="$BUILD_DIR/Intermediates" \ + ARCHS="arm64 x86_64" \ + ONLY_ACTIVE_ARCH=NO \ + MACOSX_DEPLOYMENT_TARGET=12.0 \ + CODE_SIGNING_ALLOWED=NO \ + CODE_SIGNING_REQUIRED=NO \ + CODE_SIGN_IDENTITY=- + +BUILT_APP="$BUILD_DIR/Release/love.app" +[ -d "$BUILT_APP" ] || fail "xcodebuild produced no LÖVE app at $BUILT_APP" +ditto --norsrc "$BUILT_APP" "$RUNTIME_APP" +strip_bundle_metadata "$RUNTIME_APP" + +version="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$RUNTIME_APP/Contents/Info.plist" 2>/dev/null || true)" +version_re="$(printf '%s' "$LOVE_VERSION" | sed 's/\./\\./g')" +printf '%s' "$version" | grep -Eq "^${version_re}(\.|$)" \ + || fail "built runtime reports LÖVE version '$version'" +[ -x "$RUNTIME_APP/Contents/MacOS/love" ] \ + || fail "built runtime is missing Contents/MacOS/love" +[ -f "$RUNTIME_APP/Contents/Frameworks/love.framework/love" ] \ + || fail "built runtime is missing love.framework" +otool -L "$RUNTIME_APP/Contents/Frameworks/love.framework/love" \ + | grep -q '/Metal.framework/' \ + || fail "built LÖVE runtime is not linked to Metal" + +archs="$(lipo -archs "$RUNTIME_APP/Contents/MacOS/love")" +printf '%s' "$archs" | grep -qw arm64 \ + || fail "built LÖVE app is missing arm64" +printf '%s' "$archs" | grep -qw x86_64 \ + || fail "built LÖVE app is missing x86_64" +framework_archs="$(lipo -archs "$RUNTIME_APP/Contents/Frameworks/love.framework/love")" +printf '%s' "$framework_archs" | grep -qw arm64 \ + || fail "built LÖVE framework is missing arm64" +printf '%s' "$framework_archs" | grep -qw x86_64 \ + || fail "built LÖVE framework is missing x86_64" + +say "LÖVE 12 macOS runtime: $RUNTIME_APP" diff --git a/scripts/run.sh b/scripts/run.sh index 49276927..880b2623 100755 --- a/scripts/run.sh +++ b/scripts/run.sh @@ -32,8 +32,30 @@ find_love() { return 1 } -LOVE_BIN="$(find_love)" \ - || fail "LÖVE not found, run scripts/setup.sh (or install from https://love2d.org)" +find_love12() { + local app version + for app in "${LOVE_APP:-}" "$ROOT/.bazinga/love12/love.app" "/Applications/love12.app" "$HOME/Applications/love12.app" \ + "/Applications/love.app" "$HOME/Applications/love.app"; do + [ -n "$app" ] || continue + if [ -x "$app/Contents/MacOS/love" ]; then + version="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$app/Contents/Info.plist" 2>/dev/null || true)" + if printf '%s' "$version" | grep -Eq '^12(\.|$)' \ + && otool -L "$app/Contents/Frameworks/love.framework/love" 2>/dev/null | grep -q '/Metal.framework/'; then + echo "$app/Contents/MacOS/love" + return + fi + fi + done + return 1 +} + +if [ "$(uname -s)" = "Darwin" ]; then + LOVE_BIN="$(find_love12)" \ + || fail "LÖVE 12 with Metal not found, run scripts/setup.sh or scripts/build_love_macos.sh --fetch" +else + LOVE_BIN="$(find_love)" \ + || fail "LÖVE not found, run scripts/setup.sh (or install from https://love2d.org)" +fi # SDL2 on Wayland crashes during desktop drag-and-drop in certain compositors; # default to X11/XWayland when available to ensure rock-solid drag-drop stability. diff --git a/scripts/setup.sh b/scripts/setup.sh index c1251b1d..94a208e1 100755 --- a/scripts/setup.sh +++ b/scripts/setup.sh @@ -70,11 +70,32 @@ find_love() { return 1 } -if LOVE_BIN="$(find_love)"; then +valid_love12_app() { + local app="$1" version + [ -x "$app/Contents/MacOS/love" ] || return 1 + version="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' "$app/Contents/Info.plist" 2>/dev/null || true)" + printf '%s' "$version" | grep -Eq '^12(\.|$)' || return 1 + otool -L "$app/Contents/Frameworks/love.framework/love" 2>/dev/null \ + | grep -q '/Metal.framework/' +} + +if [ "$(uname -s)" = "Darwin" ]; then + if valid_love12_app "$ROOT/.bazinga/love12/love.app"; then + say "LÖVE 12 found: $ROOT/.bazinga/love12/love.app" + elif valid_love12_app "/Applications/love12.app"; then + say "LÖVE 12 found: /Applications/love12.app" + elif valid_love12_app "$HOME/Applications/love12.app"; then + say "LÖVE 12 found: $HOME/Applications/love12.app" + elif valid_love12_app "/Applications/love.app"; then + say "LÖVE 12 found: /Applications/love.app" + elif valid_love12_app "$HOME/Applications/love.app"; then + say "LÖVE 12 found: $HOME/Applications/love.app" + else + say "building LÖVE 12 for macOS" + "$ROOT/scripts/build_love_macos.sh" --fetch + fi +elif LOVE_BIN="$(find_love)"; then say "LÖVE found: $LOVE_BIN" -elif [ "$(uname -s)" = "Darwin" ] && command -v brew >/dev/null 2>&1; then - say "installing LÖVE via Homebrew" - brew install --cask love else fail "LÖVE 11.x is not installed; install it from https://love2d.org" fi diff --git a/src/core/SaveData.lua b/src/core/SaveData.lua index e3d977f5..c0b0872f 100644 --- a/src/core/SaveData.lua +++ b/src/core/SaveData.lua @@ -140,7 +140,7 @@ function SaveData.gameFolders() local src = love.filesystem.getSource and love.filesystem.getSource() local sbd = love.filesystem.getSourceBaseDirectory and love.filesystem.getSourceBaseDirectory() - -- A packaged macOS build nests the game inside gen1recomp.app/Contents/ + -- A packaged macOS build nests the game inside gen1recomp++.app/Contents/ -- Resources, so getSource()/getSourceBaseDirectory() point INSIDE the -- bundle -- not where the player drops portable.txt (next to the .app). -- Recover the folder containing the .app so a packaged app finds its diff --git a/tests/engine/launcher_stray_mount.lua b/tests/engine/launcher_stray_mount.lua index de6d65bc..eccb8a94 100644 --- a/tests/engine/launcher_stray_mount.lua +++ b/tests/engine/launcher_stray_mount.lua @@ -15,7 +15,7 @@ love = love or require("tests.love_stub") -- scripts/build.sh mac layout: the game is an archive inside the .app, the -- player's portable folder is the one holding the .app. local PORTABLE = "/Users/p/Games" -local APP = PORTABLE .. "/gen1recomp.app" +local APP = PORTABLE .. "/gen1recomp++.app" local SOURCE = APP .. "/Contents/Resources/game.love" local BASE = APP .. "/Contents/MacOS"