lecodes-cli 2.4.5 → 2.4.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,7 +2,7 @@ import { execFileSync, spawn, spawnSync } from "node:child_process"
2
2
  import { copyFileSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, statSync, writeFileSync, linkSync } from "node:fs"
3
3
  import { homedir, tmpdir } from "node:os"
4
4
  import { delimiter, join, resolve } from "node:path"
5
- import { cachedArtifactSha, downloadReleaseArchive, fetchReleaseProduct, macPlatformKey, semverNewer, type ManifestVersion } from "./releases"
5
+ import { cachedArtifactSha, downloadReleaseArchive, fetchReleaseProduct, macPlatformKey, newestForPlatform, semverNewer, type ManifestFile, type ManifestVersion } from "./releases"
6
6
  import { CliError, note, warnErr } from "../cli"
7
7
  import { makeStackDemangler } from "../compile/nativeStack"
8
8
 
@@ -237,10 +237,12 @@ export const resolveDesktopExe = async (renderer: DesktopRenderer = "gl"): Promi
237
237
  return exe
238
238
  }
239
239
 
240
- /** Which native host runs a bundle: the desktop (a window, a GPU, pixels) or the windowless server
240
+ /** Which native host runs a bundle: the desktop (a window, a GPU, pixels), the windowless server
241
241
  * (hosts/desktop lecodes-server: the same engine on Filament's NOOP backend — the dedicated
242
- * multiplayer server, a headless client, an offline simulation run; no UI hit-test, no pixels). */
243
- export type HostKind = "desktop" | "server"
242
+ * multiplayer server, a headless client, an offline simulation run; no UI hit-test, no pixels), or
243
+ * the bake tool (lecodes-bake: that windowless host with the lightmap bake built in and the optional
244
+ * engines — physics, 2D, audio, net, navmesh — left out; `lecodes lightmap bake` runs the app in it). */
245
+ export type HostKind = "desktop" | "server" | "bake"
244
246
 
245
247
  const serverExeName = () => (process.platform === "win32" ? "lecodes-server.exe" : "lecodes-server")
246
248
 
@@ -266,8 +268,106 @@ export const resolveServerExe = async (renderer: DesktopRenderer = "gl"): Promis
266
268
  return exe
267
269
  }
268
270
 
271
+ /*
272
+ * The lightmap bake's host, lecodes-bake (hosts/desktop CREATOR_DESKTOP_BAKE): the bake is NOT in the
273
+ * shipped desktop / server hosts, it is a tool binary of its own — published as the release product
274
+ * "bake" (scripts/release/*, platform keys windows-x64 / linux-x64: the bake traces on Vulkan ray query
275
+ * or OptiX, neither of which a Mac has) from the same build machine and VERSION as the desktop host.
276
+ * Resolution: LECODES_BAKE_EXE → the cached build of the desktop host's selected version (the one
277
+ * `render --desktop` runs: the bake then comes from the same release as the engine the level renders
278
+ * with) → the newest cached one when the manifest is out of reach → a download: that version when
279
+ * the manifest carries a bake for it, else the newest bake published for this platform (the selected
280
+ * desktop may predate the first bake release). Cache: ~/.lecodes/bake/bake-<version>/, beside matc's
281
+ * and cmgen's. The renderer (gl / vulkan) does not apply: one binary, on Filament's NOOP backend.
282
+ */
283
+
284
+ const bakeExeName = () => (process.platform === "win32" ? "lecodes-bake.exe" : "lecodes-bake")
285
+
286
+ const bakePlatformKey = (): string => {
287
+ if (process.platform === "win32") return "windows-x64"
288
+ if (process.platform === "linux") return "linux-x64"
289
+ throw new CliError(`The lightmap bake host (lecodes-bake) isn't available on ${process.platform}: the bake traces with Vulkan ray query or OptiX — run it on Windows or Linux.`)
290
+ }
291
+
292
+ const bakeCacheRoot = () => join(homedir(), ".lecodes", "bake")
293
+
294
+ /** Locate lecodes-bake inside a cache / stage dir (the archive nests a `lecodes-bake/` folder). */
295
+ const findBakeExe = (dir: string): string | null => {
296
+ for (const entry of readdirSync(dir, { withFileTypes: true })) {
297
+ const p = join(dir, entry.name)
298
+ if (entry.isFile() && entry.name === bakeExeName()) return p
299
+ if (entry.isDirectory()) { const found = findBakeExe(p); if (found) return found }
300
+ }
301
+ return null
302
+ }
303
+
304
+ /** Versions with a working lecodes-bake in the cache, newest first. */
305
+ export const installedBakeVersions = (): string[] => {
306
+ try {
307
+ return readdirSync(bakeCacheRoot(), { withFileTypes: true })
308
+ .filter((e) => e.isDirectory() && e.name.startsWith("bake-"))
309
+ .map((e) => e.name.slice("bake-".length))
310
+ .filter((v) => findBakeExe(join(bakeCacheRoot(), `bake-${v}`)) !== null)
311
+ .sort(semverNewer)
312
+ } catch {
313
+ return []
314
+ }
315
+ }
316
+
317
+ /** The lightmap bake's host binary (see the block comment above). */
318
+ export const resolveBakeExe = async (): Promise<string> => {
319
+ const override = process.env.LECODES_BAKE_EXE
320
+ if (override) {
321
+ if (!existsSync(override)) throw new CliError(`LECODES_BAKE_EXE points at "${override}" which doesn't exist.`)
322
+ return resolve(override)
323
+ }
324
+ const key = bakePlatformKey()
325
+ const selected = selectedDesktopVersion()
326
+ const cachedDir = (version: string) => join(bakeCacheRoot(), `bake-${version}`)
327
+ const cachedSelected = existsSync(cachedDir(selected)) ? findBakeExe(cachedDir(selected)) : null
328
+ if (cachedSelected) return cachedSelected
329
+ let pick: { version: string, file: ManifestFile } | null
330
+ try {
331
+ const { versions } = await fetchReleaseProduct("bake", "lightmap bake host (lecodes-bake)")
332
+ const exact = versions.find((v) => v.version === selected)?.files[key]
333
+ pick = exact ? { version: selected, file: exact } : newestForPlatform(versions, key)
334
+ } catch (e) {
335
+ // Offline, or a manifest without the product yet: any cached bake host still bakes.
336
+ const newest = installedBakeVersions()[0]
337
+ if (newest) {
338
+ warnErr(`${e instanceof Error ? e.message : String(e)}\n Using the cached lightmap bake host ${newest}.`)
339
+ return findBakeExe(cachedDir(newest))!
340
+ }
341
+ throw new CliError(
342
+ `${e instanceof Error ? e.message : String(e)}\n` +
343
+ " The lightmap bake runs in lecodes-bake, downloaded from the release manifest on first use. Point LECODES_BAKE_EXE at a local build " +
344
+ "(hosts/desktop: build.ps1 -Bake → build-bake/lecodes-bake.exe; build.sh with CREATOR_DESKTOP_BAKE=ON → build-bake-linux/lecodes-bake).",
345
+ )
346
+ }
347
+ if (!pick) {
348
+ throw new CliError(
349
+ `The lightmap bake host (lecodes-bake) isn't published for ${key} yet. Point LECODES_BAKE_EXE at a local build ` +
350
+ "(hosts/desktop: build.ps1 -Bake → build-bake/lecodes-bake.exe; build.sh with CREATOR_DESKTOP_BAKE=ON → build-bake-linux/lecodes-bake).",
351
+ )
352
+ }
353
+ const dir = cachedDir(pick.version)
354
+ let exe = existsSync(dir) ? findBakeExe(dir) : null
355
+ if (!exe) {
356
+ if (pick.version !== selected) note(`No lightmap bake host is published for LeCodes Desktop ${selected} — using lecodes-bake ${pick.version}.`)
357
+ note(`Downloading the lightmap bake host (lecodes-bake ${pick.version} [${key}], ${(pick.file.size / 1024 / 1024).toFixed(1)} MB)…`)
358
+ await downloadReleaseArchive(pick.file, {
359
+ cacheRoot: bakeCacheRoot(),
360
+ dest: dir,
361
+ validate: (d) => (findBakeExe(d) ? null : "The downloaded archive doesn't contain the lecodes-bake binary."),
362
+ })
363
+ exe = findBakeExe(dir)
364
+ if (!exe) throw new CliError(`Bake host cache at ${dir} is broken — delete the folder and retry.`)
365
+ }
366
+ return exe
367
+ }
368
+
269
369
  const resolveHostExe = (host: HostKind | undefined, renderer: DesktopRenderer | undefined): Promise<string> =>
270
- host === "server" ? resolveServerExe(renderer) : resolveDesktopExe(renderer)
370
+ host === "bake" ? resolveBakeExe() : host === "server" ? resolveServerExe(renderer) : resolveDesktopExe(renderer)
271
371
 
272
372
  /** `lecodes desktop update`: move the render host to the newest published release (never below the
273
373
  * CLI's pin — a manifest that somehow regressed under it is an error, not a downgrade). Saves the
@@ -422,8 +522,8 @@ export type DesktopRenderOptions = {
422
522
  /** Which host binary to run (see DesktopRenderer). Must match the renderer the bundle's shaders
423
523
  * were staged for — resolve it once per command and pass the same value to both. */
424
524
  renderer?: DesktopRenderer
425
- /** The hidden desktop host (default: pixels, a PNG) or the windowless server host (no pixels — the
426
- * run is judged by its output alone; `outPng` is ignored). */
525
+ /** The hidden desktop host (default: pixels, a PNG) or a windowless one — the server, or the lightmap
526
+ * bake's lecodes-bake (no pixels: the run is judged by its output alone; `outPng` is ignored). */
427
527
  host?: HostKind
428
528
  width: number
429
529
  height: number
@@ -561,13 +661,14 @@ export const runDesktopDev = async (url: string, opts: DesktopWindowOpts = {}):
561
661
  }
562
662
 
563
663
  /** Run a compiled bundle in the hidden desktop host and dump the final frame as a PNG — or, with
564
- * `host: "server"`, in the windowless host, which paints nothing: the outcome is its output. */
664
+ * `host: "server"` / `"bake"`, in a windowless host, which paints nothing: the outcome is its output. */
565
665
  export const runDesktopRender = async (
566
666
  js: string,
567
667
  resources: { name: string, absPath?: string }[],
568
668
  opts: DesktopRenderOptions,
569
669
  ): Promise<DesktopRenderOutcome> => {
570
- const server = opts.host === "server"
670
+ const server = opts.host === "server" || opts.host === "bake" // windowless: no PNG
671
+ const what = opts.host === "bake" ? "bake" : server ? "server" : "desktop"
571
672
  // macOS: the in-repo Metal host (hosts/desktop, phase 11) carries the headless render contract like
572
673
  // the other two platforms (CREATOR_HIDDEN / CREATOR_DUMP_PNG / CREATOR_SCRIPT); the old Swift shell
573
674
  // did not — a cached one is refused by its missing lecodes-server, not here.
@@ -614,7 +715,7 @@ export const runDesktopRender = async (
614
715
  child.kill()
615
716
  fail(new CliError(`The native render timed out after ${opts.timeoutMs}ms (raise --timeout, or lower --frames).`))
616
717
  }, opts.timeoutMs)
617
- child.on("error", (e) => { clearTimeout(killer); fail(new CliError(`Can't run the ${server ? "server" : "desktop"} host: ${e.message}`)) })
718
+ child.on("error", (e) => { clearTimeout(killer); fail(new CliError(`Can't run the ${what} host: ${e.message}`)) })
618
719
  child.on("exit", (code) => {
619
720
  clearTimeout(killer)
620
721
  const output = demangle(chunks.join(""))
@@ -624,7 +725,7 @@ export const runDesktopRender = async (
624
725
  const detail = tail.length > 0 ? `\n${tail}` : ""
625
726
  fail(new CliError(code === 0
626
727
  ? `The renderer exited cleanly but produced no PNG — did the app open a screen?${detail}`
627
- : `The ${server ? "server host" : "renderer"} exited with code ${code}.${detail}`))
728
+ : `The ${server ? `${what} host` : "renderer"} exited with code ${code}.${detail}`))
628
729
  })
629
730
  })
630
731
  } finally {