diff --git a/issues/0051-macos-bundle-assets-cwd-relative.md b/issues/0051-macos-bundle-assets-cwd-relative.md new file mode 100644 index 0000000..2c80076 --- /dev/null +++ b/issues/0051-macos-bundle-assets-cwd-relative.md @@ -0,0 +1,110 @@ +# Symptom + +A bundled macOS `.app` built with `sx build` crashes on launch when started +via Finder double-click or `open Foo.app`, but runs fine when launched from a +shell whose CWD is the bundle directory. + +Observed: `open sx-out/macos/SxChess.app` → process exits within ~1s (segfaults +inside `stbtt_ScaleForPixelHeight` because the font buffer is null — the asset +wasn't found). +Expected: double-click / `open` launches the app and it finds its bundled +assets, same as on iOS. + +Root: assets are loaded with **CWD-relative** paths (e.g. +`"assets/fonts/default.ttf"`), but Finder/`open` start a GUI app with `CWD=/`, +so the relative path resolves against `/` and the file is missing. + +# Reproduction + +Any consumer that bundles an `assets/` dir and loads from it by relative path. +Minimal shape (real case: `/Users/agra/projects/game`): + +```sx +// main.sx — loads assets by CWD-relative path +g_pipeline.init_font("assets/fonts/default.ttf", 32.0, dpi); // -> read_file_bytes +g_chess_game.pieces.load("assets/chess/pieces.png", gpu); // -> read_file_bytes +``` + +```sh +cd game && sx build main.sx # produces sx-out/macos/SxChess.app (assets copied in) + +# Works (CWD = bundle dir, so "assets/..." resolves): +cd sx-out/macos/SxChess.app && ./SxChess + +# Fails (CWD = /, asset not found -> null buffer -> stbtt segfault): +open sx-out/macos/SxChess.app +# or double-click in Finder +``` + +`add_asset_dir("assets", "assets")` in `build.sx` correctly copies the tree +into the flat `.app` (binary at `SxChess.app/SxChess`, assets at +`SxChess.app/assets/...`), so the files ARE present in the bundle — they're just +not found because the lookup is CWD-relative and CWD isn't the bundle. + +# Root cause + +The macOS SDL startup never reorients CWD (or the asset root) to the bundle. +`SdlPlatform.init` ([library/modules/platform/sdl3.sx:35](../library/modules/platform/sdl3.sx#L35)) +calls `SDL_Init(SDL_INIT_VIDEO)` and creates the window but does no `chdir`, +so `read_file_bytes` +([library/modules/ui/glyph_cache.sx:202](../library/modules/ui/glyph_cache.sx#L202), +and the chess `#foreign read_file_bytes`) opens paths relative to whatever CWD +the launcher set — `/` under Finder/`open`. + +The other platforms already handle this: +- **iOS**: [library/modules/platform/uikit.sx:346](../library/modules/platform/uikit.sx#L346) + explicitly `chdir`s to the bundle's `resourcePath()` at startup, with the + comment "iOS apps start with CWD=/. chdir to the bundle's resourcePath so the + [assets resolve]". +- **Android**: [library/modules/platform/android.sx:71](../library/modules/platform/android.sx#L71) + routes `read_file_bytes` through `AAssetManager` so paths resolve against the + APK assets regardless of CWD. + +macOS has neither — so it only works by accident when launched from a shell +sitting in the bundle dir. + +# Investigation prompt + +For a fresh session picking this up: + +The fix mirrors the iOS precedent. In `SdlPlatform.init` +([sdl3.sx:35](../library/modules/platform/sdl3.sx#L35)), before any asset is +loaded, reorient to the bundle's resource directory **on macOS only** (leave +wasm/emscripten and the dev `sx run` path alone — those legitimately want the +project CWD). + +Recommended approach — `SDL_GetBasePath()`: +- SDL3 `SDL_GetBasePath()` returns the directory containing the executable + (for a `.app`, that's `SxChess.app/` where the assets were copied). `chdir` + to it at the top of `init` when `BuildOptions.is_macos` (gate so `sx run` + during development isn't affected — or gate on "the base path differs from + CWD and contains an `assets/` dir"). +- Add the `#foreign` decl for `SDL_GetBasePath` (returns `*u8`, SDL-owned) and + call `chdir` (already used by uikit.sx — reuse the same `#foreign`). + +Alternative (no SDL dependency): `_NSGetExecutablePath` + `dirname`, same as a +plain macOS resolve. SDL_GetBasePath is simpler and already links SDL3. + +Things to verify / watch: +- Don't chdir for the `sx run ` (JIT) dev flow or for wasm — only the + bundled AOT macOS app. The cleanest gate is the bundle context; if `init` + can't see `BuildOptions`, gate on `SDL_GetBasePath()` returning a path that + ends in `.app/` (bundled) vs the build dir (dev). +- After chdir, the existing `"assets/..."` relative loads resolve unchanged — + no call-site changes needed in consumers (chess, glyph_cache). +- Confirm the iOS path (uikit, doesn't use SdlPlatform) is untouched. + +# Verification + +```sh +cd game && sx build main.sx +open sx-out/macos/SxChess.app # must launch and stay up (board + pieces render) +# screenshot / pgrep -lf sx-out/macos/SxChess -> alive after a few seconds +``` + +Before the fix: `open` → exits ~1s (stbtt segfault, null font buffer). +After: `open` and Finder double-click both launch and render, matching the +`cd bundle && ./SxChess` behavior. + +Also re-run host + cross suites to confirm no platform-module regression: +`zig build && zig build test && bash tests/run_examples.sh`. diff --git a/library/modules/platform/sdl3.sx b/library/modules/platform/sdl3.sx index 5fb79cc..4804ba5 100644 --- a/library/modules/platform/sdl3.sx +++ b/library/modules/platform/sdl3.sx @@ -11,6 +11,33 @@ g_sdl_plat : *SdlPlatform = null; +chdir :: (path: [*]u8) -> s32 #foreign; +SDL_GetBasePath :: () -> [*]u8 #foreign; + +// A macOS `.app` launched via Finder / `open` starts with CWD=`/`, so a +// game's CWD-relative asset loads (`read_file_bytes("assets/...")`) miss and +// the app crashes loading fonts/textures. SDL's base path is the directory +// holding the executable — inside the `.app`, where the bundler copied +// `assets/`. chdir there, but ONLY when actually running from within a `.app` +// bundle, so the `sx run` dev flow (binary not bundled, assets in the project +// CWD) is left alone. Mirrors uikit.sx's iOS `chdir_to_bundle`. +sdl_chdir_to_bundle :: () { + inline if OS != .macos { return; } + bp := SDL_GetBasePath(); + if bp == null { return; } + // Reorient only when the base path lives inside a `.app` bundle. + i : s64 = 0; + found := false; + while bp[i] != 0 { + if bp[i] == 46 and bp[i + 1] == 97 and bp[i + 2] == 112 and bp[i + 3] == 112 { + found = true; + break; + } + i = i + 1; + } + if found { chdir(bp); } +} + SdlPlatform :: struct { window: *void = null; gl_ctx: *void = null; @@ -38,6 +65,8 @@ impl Platform for SdlPlatform { self.delta_time = 0.008; self.dpi_scale = 1.0; SDL_Init(SDL_INIT_VIDEO); + // Find bundled assets when launched as a `.app` (CWD=/ under Finder). + sdl_chdir_to_bundle(); inline if OS == { case .wasm: {