platform/sdl3: chdir to .app bundle on macOS so CWD-relative assets resolve

A macOS .app launched with CWD=/ (Finder/open) could not find CWD-relative
assets (read_file_bytes("assets/...")) and crashed in stbtt with a null font.
SdlPlatform.init now chdirs to SDL_GetBasePath() when running from inside a
.app bundle (detected by ".app" in the base path), mirroring uikit.sx s iOS
chdir_to_bundle. Gated so the sx run dev flow (binary not bundled) keeps the
project CWD. Verified: direct-exec with CWD=/ now stays alive (was: instant
stbtt segfault). Filed issue 0051 with the analysis.

Note: launching via Finder/open additionally triggers Gatekeeper App
Translocation for the dev-signed bundle (separate code-signing concern, not
the asset path).
This commit is contained in:
agra
2026-05-30 01:10:13 +03:00
parent 3731a200c3
commit b31fbae757
2 changed files with 139 additions and 0 deletions

View File

@@ -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 <file>` (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`.

View File

@@ -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: {