import { cpSync, existsSync, mkdirSync, readdirSync, rmSync, writeFileSync } from "node:fs" import { dirname, join, resolve } from "node:path" import { collectAssets, loadAppIdentity, RUNTIME_VERSION, SOLIDRT_VERSION, type ManifestFont } from "../lib/project" import type { Mode } from "../lib/mode" import { runnerGlLibs } from "../lib/artifacts" import { fail } from "../lib/fail" import { isolateAssetPath } from "../bundle/bundler" // The canonical flat pack folder (okf/plans/client-storage-updates.md, Pack // output): runner + manifest.json + bundle.fluxbc + assets/. The manifest // enumerates exactly the files belonging to the version - the runner is // deliberately unlisted - and, unlike dev manifests, carries the full app // identity (org, displayName). Fonts are the dev manifest's: the files the // app binds in `solidrt.fonts` and its dropped aliases, which the runner // layers over the fonts its program carries (lattice::Fonts); the defaults // live in the runtime, so a default app packs no font file at all. function hashHex(bytes: Uint8Array): string { return new Bun.CryptoHasher("sha256").update(bytes).digest("hex") } export type PackFolder = { /** The canonical manifest JSON string (serialized once, written verbatim). */ manifest: string /** Files to place in the folder: absolute source -> folder-relative path. */ copies: Array<{ from: string; to: string }> /** Build outputs to place in the folder (isolate bytecode): folder-relative path + bytes. */ files: Array<{ to: string; bytes: Buffer }> /** The font annotations the manifest carries, for the pack report. */ fonts: ManifestFont[] } // `isolates` are the app's isolate bundles compiled to bytecode; they ship as // the manifest assets isolates/.fluxbc (the production runtime has no // compiler, so pack never ships isolate source). export function buildPackFolder(mode: Mode, bytecode: Buffer, isolates: { id: string; bytecode: Buffer }[]): PackFolder { let identity = loadAppIdentity(mode.entry, mode.projectDir) // File mode collects no assets, so the dir is only ever joined in project // mode; the entry's dir stands in to keep the paths absolute. let projectDir = mode.projectDir ?? dirname(mode.entry) let { assets, fonts, icon } = collectAssets(mode.projectDir) let copies = assets.map((a) => ({ from: join(projectDir, a.path), to: a.path })) let files = isolates.map((i) => ({ to: isolateAssetPath(i.id, "fluxbc"), bytes: i.bytecode })) for (let f of files) assets.push({ path: f.to, sha256: hashHex(f.bytes), size: f.bytes.length }) assets.sort((a, b) => (a.path < b.path ? -1 : 1)) let manifest = JSON.stringify({ appId: identity.appId, org: identity.org, displayName: identity.displayName, ...(icon ? { icon } : {}), runtimeVersion: RUNTIME_VERSION, solidrtVersion: SOLIDRT_VERSION, bundle: { path: "bundle.fluxbc", sha256: hashHex(bytecode), size: bytecode.length }, ...(assets.length ? { assets } : {}), ...(fonts.length ? { fonts } : {}), }) return { manifest, copies, files, fonts } } /** * Write the folder. An existing output dir is only reused when it is empty or * already a pack folder (has a manifest.json) - then the files this pack owns * (runner, manifest, bundle, assets/) are replaced; anything else in it is * left alone but never a reason to touch an unrelated directory. */ export function writePackFolder(outDir: string, runnerPath: string, bytecode: Buffer, folder: PackFolder) { let existing = existsSync(outDir) ? readdirSync(outDir) : null if (existing && existing.length > 0 && !existing.includes("manifest.json")) { fail(`${resolve(outDir)} exists and is not a pack folder; choose another --output`) } let runnerName = "solidrt" + (process.platform === "win32" ? ".exe" : "") let glLibs = runnerGlLibs(runnerPath) mkdirSync(outDir, { recursive: true }) rmSync(join(outDir, "assets"), { recursive: true, force: true }) rmSync(join(outDir, "isolates"), { recursive: true, force: true }) for (let name of ["manifest.json", "bundle.fluxbc", runnerName, ...glLibs.map((lib) => lib.name)]) { rmSync(join(outDir, name), { force: true }) } // Dereference: the runner path may itself be a symlink (contributor dist // layouts); the folder must carry the real binary. cpSync(runnerPath, join(outDir, runnerName), { dereference: true }) if (process.platform !== "win32") { Bun.spawnSync(["chmod", "+x", join(outDir, runnerName)]) } // The runner loads its GL libraries from next to itself; a folder pack must // carry them like the platform package does. for (let lib of glLibs) { cpSync(lib.path, join(outDir, lib.name), { dereference: true }) } writeFileSync(join(outDir, "bundle.fluxbc"), bytecode) writeFileSync(join(outDir, "manifest.json"), folder.manifest) for (let { from, to } of folder.copies) { let dest = join(outDir, to) mkdirSync(dirname(dest), { recursive: true }) cpSync(from, dest) } for (let { to, bytes } of folder.files) { let dest = join(outDir, to) mkdirSync(dirname(dest), { recursive: true }) writeFileSync(dest, bytes) } }