# Crash BASIC 1.26.1 Release Notes

*~written by Crash-tan~*

## THE "BUNDLES THAT ACTUALLY LAUNCH" PATCH

A run of stacked packaging bugs — some inherited, some introduced by 1.26.0's ffmpeg bundling work, one only reproducible after long sessions on iOS — got traced and fixed in one pass. If you've ever generated a game bundle, double-clicked it, and watched it silently die, this release is for you.

### Fixed: macOS bundles produced by crashpackager were unsigned and SIGKILLed at launch

The packager was copying the `crashplayer` binary out of `CrashPlayer.app` and dropping it into a freshly-assembled `<Game>.app` without re-sealing the new bundle. The binary's embedded signature still declared `Identifier=com.crashbasic.crashplayer`, but the new bundle's `Info.plist` had `CFBundleIdentifier=com.crashbasic.<game>`. AMFI on Apple Silicon noticed the mismatch at exec time and the kernel sent SIGKILL before `main()` ran. From the user's terminal: `zsh: killed`. From a double-click: nothing visible at all — the dock icon flashed once and the process disappeared.

The fix moves bundle re-sealing into the packager itself. After the new bundle is fully assembled (binary + Frameworks + Info.plist + icon), crashpackager runs `codesign --force --sign - --deep` on the bundle. That overwrites the inherited Developer-ID signature with an ad-hoc signature whose identifier matches the new `CFBundleIdentifier`, and walks every Mach-O inside the bundle so dyld's Library Validation isn't unhappy when the main exe loads its own dylibs. Hardened Runtime is intentionally NOT re-applied here — `--options=runtime` plus ad-hoc means each binary has no Team ID, and Library Validation treats "no team" as "different team" between separately-signed binaries, blocking dyld loads. Dropping HR is the only path that actually launches the bundle locally; users layering a real Developer ID on top for distribution get HR back automatically when they sign with `--options=runtime`.

### Added: Optional Developer ID signing in crashpackager

For distribution builds, you can now hand crashpackager your Developer ID directly:

- CLI: `--apple-signing-identity "Developer ID Application: Your Name (TEAMID)"`
- GUI: a "macOS signing" row appears in the Package tab when macOS is checked
- Both fall back to `$APPLE_SIGNING_IDENTITY` if the flag/field is empty

When provided, crashpackager runs `codesign --force --options=runtime --deep --timestamp --sign <identity>` instead of the ad-hoc sign. The result is bundle-ready for notarization. Notarization itself stays manual — `xcrun notarytool` and `xcrun stapler` belong in your release pipeline, not in the packager — but the generated `<game>-macos-README.txt` (see below) walks you through the exact commands.

### Added: `<game>-macos-README.txt` next to every macOS bundle

Sitting alongside the `.app` in the output directory is a generated README that explains the bundle's signing state in plain English and lists the next steps. Three branches:

- **Ad-hoc signed** (default): bundle launches locally, but Gatekeeper rejects any download. The README shows both the `--apple-signing-identity` form for re-running through crashpackager and the manual `codesign` invocation.
- **Developer ID signed**: bundle is distribution-ready but not notarized. The README walks through the `ditto -c -k --keepParent` → `xcrun notarytool submit --wait` → `xcrun stapler staple` sequence, including the keychain-profile setup hint.
- **Unsigned** (cross-platform host): produced by Linux/Windows hosts that can't run codesign. The README explains both options — sign on a Mac, or sign cross-platform with rcodesign — with links to the relevant docs.

Every branch closes with a "BACKGROUND" section explaining *why* signing has to happen at packaging time and not earlier (the inherited signature from `CrashPlayer.app` can't survive the bundle-identifier change). For users hitting this for the first time the explanation matters — otherwise it looks like an arbitrary roadblock rather than how AMFI actually works.

### Fixed: macOS bundles had an empty `Frameworks/` directory

The 1.26.0 ffmpeg-bundling work moved seven `libav*` / `libsw*` dylibs and the SDL2 frameworks into `CrashPlayer.app/Contents/Frameworks/`, with `@executable_path/../Frameworks/...` install names rewritten on the binary. crashpackager's DMG-extract path only iterated `Contents/MacOS/` and never copied `Contents/Frameworks/` over. So every macOS bundle generated since 1.26.0 had `Frameworks/` populated by an empty SDL2 symlink shim, no ffmpeg dylibs, and a binary that exited via dyld SIGKILL the moment it tried to resolve `@executable_path/../Frameworks/libavcodec.62.dylib`. Same root cause whether you went through the DMG path or the host-binary path.

The fix extracts a shared `copy_app_contents(src_contents, dest_contents)` helper that pulls both `MacOS/` and `Frameworks/` from a source `CrashPlayer.app/Contents/`. Both extraction paths now use it: DMG mount → call helper, on-disk `/Applications/CrashPlayer.app` → call helper. Symlinks inside `*.framework` directories (`Versions/Current → A`, top-level binary aliases) are preserved as real filesystem symlinks — codesign refuses to operate on a framework whose symlinks have been flattened ("bundle format is ambiguous"), and dyld resolves through them at load time.

### Added: Cross-platform macOS bundle creation via zip artifact

`build-crashplayer-dist.sh` now `ditto -c -k --keepParent`s `CrashPlayer.app` into `CrashPlayer-<version>-macos-<arch>.zip` alongside the existing DMG. The manifest exposes both. crashpackager prefers the zip when available (`extract_app_zip` is pure-Rust, doesn't shell out to `hdiutil`) and falls back to the DMG only on macOS hosts running an older manifest. The upload + manifest-rebuild scripts ship the zip end-to-end.

Practical effect: a Linux host with crashpackager, a `.crashcart`, and a manifest URL can now produce a working `<Game>.app`. The bundle comes out unsigned (you need a Mac or rcodesign for that — the README explains the options), but assembly itself is fully cross-platform. macOS bundle creation is no longer gated to a Mac. Build-time signing and notarization still are, and probably always will be.

### Fixed: Windows `.cmd` launcher pointed at the wrong directory

The published Windows zip wraps every dll and `crashplayer.exe` inside a top-level `windows-x64/` directory. `extract::extract_zip` happily extracts as-is, leaving:

```
<game>-windows/
  <game>.cmd                  ← packaged here
  windows-x64/                ← but crashplayer.exe is one level deeper
    crashplayer.exe
    *.dll
```

The `.cmd` did `cd /d "%~dp0"` then `start "" "crashplayer.exe" ...`, but `crashplayer.exe` wasn't in `%~dp0` — it was a level below. Result: command processor printed nothing visible, the start command silently failed, and the Windows bundle launched into the void.

The Windows packager now hoists the contents of any single top-level directory up into the output root after extraction. `crashplayer.exe`, the dlls, and the `.crashcart` all sit next to the `.cmd` again, exactly where the launcher script's `%~dp0` resolves to.

### Fixed: iOS exit hang stops blocking the main thread indefinitely

iOS-only intermittent hang on in-game exit: everything stops except the music; the main thread blocks somewhere in `cleanup_game_state()` so CADisplayLink can't tick again, but CoreAudio's AudioUnit thread keeps the MP3 alive. Suspect-list is well-traced (`AudioComponentInstanceDispose` deadlocking on a corrupted callback was the leading theory; layer-shader scenes increased the hit rate, suggesting the interpreter / RENDER thread can also stall during teardown). Reproducing this reliably is hard — works fine attached to the debugger, only fails detached, only sometimes.

The mitigation is a watchdog rather than a root-cause fix. `cleanup_game_state()` now:

- Sets `kill_signal` first, before any waiting, so cleanup triggered by natural program end (not just window-close) unwinds the interpreter promptly.
- Polls `interpreter_handle.is_finished()` for up to 1.5 seconds instead of blocking on `join()`. If the interpreter doesn't finish, the JoinHandle is dropped and the thread runs detached.
- Wraps `AudioCommand::Shutdown` with an observer thread that logs a WARN if it overruns 2 seconds. (`AudioManager` is `!Send`, so the shutdown call itself can't move off the main thread.)
- Arms a hard-kill watchdog at 5 seconds. If the entire pipeline hasn't completed by then, it calls `std::process::exit(exit_code)` so the user can re-launch the app instead of needing to force-quit.

The watchdog WARN logs are also the diagnostic — when this hangs again on a real device, the console line tells us *which* phase overran (interpreter join vs audio shutdown vs everything-else), which is exactly the breadcrumb the original hang lacked.

### Improved: Packaging GUI gets a status dialog and signing reminder

The Package tab's inline 80-pixel log strip could not show a full path on a single line. The status output now lives in a properly-sized popup dialog that opens automatically when "Package" is clicked: 640px wide, scrollable, sticks to the bottom so the latest progress line is always visible, anchored near the top of the launcher window so the title bar (and the Close button) stay reachable on small windows.

When a macOS-included run completes successfully, a follow-up "macOS code-signing reminder" dialog opens explaining the bundle's state and pointing at the generated README. Two variants — ad-hoc (with the steps to upgrade to a Developer ID) and Developer ID (with the notarization sequence). Closes with a single "Got it" button. Skipped on runs that didn't include macOS.

### Improved: Linux bundle README documents SDL2/SDL2_mixer dependencies

The Linux tarball ships only `crashplayer` and the docs — no SDL2 `.so` files, because the convention on Linux is to depend on the distro's package manager. The generated `README.txt` in Linux bundles now includes a `REQUIREMENTS` section with the install command for Debian/Ubuntu (apt), Fedora/RHEL (dnf), and Arch (pacman). Saves the recipient one round-trip when their distro doesn't already have SDL2 installed.

### Improved: macOS icon application now refreshes Finder's icon cache

`build_macos_icon` (`sips` + `iconutil`) was producing a valid `.icns` with all ten canonical variants, but Finder kept showing the previously-displayed (often generic) icon for the same bundle path because `iconservicesagent` caches by path. crashpackager now `touch -c`s the assembled bundle and runs `lsregister -f` after signing, so LaunchServices re-reads the bundle's metadata and Finder pulls the new icon immediately. Best-effort — failures don't break packaging.

The `sips` loop also got tightened: it now emits only the canonical 1x/2x sizes (16, 32, 128, 256, 512). Previously it spent invocations on `icon_64x64.png` and `icon_1024x1024.png`, which `iconutil` silently dropped from the iconset. Same `.icns` content, less wasted work.
