import { promises as fs } from "node:fs"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { unpackProject, decodeData } from "../engine/archive.ts"; import { BuildManager } from "../server/builds.ts"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const arg = (n: string) => { const i = process.argv.indexOf(n); return i < 0 ? undefined : process.argv[i + 1]; }; const input = path.resolve(arg("--project") || "projects/MyGame"); let manager: BuildManager | undefined; try { const isDir = (await fs.stat(input)).isDirectory(); const project = unpackProject( new Uint8Array( await fs.readFile(isDir ? path.join(input, "project.forma.json") : input), ), ); const base = isDir ? input : path.dirname(input); const read = async (uri: string) => { if (uri.startsWith("data:")) return decodeData(uri); const clean = uri.replace(/^\/+/, "").replace(/^\.\//, ""); if (!/^(assets|engine)\/[a-zA-Z0-9_.-]+$/.test(clean)) throw Error("Invalid asset path"); const safe = async (folder: string) => { const file = await fs.realpath(path.join(folder, clean)); if (!file.startsWith((await fs.realpath(folder)) + path.sep)) throw Error("Asset escapes project"); return new Uint8Array(await fs.readFile(file)); }; if (clean.startsWith("assets/")) try { return await safe(base); } catch (e: any) { if (e.code !== "ENOENT") throw e; } return safe(path.join(root, "public")); }; manager = new BuildManager(base, read); await manager.init(); const options = { target: arg("--target") || "linux", name: arg("--name") || project.name, appId: arg("--app-id") || "games.forma.mygame", version: arg("--version") || "1.0.0", versionCode: Number(arg("--version-code") || 1), mode: arg("--mode") || "debug", fullscreen: process.argv.includes("--fullscreen"), }; const job = await manager.start(project, options, project.revision); let shown = 0; const stop = () => void manager!.cancel(job.id); process.once("SIGINT", stop); process.once("SIGTERM", stop); for (;;) { const j = manager.get(job.id); for (const line of j.logs.slice(shown)) console.log(line); shown = j.logs.length; if (!["queued", "building"].includes(j.status)) { if (j.status !== "succeeded") throw Error(j.error || j.status); for (const file of j.artifacts || []) console.log(await manager.artifact(j.id, file.name)); break; } await new Promise((r) => setTimeout(r, 750)); } } catch (e) { console.error(String(e)); process.exitCode = 1; } finally { await manager?.close(); }