From ccb4c3200812c1686e1dbb61fec187f5080d3b91 Mon Sep 17 00:00:00 2001 From: euxaristia Date: Fri, 28 Aug 2026 01:12:01 -0400 Subject: [PATCH] fix(backend): select the vendored toolchain from the driver, not the target The vendored quickjs and mbedTLS recipes and the zlib link keyed their toolchain choices on `driver.target === null`, which conflates a build being host-native with a POSIX toolchain and system libraries being present. Those are independent: under SCRIPTC_CC=zigcc a host build handed a clang-built vendor archive to a zig link, and on Windows it could not work at all, since there is no system `ar` and no system libz. Dispatch on the driver instead, matching the rule ensureLreObjects and ensureZlibObjects already follow. Behavior for the bare-clang host driver and for every cross build is unchanged. Refs #252 --- .../compiler/src/backend/native-toolchain.ts | 20 +++--- .../src/backend/vendor-archives.test.ts | 61 +++++++++++++++++++ .../compiler/src/backend/vendor-archives.ts | 40 +++++++++--- 3 files changed, 105 insertions(+), 16 deletions(-) create mode 100644 packages/compiler/src/backend/vendor-archives.test.ts diff --git a/packages/compiler/src/backend/native-toolchain.ts b/packages/compiler/src/backend/native-toolchain.ts index d66d2db7e..6ed941006 100644 --- a/packages/compiler/src/backend/native-toolchain.ts +++ b/packages/compiler/src/backend/native-toolchain.ts @@ -14,6 +14,7 @@ import { createVendorArchives, MBEDTLS_VERSION, QJS_COMMIT, + usesVendoredZlib, ZLIB_VERSION, } from "./vendor-archives.js"; import { @@ -4210,11 +4211,12 @@ async function compileCInternal( let lreObjects = regex && !dynamic ? lreObjectPaths(sanitize, driver, vendorBuildIdentity, vendorCacheRoot) : []; - // Vendored zlib is the CROSS story only — host builds keep the exact - // historical `-lz` system link (see CcOptions.zlib). The native fetch's - // gzip decoder rides the same objects/link. + // Vendored zlib serves every cross build and every zig-driven host build; + // only a bare-clang host build keeps the exact historical `-lz` system + // link (see CcOptions.zlib). The native fetch's gzip decoder rides the + // same objects/link. let zlibObjects = - ((opts.zlib ?? false) || nativeFetch) && driver.target !== null + ((opts.zlib ?? false) || nativeFetch) && usesVendoredZlib(driver) ? zlibObjectPaths(sanitize, driver, vendorBuildIdentity, vendorCacheRoot) : []; // The libcurl import stub is likewise CROSS-only — host builds keep the @@ -4362,11 +4364,11 @@ async function compileCInternal( // libz on hosts, the vendored per-target objects on cross builds) // also serves the native fetch's gzip decoder — spread exactly once. ...(opts.zlib - ? driver.target !== null + ? usesVendoredZlib(driver) ? ["-I", vendorZlibDir(), rt(join(rtDir, "scr_zlib.c")), ...zlibObjects] : [rt(join(rtDir, "scr_zlib.c"))] : nativeFetch - ? driver.target !== null + ? usesVendoredZlib(driver) ? ["-I", vendorZlibDir(), ...zlibObjects] : [] : []), @@ -4493,7 +4495,7 @@ async function compileCInternal( // --as-needed: host libz must follow scr_zlib.c/scr_fetch.c and every // generated/native input that references inflate symbols. Cross // builds use vendored zlib objects in the input section above. - ...(((opts.zlib ?? false) || nativeFetch) && driver.target === null + ...(((opts.zlib ?? false) || nativeFetch) && !usesVendoredZlib(driver) ? ["-lz"] : []), // glibc keeps libm separate from libc. This must trail the generated @@ -4694,7 +4696,7 @@ async function compileCInternal( ...(tlsCa && targetPlatform(driver) === "win32" ? ["-lcrypt32"] : []), ...(curlFetch && driver.target === null ? ["-lcurl"] : []), ...(dynamic && !driver.linkArgs.includes("-lm") ? ["-lm"] : []), - ...(((opts.zlib ?? false) || nativeFetch) && driver.target === null ? ["-lz"] : []), + ...(((opts.zlib ?? false) || nativeFetch) && !usesVendoredZlib(driver) ? ["-lz"] : []), ...driver.linkArgs, ]; // Both the wrapper dry run and dependency trace need the real build's @@ -4714,7 +4716,7 @@ async function compileCInternal( ...(dynamic && targetPlatform(driver) === "win32" ? ["-Wl,--stack,8388608"] : []), - ...(((opts.zlib ?? false) || nativeFetch) && driver.target === null ? ["-lz"] : []), + ...(((opts.zlib ?? false) || nativeFetch) && !usesVendoredZlib(driver) ? ["-lz"] : []), ...driver.linkArgs, ]; // A complete hit is checked before cross-target curl's generated import stub diff --git a/packages/compiler/src/backend/vendor-archives.test.ts b/packages/compiler/src/backend/vendor-archives.test.ts new file mode 100644 index 000000000..9959722c1 --- /dev/null +++ b/packages/compiler/src/backend/vendor-archives.test.ts @@ -0,0 +1,61 @@ +import { expect, test } from "vitest"; +import { resolveCc } from "./native-toolchain.js"; +import { driverUsesZig, usesVendoredZlib, vendorArArgv } from "./vendor-archives.js"; + +/** The vendored-prerequisite recipes and the zlib link once keyed their + * toolchain choices on `driver.target === null` — "is this a host build?" — + * which conflates the build being native with a POSIX toolchain and system + * libraries being present. Those are independent: `SCRIPTC_CC=zigcc` with no + * `SCRIPTC_TARGET` is a host build driven by zig, which supplies its own + * archiver and needs the vendored zlib. These pin the driver-based dispatch. */ + +test("bare clang host driver keeps the historical system ar and -lz", () => { + const driver = resolveCc({}, "linux"); + + expect(driver.argv).toEqual(["clang"]); + expect(driver.target).toBeNull(); + expect(driverUsesZig(driver)).toBe(false); + expect(vendorArArgv(driver)).toEqual(["ar"]); + expect(usesVendoredZlib(driver)).toBe(false); +}); + +test("host-native zigcc archives with zig ar, not a system ar", () => { + const driver = resolveCc({ SCRIPTC_CC: "zigcc" }, "linux"); + + // A host build (target === null) that is nonetheless driven by zig. + expect(driver.argv).toEqual(["zig", "cc"]); + expect(driver.target).toBeNull(); + expect(driverUsesZig(driver)).toBe(true); + // Regression: keying on `target === null` yielded ["ar"] here, handing a + // clang-built archive to a zig link — and failing outright on hosts with + // no system ar at all. + expect(vendorArArgv(driver)).toEqual(["zig", "ar"]); +}); + +test("host-native zigcc links the vendored zlib, not a system -lz", () => { + const driver = resolveCc({ SCRIPTC_CC: "zigcc" }, "win32"); + + expect(driver.target).toBeNull(); + // Regression: keying on `target === null` selected the system `-lz` (and + // omitted the vendored zlib headers), which no zig host toolchain provides. + expect(usesVendoredZlib(driver)).toBe(true); +}); + +test("cross builds keep using the zig toolchain and vendored zlib", () => { + const driver = resolveCc( + { SCRIPTC_CC: "zigcc", SCRIPTC_TARGET: "x86_64-linux-gnu" }, + "linux", + ); + + expect(driver.target).toBe("x86_64-linux-gnu"); + expect(driverUsesZig(driver)).toBe(true); + expect(vendorArArgv(driver)).toEqual(["zig", "ar"]); + expect(usesVendoredZlib(driver)).toBe(true); +}); + +test("the archiver spelling tracks the driver argv rather than a literal", () => { + // vendorArArgv must derive the zig spelling from argv[0], so a renamed or + // absolute driver spelling still archives with its own `ar` subcommand. + expect(vendorArArgv({ argv: ["zig", "cc"] })).toEqual(["zig", "ar"]); + expect(vendorArArgv({ argv: ["clang"] })).toEqual(["ar"]); +}); diff --git a/packages/compiler/src/backend/vendor-archives.ts b/packages/compiler/src/backend/vendor-archives.ts index 9bcfadbf0..5a3a02864 100644 --- a/packages/compiler/src/backend/vendor-archives.ts +++ b/packages/compiler/src/backend/vendor-archives.ts @@ -19,6 +19,31 @@ export const QJS_ENGINE_SOURCES = ["dtoa.c", "libregexp.c", "libunicode.c", "qui export const LRE_SOURCES = ["libregexp.c", "libunicode.c"] as const; export const ZLIB_SOURCES = ["adler32.c", "compress.c", "crc32.c", "deflate.c", "infback.c", "inffast.c", "inflate.c", "inftrees.c", "trees.c", "uncompr.c", "zutil.c"] as const; +/** A zig driver supplies its own archiver (`zig ar`) and its own vendored + * zlib. Only the bare-clang host driver may assume a system `ar` and a system + * `-lz`. + * + * Keying those choices on `driver.target === null` conflates "this build is + * native" with "a POSIX toolchain and system libraries are present". They are + * independent: under SCRIPTC_CC=zigcc a host build silently mixes a clang-built + * vendor archive into a zig link, and on Windows it cannot work at all, since + * there is no `ar` and no system libz. Mirrors the dispatch that + * ensureLreObjects and ensureZlibObjects already use. */ +export function driverUsesZig(driver: Pick): boolean { + return driver.argv[0] === "zig"; +} + +/** The archiver spelling for a vendored prerequisite built with `driver`. */ +export function vendorArArgv(driver: Pick): string[] { + return driverUsesZig(driver) ? [...driver.argv.slice(0, 1), "ar"] : ["ar"]; +} + +/** Whether this build links scriptc's vendored zlib objects instead of a + * system `-lz`: every cross build, and every zig-driven host build. */ +export function usesVendoredZlib(driver: Pick): boolean { + return driver.target !== null || driverUsesZig(driver); +} + export interface VendorArchiveContext { runtimeSrcDir(): string; targetPlatform(driver: CcDriver): string; @@ -91,13 +116,13 @@ export function createVendorArchives(context: VendorArchiveContext) { driver: Pick, environmentFingerprint: string, ): Promise { - // Native vendor recipes additionally use bare clang/ar; cross + // Bare-clang host recipes additionally use clang/ar; zig-driven // recipes use the zig driver for compilation and `zig ar`. Include every // executable that can affect the cached prerequisite, not just the // final program's driver. const commands = [ driver.argv[0] ?? "clang", - ...(driver.target === null ? ["clang", "ar"] : []), + ...(driver.target === null && !driverUsesZig(driver) ? ["clang", "ar"] : []), ].filter((command, index, all) => all.indexOf(command) === index); const identities = await Promise.all( commands.map(async (command) => { @@ -240,8 +265,8 @@ export function createVendorArchives(context: VendorArchiveContext) { async function buildEngineArchiveDirect(sanitize: boolean, driver: CcDriver, cacheRoot: string, cacheDir: string): Promise { const vendor = vendorEngineDir(); const archive = join(cacheDir, "libqjs.a"); - const compileArgv = driver.target === null ? ["clang"] : driver.argv; - const arArgv = driver.target === null ? ["ar"] : [...driver.argv.slice(0, 1), "ar"]; + const compileArgv = driver.argv; + const arArgv = vendorArArgv(driver); const cflags = [ "-std=gnu11", ...driver.targetArgs, @@ -525,7 +550,8 @@ export function createVendorArchives(context: VendorArchiveContext) { * SCRIPTC_TARGET adds a per-target cache flavor (the lre-objects story): * TUs compile with `zig cc -target ` and the archive is packed * with `zig ar` (llvm-ar — the host BSD ar has no business indexing ELF - * objects). Host builds keep the exact historical clang + ar recipe. */ + * objects). Bare-clang host builds keep the exact historical clang + ar + * recipe; a zig-driven host build uses zig cc and `zig ar` like a cross one. */ async function ensureTlsArchive( sanitize: boolean, driver: CcDriver, @@ -533,8 +559,8 @@ export function createVendorArchives(context: VendorArchiveContext) { cacheRoot: string = vendorBuildCacheRoot(), ): Promise { const flavor = `${sanitize ? "asan" : "plain"}-${vendorCacheTargetFlavor(driver)}-${buildIdentity}`; - const compileArgv = driver.target !== null ? driver.argv : ["clang"]; - const arArgv = driver.target !== null ? [...driver.argv.slice(0, 1), "ar"] : ["ar"]; + const compileArgv = driver.argv; + const arArgv = vendorArArgv(driver); const vendor = vendorTlsDir(); const archive = tlsArchivePath(sanitize, driver, buildIdentity, cacheRoot); if (await validVendorArtifact(archive)) return archive;