287 lines
8.5 KiB
JavaScript
287 lines
8.5 KiB
JavaScript
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 installAppDependencies(appDir, stagingRoot) {
|
|
const depsDir = resolve(stagingRoot, "prod-deps");
|
|
rmSync(depsDir, { recursive: true, force: true });
|
|
mkdirSync(depsDir, { recursive: true });
|
|
|
|
cpSync(resolve(appRoot, "package.json"), resolve(depsDir, "package.json"));
|
|
cpSync(packageLockPath, resolve(depsDir, "package-lock.json"));
|
|
|
|
run("npm", ["ci", "--omit=dev", "--ignore-scripts", "--no-audit", "--no-fund", "--loglevel", "error"], {
|
|
cwd: depsDir,
|
|
});
|
|
|
|
cpSync(resolve(depsDir, "node_modules"), resolve(appDir, "node_modules"), { recursive: true });
|
|
}
|
|
|
|
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);
|
|
installAppDependencies(appDir, stagingRoot);
|
|
|
|
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();
|