From 3254d8e81ef111d6edb8581116bc20b1871d80ed Mon Sep 17 00:00:00 2001 From: Orkuncakilkaya Date: Tue, 29 Sep 2026 17:00:29 +0300 Subject: [PATCH] ludic bundle: `app env "K=V"` - the environment Launch Services starts the app with (LSEnvironment) A switch a library reads only as a process starts - libmalloc's MallocLargeCache, which on a game keeps ~200-300 MB of freed load buffers - has to be in the environment before main. For a .app started from Finder, the Dock or `open` that is Info.plist's LSEnvironment; `app env` is repeatable and each K=V becomes a string in it. bundle_case's probe app carries one and checks it with plutil. Co-Authored-By: Claude Opus 5.5 --- tools/ludic-cli/bundle.ludic | 20 ++++++++++++++++++++ tools/ludic-cli/test.ludic | 4 ++++ 2 files changed, 24 insertions(+) diff --git a/tools/ludic-cli/bundle.ludic b/tools/ludic-cli/bundle.ludic index 1591e630..c79961f8 100644 --- a/tools/ludic-cli/bundle.ludic +++ b/tools/ludic-cli/bundle.ludic @@ -26,6 +26,7 @@ # app category "public.app-category.adventure-games" # app copyright "(c) 2026 Workshopsoft" # app min_macos "12.0" +# app env "MallocLargeCache=0" (repeatable: LSEnvironment, what Launch Services starts it with) # # Only `app name` is really needed; everything else has a defensible default # derived from the manifest. @@ -187,6 +188,7 @@ function info_plist(m: Manifest, exe: pointer, has_icon: bool) -> pointer { # A game renders at the display's real resolution; without this the window is # upscaled from 1x and everything drawn in it is soft on any Retina screen. s = s + plist_bool("NSHighResolutionCapable", true) + s = s + plist_env(m) s = s + "" + nl() s = s + "" + nl() return s @@ -204,6 +206,24 @@ function plist_str(k: pointer, v: pointer) -> pointer { return ` {k}` + nl() + ` {xml_escape(v)}` + nl() } +# every `app env "K=V"`: the environment Launch Services starts the app with (Finder, `open`, the +# Dock) - a switch a library reads only as a process starts, such as libmalloc's MallocLargeCache +function plist_env(m: Manifest) -> pointer { + var body = "" + var i = 0 + while i + 1 < len(m.app) { + if m.app[i] == "env" { + let kv = m.app[i + 1] + var eq = 0 + while eq < len(kv) and kv[eq] != '=' { eq += 1 } + if eq > 0 and eq < len(kv) { body = body + plist_str(kv[0..eq], kv[eq + 1..len(kv)]) } + } + i += 2 + } + if body == "" { return "" } + return " LSEnvironment" + nl() + " " + nl() + body + " " + nl() +} + function plist_bool(k: pointer, v: bool) -> pointer { var t = "" if v { t = "" } diff --git a/tools/ludic-cli/test.ludic b/tools/ludic-cli/test.ludic index 615471ec..576f0a05 100644 --- a/tools/ludic-cli/test.ludic +++ b/tools/ludic-cli/test.ludic @@ -370,6 +370,9 @@ function bundle_case() -> void { # the identifier is derived from the package path when the manifest omits it let id = capture_line(`plutil -extract CFBundleIdentifier raw "{app}/Contents/Info.plist" 2>/dev/null`) if id != "io.workshopsoft.probe-app" { bad2(lbl, `bundle id came out [{id}]`); return } + # `app env` is what Launch Services starts it with (a Finder or `open` start) + let env = capture_line(`plutil -extract LSEnvironment.MallocLargeCache raw "{app}/Contents/Info.plist" 2>/dev/null`) + if env != "0" { bad2(lbl, `LSEnvironment.MallocLargeCache came out [{env}]`); return } if not shq(`codesign --verify "{app}" > /dev/null 2>&1`) { bad2(lbl, "the signature does not verify"); return } # and it runs from a directory with none of its assets, reading them from the pack let got = s_trim(capture(`cd / && "{app}/Contents/MacOS/Probe App" 2>&1`)) @@ -388,6 +391,7 @@ function bundle_manifest_src() -> pointer { s = s + "app name \"Probe App\"" + nl() s = s + "app icon \"assets/icon.png\"" + nl() s = s + "app copyright \"(c) 2026 A & B\"" + nl() + s = s + "app env \"MallocLargeCache=0\"" + nl() s = s + "pack \"assets\"" + nl() return s }