feat(windows): the ludic CLI and ludic bundle on Windows
The CLI's shell commands go through shell(), which is run() on POSIX and a scratch script handed to Git for Windows' bash on Windows (exit codes read directly there). compile_app links through `ludicc -o` on Windows, ludic run starts the .exe, and ludic-dev build and ensure_ludicc assemble selfhost/ludicc.win.seed.ll, which ludic-dev reseed now writes beside the macOS seed. ludic bundle makes build/<name>/ with a GUI-subsystem exe carrying the .ico beside `app icon` as an llvm-rc resource, game.lpak and packs.index; ludicc gains --gui and --link, and quotes its whole link line for cmd.exe. Verified: ludic-dev test 135/135, selfhost-test 32/32; on the PC a checkout bootstraps from the Windows seed, ludic-dev build makes the toolchain, and `ludic bundle` makes build/Maroon Lake/, which runs from its own folder. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
63d9f61bf1
commit
55b0e2ebf6
22 changed files with 112450 additions and 818 deletions
|
|
@ -132,7 +132,7 @@ function build_icon(src: pointer, out_icns: pointer) -> bool {
|
|||
return false
|
||||
}
|
||||
let set = `{tmp_dir()}/AppIcon.iconset`
|
||||
run(`rm -rf {set} && mkdir -p {set}`)
|
||||
shell(`rm -rf {set} && mkdir -p {set}`)
|
||||
if not icon_size(src, set, 16, "16x16") { return false }
|
||||
if not icon_size(src, set, 32, "16x16@2x") { return false }
|
||||
if not icon_size(src, set, 32, "32x32") { return false }
|
||||
|
|
@ -229,9 +229,108 @@ function xml_escape(s: pointer) -> pointer {
|
|||
|
||||
# ---- the command ------------------------------------------------------------
|
||||
|
||||
# ---- Windows: a folder --------------------------------------------------------
|
||||
#
|
||||
# A Windows game ships as a directory a player unzips anywhere and double-clicks
|
||||
# inside:
|
||||
#
|
||||
# build/<name>/<name>.exe windowed, GUI subsystem, the icon linked in as a resource
|
||||
# build/<name>/game.lpak the same pack a .app carries
|
||||
# build/<name>/packs.index pack / home / splash, read beside the executable
|
||||
#
|
||||
# `home <name>` points the game at %APPDATA%\<name>, so saves live where a Windows
|
||||
# game's saves live and the folder itself stays read-only, as a .app does.
|
||||
#
|
||||
# The icon is `app icon` with its extension changed to .ico, when that file exists:
|
||||
# Windows reads icons from a resource inside the executable, and the resource
|
||||
# compiler takes .ico only. A project makes one from the same artwork as its .icns.
|
||||
|
||||
# an LLVM tool beside the clang this toolchain uses (llvm-rc, ...)
|
||||
function llvm_tool(name: pointer) -> pointer {
|
||||
let env = getenv("LUDIC_CC")
|
||||
if env == null and Fs.exists(`C:/Program Files/LLVM/bin/{name}.exe`) { return `"C:/Program Files/LLVM/bin/{name}.exe"` }
|
||||
return name
|
||||
}
|
||||
|
||||
# the icon resource for the executable, or "" when there is no .ico to make it from
|
||||
function windows_icon_res(icon: pointer) -> pointer {
|
||||
if icon == "" { return "" }
|
||||
var ico = icon
|
||||
let n = len(icon)
|
||||
if n > 4 and icon[n - 4..n] == ".png" { ico = icon[0..n - 4] + ".ico" }
|
||||
if not Fs.exists(ico) { return "" }
|
||||
let rc = tmp_path("app.rc")
|
||||
let res = tmp_path("app.res")
|
||||
# the resource compiler resolves a relative name against its own directory, not ours
|
||||
let abs = win_slashes(capture_line(`cygpath -m "$(pwd)/{ico}"`))
|
||||
if not write_file(rc, `1 ICON "{abs}"` + nl()) { return "" }
|
||||
# -fo rather than /FO: Git Bash rewrites an argument that starts with "/" into a path
|
||||
if not shq(`{llvm_tool("llvm-rc")} -fo "{res}" "{rc}"`) {
|
||||
err("ludic bundle: warning: the icon resource did not compile; the game has no icon\n")
|
||||
return ""
|
||||
}
|
||||
return res
|
||||
}
|
||||
|
||||
function cmd_bundle_windows() -> int {
|
||||
let entry = parse_build_args(2)
|
||||
if g_argerr { return 1 }
|
||||
if entry == "" { return no_entry() }
|
||||
|
||||
let m = read_root_manifest()
|
||||
let name = app_name(m)
|
||||
let root = `build/{name}`
|
||||
shell(`rm -rf "{root}"`)
|
||||
shell(`mkdir -p "{root}"`)
|
||||
|
||||
# 1. the executable - staged under a name with no spaces, as on macOS
|
||||
let res = windows_icon_res(manifest_app(m, "icon"))
|
||||
let staged = tmp_path("bundle_exe")
|
||||
var flags = "--windowed --gui"
|
||||
if res != "" { flags = flags + ` --link "{res}"` }
|
||||
if not shq(`{ludicc()} {flags} {entry} -o {staged}`) { return 1 }
|
||||
if not shq(`cp "{staged}.exe" "{root}/{name}.exe"`) {
|
||||
err("ludic bundle: could not place the executable\n")
|
||||
return 1
|
||||
}
|
||||
|
||||
# 2. the assets, exactly as the .app packs them
|
||||
let roots = pack_roots(m, 999)
|
||||
var packed = false
|
||||
let files = pack_gather_all(roots)
|
||||
pack_add_packages(files)
|
||||
pack_sort(files)
|
||||
if len(files.names) > 0 {
|
||||
let staged_pak = tmp_path("game.lpak")
|
||||
if not pack_write(staged_pak, files) { return 1 }
|
||||
if not shq(`cp "{staged_pak}" "{root}/game.lpak"`) {
|
||||
err("ludic bundle: could not place the asset pack\n")
|
||||
return 1
|
||||
}
|
||||
packed = true
|
||||
}
|
||||
|
||||
# 3. what to mount and where the game writes
|
||||
var idx = ""
|
||||
if packed { idx = idx + "pack game.lpak" + nl() }
|
||||
idx = idx + `home {name}` + nl()
|
||||
let splash = manifest_app(m, "splash")
|
||||
if splash != "" and packed {
|
||||
idx = idx + `splash {splash}` + nl()
|
||||
idx = idx + `splashbg {string(hex_color(manifest_app(m, "splash_bg")))}` + nl()
|
||||
}
|
||||
write_file(`{root}/packs.index`, idx)
|
||||
|
||||
print(`bundled {root}`)
|
||||
if res == "" { print(" no icon: put an .ico beside `app icon` in package.ludic (assets/app/icon.ico)") }
|
||||
if not packed { print(" no assets packed: nothing under assets/ and no `pack` line in package.ludic") }
|
||||
return 0
|
||||
}
|
||||
|
||||
function cmd_bundle() -> int {
|
||||
if host_windows() { return cmd_bundle_windows() }
|
||||
if not is_darwin() {
|
||||
err("ludic bundle: a .app is a macOS bundle; this host is not macOS\n")
|
||||
err("ludic bundle: this host is neither macOS (a .app) nor Windows (a folder)\n")
|
||||
return 1
|
||||
}
|
||||
let entry = parse_build_args(2)
|
||||
|
|
@ -247,8 +346,8 @@ function cmd_bundle() -> int {
|
|||
# and in `ps`, so it takes the display name rather than the file name - spaces
|
||||
# and all, which is what every shipped Mac application does.
|
||||
let contents = `{root}/Contents`
|
||||
run(`rm -rf "{root}"`)
|
||||
run(`mkdir -p "{contents}/MacOS" "{contents}/Resources"`)
|
||||
shell(`rm -rf "{root}"`)
|
||||
shell(`mkdir -p "{contents}/MacOS" "{contents}/Resources"`)
|
||||
|
||||
# 1. the binary, built windowed - a bundled game never wants the headless path.
|
||||
# It is compiled to a scratch path and copied in, because a display name is
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue