Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions packages/compiler/src/backend/native-toolchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
createVendorArchives,
MBEDTLS_VERSION,
QJS_COMMIT,
usesVendoredZlib,
ZLIB_VERSION,
} from "./vendor-archives.js";
import {
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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]
: []
: []),
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
61 changes: 61 additions & 0 deletions packages/compiler/src/backend/vendor-archives.test.ts
Original file line number Diff line number Diff line change
@@ -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"]);
});
40 changes: 33 additions & 7 deletions packages/compiler/src/backend/vendor-archives.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<CcDriver, "argv">): boolean {
return driver.argv[0] === "zig";
}

/** The archiver spelling for a vendored prerequisite built with `driver`. */
export function vendorArArgv(driver: Pick<CcDriver, "argv">): 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<CcDriver, "argv" | "target">): boolean {
return driver.target !== null || driverUsesZig(driver);
}

export interface VendorArchiveContext {
runtimeSrcDir(): string;
targetPlatform(driver: CcDriver): string;
Expand Down Expand Up @@ -91,13 +116,13 @@ export function createVendorArchives(context: VendorArchiveContext) {
driver: Pick<CcDriver, "argv" | "target">,
environmentFingerprint: string,
): Promise<string> {
// 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) => {
Expand Down Expand Up @@ -240,8 +265,8 @@ export function createVendorArchives(context: VendorArchiveContext) {
async function buildEngineArchiveDirect(sanitize: boolean, driver: CcDriver, cacheRoot: string, cacheDir: string): Promise<string> {
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,
Expand Down Expand Up @@ -525,16 +550,17 @@ export function createVendorArchives(context: VendorArchiveContext) {
* SCRIPTC_TARGET adds a per-target cache flavor (the lre-objects story):
* TUs compile with `zig cc -target <triple>` 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,
buildIdentity: string,
cacheRoot: string = vendorBuildCacheRoot(),
): Promise<string> {
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;
Expand Down