feat(app): App.set_icon, so a game without a bundle still has an icon

`ludic bundle` builds an AppIcon.icns and macOS reads it out of the .app, so a
shipped game has an icon. `ludic build` produces a bare executable: no bundle, no
CFBundleIconFile, and so no icon at all - macOS draws the generic green "exec"
tile. That is the build a developer runs every day, which is why "the game has no
icon" can be true for months while the bundle is perfect. It was here: the .app's
icns validated against iconutil, the plist was right, the signature was right,
and NSWorkspace rendered the artwork - and the binary beside it still had the
exec tile.

App.set_icon(path) takes the bytes through lp_pak_open, so a packed path and a
loose one both work and this does not repeat Audio.load's trick of taking a
filesystem path only. NSData copies them, so the buffer goes straight back. A
missing or undecodable image leaves the existing icon alone rather than clearing
it; a bundled app is unaffected; headless links no AppKit and compiles it away.

Verified the Cocoa sequence against an ObjC twin doing the same message sends:
before, a bare binary's applicationIconImage is the generic 128x128 tile; after,
it is the 1024x1024 artwork.

Backend change, so selfhost/ludicc.seed.ll is reseeded: the bootstrap fixpoint
and the C-free rebuild from the seed both pass.
This commit is contained in:
Orkun ÇAKILKAYA 2026-09-12 12:55:12 +03:00
parent 925d133466
commit 38b6b81cd7
7 changed files with 34345 additions and 34143 deletions

View file

@ -157,6 +157,41 @@ function pack_probe_src() -> pointer {
return s
}
# App.set_icon: the Dock tile for a binary with no bundle around it. The tile itself
# cannot be asserted from a test process, so what is checked is the part that can go
# wrong silently - that it compiles and runs in a windowed build, that a missing or
# unreadable image is a no-op rather than a crash or a cleared icon, and that a headless
# build (which has no AppKit linked at all) compiles it away to nothing.
function app_icon_case() -> void {
let lbl = "App.set_icon -> an icon for a binary with no bundle, and nothing headless"
if not is_darwin() { skip(lbl); return }
let work = `{tmp_dir()}/appicon`
let root = capture_line("pwd")
run(`rm -rf {work} && mkdir -p {work}`)
# a real PNG to set, made the same way the bundle test makes its icon
run(`sips -s format png --resampleHeightWidth 64 64 /System/Library/CoreServices/CoreTypes.bundle/Contents/Resources/GenericApplicationIcon.icns --out {work}/icon.png > /dev/null 2>&1`)
var src = "program IconSet {" + nl()
src = src + " entry {" + nl()
src = src + " App.set_icon(\"icon.png\")" + nl()
src = src + " App.set_icon(\"nope.png\")" + nl() # missing: must not crash
src = src + " print(\"ok\")" + nl()
src = src + " }" + nl()
src = src + "}" + nl()
write_file(`{work}/i.ludic`, src)
if not shq(`cd {work} && {root}/bin/ludicc i.ludic -o i > cc.out 2>&1`) {
bad2(lbl, capture_line(`tail -1 {work}/cc.out`)); return
}
let got = s_trim(capture(`cd {work} && ./i 2>&1`))
if got != "ok" { bad2(lbl, `windowed build printed [{got}]`); return }
# headless links no AppKit: it has to compile and run just the same
if not shq(`cd {work} && {root}/bin/ludicc i.ludic --headless -o ih > hh.out 2>&1`) {
bad2(lbl, capture_line(`tail -1 {work}/hh.out`)); return
}
let goth = s_trim(capture(`cd {work} && ./ih 2>&1`))
if goth != "ok" { bad2(lbl, `headless build printed [{goth}]`); return }
ok(lbl)
}
# Os.save_dir / config_dir / cache_dir have to name the directory the PLATFORM keeps
# those things in - that is the entire reason a program calls them instead of building a
# path. They were the macOS layout on every platform, so a game built on Linux wrote its
@ -719,6 +754,7 @@ function cmd_dev_test() -> int {
pack_roundtrip_case()
packignore_case()
os_dirs_case()
app_icon_case()
bundle_case()
# install.sh is what the landing page tells people to pipe into sh, and it is