import { constants, copyFileSync, chmodSync, existsSync, mkdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from "node:fs"; import { createRequire } from "node:module"; import { dirname, isAbsolute, join, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; export function nativeCliPackage(platform, architecture, libc) { const host = `${platform}-${architecture}`; if (host === "darwin-arm64" || host === "darwin-x64") return `@scriptc/cli-${host}`; if (host === "win32-x64") return "@scriptc/cli-win32-x64-msvc"; if (host === "linux-x64" || host === "linux-arm64") { if (libc !== "glibc" && libc !== "musl") throw new Error("could not identify the Linux C library"); return `@scriptc/cli-${host}-${libc === "musl" ? "musl" : "gnu"}`; } throw new Error(`scriptc has no native command for ${host}`); } function hostLibc() { if (process.platform !== "linux") return null; const report = process.report.getReport(); if (report.header.glibcVersionRuntime) return "glibc"; if (report.sharedObjects.some((path) => /(?:^|\/)ld-musl-|libc\.musl-/.test(path))) return "musl"; // A statically linked Node has no loaded libc in its process report. // Only select musl when its loader is present for this architecture. const arch = process.arch === "arm64" ? "aarch64" : "x86_64"; return existsSync(`/lib/ld-musl-${arch}.so.1`) ? "musl" : null; } export function relocateToolchain(manifest, sourceDirectory, destinationDirectory) { const relocated = { ...manifest }; const pathFrom = (value) => relative(destinationDirectory, resolve(sourceDirectory, value)); for (const name of ["ts7", "llvm_package", "runtime_pack", "runtime_sources", "declarations", "comptime", "wasi_node_runner"]) { if (typeof relocated[name] === "string") relocated[name] = pathFrom(relocated[name]); } for (const name of ["linker", "dsymutil", "archiver", "relocatable_linker"]) { const value = relocated[name]; if (typeof value === "string" && !isAbsolute(value) && /[/\\]/.test(value)) relocated[name] = pathFrom(value); } if (relocated.runtime_packs) relocated.runtime_packs = relocated.runtime_packs.map((pack) => ({ ...pack, path: pathFrom(pack.path) })); return relocated; } export function installNativeCli(directory, packageName = nativeCliPackage(process.platform, process.arch, hostLibc())) { directory = realpathSync(directory); const require = createRequire(join(directory, "package.json")); const packageManifest = JSON.parse(readFileSync(join(directory, "package.json"), "utf8")); const version = packageManifest.version; let platformManifest; try { platformManifest = require.resolve(`${packageName}/package.json`); } catch { throw new Error(`scriptc requires ${packageName}@${version}; reinstall with optional dependencies enabled`); } const identity = JSON.parse(readFileSync(platformManifest, "utf8")); if (identity.name !== packageName || identity.version !== version) throw new Error(`scriptc requires ${packageName}@${version}, found ${identity.version}`); const sourceDirectory = join(dirname(platformManifest), "dist", "bin"); const source = join(sourceDirectory, process.platform === "win32" ? "scriptc.exe" : "scriptc"); const original = JSON.parse(readFileSync(source + ".json", "utf8")); if (original.schema !== "scriptc.native-toolchain.v1" || original.compiler_version !== version) throw new Error("native compiler toolchain version does not match this installation"); const bin = join(directory, "bin"); mkdirSync(bin, { recursive: true }); const manifest = relocateToolchain(original, sourceDirectory, bin); const packs = new Map((manifest.runtime_packs ?? []).map((pack) => [pack.target, pack])); for (const suffix of ["darwin-arm64", "darwin-x64", "linux-x64-gnu", "linux-arm64-gnu", "linux-x64-musl", "linux-arm64-musl", "win32-x64-msvc", "wasm32-wasi", "ios-arm64", "ios-simulator-arm64", "android-arm64"]) { const name = `@scriptc/runtime-${suffix}`; if (packageManifest.optionalDependencies?.[name] === undefined) continue; let path; try { path = require.resolve(`${name}/package.json`); } catch { continue; } const packIdentity = JSON.parse(readFileSync(path, "utf8")); if (packIdentity.name !== name || packIdentity.version !== version) throw new Error(`scriptc requires ${name}@${version}`); const pack = JSON.parse(readFileSync(join(dirname(path), "runtime-pack.json"), "utf8")); if (pack.schema !== "scriptc.runtime-pack.v1" || pack.package !== name || pack.version !== version) { throw new Error(`invalid runtime pack installed for ${name}@${version}`); } if (!packs.has(pack.target.name)) packs.set(pack.target.name, { target: pack.target.name, path: relative(bin, dirname(path)) }); } manifest.runtime_packs = [...packs.values()]; // npm normalizes modes for payloads outside package bin entries. These // tools are launched directly by the compiler after installation. if (process.platform !== "win32") { for (const path of [manifest.ts7, manifest.comptime, join(manifest.llvm_package, "bin/scriptc-llvm-codegen")]) { chmodSync(resolve(bin, path), 0o755); } } // The common filename lets npm's Windows shim invoke a native executable. // POSIX bin links also execute this file directly, without a JS launcher. const destination = join(bin, "scriptc.exe"); const staged = destination + `.install-${process.pid}`; try { copyFileSync(source, staged, constants.COPYFILE_FICLONE); chmodSync(staged, 0o755); writeFileSync(staged + ".json", JSON.stringify(manifest, null, 2) + "\n"); renameSync(staged + ".json", destination + ".json"); renameSync(staged, destination); } finally { rmSync(staged, { force: true }); rmSync(staged + ".json", { force: true }); } return destination; } if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) { try { const directory = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const manifest = JSON.parse(readFileSync(join(directory, "package.json"), "utf8")); // A source checkout builds the native distribution separately. pnpm pack // replaces workspace ranges with release versions before publication. const workspace = Object.values(manifest.optionalDependencies ?? {}).some((value) => String(value).startsWith("workspace:")); if (!workspace) installNativeCli(directory); } catch (error) { process.stderr.write(`scriptc: ${error instanceof Error ? error.message : String(error)}\n`); process.exitCode = 1; } }