import { chmodSync, cpSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, renameSync, rmSync, statSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { basename, dirname, join, resolve } from "node:path"; import { spawnSync } from "node:child_process"; const appRoot = resolve(import.meta.dirname, ".."); const packageJson = JSON.parse(readFileSync(resolve(appRoot, "package.json"), "utf8")); const packageLockPath = resolve(appRoot, "package-lock.json"); const bundledNodeVersion = process.env.FEYNMAN_BUNDLED_NODE_VERSION ?? process.version.slice(1); function fail(message) { console.error(`[feynman] ${message}`); process.exit(1); } function run(command, args, options = {}) { const result = spawnSync(command, args, { stdio: "inherit", ...options, }); if (result.status !== 0) { fail(`${command} ${args.join(" ")} failed with code ${result.status ?? 1}`); } } function runCapture(command, args, options = {}) { const result = spawnSync(command, args, { encoding: "utf8", stdio: ["ignore", "pipe", "pipe"], ...options, }); if (result.status !== 0) { const errorOutput = result.stderr?.trim() || result.stdout?.trim() || "unknown error"; fail(`${command} ${args.join(" ")} failed: ${errorOutput}`); } return result.stdout.trim(); } function detectTarget() { if (process.platform === "darwin" && process.arch === "arm64") { return { id: "darwin-arm64", nodePlatform: "darwin", nodeArch: "arm64", bundleExtension: "tar.gz", launcher: "unix", }; } if (process.platform === "darwin" && process.arch === "x64") { return { id: "darwin-x64", nodePlatform: "darwin", nodeArch: "x64", bundleExtension: "tar.gz", launcher: "unix", }; } if (process.platform === "linux" && process.arch === "arm64") { return { id: "linux-arm64", nodePlatform: "linux", nodeArch: "arm64", bundleExtension: "tar.gz", launcher: "unix", }; } if (process.platform === "linux" && process.arch === "x64") { return { id: "linux-x64", nodePlatform: "linux", nodeArch: "x64", bundleExtension: "tar.gz", launcher: "unix", }; } if (process.platform === "win32" && process.arch === "arm64") { return { id: "win32-arm64", nodePlatform: "win", nodeArch: "arm64", bundleExtension: "zip", launcher: "windows", }; } if (process.platform === "win32" && process.arch === "x64") { return { id: "win32-x64", nodePlatform: "win", nodeArch: "x64", bundleExtension: "zip", launcher: "windows", }; } fail(`unsupported platform ${process.platform}/${process.arch}`); } function nodeArchiveName(target) { if (target.nodePlatform === "win") { return `node-v${bundledNodeVersion}-${target.nodePlatform}-${target.nodeArch}.zip`; } return `node-v${bundledNodeVersion}-${target.nodePlatform}-${target.nodeArch}.tar.xz`; } function ensureBundledWorkspace() { run(process.execPath, [resolve(appRoot, "scripts", "prepare-runtime-workspace.mjs")], { cwd: appRoot }); } function copyPackageFiles(appDir) { cpSync(resolve(appRoot, "package.json"), resolve(appDir, "package.json")); for (const entry of packageJson.files) { const normalized = entry.endsWith("/") ? entry.slice(0, -1) : entry; const source = resolve(appRoot, normalized); if (!existsSync(source)) continue; const destination = resolve(appDir, normalized); mkdirSync(dirname(destination), { recursive: true }); cpSync(source, destination, { recursive: true }); } cpSync(packageLockPath, resolve(appDir, "package-lock.json")); } function extractTarball(archivePath, destination, compressionFlag) { run("tar", [compressionFlag, archivePath, "-C", destination]); } function extractZip(archivePath, destination) { if (process.platform === "win32") { run("powershell", [ "-NoProfile", "-Command", `Expand-Archive -LiteralPath '${archivePath.replace(/'/g, "''")}' -DestinationPath '${destination.replace(/'/g, "''")}' -Force`, ]); return; } run("unzip", ["-q", archivePath, "-d", destination]); } function findSingleDirectory(path) { const entries = readdirSync(path).filter((entry) => !entry.startsWith(".")); if (entries.length !== 1) { fail(`expected exactly one directory in ${path}, found: ${entries.join(", ")}`); } const child = resolve(path, entries[0]); if (!statSync(child).isDirectory()) { fail(`expected ${child} to be a directory`); } return child; } function installBundledNode(bundleRoot, target, stagingRoot) { const archiveName = nodeArchiveName(target); const archivePath = resolve(stagingRoot, archiveName); const url = `https://nodejs.org/dist/v${bundledNodeVersion}/${archiveName}`; run("curl", ["-fsSL", url, "-o", archivePath]); const extractRoot = resolve(stagingRoot, "node-dist"); mkdirSync(extractRoot, { recursive: true }); if (archiveName.endsWith(".zip")) { extractZip(archivePath, extractRoot); } else { extractTarball(archivePath, extractRoot, "-xJf"); } const extractedDir = findSingleDirectory(extractRoot); renameSync(extractedDir, resolve(bundleRoot, "node")); } function writeLauncher(bundleRoot, target) { if (target.launcher === "unix") { const launcherPath = resolve(bundleRoot, "feynman"); writeFileSync( launcherPath, [ "#!/bin/sh", "set -eu", 'ROOT="$(CDPATH= cd -- "$(dirname -- "$0")" && pwd)"', 'exec "$ROOT/node/bin/node" "$ROOT/app/bin/feynman.js" "$@"', "", ].join("\n"), "utf8", ); chmodSync(launcherPath, 0o755); return; } writeFileSync( resolve(bundleRoot, "feynman.cmd"), [ "@echo off", "setlocal", 'set "ROOT=%~dp0"', '"%ROOT%node\\node.exe" "%ROOT%app\\bin\\feynman.js" %*', "", ].join("\r\n"), "utf8", ); writeFileSync( resolve(bundleRoot, "feynman.ps1"), [ '$Root = Split-Path -Parent $MyInvocation.MyCommand.Path', '& "$Root\\node\\node.exe" "$Root\\app\\bin\\feynman.js" @args', "", ].join("\r\n"), "utf8", ); } function validateBundle(bundleRoot, target) { const nodeExecutable = target.launcher === "windows" ? resolve(bundleRoot, "node", "node.exe") : resolve(bundleRoot, "node", "bin", "node"); run(nodeExecutable, ["-e", "require('./app/.feynman/npm/node_modules/better-sqlite3'); console.log('better-sqlite3 ok')"], { cwd: bundleRoot, }); } function packBundle(bundleRoot, target, outDir) { const archiveName = `${basename(bundleRoot)}.${target.bundleExtension}`; const archivePath = resolve(outDir, archiveName); rmSync(archivePath, { force: true }); if (target.bundleExtension === "zip") { if (process.platform === "win32") { run("powershell", [ "-NoProfile", "-Command", `Compress-Archive -Path '${bundleRoot.replace(/'/g, "''")}\\*' -DestinationPath '${archivePath.replace(/'/g, "''")}' -Force`, ]); } else { run("zip", ["-qr", archivePath, basename(bundleRoot)], { cwd: resolve(bundleRoot, "..") }); } return archivePath; } run("tar", ["-czf", archivePath, basename(bundleRoot)], { cwd: resolve(bundleRoot, "..") }); return archivePath; } function main() { const target = detectTarget(); const stagingRoot = mkdtempSync(join(tmpdir(), "feynman-native-")); const outDir = resolve(appRoot, "dist", "release"); const bundleRoot = resolve(stagingRoot, `feynman-${packageJson.version}-${target.id}`); const appDir = resolve(bundleRoot, "app"); mkdirSync(outDir, { recursive: true }); mkdirSync(appDir, { recursive: true }); ensureBundledWorkspace(); copyPackageFiles(appDir); run("npm", ["ci", "--omit=dev", "--ignore-scripts", "--no-audit", "--no-fund", "--loglevel", "error"], { cwd: appDir }); const appFeynmanDir = resolve(appDir, ".feynman"); extractTarball(resolve(appFeynmanDir, "runtime-workspace.tgz"), appFeynmanDir, "-xzf"); rmSync(resolve(appFeynmanDir, "runtime-workspace.tgz"), { force: true }); run(process.execPath, [resolve(appDir, "scripts", "patch-embedded-pi.mjs")], { cwd: appDir }); installBundledNode(bundleRoot, target, stagingRoot); writeLauncher(bundleRoot, target); validateBundle(bundleRoot, target); const archivePath = packBundle(bundleRoot, target, outDir); console.log(`[feynman] native bundle ready: ${archivePath}`); } main();