diff --git a/.github/workflows/build-macos.yml b/.github/workflows/build-macos.yml new file mode 100644 index 0000000000..4db9e500c9 --- /dev/null +++ b/.github/workflows/build-macos.yml @@ -0,0 +1,112 @@ +name: Build macOS arm64 + +# Native Apple Silicon (arm64) build. Runs on the GitHub-hosted macOS runners, +# which are arm64 from macos-14 onwards. Only fires when files that affect the +# macOS build change, so doc-only pushes don't burn runner time. +on: + push: + branches: [master] + paths: + - 'src/**' + - 'res/**' + - 'macos.mk' + - 'version.mk' + - 'tools/build_macos_deps.sh' + - 'tools/make_macos_app.sh' + - '.github/workflows/build-macos.yml' + pull_request: + paths: + - 'src/**' + - 'res/**' + - 'macos.mk' + - 'version.mk' + - 'tools/build_macos_deps.sh' + - 'tools/make_macos_app.sh' + - '.github/workflows/build-macos.yml' + workflow_dispatch: + +jobs: + build-macos: + name: "Build macOS arm64" + runs-on: macos-15 + + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install Homebrew dependencies + run: | + set -eux + brew install \ + pkg-config \ + sdl2 sdl2_image sdl2_mixer sdl2_net \ + ffmpeg \ + luajit \ + openal-soft \ + libspng \ + minizip \ + miniupnpc \ + libnatpmp \ + zlib \ + curl \ + dylibbundler + + # The source-only deps (astronomy/centijson/enet6) are cloned from pinned + # upstreams and compiled to static libs. Cache them keyed on the build + # script so they only rebuild when that script changes. + - name: Cache source-only dependencies + id: deps-cache + uses: actions/cache@v4 + with: + path: | + deps/astronomy + deps/centijson + deps/enet6 + key: macos-deps-${{ hashFiles('tools/build_macos_deps.sh') }} + + - name: Build source-only dependencies (astronomy, centijson, enet6) + if: steps.deps-cache.outputs.cache-hit != 'true' + run: | + set -eux + ./tools/build_macos_deps.sh + + - name: Build + run: | + set -eux + VER_MAJOR=$(grep -E '^VER_MAJOR=' version.mk | cut -d= -f2) + VER_MINOR=$(grep -E '^VER_MINOR=' version.mk | cut -d= -f2) + VER_RELEASE=$(grep -E '^VER_RELEASE=' version.mk | cut -d= -f2) + BUILD_NUMBER=$(git rev-list --count HEAD) + VER_SUFFIX=CI_$(git rev-parse --short=7 HEAD) + ARTIFACT_NAME=keeperfx-macos_arm64-${VER_MAJOR}_${VER_MINOR}_${VER_RELEASE}_${BUILD_NUMBER}_${VER_SUFFIX} + echo "ARTIFACT_NAME=$ARTIFACT_NAME" >> "$GITHUB_ENV" + make -f macos.mk BUILD_NUMBER="$BUILD_NUMBER" VER_SUFFIX="$VER_SUFFIX" -j"$(sysctl -n hw.ncpu)" + + - name: Verify binary is a native arm64 Mach-O + run: | + set -eux + file bin/keeperfx + lipo -archs bin/keeperfx | grep -qw arm64 + + - name: Package self-contained KeeperFX.app + run: | + set -eux + ./tools/make_macos_app.sh + # Fail if any Homebrew path leaked into the bundle (i.e. it isn't + # actually self-contained). + ! grep -rl "/opt/homebrew" dist/KeeperFX.app/Contents/libs 2>/dev/null + otool -L dist/KeeperFX.app/Contents/MacOS/keeperfx | grep -q "@executable_path/../libs" + codesign --verify --verbose=2 dist/KeeperFX.app + + - name: Upload engine binary + uses: actions/upload-artifact@v4 + with: + name: ${{ env.ARTIFACT_NAME }} + path: bin/keeperfx + + - name: Upload KeeperFX.app + uses: actions/upload-artifact@v4 + with: + name: ${{ env.ARTIFACT_NAME }}-app + path: dist/KeeperFX.app diff --git a/.gitignore b/.gitignore index 05402f6ff6..5cb38d3ed6 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ /.local /bin +/dist +/game /obj /sdl /pkg diff --git a/README.md b/README.md index 7f49ea3ea2..37b51767d5 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,7 @@ Currently the whole codebase of Dungeon Keeper is remade and all code has been r ## Features - Windows 7/10/11 support +- Native Linux and macOS (Apple Silicon / arm64) builds - Higher screen resolutions - Increased FPS, decoupled gfx and game logic - Improved and modernized controls @@ -45,6 +46,64 @@ You will need the original Dungeon Keeper files, either from an old CD or from t or [Steam](https://store.steampowered.com/app/1996630/Dungeon_Keeper_Gold/). +## macOS (Apple Silicon) + +This fork adds a **native Apple Silicon (arm64) build** — a real Mach-O binary, +no Rosetta or emulation. It compiles, runs, and plays. See +[`docs/MACOS_ARM64_PORT.md`](docs/MACOS_ARM64_PORT.md) for the full write-up. + +### Build from source + +```sh +brew install pkg-config sdl2 sdl2_image sdl2_mixer sdl2_net ffmpeg luajit \ + openal-soft libspng minizip miniupnpc libnatpmp zlib curl dylibbundler +./tools/build_macos_deps.sh # one-time: builds astronomy/centijson/enet6 +make -f macos.mk -j"$(sysctl -n hw.ncpu)" # -> bin/keeperfx (arm64 Mach-O) +``` + +CI builds and verifies this on every push (`.github/workflows/build-macos.yml`), +and uploads a ready-to-run `KeeperFX.app` as a build artifact. + +### Package a self-contained `KeeperFX.app` + +```sh +tools/make_macos_app.sh # -> dist/KeeperFX.app +``` + +This bundles the engine's libraries (via `dylibbundler`, incl. the SDL3 that +`sdl2-compat` loads at runtime) and ad-hoc signs it, so the `.app` runs on any +Apple Silicon Mac with **no Homebrew installed**. + +### Running: required game files & layout + +Like every KeeperFX build, the macOS build needs a game directory populated with +the KeeperFX data **plus the original Dungeon Keeper files** listed in +[`docs/files_required_from_original_dk.txt`](docs/files_required_from_original_dk.txt). +The install gate is simply the presence of `data/bluepal.dat`. + +`KeeperFX.app` is a *drop-in* engine: on startup it locates itself and changes +the working directory to the folder that contains the `.app`, so it finds the +game data sitting next to it. The layout is: + +``` +YourKeeperFX/ <- any folder (including Desktop/Documents) +├── KeeperFX.app <- drop the app in here, next to the data +├── data/ sound/ ldata/ fxdata/ creatrs/ campgns/ levels/ music/ ... +└── keeperfx.cfg +``` + +Double-click `KeeperFX.app`. On first launch: +- If Gatekeeper blocks it (ad-hoc signed, not notarized), right-click → Open once. +- If the folder is privacy-protected (Desktop, Documents, Downloads), macOS asks + to let KeeperFX access files there — click **Allow**. It then runs from any + location. + +> Tip: if you have GOG's *Dungeon Keeper Gold* installed, the required original +> files ship **uncompressed** inside the app bundle at +> `Contents/Resources/game/{DATA,SOUND}/` (and the `keeper0*.ogg` soundtrack in +> its game root) — no CD-image extraction needed. Copy them in with lowercase names. + + ## Development To get started with KeeperFX development, refer to the [Development Guide](https://github.com/dkfans/keeperfx/wiki/Building-KeeperFX) for detailed instructions on setting up a development environment and building KeeperFX from source. diff --git a/docs/MACOS_ARM64_PORT.md b/docs/MACOS_ARM64_PORT.md new file mode 100644 index 0000000000..7657f7ea4e --- /dev/null +++ b/docs/MACOS_ARM64_PORT.md @@ -0,0 +1,140 @@ +# Native macOS (Apple Silicon / arm64) build + +KeeperFX builds and runs **natively on Apple Silicon** — a Mach-O arm64 binary, +no Rosetta, no emulation. It has been verified end to end: it compiles, launches, +loads the original Dungeon Keeper data, and runs into live gameplay. + +The macOS build reuses the native **Linux** build (`linux.mk`), which already does +almost all of the cross-platform work; `macos.mk` is the Linux build adapted for +arm64 + Homebrew. + +## Why this is tractable + +The hard part of porting KeeperFX (the historic hooking into the original 32-bit +`DK.exe` at fixed addresses) is **gone** — the engine is fully decompiled and +standalone. Concretely: + +- **64-bit clean.** The Linux build compiles with `-march=x86-64`; there is no + 32-bit lock-in. (Critical, since macOS has had zero 32-bit support since + Catalina.) +- **SDL2 is the platform layer** for video, input, audio and networking. The + renderer is software/surface based — no OpenGL or DirectX in the hot path. +- The platform-specific spots are already guarded: `src/bflib_cpu.c` (`cpuid` + asm behind `__i386__ || __x86_64__`), `src/bflib_crash.c` (POSIX path with a + non-Linux fallback), `src/linux.cpp` (pure POSIX, reused as-is), and every + `#include ` sits behind `#ifdef _WIN32` / `__MINGW32__`. + +## Source changes that *were* needed + +Compiling on arm64 did require a few small edits to shared headers (they also +help a native Linux build and change no behaviour on Windows): + +- **Un-packing in-memory-only structs.** Several structs were declared inside + `#pragma pack(1)` blocks (`GuiButton`/`GuiMenu` in `bflib_guibtns.h`; + `NamedCommand`/`NamedField`/`ConfigFileData` in `config.h`; `GamekeySettings` + in `front_input.h`; `DemoItem` in `frontend.h`). None of these are serialized + to disk — they are runtime helpers full of pointers and function pointers. On + arm64, statically-initialized packed structs place those pointers at unaligned + offsets that the linker rejects. They are now left at natural alignment. +- **`src/globals.h`** — the POSIX ``/`` include path now also + fires on `__APPLE__` / `__unix__`, not just the legacy `unix` macro. + +## Prerequisites + +1. **Xcode Command Line Tools:** `xcode-select --install` +2. **Homebrew:** https://brew.sh +3. Homebrew libs: `sdl2 sdl2_image sdl2_mixer sdl2_net ffmpeg luajit openal-soft + libspng minizip miniupnpc libnatpmp zlib curl` (plus `innoextract`/`sevenzip` + if you extract original DK data from a GOG installer). + +## Build + +```sh +# from the repo root +./tools/build_macos_deps.sh # one-time (and after dep bumps) +make -f macos.mk -j"$(sysctl -n hw.ncpu)" +``` + +`build_macos_deps.sh` builds the three deps Homebrew doesn't publish for arm64 +(`centijson`, `astronomy`, `enet6`) from source into `deps/`, where `macos.mk` +expects them. + +Output binary: `bin/keeperfx` (Mach-O arm64). + +## Running + +Run the binary from a directory populated with a KeeperFX install **plus the +files listed in `docs/files_required_from_original_dk.txt`** copied from an +original Dungeon Keeper (CD / GOG / Steam / Origin). KeeperFX's install gate is +simply a check for `data/bluepal.dat`. + +> Tip: on a machine with the GOG "Dungeon Keeper Gold" app installed, all 16 +> required files ship **uncompressed** inside the app bundle under +> `Contents/Resources/game/{DATA,SOUND}/` — no `game.gog` CD-image extraction is +> needed. Copy them in with lowercase names. + +Benign log noise on first run: missing `font12/16.fxfont` (optional Unicode +fonts; it falls back to the `.fon` bitmap fonts) and `colours.col` / `tables.dat` +/ `alpha.col` "errors" that are regenerated at runtime from the palette. + +## Packaging a self-contained `KeeperFX.app` + +`tools/make_macos_app.sh` turns `bin/keeperfx` into a redistributable +`dist/KeeperFX.app` that runs on any Apple Silicon Mac with **no Homebrew**: + +```sh +make -f macos.mk && tools/make_macos_app.sh +``` + +What it does, and the non-obvious bits it handles: + +- **Bundles the dylibs.** `dylibbundler` copies every non-system dylib into + `Contents/libs` and rewrites the engine's load paths to `@executable_path/../libs`. +- **SDL3, which `dylibbundler` can't see.** Homebrew's `sdl2` is actually + `sdl2-compat` — a thin `libSDL2` that **`dlopen`s SDL3 at runtime**, so SDL3 is + not a link-time dependency and the bundling pass misses it (the shim then dies + in its initializer). The shim's first search path is `@loader_path/libSDL3.dylib` + (note: *unversioned*), so the script copies `libSDL3.0.dylib` in as + `Contents/libs/libSDL3.dylib` and the shim finds it with no env vars or rpath. +- **A drop-in, self-locating engine.** Finder starts apps with `cwd=/`, but the + game data lives in the folder the user dropped the `.app` into. The engine is + the bundle's main executable and, on startup, `macos_chdir_to_bundle_parent()` + (in `src/linux.cpp`) resolves its own path via `_NSGetExecutablePath`; if it is + running from inside a `*.app/Contents/MacOS/`, it `chdir`s to the folder + containing the `.app`. Run outside a bundle (a developer launching + `bin/keeperfx` directly) and the working directory is left untouched. This + replaces the older bash-wrapper launcher. +- **Folder-access permission prompt.** Because the engine is a real signed main + executable (not a script) and `Info.plist` declares `NSDesktopFolderUsage`/ + `NSDocumentsFolderUsage`/`NSDownloadsFolderUsageDescription`, macOS shows a + normal privacy prompt the first time the app reads data from a protected folder + (Desktop/Documents/Downloads). The user clicks **Allow** once and it works from + any location. (A bash-wrapper main executable is *silently denied* instead of + prompted, which is why the wrapper approach failed on the Desktop.) +- **Ad-hoc code signing.** arm64 requires every Mach-O to be signed to run. The + dylibs are signed first (inside-out), then the whole bundle — sealing the + engine and giving macOS a stable app identity to attach the folder-access grant + to. Users still clear Gatekeeper once (right-click → Open) since it isn't + notarized. + +CI runs this and uploads the `.app` as an artifact +(`.github/workflows/build-macos.yml`). + +## Optional enhancements + +- **Crash register dump.** Add an `#elif defined(__APPLE__) && defined(__aarch64__)` + branch to `log_posix_context()` in `src/bflib_crash.c` reading + `((ucontext_t*)context)->uc_mcontext->__ss.__pc` / `__sp`. Nicer crash logs; + not required. +- **Notarization.** Replace the ad-hoc signature with a Developer ID signature + + `notarytool` so the `.app` opens without the Gatekeeper right-click dance. +- **Share the source list.** Factor the common `KFX_SOURCES` out of `linux.mk` + so `macos.mk` can include it instead of duplicating (keeps them from drifting + when upstream adds/removes a source file). + +## Staying in sync with upstream + +The port adds only new files (`macos.mk`, `tools/build_macos_deps.sh`, this doc) +plus a handful of guarded header edits, so pulling from upstream rarely +conflicts. If upstream adds or removes a file in `linux.mk`'s `KFX_SOURCES`, +mirror that one line into `macos.mk`. diff --git a/macos.mk b/macos.mk new file mode 100644 index 0000000000..a30bb9aaed --- /dev/null +++ b/macos.mk @@ -0,0 +1,403 @@ +# Native Apple Silicon (arm64) macOS build for KeeperFX. +# Usage: make -f macos.mk (uses clang, Homebrew libraries) +# +# Requires Homebrew packages: +# pkg-config sdl2 sdl2_image sdl2_mixer sdl2_net ffmpeg luajit +# openal-soft libspng minizip miniupnpc libnatpmp zlib curl +# The source-only deps (astronomy, centijson, enet6) are built into deps/ +# as arm64 static libraries by tools/build_macos_deps.sh. + +include version.mk + +BUILD_NUMBER ?= $(VER_BUILD) +VER_SUFFIX ?= Prototype +VER_STRING = $(VER_MAJOR).$(VER_MINOR).$(VER_RELEASE).$(BUILD_NUMBER) $(VER_SUFFIX) + +MKDIR ?= mkdir -p +STRIP ?= strip +ECHO ?= echo +MV ?= mv -f + +CC ?= clang +CXX ?= clang++ + +BREW ?= $(shell brew --prefix) +# openal-soft, curl and zlib are keg-only in Homebrew; their pkgconfig dirs are +# embedded into the pkg-config invocation so every $(shell) call below sees them. +KFX_PKG_CONFIG_PATH := $(BREW)/opt/openal-soft/lib/pkgconfig:$(BREW)/opt/curl/lib/pkgconfig:$(BREW)/opt/zlib/lib/pkgconfig +PKG_CONFIG ?= PKG_CONFIG_PATH="$(KFX_PKG_CONFIG_PATH):$$PKG_CONFIG_PATH" $(BREW)/bin/pkg-config + +KFX_SOURCES = \ +src/actionpt.c \ +src/api.c \ +src/ariadne.c \ +src/ariadne_edge.c \ +src/ariadne_findcache.c \ +src/ariadne_naviheap.c \ +src/ariadne_navitree.c \ +src/ariadne_points.c \ +src/ariadne_regions.c \ +src/ariadne_tringls.c \ +src/ariadne_wallhug.c \ +src/bflib_basics.c \ +src/bflib_coroutine.c \ +src/bflib_cpu.c \ +src/bflib_crash.c \ +src/bflib_datetm.cpp \ +src/bflib_dernc.c \ +src/bflib_enet.cpp \ +src/net_portforward.cpp \ +src/bflib_fileio.c \ +src/bflib_filelst.c \ +src/bflib_fmvids.cpp \ +src/bflib_guibtns.c \ +src/bflib_input_joyst.cpp \ +src/bflib_inputctrl.cpp \ +src/bflib_keybrd.c \ +src/bflib_main.cpp \ +src/bflib_math.c \ +src/bflib_mouse.cpp \ +src/bflib_mshandler.cpp \ +src/bflib_mspointer.cpp \ +src/bflib_netsession.c \ +src/bflib_netsp.cpp \ +src/net_exchange_common.c \ +src/net_exchange_gameplay.c \ +src/net_main.c \ +src/net_lobby.c \ +src/bflib_planar.c \ +src/bflib_render.c \ +src/bflib_render_gpoly.c \ +src/bflib_render_trig.c \ +src/bflib_sndlib.cpp \ +src/bflib_sound.c \ +src/bflib_sprfnt.c \ +src/bflib_string.c \ +src/bflib_text.c \ +src/bflib_video.c \ +src/bflib_vidraw.c \ +src/bflib_vidraw_spr_norm.c \ +src/bflib_vidraw_spr_onec.c \ +src/bflib_vidraw_spr_remp.c \ +src/bflib_vidsurface.c \ +src/button_snapping.c \ +src/config.c \ +src/config_campaigns.c \ +src/config_creature.c \ +src/config_crtrmodel.c \ +src/config_crtrstates.c \ +src/config_keeperfx.c \ +src/config_lenses.c \ +src/config_magic.c \ +src/config_mods.c \ +src/config_objects.c \ +src/config_players.c \ +src/config_powerhands.c \ +src/config_rules.c \ +src/config_sounds.c \ +src/config_settings.c \ +src/config_slabsets.c \ +src/config_strings.c \ +src/config_terrain.c \ +src/config_cubes.c \ +src/config_textures.c \ +src/config_translation.c \ +src/config_trapdoor.c \ +src/config_spritecolors.c \ +src/console_cmd.c \ +src/custom_sprites.c \ +src/creature_battle.c \ +src/creature_control.c \ +src/creature_graphics.c \ +src/creature_groups.c \ +src/creature_instances.c \ +src/creature_jobs.c \ +src/creature_senses.c \ +src/creature_states.c \ +src/creature_states_barck.c \ +src/creature_states_combt.c \ +src/creature_states_gardn.c \ +src/creature_states_guard.c \ +src/creature_states_hero.c \ +src/creature_states_lair.c \ +src/creature_states_mood.c \ +src/creature_states_pray.c \ +src/creature_states_prisn.c \ +src/creature_states_rsrch.c \ +src/creature_states_scavn.c \ +src/creature_states_spdig.c \ +src/creature_states_tortr.c \ +src/creature_states_train.c \ +src/creature_states_tresr.c \ +src/creature_states_wrshp.c \ +src/cursor_tag.c \ +src/dungeon_data.c \ +src/dungeon_stats.c \ +src/engine_arrays.c \ +src/engine_camera.c \ +src/local_camera.c \ +src/engine_lenses.c \ +src/engine_redraw.c \ +src/engine_render.c \ +src/engine_render_data.cpp \ +src/engine_textures.c \ +src/front_credits.c \ +src/front_easter.c \ +src/front_fmvids.c \ +src/front_highscore.c \ +src/front_input.c \ +src/front_landview.c \ +src/front_landview_multiplayer.c \ +src/front_lvlstats.c \ +src/front_lvlstats_data.cpp \ +src/front_network.c \ +src/front_simple.c \ +src/front_torture.c \ +src/front_torture_data.cpp \ +src/frontend.cpp \ +src/frontmenu_select.c \ +src/frontmenu_ingame_evnt.c \ +src/frontmenu_ingame_evnt_data.cpp \ +src/frontmenu_ingame_map.c \ +src/frontmenu_ingame_opts.c \ +src/frontmenu_ingame_opts_data.cpp \ +src/frontmenu_ingame_tabs.c \ +src/frontmenu_ingame_tabs_data.cpp \ +src/frontmenu_net.c \ +src/frontmenu_net_data.cpp \ +src/frontmenu_options.c \ +src/frontmenu_options_data.cpp \ +src/frontmenu_saves.c \ +src/frontmenu_saves_data.cpp \ +src/frontmenu_select_data.cpp \ +src/frontmenu_specials.c \ +src/game_heap.c \ +src/game_legacy.c \ +src/game_loop.c \ +src/game_lghtshdw.c \ +src/game_merge.c \ +src/game_saves.c \ +src/gui_boxmenu.c \ +src/gui_draw.c \ +src/gui_frontbtns.c \ +src/gui_frontmenu.c \ +src/gui_msgs.c \ +src/gui_parchment.c \ +src/gui_soundmsgs.cpp \ +src/gui_tooltips.c \ +src/gui_topmsg.c \ +src/highscores.c \ +src/kjm_input.c \ +src/lens_api.c \ +src/config_effects.c \ +src/kfx_memory.c \ +src/kfx/lense/DisplacementEffect.cpp \ +src/kfx/lense/FlyeyeEffect.cpp \ +src/kfx/lense/LensEffect.cpp \ +src/kfx/lense/LensManager.cpp \ +src/kfx/lense/LuaLensEffect.cpp \ +src/kfx/lense/MistEffect.cpp \ +src/kfx/lense/OverlayEffect.cpp \ +src/kfx/lense/PaletteEffect.cpp \ +src/light_data.c \ +src/linux.cpp \ +src/lua_api.c \ +src/lua_api_lens.c \ +src/lua_api_map.c \ +src/lua_api_player.c \ +src/lua_api_room.c \ +src/lua_api_things.c \ +src/lua_api_slabs.c \ +src/lua_api_sound.c \ +src/lua_base.c \ +src/lua_api_camera.c \ +src/lua_cfg_funcs.c \ +src/lua_params.c \ +src/lua_triggers.c \ +src/lua_utils.c \ +src/lvl_filesdk1.c \ +src/lvl_script.c \ +src/lvl_script_commands.c \ +src/lvl_script_commands_old.c \ +src/lvl_script_lib.c \ +src/lvl_script_conditions.c \ +src/lvl_script_value.c \ +src/magic_powers.c \ +src/main.cpp \ +src/main_game.c \ +src/map_blocks.c \ +src/map_columns.c \ +src/map_ceiling.c \ +src/map_data.c \ +src/map_events.c \ +src/map_locations.c \ +src/map_utils.c \ +src/moonphase.c \ +src/net_checksums.c \ +src/net_game.c \ +src/net_holepunch.c \ +src/net_matchmaking.c \ +src/net_lan.c \ +src/net_input_lag.c \ +src/net_resync.cpp \ +src/packets.c \ +src/packets_cheats.c \ +src/packets_input.c \ +src/packets_misc.c \ +src/player_compchecks.c \ +src/player_compevents.c \ +src/player_complookup.c \ +src/config_compp.c \ +src/player_compprocs.c \ +src/player_comptask.c \ +src/player_computer.c \ +src/player_computer_data.cpp \ +src/player_data.c \ +src/player_instances.c \ +src/player_utils.c \ +src/power_hand.c \ +src/power_process.c \ +src/power_specials.c \ +src/room_data.c \ +src/room_entrance.c \ +src/room_garden.c \ +src/room_graveyard.c \ +src/room_jobs.c \ +src/room_lair.c \ +src/room_library.c \ +src/room_list.c \ +src/room_scavenge.c \ +src/room_treasure.c \ +src/room_util.c \ +src/room_workshop.c \ +src/roomspace.c \ +src/roomspace_detection.c \ +src/roomspace_prediction.c \ +src/scrcapt.c \ +src/slab_data.c \ +src/sounds.c \ +src/sound_manager.cpp \ +src/spdigger_stack.c \ +src/spritesheet.cpp \ +src/tasks_list.c \ +src/thing_corpses.c \ +src/thing_creature.c \ +src/thing_data.c \ +src/thing_doors.c \ +src/thing_effects.c \ +src/thing_factory.c \ +src/thing_list.c \ +src/thing_navigate.c \ +src/thing_objects.c \ +src/thing_physics.c \ +src/thing_shots.c \ +src/thing_stats.c \ +src/thing_traps.c \ +src/value_util.c \ +src/vidfade.c \ +src/vidmode_data.cpp \ +src/vidmode.c + +KFX_C_SOURCES = $(filter %.c,$(KFX_SOURCES)) +KFX_CXX_SOURCES = $(filter %.cpp,$(KFX_SOURCES)) +KFX_C_OBJECTS = $(patsubst src/%.c,obj/%.o,$(KFX_C_SOURCES)) +KFX_CXX_OBJECTS = $(patsubst src/%.cpp,obj/%.o,$(KFX_CXX_SOURCES)) +KFX_OBJECTS = $(KFX_C_OBJECTS) $(KFX_CXX_OBJECTS) + +KFX_INCLUDES = \ + -Ideps/centijson/include \ + -Ideps/centitoml \ + -Ideps/astronomy/include \ + -Ideps/enet6/include \ + -I$(BREW)/include \ + $(shell $(PKG_CONFIG) --cflags-only-I luajit) \ + $(shell $(PKG_CONFIG) --cflags-only-I libavformat) \ + $(shell $(PKG_CONFIG) --cflags-only-I openal) \ + $(shell $(PKG_CONFIG) --cflags-only-I sdl2) + +ARCHFLAGS = -arch arm64 + +WARNFLAGS = -Wall -Wextra -Wno-unused-parameter -Wno-unknown-pragmas -Wno-sign-compare \ + -Wno-deprecated-declarations -Wno-format-truncation -Wno-nullability-completeness + +# macOS's requires _XOPEN_SOURCE; _DARWIN_C_SOURCE re-exposes the +# full BSD/Darwin namespace (dladdr, SIGWINCH/SIGIO, etc.) that _XOPEN_SOURCE +# would otherwise hide and which the POSIX code paths rely on. +DARWINFLAGS = -D_XOPEN_SOURCE=700 -D_DARWIN_C_SOURCE + +KFX_CFLAGS += -g -DDEBUG -DBFDEBUG_LEVEL=0 -O3 $(ARCHFLAGS) -std=gnu11 $(DARWINFLAGS) $(KFX_INCLUDES) $(WARNFLAGS) +KFX_CXXFLAGS += -g -DDEBUG -DBFDEBUG_LEVEL=0 -O3 $(ARCHFLAGS) -std=gnu++20 $(DARWINFLAGS) $(KFX_INCLUDES) $(WARNFLAGS) + +# No -ld_classic and no -mno-global-merge: KeeperFX's in-memory-only #pragma +# pack(1) tables that embed pointers (GUI/config structs, the TbLoadFiles loader +# lists) are unpacked in the shared headers, so every statically-initialized +# pointer is naturally aligned and the modern linker (ld-prime) emits them fine. +KFX_LDFLAGS += \ + -g \ + $(ARCHFLAGS) \ + -Wl,-export_dynamic \ + -L$(BREW)/lib \ + -Ldeps/astronomy -lastronomy \ + -Ldeps/centijson -ljson \ + -Ldeps/enet6 -lenet6 \ + $(shell $(PKG_CONFIG) --libs sdl2) \ + $(shell $(PKG_CONFIG) --libs SDL2_mixer) \ + $(shell $(PKG_CONFIG) --libs SDL2_net) \ + $(shell $(PKG_CONFIG) --libs SDL2_image) \ + $(shell $(PKG_CONFIG) --libs libavformat) \ + $(shell $(PKG_CONFIG) --libs libavcodec) \ + $(shell $(PKG_CONFIG) --libs libswresample) \ + $(shell $(PKG_CONFIG) --libs libavutil) \ + $(shell $(PKG_CONFIG) --libs openal) \ + $(shell $(PKG_CONFIG) --libs luajit) \ + $(shell $(PKG_CONFIG) --libs spng) \ + $(shell $(PKG_CONFIG) --libs minizip) \ + $(shell $(PKG_CONFIG) --libs zlib) \ + $(shell $(PKG_CONFIG) --libs libcurl) \ + -lminiupnpc \ + -lnatpmp \ + -liconv + +TOML_SOURCES = \ + deps/centitoml/toml_api.c + +TOML_OBJECTS = $(patsubst deps/centitoml/%.c,obj/centitoml/%.o,$(TOML_SOURCES)) + +TOML_INCLUDES = \ + -Ideps/centijson/include + +TOML_CFLAGS += -O3 $(ARCHFLAGS) -std=gnu11 $(TOML_INCLUDES) -Wall -Wextra -Wno-unused-parameter + +all: bin/keeperfx + +clean: + rm -rf obj bin src/ver_defs.h + +.PHONY: all clean + +bin/keeperfx: $(KFX_OBJECTS) $(TOML_OBJECTS) | bin + $(CXX) -o $@ $(KFX_OBJECTS) $(TOML_OBJECTS) $(KFX_LDFLAGS) + +$(KFX_C_OBJECTS): obj/%.o: src/%.c src/ver_defs.h | obj + $(MKDIR) $(dir $@) + $(CC) $(KFX_CFLAGS) -c $< -o $@ + +$(KFX_CXX_OBJECTS): obj/%.o: src/%.cpp src/ver_defs.h | obj + $(MKDIR) $(dir $@) + $(CXX) $(KFX_CXXFLAGS) -c $< -o $@ + +$(TOML_OBJECTS): obj/centitoml/%.o: deps/centitoml/%.c | obj/centitoml + $(CC) $(TOML_CFLAGS) -c $< -o $@ + +bin obj obj/centitoml: + $(MKDIR) $@ + +src/ver_defs.h: version.mk + $(ECHO) "#define VER_MAJOR $(VER_MAJOR)" > $@.swp + $(ECHO) "#define VER_MINOR $(VER_MINOR)" >> $@.swp + $(ECHO) "#define VER_RELEASE $(VER_RELEASE)" >> $@.swp + $(ECHO) "#define VER_BUILD $(BUILD_NUMBER)" >> $@.swp + $(ECHO) "#define VER_STRING \"$(VER_STRING)\"" >> $@.swp + $(ECHO) "#define PACKAGE_SUFFIX \"$(VER_SUFFIX)\"" >> $@.swp + $(ECHO) "#define GIT_REVISION \"$(shell git describe --always)\"" >> $@.swp + $(MV) $@.swp $@ diff --git a/src/bflib_filelst.h b/src/bflib_filelst.h index 9365e806c7..21420a03ed 100644 --- a/src/bflib_filelst.h +++ b/src/bflib_filelst.h @@ -26,8 +26,8 @@ extern "C" { #endif /******************************************************************************/ -#pragma pack(1) - +// TbLoadFiles/V2 are in-memory static loader lists (pointers + function pointers), +// never serialized; keep them naturally aligned so arm64 can link without -ld_classic. struct TbLoadFiles; typedef const char * ModifyDataLoadFnameFunc(const char *); @@ -51,9 +51,7 @@ struct TbLoadFilesV2 { LoadFilesGetSizeFunc GetSizeFunc; LoadFilesUnpackFunc UnpackFunc; }; - -#pragma pack() -/******************************************************************************/ +/*****************************************************************************/ const char * defaultModifyDataLoadFilename(const char *); ModifyDataLoadFnameFunc *LbDataLoadSetModifyFilenameFunction(ModifyDataLoadFnameFunc *newfunc); extern ModifyDataLoadFnameFunc *modify_data_load_filename_function; diff --git a/src/bflib_guibtns.h b/src/bflib_guibtns.h index 6ed4bec6f8..06dddb8018 100644 --- a/src/bflib_guibtns.h +++ b/src/bflib_guibtns.h @@ -29,7 +29,10 @@ extern "C" { #endif /******************************************************************************/ -#pragma pack(1) +// These GUI structs are in-memory runtime definitions holding pointers and +// callback function pointers; they are never serialized. They must stay at +// natural alignment: on arm64 (Apple Silicon) statically-initialized packed +// structs place pointers at unaligned offsets that the linker rejects. struct GuiButton; struct GuiMenu; @@ -217,7 +220,6 @@ struct EventTypeInfo { // Exported variables extern char backup_input_field[INPUT_FIELD_LEN]; extern struct GuiButton *input_button; -#pragma pack() /******************************************************************************/ extern TbCharCount input_field_pos; /******************************************************************************/ diff --git a/src/config.h b/src/config.h index db6b36ff91..ae26c87e69 100644 --- a/src/config.h +++ b/src/config.h @@ -118,8 +118,11 @@ enum TbConfigLoadFlags { CnfLd_PreListed = 0x08, /**< Already parsed the names. */ }; -#pragma pack(1) - +// NOTE: The structs below (NamedCommand, NamedField, NamedFieldSet, +// ConfigFileData, ...) are in-memory runtime helpers full of pointers and +// function pointers; none are serialized to disk. They must NOT be byte-packed: +// on arm64 (Apple Silicon) packed pointers are unaligned and the linker rejects +// them. Left at natural alignment on purpose. /******************************************************************************/ @@ -259,7 +262,6 @@ struct ConfigFileData{ /******************************************************************************/ extern char keeper_runtime_directory[152]; -#pragma pack() /******************************************************************************/ extern unsigned long text_line_number; /******************************************************************************/ diff --git a/src/front_input.h b/src/front_input.h index 9be793f705..cf81e27688 100644 --- a/src/front_input.h +++ b/src/front_input.h @@ -140,6 +140,9 @@ struct GuiLayer { long current_gui_layer; }; +// Static lookup table (never serialized) that embeds a pointer; keep it at +// natural alignment so arm64 can link its statically-initialized array. +#pragma pack() struct GamekeySettings { const char* toml_name; TextStringId string_id; // For display in the key binding menu @@ -149,6 +152,7 @@ struct GamekeySettings { uint8_t binding_menu_visibility; }; +#pragma pack(1) extern const struct GamekeySettings game_key_settings[GAME_KEYS_COUNT]; diff --git a/src/frontend.h b/src/frontend.h index ee1d56033f..983ccb2bf2 100644 --- a/src/frontend.h +++ b/src/frontend.h @@ -234,13 +234,17 @@ struct GuiMenu; struct GuiButton; struct TbLoadFiles; -struct DemoItem { //sizeof = 5 +// Static demo[] table embeds a pointer (fname); keep natural alignment so the +// arm64 linker accepts its statically-initialized array. +#pragma pack() +struct DemoItem { uint8_t kind; union { FrontendMenuState state; const char *fname; }; }; +#pragma pack(1) struct NetMessage { // sizeof = 0x41 unsigned char plyr_idx; diff --git a/src/globals.h b/src/globals.h index 1089dac40e..81e34d968a 100644 --- a/src/globals.h +++ b/src/globals.h @@ -31,7 +31,7 @@ #include #include -#if defined(unix) && !defined(GO32) +#if (defined(unix) || defined(__unix__) || defined(__APPLE__)) && !defined(GO32) #include #include #include diff --git a/src/linux.cpp b/src/linux.cpp index a0087e283b..73486860bd 100644 --- a/src/linux.cpp +++ b/src/linux.cpp @@ -14,6 +14,11 @@ #include #include #include +#ifdef __APPLE__ +#include +#include +#include +#endif extern "C" const char * get_os_version() { return "Linux"; @@ -167,6 +172,39 @@ extern "C" void LbFileFindEnd(TbFileFind * ff) delete ff; } +#ifdef __APPLE__ +// On macOS the engine is the .app's main executable. Finder launches apps with +// the working directory set to "/", while the game data (data/, sound/, fxdata/, +// campgns/, ...) lives in the folder the user dropped the .app into. Locate our +// own executable and, when we are running from inside a .app/Contents/ +// MacOS/ bundle, chdir to the folder that contains the .app so those files are +// found. Run outside a bundle (e.g. a developer launching bin/keeperfx from the +// game directory) and the working directory is left untouched. +static void macos_chdir_to_bundle_parent(void) +{ + char exe[PATH_MAX]; + uint32_t size = sizeof(exe); + if (_NSGetExecutablePath(exe, &size) != 0) + return; + char resolved[PATH_MAX]; + if (realpath(exe, resolved) == nullptr) + return; + char *marker = strstr(resolved, "/Contents/MacOS/"); + if (marker == nullptr) + return; // not inside an .app bundle; leave the working directory alone + *marker = '\0'; // resolved -> ".../.app" + char *slash = strrchr(resolved, '/'); + if (slash == nullptr) + return; + *slash = '\0'; // resolved -> folder containing the .app + if (chdir(resolved) != 0) + return; +} +#endif + extern "C" int main(int argc, char *argv[]) { +#ifdef __APPLE__ + macos_chdir_to_bundle_parent(); +#endif return kfxmain(argc, argv); } diff --git a/tools/build_macos_deps.sh b/tools/build_macos_deps.sh new file mode 100755 index 0000000000..248d837338 --- /dev/null +++ b/tools/build_macos_deps.sh @@ -0,0 +1,42 @@ +#!/bin/sh +# Build the source-only KeeperFX dependencies as arm64 static libraries for a +# native Apple Silicon macOS build. These libraries have no Homebrew formula, so +# we compile them from upstream source into deps/ where macos.mk expects them. +# +# deps/astronomy/{include/astronomy.h, libastronomy.a} +# deps/centijson/{include/*.h, libjson.a} +# deps/enet6/{include/enet6/*.h, libenet6.a} +# +# The Homebrew-provided deps (SDL2, ffmpeg, luajit, openal-soft, libspng, +# minizip, miniupnpc, libnatpmp, zlib, curl) are resolved via pkg-config. +set -e + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +WORK="$(mktemp -d)" +CFLAGS="-O3 -arch arm64" +echo "Working in $WORK" + +echo "== centijson ==" +git clone --depth 1 https://github.com/mity/centijson.git "$WORK/centijson" +mkdir -p "$ROOT/deps/centijson/include" +cp "$WORK"/centijson/src/*.h "$ROOT/deps/centijson/include/" +( cd "$WORK/centijson/src" && clang $CFLAGS -c json.c json-dom.c json-ptr.c value.c ) +ar rcs "$ROOT/deps/centijson/libjson.a" "$WORK"/centijson/src/*.o + +echo "== astronomy ==" +git clone --depth 1 https://github.com/cosinekitty/astronomy.git "$WORK/astronomy" +mkdir -p "$ROOT/deps/astronomy/include" +cp "$WORK/astronomy/source/c/astronomy.h" "$ROOT/deps/astronomy/include/" +( cd "$WORK/astronomy/source/c" && clang $CFLAGS -c astronomy.c ) +ar rcs "$ROOT/deps/astronomy/libastronomy.a" "$WORK"/astronomy/source/c/astronomy.o + +echo "== enet6 ==" +git clone --depth 1 https://github.com/SirLynix/enet6.git "$WORK/enet6" +mkdir -p "$ROOT/deps/enet6/include/enet6" +cp -r "$WORK"/enet6/include/enet6/* "$ROOT/deps/enet6/include/enet6/" +( cd "$WORK/enet6/src" && clang $CFLAGS -DHAS_SOCKLEN_T=1 -I"$WORK/enet6/include" \ + -c address.c callbacks.c compress.c host.c list.c packet.c peer.c protocol.c unix.c ) +ar rcs "$ROOT/deps/enet6/libenet6.a" "$WORK"/enet6/src/*.o + +rm -rf "$WORK" +echo "Done. Built deps/{astronomy,centijson,enet6} for arm64." diff --git a/tools/make_macos_app.sh b/tools/make_macos_app.sh new file mode 100755 index 0000000000..56fc4c3587 --- /dev/null +++ b/tools/make_macos_app.sh @@ -0,0 +1,197 @@ +#!/bin/bash +# Assemble a self-contained, ad-hoc-signed KeeperFX.app for Apple Silicon. +# +# The binary produced by macos.mk links against Homebrew dylibs by absolute +# path (/opt/homebrew/...). This script copies those dylibs into the bundle and +# rewrites the load paths (via dylibbundler) so the .app runs on any arm64 Mac +# with no Homebrew installed. +# +# The app is a "drop-in" engine: the engine is the bundle's main executable and, +# on startup, chdir's to the folder CONTAINING the .app (see +# macos_chdir_to_bundle_parent in src/linux.cpp). So a user drops KeeperFX.app +# next to their KeeperFX game data (the folder with data/, sound/, fxdata/, +# campgns/, ...) and double-clicks it. +# +# Because the engine is a real signed main executable (not a script wrapper) and +# the Info.plist declares folder-access usage strings, macOS shows a normal +# permission prompt the first time it reads data from a privacy-protected folder +# (Desktop/Documents/Downloads) — so it works from any location once allowed. +# +# Usage: tools/make_macos_app.sh [path/to/keeperfx-binary] [output-dir] +# defaults: bin/keeperfx -> dist/KeeperFX.app +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +BIN="${1:-$ROOT/bin/keeperfx}" +OUTDIR="${2:-$ROOT/dist}" +APP="$OUTDIR/KeeperFX.app" + +if [ ! -x "$BIN" ]; then + echo "error: binary not found or not executable: $BIN" >&2 + echo "build it first: make -f macos.mk" >&2 + exit 1 +fi +if ! command -v dylibbundler >/dev/null 2>&1; then + echo "error: dylibbundler not found (brew install dylibbundler)" >&2 + exit 1 +fi + +VER_MAJOR=$(grep -E '^VER_MAJOR=' "$ROOT/version.mk" | cut -d= -f2) +VER_MINOR=$(grep -E '^VER_MINOR=' "$ROOT/version.mk" | cut -d= -f2) +VER_RELEASE=$(grep -E '^VER_RELEASE=' "$ROOT/version.mk" | cut -d= -f2) +VERSION="${VER_MAJOR}.${VER_MINOR}.${VER_RELEASE}" + +echo "== assembling $APP (version $VERSION) ==" +rm -rf "$APP" +mkdir -p "$APP/Contents/MacOS" "$APP/Contents/Resources" "$APP/Contents/libs" + +# The engine is the bundle's main executable (CFBundleExecutable=keeperfx). +cp "$BIN" "$APP/Contents/MacOS/keeperfx" +chmod +x "$APP/Contents/MacOS/keeperfx" + +# Bundle & fix all non-system dylibs into Contents/libs, rewriting the binary's +# load paths to @executable_path/../libs. +dylibbundler \ + --overwrite-dir \ + --bundle-deps \ + --fix-file "$APP/Contents/MacOS/keeperfx" \ + --dest-dir "$APP/Contents/libs" \ + --install-path "@executable_path/../libs" + +LIBS="$APP/Contents/libs" + +# dylibbundler only follows LC_LOAD_DYLIB entries (what `otool -L` shows). Any +# library a dependency pulls in at runtime via dlopen()/SDL_LoadObject is +# invisible to it, so those must be added by hand — AND then re-processed so +# their own transitive dependencies get bundled and path-rewritten too. (The +# original SDL3 handling copied the file but skipped that second step, leaving +# any of SDL3's own non-system deps dangling at /opt/homebrew.) +# +# add_runtime_dylib [dest-name] +add_runtime_dylib() { + local src="$1" name="${2:-$(basename "$1")}" + [ -f "$src" ] || return 0 + [ -f "$LIBS/$name" ] && return 0 + echo "== bundling runtime (dlopen'd) dylib: $name ==" + cp "$src" "$LIBS/$name" + chmod u+w "$LIBS/$name" + install_name_tool -id "@executable_path/../libs/$name" "$LIBS/$name" + # Pull in and rewrite THIS library's own dependencies. Use --overwrite-files, + # NOT --overwrite-dir: dest-dir IS $LIBS (the file we're fixing lives in it), + # and --overwrite-dir would `rm -r` that directory first — deleting the + # fix-file and every dylib the main pass already bundled. --overwrite-files + # overwrites individual files in place and leaves the rest of $LIBS intact. + dylibbundler \ + --overwrite-files \ + --bundle-deps \ + --fix-file "$LIBS/$name" \ + --dest-dir "$LIBS" \ + --install-path "@executable_path/../libs" >/dev/null +} + +# --- SDL3 (dlopen'd by the sdl2-compat shim as @loader_path/libSDL3.dylib) --- +# The shim's first search path is @loader_path, i.e. right next to libSDL2 in +# Contents/libs, so the unversioned name is what it looks for. +if [ -f "$LIBS/libSDL2-2.0.0.dylib" ]; then + add_runtime_dylib "$(brew --prefix sdl3 2>/dev/null)/lib/libSDL3.0.dylib" "libSDL3.dylib" +fi + +# --- SDL2_mixer audio decoders (dlopen'd by name at runtime) ----------------- +# KeeperFX plays music through SDL2_mixer (Mix_LoadMUS). SDL2_mixer loads its +# Ogg/FLAC/MP3/Opus/MOD decoders dynamically, so dylibbundler never sees them: +# on a machine WITH Homebrew they resolve from /opt/homebrew and music plays; +# on a machine WITHOUT it they fail and music goes silent. Mirror the decoder +# set into the bundle under both its versioned and unversioned SONAMEs (mixer +# may request either). +# +# NOTE: copying them here is necessary but may not be sufficient — a bare-name +# dlopen does not search Contents/libs. Whether this alone fixes playback MUST +# be verified on-device (see HANDOFF_MACOS_AUDIO.md); if the decoders turn out +# to be truly dlopen'd rather than linked, the durable fix is to statically link +# them into SDL2_mixer. This block is harmless either way (dylibbundler dedups). +if [ -f "$LIBS/libSDL2_mixer-2.0.0.dylib" ]; then + BREW_LIB="$(brew --prefix)/lib" + for stem in libvorbisfile libvorbis libogg libFLAC libmpg123 \ + libopusfile libopus libxmp libmodplug libwavpack libfluidsynth; do + real="$(ls "$BREW_LIB/$stem".*.dylib 2>/dev/null | head -n1 || true)" + [ -n "$real" ] || continue + add_runtime_dylib "$real" "$(basename "$real")" # versioned SONAME + add_runtime_dylib "$real" "$stem.dylib" # unversioned alias + done +fi + +# Icon (optional): build a .icns from the repo's PNG icon set via iconutil. +ICON_KEY='' +if command -v iconutil >/dev/null 2>&1 && [ -f "$ROOT/res/keeperfx_icon016-08bpp.png" ]; then + ICONSET="$(mktemp -d)/KeeperFX.iconset" + mkdir -p "$ICONSET" + cp "$ROOT/res/keeperfx_icon016-08bpp.png" "$ICONSET/icon_16x16.png" + cp "$ROOT/res/keeperfx_icon032-08bpp.png" "$ICONSET/icon_16x16@2x.png" + cp "$ROOT/res/keeperfx_icon032-08bpp.png" "$ICONSET/icon_32x32.png" + cp "$ROOT/res/keeperfx_icon064-08bpp.png" "$ICONSET/icon_32x32@2x.png" + cp "$ROOT/res/keeperfx_icon128-24bpp.png" "$ICONSET/icon_128x128.png" + cp "$ROOT/res/keeperfx_icon256-24bpp.png" "$ICONSET/icon_128x128@2x.png" + cp "$ROOT/res/keeperfx_icon256-24bpp.png" "$ICONSET/icon_256x256.png" + cp "$ROOT/res/keeperfx_icon512-24bpp.png" "$ICONSET/icon_256x256@2x.png" + cp "$ROOT/res/keeperfx_icon512-24bpp.png" "$ICONSET/icon_512x512.png" + if iconutil -c icns "$ICONSET" -o "$APP/Contents/Resources/KeeperFX.icns" 2>/dev/null; then + ICON_KEY='CFBundleIconFileKeeperFX' + fi +fi + +cat > "$APP/Contents/Info.plist" < + + + + CFBundleNameKeeperFX + CFBundleDisplayNameKeeperFX + CFBundleIdentifiercom.keeperfx.keeperfx + CFBundleVersion${VERSION} + CFBundleShortVersionString${VERSION} + CFBundlePackageTypeAPPL + CFBundleExecutablekeeperfx + ${ICON_KEY} + LSMinimumSystemVersion11.0 + LSArchitecturePriorityarm64 + NSHighResolutionCapable + NSDesktopFolderUsageDescriptionKeeperFX needs to read its game data files (data, sound, campaigns) from the folder it was placed in. + NSDocumentsFolderUsageDescriptionKeeperFX needs to read its game data files (data, sound, campaigns) from the folder it was placed in. + NSDownloadsFolderUsageDescriptionKeeperFX needs to read its game data files (data, sound, campaigns) from the folder it was placed in. + NSRemovableVolumesUsageDescriptionKeeperFX needs to read its game data files (data, sound, campaigns) from the folder it was placed in. + + +PLIST + +echo 'APPL????' > "$APP/Contents/PkgInfo" + +# Ad-hoc code signatures. arm64 requires every Mach-O to carry at least an +# ad-hoc signature to execute. Sign the bundled dylibs first (inside-out), then +# sign the whole bundle: that seals the engine main executable and gives macOS a +# stable app identity to attach the user's folder-access grant to (needed for +# the privacy prompt described in the Info.plist usage strings). +find "$APP/Contents/libs" -name '*.dylib' -exec codesign --force --sign - {} + +codesign --force --sign - "$APP" + +echo "== verifying bundle is self-contained (no Homebrew/local paths) ==" +# Walk EVERY Mach-O — the main binary and every bundled dylib — not just the +# main binary. A link-time dep of a bundled library that dylibbundler failed to +# rewrite would leak here. (This cannot see a *missing* dlopen'd library, which +# is referenced by bare name; that class of gap is verified at runtime instead.) +leaked=0 +for macho in "$APP/Contents/MacOS/keeperfx" "$LIBS"/*.dylib; do + [ -f "$macho" ] || continue + if otool -L "$macho" | grep -Eq "/opt/homebrew|/usr/local/"; then + echo "error: $(basename "$macho") still references a non-bundled path:" >&2 + otool -L "$macho" | grep -E "/opt/homebrew|/usr/local/" | sed 's/^/ /' >&2 + leaked=1 + fi +done +[ "$leaked" -eq 0 ] || { echo "bundling incomplete — see above" >&2; exit 1; } + +echo "== done: $APP ==" +echo " bundled dylibs: $(ls "$APP/Contents/libs" | wc -l | tr -d ' ')" +# Verify the whole bundle carries a valid signature (this is what macOS checks +# at launch and what the TCC privacy grant is attributed to). +codesign --verify --verbose=2 "$APP" \ + && echo " bundle: signed & valid (ad-hoc)"