#!/usr/bin/env node import { createHash } from "node:crypto"; import { spawnSync } from "node:child_process"; import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { dirname, join, resolve } from "node:path"; const [mode, infoArg, outputArg] = process.argv.slice(2); if ((mode !== "cc" && mode !== "ld") || !infoArg || !outputArg) { console.error("usage: node link.mjs "); process.exitCode = 2; } else { const info = JSON.parse(readFileSync(infoArg, "utf8")); if (info.schema !== "scriptc.native-link-info.v1") { throw new Error(`unsupported native link info schema: ${info.schema}`); } const runtimePackage = JSON.parse( readFileSync(join(info.runtime_pack.root, "package.json"), "utf8"), ); if ( runtimePackage.name !== info.runtime_pack.package || runtimePackage.version !== info.runtime_pack.version ) { throw new Error( `runtime pack identity mismatch: expected ${info.runtime_pack.package}@${info.runtime_pack.version} ` + `at ${info.runtime_pack.root}, found ${runtimePackage.name ?? ""}@${runtimePackage.version ?? ""}`, ); } const run = (command, args, options = {}) => { const result = spawnSync(command, args, { stdio: "inherit", ...options }); if (result.error) throw result.error; if (result.status !== 0) throw new Error(`${command} exited ${result.status}`); }; const capture = (command, args) => { const result = spawnSync(command, args, { encoding: "utf8" }); if (result.error) throw result.error; if (result.status !== 0) throw new Error(result.stderr || `${command} exited ${result.status}`); return result.stdout.trim(); }; const infoPath = resolve(infoArg); const output = resolve(outputArg); const buildDir = join(dirname(infoPath), `.native-link-${mode}`); rmSync(buildDir, { recursive: true, force: true }); mkdirSync(buildDir, { recursive: true }); const runtimeObjects = []; const vendorArchives = []; for (const [kind, artifacts] of [["objects", info.runtime_pack.objects], ["archives", info.runtime_pack.archives]]) { for (const artifact of artifacts) { if (artifact.path.startsWith("/") || artifact.path.split(/[\\/]/).includes("..")) throw new Error("invalid runtime artifact path"); const bytes = readFileSync(join(info.runtime_pack.root, artifact.path)); if (bytes.length !== artifact.size || createHash("sha256").update(bytes).digest("hex") !== artifact.sha256) { throw new Error(`runtime pack hash mismatch: ${artifact.path}`); } const destination = join(buildDir, artifact.path); mkdirSync(dirname(destination), { recursive: true }); writeFileSync(destination, bytes, { mode: 0o400 }); (kind === "objects" ? runtimeObjects : vendorArchives).push(destination); } } const inputs = [ info.program.object, ...info.ffi.libraries, ...runtimeObjects, ...vendorArchives, ]; const libraries = info.link.system_libraries.map((name) => `-l${name}`); const frameworks = info.link.frameworks.flatMap((name) => ["-framework", name]); if (mode === "cc") { // Darwin compiler drivers add libSystem themselves. It remains explicit // in the document because a direct ld invocation must name it. const driverLibraries = info.link.system_libraries .filter((name) => name !== "System") .map((name) => `-l${name}`); run("clang", [ ...info.link.driver_flags, ...inputs, ...driverLibraries, ...frameworks, "-o", output, ]); } else { const sdk = capture("xcrun", ["--sdk", "macosx", "--show-sdk-path"]); const sdkVersion = capture("xcrun", ["--sdk", "macosx", "--show-sdk-version"]); const linker = capture("xcrun", ["--sdk", "macosx", "--find", "ld"]); run(linker, [ "-arch", info.target.architecture, "-platform_version", "macos", info.target.minimum_os, sdkVersion, "-syslibroot", sdk, ...(info.link.driver_flags.includes("-Wl,-dead_strip") ? ["-dead_strip"] : []), ...inputs, ...libraries, ...frameworks, "-o", output, ]); } }