Files
quarrel07 aa6c9c10ca macOS: package the game as a proper SpaghettiKart.app bundle (#724)
* macOS: package the game as a proper SpaghettiKart.app bundle

Turns the bare Spaghettify executable into a self-contained SpaghettiKart.app:

- cmake/macos/apple_bundle.cmake: bundle metadata (using the existing
  Info.plist, which already described a bundle), app icon, runtime resources
  (config.yml, yamls, meta, spaghetti.o2r) into Contents/Resources, dylib
  relinking for local Homebrew builds (no-op with static vcpkg deps), and
  ad-hoc codesigning so the app launches without 'damaged app' warnings
- App icon: on Xcode 26+ the Icon Composer package (SpaghettiKartIcon.icon,
  drawn from the existing HM ship-kart icon.png art) compiles with actool
  into an Assets.car, giving the native Liquid Glass look on macOS 26+, plus
  a flattened icns for older systems; older Xcode falls back to the previous
  sips/iconutil flat icon. Supersedes the CreateOSXIcons target.
- Game.cpp: default SHIP_HOME to ~/Library/Application Support/SpaghettiKart
  on macOS so first-run extraction, saves, config, and mods land there
  instead of scattering into the user's home folder; a user-set SHIP_HOME
  still wins
- Info.plist: NSHighResolutionCapable for proper Retina rendering
- CI: mac jobs now ship the .app (ditto zip so the executable bit survives
  artifact upload)
- the yamls-next-to-exe copy is scoped to non-Darwin; on macOS those files
  live in Contents/Resources instead

* macOS: ship prebuilt icon artifacts instead of compiling them at build time

Commits the compiled Assets.car (Liquid Glass icon for macOS 26+) and the
flattened SpaghettiKart.icns, both built from the in-repo Icon Composer
package. The build now just copies them into the bundle, which removes the
actool machinery and the Xcode 26 requirement entirely: every build on any
machine, including CI, gets the real icon.

Also adds a staleness guard: the artifacts carry a hash stamp of the
icon.png they were made from, and if icon.png ever changes without them
being regenerated, the build automatically generates a flat icns from the
current icon.png instead, so updating the logo never requires any macOS
work and an outdated icon can never ship. Regeneration steps are documented
in apple_bundle.cmake.

* Rename the icon layer to ship-kart-layer.png

Group 2.png was the design tool's default layer name; name it for what it
is. Verified the Icon Composer package still compiles with actool after the
rename; the committed Assets.car is unaffected (compiled, self-contained).

---------

Co-authored-by: MegaMech <MegaMech@users.noreply.github.com>
2026-08-01 19:35:51 -06:00

167 lines
9.5 KiB
CMake

# macOS .app bundle assembly for SpaghettiKart.
#
# Turns the plain executable into SpaghettiKart.app (MACOSX_BUNDLE), bundles the
# runtime resources the game and the first-run ROM extractor need into
# Contents/Resources, relinks any non-system dylibs into Contents/Frameworks,
# and ad-hoc codesigns the result so it launches without "damaged app" warnings.
#
# Resource layout the runtime expects on macOS:
# * Ship::Context::GetAppBundlePath() -> <App>.app/Contents/Resources (read-only)
# - spaghetti.o2r : packed port assets (GenerateO2R target)
# - config.yml, yamls/, meta/: asset definitions + mods.toml read by the
# first-run ROM extractor (GameExtractor)
# * Ship::Context::GetAppDirectoryPath() -> SHIP_HOME
# (~/Library/Application Support/SpaghettiKart, defaulted in Game.cpp)
# - mk64.o2r is extracted here on first run from the user's ROM, alongside
# config/saves/mods, instead of scattering into the home folder
set(MACOS_DIR ${CMAKE_SOURCE_DIR}/cmake/macos)
set(ENTITLEMENTS_FILE ${MACOS_DIR}/entitlements.plist)
option(SPAGHETTI_BUNDLE_DEPS "Relink and bundle dylibs into the .app so it is portable" ON)
# ---------------------------------------------------------------------------
# Bundle metadata. OUTPUT_NAME "SpaghettiKart" makes the bundle SpaghettiKart.app
# with Contents/MacOS/SpaghettiKart, matching CFBundleExecutable in Info.plist
# (the build target itself stays named "Spaghettify").
# ---------------------------------------------------------------------------
set_target_properties(${PROJECT_NAME} PROPERTIES
OUTPUT_NAME "SpaghettiKart"
MACOSX_BUNDLE TRUE
MACOSX_BUNDLE_INFO_PLIST ${CMAKE_SOURCE_DIR}/Info.plist
XCODE_ATTRIBUTE_CODE_SIGN_IDENTITY "-"
XCODE_ATTRIBUTE_CODE_SIGN_ENTITLEMENTS ${ENTITLEMENTS_FILE}
)
# ---------------------------------------------------------------------------
# App icon.
#
# The repo ships the compiled icon artifacts (same approach as other ports that
# ship a prebuilt Assets.car), so no special Xcode is needed at build time:
# - Assets.car : compiled asset catalog with the layered Liquid Glass
# icon, used on macOS 26+ via CFBundleIconName
# ("SpaghettiKartIcon").
# - SpaghettiKart.icns : flattened icon for macOS versions before 26, via
# CFBundleIconFile.
# Both are built from cmake/macos/SpaghettiKartIcon.icon (Icon Composer package;
# the ship-kart art from icon.png with the subject separated from the background
# so the system can render the glass depth).
#
# Staleness guard: if icon.png ever changes without the icon artifacts being
# regenerated, the build detects it via the source hash stamp and falls back to
# generating a flat icns from the CURRENT icon.png with sips/iconutil, so an
# outdated icon can never ship and updating the logo requires no macOS work.
#
# To regenerate after a logo change (needs a Mac with Xcode 26+):
# 1. Recreate the cut-out art layer and update SpaghettiKartIcon.icon in
# Apple's Icon Composer.
# 2. xcrun actool cmake/macos/SpaghettiKartIcon.icon --compile /tmp/iconout \
# --app-icon SpaghettiKartIcon --output-partial-info-plist /tmp/iconout/p.plist \
# --platform macosx --target-device mac --minimum-deployment-target 11.0
# 3. cp /tmp/iconout/Assets.car cmake/macos/Assets.car
# cp /tmp/iconout/SpaghettiKartIcon.icns cmake/macos/SpaghettiKart.icns
# 4. shasum -a 256 icon.png > cmake/macos/SpaghettiKartIcon.icon.source-sha256
# ---------------------------------------------------------------------------
set(ICON_SOURCE_STAMP ${MACOS_DIR}/SpaghettiKartIcon.icon.source-sha256)
set(ICON_PREBUILT_CAR ${MACOS_DIR}/Assets.car)
set(ICON_PREBUILT_ICNS ${MACOS_DIR}/SpaghettiKart.icns)
set(ICNS_FILE ${CMAKE_BINARY_DIR}/macosx/SpaghettiKart.icns)
file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/macosx)
set(ICON_SOURCE_FRESH FALSE)
if(EXISTS ${ICON_SOURCE_STAMP} AND EXISTS ${ICON_PREBUILT_CAR} AND EXISTS ${ICON_PREBUILT_ICNS})
file(SHA256 ${CMAKE_SOURCE_DIR}/icon.png ICON_PNG_HASH)
file(READ ${ICON_SOURCE_STAMP} ICON_STAMP_HASH)
string(STRIP "${ICON_STAMP_HASH}" ICON_STAMP_HASH)
if(ICON_PNG_HASH STREQUAL ICON_STAMP_HASH)
set(ICON_SOURCE_FRESH TRUE)
else()
message(STATUS "App icon: icon.png changed since the icon artifacts were made; using a flat icon from the current icon.png")
endif()
endif()
if(ICON_SOURCE_FRESH)
message(STATUS "App icon: prebuilt Liquid Glass catalog + flattened icns fallback")
file(COPY_FILE ${ICON_PREBUILT_ICNS} ${ICNS_FILE})
file(COPY_FILE ${ICON_PREBUILT_CAR} ${CMAKE_BINARY_DIR}/macosx/Assets.car)
set(ICON_BUNDLE_FILES ${CMAKE_BINARY_DIR}/macosx/Assets.car ${ICNS_FILE})
set(SPAGHETTI_ICON_HAS_GLASS TRUE)
else()
set(ICON_SRC ${CMAKE_SOURCE_DIR}/icon.png)
set(ICONSET_DIR ${CMAKE_BINARY_DIR}/macosx/SpaghettiKart.iconset)
file(MAKE_DIRECTORY ${ICONSET_DIR})
foreach(SPEC "16;icon_16x16" "32;icon_16x16@2x" "32;icon_32x32" "64;icon_32x32@2x"
"128;icon_128x128" "256;icon_128x128@2x" "256;icon_256x256"
"512;icon_256x256@2x" "512;icon_512x512" "1024;icon_512x512@2x")
list(GET SPEC 0 SZ)
list(GET SPEC 1 NAME)
execute_process(COMMAND sips -z ${SZ} ${SZ} ${ICON_SRC} --out ${ICONSET_DIR}/${NAME}.png OUTPUT_QUIET ERROR_QUIET)
endforeach()
execute_process(COMMAND iconutil -c icns -o ${ICNS_FILE} ${ICONSET_DIR})
set(ICON_BUNDLE_FILES ${ICNS_FILE})
set(SPAGHETTI_ICON_HAS_GLASS FALSE)
endif()
set_source_files_properties(${ICON_BUNDLE_FILES} PROPERTIES GENERATED TRUE MACOSX_PACKAGE_LOCATION "Resources")
target_sources(${PROJECT_NAME} PRIVATE ${ICON_BUNDLE_FILES})
if(NOT SPAGHETTI_ICON_HAS_GLASS)
# No asset catalog in this build: remove CFBundleIconName so macOS 26+
# falls back to CFBundleIconFile instead of showing a generic icon.
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND plutil -remove CFBundleIconName "$<TARGET_BUNDLE_DIR:${PROJECT_NAME}>/Contents/Info.plist" || true
VERBATIM
)
endif()
# Ensure the packed port assets (spaghetti.o2r) are generated before the app
# links, so the POST_BUILD step below has them to copy into the bundle.
# (mk64.o2r is created at runtime from the user's ROM into SHIP_HOME, so it is
# intentionally not a build dependency.)
add_dependencies(${PROJECT_NAME} GenerateO2R)
# ---------------------------------------------------------------------------
# Copy runtime resources into Contents/Resources after the app links.
# ---------------------------------------------------------------------------
set(RES_DIR "$<TARGET_BUNDLE_DIR:${PROJECT_NAME}>/Contents/Resources")
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND} -E make_directory "${RES_DIR}"
COMMAND ${CMAKE_COMMAND} -E copy_if_different "${CMAKE_SOURCE_DIR}/config.yml" "${RES_DIR}/config.yml"
COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_SOURCE_DIR}/yamls" "${RES_DIR}/yamls"
COMMAND ${CMAKE_COMMAND} -E copy_directory "${CMAKE_SOURCE_DIR}/meta" "${RES_DIR}/meta"
# spaghetti.o2r is produced by the GenerateO2R target; copy if present (CI
# builds it in a separate job and inserts it into the bundle afterwards).
COMMAND bash -c "[ -f '${CMAKE_BINARY_DIR}/spaghetti.o2r' ] && cp '${CMAKE_BINARY_DIR}/spaghetti.o2r' '${RES_DIR}/spaghetti.o2r' || ([ -f '${CMAKE_SOURCE_DIR}/spaghetti.o2r' ] && cp '${CMAKE_SOURCE_DIR}/spaghetti.o2r' '${RES_DIR}/spaghetti.o2r' || echo 'note: spaghetti.o2r not found - build the GenerateO2R target, then rebuild')"
COMMENT "Bundling SpaghettiKart resources into the .app"
VERBATIM
)
# ---------------------------------------------------------------------------
# Relink dylibs into Contents/Frameworks (portable .app) and codesign.
# With static vcpkg dependencies (CI) this is a near no-op; it matters for
# local builds against Homebrew/MacPorts dylibs.
# ---------------------------------------------------------------------------
if (SPAGHETTI_BUNDLE_DEPS)
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND ${CMAKE_COMMAND}
-DAPP_BUNDLE=$<TARGET_BUNDLE_DIR:${PROJECT_NAME}>
-DEXECUTABLE_NAME=SpaghettiKart
-P ${MACOS_DIR}/fixup_bundle.cmake
COMMAND bash -c "install_name_tool -add_rpath '@executable_path/../Frameworks/' '$<TARGET_BUNDLE_DIR:${PROJECT_NAME}>/Contents/MacOS/SpaghettiKart' 2>/dev/null || true"
# Homebrew's "sdl2" is sdl2-compat, a shim that dlopen()s libSDL3.dylib
# from @loader_path at runtime. fixup_bundle can't follow a dlopen, so if
# a dynamic libSDL2 was bundled, copy SDL3 in next to it (= @loader_path)
# or the app aborts with "Failed loading SDL3 library." SDL3 itself only
# links system frameworks. Static SDL2 builds (vcpkg) skip this entirely.
COMMAND bash -c "FRAMEWORKS='$<TARGET_BUNDLE_DIR:${PROJECT_NAME}>/Contents/Frameworks'; if ls \"$FRAMEWORKS\"/libSDL2*.dylib >/dev/null 2>&1; then SDL3_LIB=$(brew --prefix sdl3 2>/dev/null)/lib/libSDL3.0.dylib; if [ -f \"$SDL3_LIB\" ]; then cp \"$SDL3_LIB\" \"$FRAMEWORKS/libSDL3.dylib\" && chmod u+w \"$FRAMEWORKS/libSDL3.dylib\"; else echo 'warning: bundled libSDL2 is sdl2-compat but Homebrew sdl3 was not found'; fi; fi"
COMMENT "Relinking dylibs into the .app bundle"
VERBATIM
)
endif()
add_custom_command(TARGET ${PROJECT_NAME} POST_BUILD
COMMAND codesign --force --deep --sign - --options runtime --entitlements ${ENTITLEMENTS_FILE} "$<TARGET_BUNDLE_DIR:${PROJECT_NAME}>"
COMMENT "Ad-hoc codesigning SpaghettiKart.app"
VERBATIM
)