mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-11-02 07:43:42 +08:00
Scope-B milestone NI-1, runtime side. Programs whose IR has a `nodeIsland.*` operation link scr_node_island.c and the SCR_NODE_ISLANDS build of scr_async.c; every other program links neither, byte for byte. - scr_node_island.c dlopens the bridge of a libnode pack on the first call (RTLD_NOW | RTLD_GLOBAL), checks its bridge ABI and that it links exactly the Node version recorded at build time, starts one Node environment and runs the embedded bootstrap (scr_node_island_host.cjs). A run that never calls into Node never maps libnode. The pack directory is SCRIPTC_LIBNODE at run time, else the recorded path; a `$ORIGIN/` path resolves against the executable's directory (the beside packaging, and the hook for shipping Node with the program). - scr_node_bridge.h is the C ABI between the runtime and the bridge; the runtime includes no Node or V8 header. The bridge (packages/libnode-common/bridge/scr_node_bridge.cc) initializes Node once with no default signal handling, no debug-signal handler or inspector, no stdio reinitialization and one platform worker, creates the environment from the built-in snapshot, routes Node's process exit to the runtime's exit sequence, promotes libnode to the global scope for native addons, and runs calls in a CallbackScope with a TryCatch. build-pack.mjs assembles a pack (libnode, the bridge, manifest.json) from a Node --shared build and the matching headers. - The SCR_NODE_ISLANDS build of scr_async.c adds a loop slot through which the runtime's loop drives Node's until NI-3 hosts the runtime's queues on Node's libuv: it keeps the program alive while Node has ref'd work, sleeps on Node's backend fd up to its next timer, and runs one non-blocking turn per loop turn. - The `nodeIsland` runtime feature (rule, link features, unit table, pack matrix row and scr_async.c variant), the nodeIsland.call/callSync libCalls, the install call in main, and the build-time record of the pack (node-islands/libnode.ts: --libnode=<pack>|beside or SCRIPTC_LIBNODE, SC8005 when none is usable, Node >=24.13.1). - The zero-cost gate and the bench nm test require that no good-code executable defines a Node island symbol. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
109 lines
4.0 KiB
JavaScript
109 lines
4.0 KiB
JavaScript
#!/usr/bin/env node
|
|
/* Assembles a libnode pack: the directory a scriptc program with Node
|
|
* islands loads Node from (scope-B design §2.16.3). Until the pack CI
|
|
* matrix (NI-13) publishes @scriptc/libnode-<platform> packages, packs are
|
|
* assembled by hand from a Node `--shared` build and the matching headers:
|
|
*
|
|
* node packages/libnode-common/build-pack.mjs \
|
|
* --libnode <path to libnode.so.<ABI> or libnode.<ABI>.dylib> \
|
|
* --headers <include/node directory of the same Node release> \
|
|
* --out <pack directory> [--license <Node's LICENSE>] [--cxx <compiler>]
|
|
*
|
|
* The pack holds libnode, the bridge (bridge/scr_node_bridge.cc) compiled
|
|
* against those headers, Node's license and manifest.json, which the build
|
|
* of a program with Node islands reads (packages/compiler/src/node-islands/
|
|
* libnode.ts). Programs find a pack through SCRIPTC_LIBNODE or --libnode. */
|
|
import { execFileSync } from "node:child_process";
|
|
import { copyFileSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
|
|
import { createHash } from "node:crypto";
|
|
import { basename, join, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { parseArgs } from "node:util";
|
|
|
|
const here = fileURLToPath(new URL(".", import.meta.url));
|
|
const runtimeSrc = join(here, "../runtime/src");
|
|
|
|
/** The bridge ABI the runtime expects (scr_node_bridge.h). */
|
|
function bridgeAbi() {
|
|
const header = readFileSync(join(runtimeSrc, "scr_node_bridge.h"), "utf8");
|
|
return Number(/#define SCR_NODE_BRIDGE_ABI (\d+)u/.exec(header)[1]);
|
|
}
|
|
|
|
/** The Node version and NODE_MODULE_VERSION of a header directory. */
|
|
export function nodeVersionOf(headers) {
|
|
const text = readFileSync(join(headers, "node_version.h"), "utf8");
|
|
const define = (name) => Number(new RegExp(`#define ${name} (\\d+)`).exec(text)[1]);
|
|
return {
|
|
nodeVersion: `${define("NODE_MAJOR_VERSION")}.${define("NODE_MINOR_VERSION")}.${define("NODE_PATCH_VERSION")}`,
|
|
moduleVersion: define("NODE_MODULE_VERSION"),
|
|
};
|
|
}
|
|
|
|
function fileEntry(path) {
|
|
return {
|
|
size: statSync(path).size,
|
|
sha256: createHash("sha256").update(readFileSync(path)).digest("hex"),
|
|
};
|
|
}
|
|
|
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
const { values } = parseArgs({
|
|
options: {
|
|
libnode: { type: "string" },
|
|
headers: { type: "string" },
|
|
out: { type: "string" },
|
|
license: { type: "string" },
|
|
cxx: { type: "string", default: process.platform === "darwin" ? "clang++" : "g++" },
|
|
},
|
|
});
|
|
if (!values.libnode || !values.headers || !values.out) {
|
|
console.error("usage: build-pack.mjs --libnode <lib> --headers <include/node> --out <dir>");
|
|
process.exit(2);
|
|
}
|
|
const out = resolve(values.out);
|
|
const headers = resolve(values.headers);
|
|
mkdirSync(out, { recursive: true });
|
|
const libnodeName = basename(values.libnode);
|
|
copyFileSync(values.libnode, join(out, libnodeName));
|
|
const bridgeName =
|
|
process.platform === "darwin" ? "libscr_node_bridge.dylib" : "libscr_node_bridge.so";
|
|
const link =
|
|
process.platform === "darwin"
|
|
? ["-dynamiclib", join(out, libnodeName), "-Wl,-rpath,@loader_path"]
|
|
: ["-shared", `-L${out}`, `-l:${libnodeName}`, "-Wl,-rpath,$ORIGIN"];
|
|
execFileSync(
|
|
values.cxx,
|
|
[
|
|
"-std=c++20",
|
|
"-O2",
|
|
"-fPIC",
|
|
"-fvisibility=hidden",
|
|
"-I",
|
|
runtimeSrc,
|
|
"-I",
|
|
headers,
|
|
join(here, "bridge/scr_node_bridge.cc"),
|
|
...link,
|
|
"-o",
|
|
join(out, bridgeName),
|
|
],
|
|
{ stdio: "inherit" },
|
|
);
|
|
if (values.license) copyFileSync(values.license, join(out, "LICENSE"));
|
|
const { nodeVersion, moduleVersion } = nodeVersionOf(headers);
|
|
const manifest = {
|
|
schema: "scriptc.libnode-pack.v1",
|
|
nodeVersion,
|
|
moduleVersion,
|
|
bridgeAbi: bridgeAbi(),
|
|
platform: process.platform,
|
|
arch: process.arch,
|
|
files: {
|
|
[libnodeName]: fileEntry(join(out, libnodeName)),
|
|
[bridgeName]: fileEntry(join(out, bridgeName)),
|
|
},
|
|
};
|
|
writeFileSync(join(out, "manifest.json"), `${JSON.stringify(manifest, null, 2)}\n`);
|
|
console.log(`libnode pack for Node ${nodeVersion} in ${out}`);
|
|
}
|