mirror of
https://github.com/vercel-labs/scriptc.git
synced 2026-10-22 05:59:31 +08:00
- Add glibc feature visibility and trailing libm linkage. - Gate native host clang builds with regression coverage and Ubuntu CI. Co-authored-by: farangkao <7920378+farangkao@users.noreply.github.com> Co-authored-by: Swadhin Biswas <107450069+swadhinbiswas@users.noreply.github.com>
255 lines
12 KiB
TypeScript
255 lines
12 KiB
TypeScript
/* The SCRIPTC_CC / SCRIPTC_TARGET driver contract (cc.ts):
|
|
*
|
|
* - The DEFAULT path is pinned: no SCRIPTC_CC (or SCRIPTC_CC=clang) resolves to
|
|
* ["clang"]. macOS keeps zero extra args; host Linux adds glibc's
|
|
* -D_GNU_SOURCE compile flag and trailing -lm link library.
|
|
* SCRIPTC_TARGET without zigcc is an error, not a silently different clang
|
|
* invocation.
|
|
* - SCRIPTC_CC=zigcc drives `zig cc`. Host-native zigcc builds must produce a
|
|
* working binary (zig cc is clang underneath); SCRIPTC_TARGET cross builds
|
|
* must produce an ELF for linux triples and reject the features whose
|
|
* inputs are host-built (vendored archives, system libs, kqueue units).
|
|
*
|
|
* The native-clang leg runs everywhere (and is selected alone by the
|
|
* ubuntu/glibc CI job). The zig-requiring legs skip when zig is not on
|
|
* PATH — the driver pins above run everywhere.
|
|
*/
|
|
import { execFile, execFileSync } from "node:child_process";
|
|
import { mkdtemp, readFile, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { promisify } from "node:util";
|
|
import { describe, expect, test } from "vitest";
|
|
import { compileC, resolveCc } from "../src/backend/cc.js";
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
|
|
function zigOnPath(): boolean {
|
|
try {
|
|
execFileSync("zig", ["version"], { stdio: "ignore" });
|
|
return true;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
test("default driver keeps bare clang while selecting host platform flags", () => {
|
|
for (const env of [{}, { SCRIPTC_CC: "clang" }, { SCRIPTC_CC: "" }]) {
|
|
const d = resolveCc(env, "darwin");
|
|
expect(d.argv).toEqual(["clang"]);
|
|
expect(d.target).toBeNull();
|
|
expect(d.targetArgs).toEqual([]);
|
|
expect(d.linkArgs).toEqual([]);
|
|
|
|
const linux = resolveCc(env, "linux");
|
|
expect(linux.argv).toEqual(["clang"]);
|
|
expect(linux.target).toBeNull();
|
|
expect(linux.targetArgs).toEqual(["-D_GNU_SOURCE"]);
|
|
expect(linux.linkArgs).toEqual(["-lm"]);
|
|
}
|
|
});
|
|
|
|
test("SCRIPTC_TARGET without zigcc is an error, never a silent clang cross build", () => {
|
|
expect(() => resolveCc({ SCRIPTC_TARGET: "aarch64-linux-gnu" })).toThrow(/requires SCRIPTC_CC=zigcc/);
|
|
expect(() => resolveCc({ SCRIPTC_CC: "clang", SCRIPTC_TARGET: "aarch64-linux-gnu" })).toThrow(/requires SCRIPTC_CC=zigcc/);
|
|
});
|
|
|
|
test("unknown SCRIPTC_CC values are rejected", () => {
|
|
expect(() => resolveCc({ SCRIPTC_CC: "gcc" })).toThrow(/unknown SCRIPTC_CC/);
|
|
});
|
|
|
|
test("zigcc resolves to `zig cc`; linux triples add -target and -D_GNU_SOURCE", () => {
|
|
const native = resolveCc({ SCRIPTC_CC: "zigcc" }, "darwin");
|
|
expect(native.argv).toEqual(["zig", "cc"]);
|
|
expect(native.targetArgs).toEqual([]);
|
|
expect(native.linkArgs).toEqual([]);
|
|
|
|
const nativeLinux = resolveCc({ SCRIPTC_CC: "zigcc" }, "linux");
|
|
expect(nativeLinux.targetArgs).toEqual(["-D_GNU_SOURCE"]);
|
|
expect(nativeLinux.linkArgs).toEqual(["-lm"]);
|
|
|
|
const cross = resolveCc({ SCRIPTC_CC: "zigcc", SCRIPTC_TARGET: "aarch64-linux-gnu.2.36" });
|
|
expect(cross.argv).toEqual(["zig", "cc"]);
|
|
expect(cross.targetArgs).toEqual(["-target", "aarch64-linux-gnu.2.36", "-D_GNU_SOURCE"]);
|
|
expect(cross.linkArgs).toEqual(["-lm"]);
|
|
|
|
// Non-linux triples get the -target but not glibc's visibility macro.
|
|
const mac = resolveCc({ SCRIPTC_CC: "zigcc", SCRIPTC_TARGET: "aarch64-macos" });
|
|
expect(mac.targetArgs).toEqual(["-target", "aarch64-macos"]);
|
|
expect(mac.linkArgs).toEqual([]);
|
|
|
|
// Windows triples too: mingw-w64 headers expose everything by default.
|
|
const win = resolveCc({ SCRIPTC_CC: "zigcc", SCRIPTC_TARGET: "x86_64-windows-gnu" });
|
|
expect(win.targetArgs).toEqual(["-target", "x86_64-windows-gnu"]);
|
|
expect(win.linkArgs).toEqual([]);
|
|
});
|
|
|
|
/** Runs body with SCRIPTC_CC/SCRIPTC_TARGET set, restoring the previous values. */
|
|
async function withCcEnv(cc: string | undefined, target: string | undefined, body: () => Promise<void>): Promise<void> {
|
|
const prevCc = process.env["SCRIPTC_CC"];
|
|
const prevTarget = process.env["SCRIPTC_TARGET"];
|
|
if (cc === undefined) delete process.env["SCRIPTC_CC"];
|
|
else process.env["SCRIPTC_CC"] = cc;
|
|
if (target === undefined) delete process.env["SCRIPTC_TARGET"];
|
|
else process.env["SCRIPTC_TARGET"] = target;
|
|
try {
|
|
await body();
|
|
} finally {
|
|
if (prevCc === undefined) delete process.env["SCRIPTC_CC"];
|
|
else process.env["SCRIPTC_CC"] = prevCc;
|
|
if (prevTarget === undefined) delete process.env["SCRIPTC_TARGET"];
|
|
else process.env["SCRIPTC_TARGET"] = prevTarget;
|
|
}
|
|
}
|
|
|
|
const HOST_CLANG_C = '#include <stdio.h>\nint main(void) { printf("clang says hi\\n"); return 0; }\n';
|
|
|
|
test("host-native clang static build compiles the runtime and runs", async () => {
|
|
const dir = await mkdtemp(join(tmpdir(), "scr-host-clang-"));
|
|
const cPath = join(dir, "program.c");
|
|
await writeFile(cPath, HOST_CLANG_C);
|
|
const outPath = join(dir, "program");
|
|
// This exact default path caught both Linux regressions: without
|
|
// -D_GNU_SOURCE the glibc headers hide declarations used by the runtime;
|
|
// with the macro but no trailing -lm, the runtime's fmod/exp2 references
|
|
// fail to link. macOS keeps exercising its unchanged bare-clang path.
|
|
await withCcEnv(undefined, undefined, () => compileC({ cPath, outPath }));
|
|
const { stdout } = await execFileAsync(outPath);
|
|
expect(stdout).toBe("clang says hi\n");
|
|
});
|
|
|
|
const HELLO_C = '#include <stdio.h>\nint main(void) { printf("zigcc says hi\\n"); return 0; }\n';
|
|
|
|
describe.skipIf(!zigOnPath())("zig cc builds (zig on PATH)", () => {
|
|
test("host-native zigcc build compiles the runtime and runs", async () => {
|
|
const dir = await mkdtemp(join(tmpdir(), "scr-zigcc-"));
|
|
const cPath = join(dir, "program.c");
|
|
await writeFile(cPath, HELLO_C);
|
|
const outPath = join(dir, "program");
|
|
await withCcEnv("zigcc", undefined, () => compileC({ cPath, outPath }));
|
|
const { stdout } = await execFileAsync(outPath);
|
|
expect(stdout).toBe("zigcc says hi\n");
|
|
});
|
|
|
|
test("cross build for aarch64-linux-gnu produces an ELF", async () => {
|
|
const dir = await mkdtemp(join(tmpdir(), "scr-zigcc-cross-"));
|
|
const cPath = join(dir, "program.c");
|
|
await writeFile(cPath, HELLO_C);
|
|
const outPath = join(dir, "program");
|
|
await withCcEnv("zigcc", "aarch64-linux-gnu.2.36", () => compileC({ cPath, outPath }));
|
|
const magic = (await readFile(outPath)).subarray(0, 4);
|
|
expect([...magic]).toEqual([0x7f, 0x45, 0x4c, 0x46]); // \x7fELF
|
|
});
|
|
|
|
test("linux cross builds accept fetch natively (no libcurl); the curl reference keeps its soname stub", async () => {
|
|
const dir = await mkdtemp(join(tmpdir(), "scr-zigcc-gate-"));
|
|
const cPath = join(dir, "program.c");
|
|
// Reference the fetch unit the way every emitted fetch program does
|
|
// (emitter.ts emits the scr_fetch_install call): an unreferenced unit
|
|
// would let the linker drop the dependency chain and prove nothing.
|
|
await writeFile(cPath, 'void scr_fetch_install(void);\nint main(void) {\n scr_fetch_install();\n return 0;\n}\n');
|
|
const outPath = join(dir, "program");
|
|
await withCcEnv("zigcc", "aarch64-linux-gnu.2.36", async () => {
|
|
// The NATIVE fetch rides the socket units (scr_net/scr_http/scr_tls
|
|
// + the vendored zlib objects) — the produced ELF carries NO
|
|
// libcurl dependency at all.
|
|
await compileC({ cPath, outPath, dynamic: true, fetch: true });
|
|
const elf = await readFile(outPath);
|
|
expect([...elf.subarray(0, 4)]).toEqual([0x7f, 0x45, 0x4c, 0x46]); // \x7fELF
|
|
expect(elf.includes("libcurl.so.4")).toBe(false);
|
|
// The retired curl REFERENCE (SCRIPTC_FETCH_CURL=1) still links the
|
|
// generated import stub: the binary records DT_NEEDED libcurl.so.4
|
|
// — the string sits verbatim in the ELF's dynamic string table —
|
|
// and binds the target system's real libcurl at load time.
|
|
process.env["SCRIPTC_FETCH_CURL"] = "1";
|
|
try {
|
|
await compileC({ cPath, outPath, dynamic: true, fetch: true });
|
|
const curlElf = await readFile(outPath);
|
|
expect([...curlElf.subarray(0, 4)]).toEqual([0x7f, 0x45, 0x4c, 0x46]);
|
|
expect(curlElf.includes("libcurl.so.4")).toBe(true);
|
|
} finally {
|
|
delete process.env["SCRIPTC_FETCH_CURL"];
|
|
}
|
|
});
|
|
}, 600_000);
|
|
|
|
test("cross build for x86_64-windows-gnu produces a PE and accepts events/net/http/dgram/tls/watch/zlib/--dynamic/fetch", async () => {
|
|
const dir = await mkdtemp(join(tmpdir(), "scr-zigcc-win-"));
|
|
const cPath = join(dir, "program.c");
|
|
await writeFile(cPath, HELLO_C);
|
|
const outPath = join(dir, "program.exe");
|
|
await withCcEnv("zigcc", "x86_64-windows-gnu", async () => {
|
|
await compileC({ cPath, outPath });
|
|
const magic = (await readFile(outPath)).subarray(0, 2);
|
|
expect([...magic]).toEqual([0x4d, 0x5a]); // MZ
|
|
// The units with Windows arms link and produce a PE: events (CRT
|
|
// signal + stdin probes), net/http (winsock + the WSAPoll poller
|
|
// backend), dgram/dns (the winsock datagram arm + ws2tcpip's
|
|
// getaddrinfo), tls (mbedTLS compiled for the triple, -lbcrypt for
|
|
// its entropy poll), watch (ReadDirectoryChangesW), zlib
|
|
// (per-target vendored objects).
|
|
await compileC({ cPath, outPath, events: true, net: true, http: true, dgram: true, watch: true, zlib: true, tls: true });
|
|
const magic2 = (await readFile(outPath)).subarray(0, 2);
|
|
expect([...magic2]).toEqual([0x4d, 0x5a]);
|
|
// --dynamic: the engine archive cross-builds for the windows triple
|
|
// (buildEngineArchiveCross), the island's win32 arms (_msize, the
|
|
// winsock hostname) compile, and the link carries the 8MB PE stack
|
|
// reserve for ISL_MAIN_STACK_BUDGET. The Windows differential lane
|
|
// verifies the @dynamic corpus at runtime against the box's Node.
|
|
await compileC({ cPath, outPath, dynamic: true });
|
|
const magic3 = (await readFile(outPath)).subarray(0, 2);
|
|
expect([...magic3]).toEqual([0x4d, 0x5a]);
|
|
// fetch: NATIVE on win32 too (the socket units' win32 arms + the
|
|
// vendored zlib objects — no libcurl contract needed). The retired
|
|
// curl reference's soname-stub arm stays linux-only.
|
|
await compileC({ cPath, outPath, dynamic: true, fetch: true });
|
|
const magic4 = (await readFile(outPath)).subarray(0, 2);
|
|
expect([...magic4]).toEqual([0x4d, 0x5a]);
|
|
process.env["SCRIPTC_FETCH_CURL"] = "1";
|
|
try {
|
|
await expect(compileC({ cPath, outPath, dynamic: true, fetch: true })).rejects.toThrow(/fetch.*not supported under a cross target/s);
|
|
} finally {
|
|
delete process.env["SCRIPTC_FETCH_CURL"];
|
|
}
|
|
});
|
|
}, 600_000);
|
|
|
|
test("regex cross-compiles: the vendored libregexp objects build per target", async () => {
|
|
const dir = await mkdtemp(join(tmpdir(), "scr-zigcc-lre-"));
|
|
const cPath = join(dir, "program.c");
|
|
await writeFile(cPath, HELLO_C);
|
|
await withCcEnv("zigcc", "x86_64-windows-gnu", async () => {
|
|
await compileC({ cPath, outPath: join(dir, "win.exe"), regex: true });
|
|
const magic = (await readFile(join(dir, "win.exe"))).subarray(0, 2);
|
|
expect([...magic]).toEqual([0x4d, 0x5a]);
|
|
});
|
|
await withCcEnv("zigcc", "aarch64-linux-gnu.2.36", async () => {
|
|
await compileC({ cPath, outPath: join(dir, "linux"), regex: true });
|
|
const magic = (await readFile(join(dir, "linux"))).subarray(0, 4);
|
|
expect([...magic]).toEqual([0x7f, 0x45, 0x4c, 0x46]);
|
|
});
|
|
});
|
|
|
|
test("cross builds accept the event-loop units, regex, zlib, and tls (per-target poller backends, lre and zlib objects, mbedTLS)", async () => {
|
|
const dir = await mkdtemp(join(tmpdir(), "scr-zigcc-units-"));
|
|
const cPath = join(dir, "program.c");
|
|
await writeFile(cPath, HELLO_C);
|
|
const outPath = join(dir, "program");
|
|
await withCcEnv("zigcc", "aarch64-linux-gnu.2.36", () =>
|
|
compileC({ cPath, outPath, net: true, http: true, dgram: true, watch: true, events: true, regex: true, zlib: true, tls: true }),
|
|
);
|
|
const magic = (await readFile(outPath)).subarray(0, 4);
|
|
expect([...magic]).toEqual([0x7f, 0x45, 0x4c, 0x46]); // \x7fELF
|
|
});
|
|
|
|
test("cross builds accept --dynamic (the engine archive builds per target, no CMake)", async () => {
|
|
const dir = await mkdtemp(join(tmpdir(), "scr-zigcc-dyn-"));
|
|
const cPath = join(dir, "program.c");
|
|
await writeFile(cPath, HELLO_C);
|
|
const outPath = join(dir, "program");
|
|
await withCcEnv("zigcc", "aarch64-linux-gnu.2.36", () => compileC({ cPath, outPath, dynamic: true }));
|
|
const magic = (await readFile(outPath)).subarray(0, 4);
|
|
expect([...magic]).toEqual([0x7f, 0x45, 0x4c, 0x46]); // \x7fELF
|
|
}, 600_000);
|
|
});
|