diff --git a/packages/compiler/src/backend/cc.ts b/packages/compiler/src/backend/cc.ts index cc317dec1..a9423e1bd 100644 --- a/packages/compiler/src/backend/cc.ts +++ b/packages/compiler/src/backend/cc.ts @@ -716,12 +716,108 @@ function androidNdkSysroot(env: NodeJS.ProcessEnv): string { ); } +/** MinGW-w64 install roots to probe, in order, when SCRIPTC_MINGW_ROOT is + * unset — mirrors the Android NDK auto-discovery above (explicit env var + * first, then the common install locations for the platform). MSYS2's own + * default (C:\msys64\mingw64) covers both its official installer and every + * package manager that wraps it (winget, choco, scoop); a bare mingw-w64 + * standalone install commonly lands at C:\mingw64. */ +function mingwRootCandidates(env: NodeJS.ProcessEnv): string[] { + const explicit = env["SCRIPTC_MINGW_ROOT"]; + if (explicit !== undefined && explicit !== "") return [explicit]; + return ["C:\\msys64\\mingw64", "C:\\mingw64", "C:\\msys2\\mingw64"]; +} + +function findMingwRoot(env: NodeJS.ProcessEnv): string { + for (const root of mingwRootCandidates(env)) { + if (existsSync(join(root, "include", "dirent.h"))) return root; + } + throw new Error( + "no MinGW-w64 install was found — clang's own default target on Windows is the MSVC ABI, " + + "whose C runtime has none of the POSIX headers (dirent.h, unistd.h) or types (ssize_t) " + + "this project's runtime C sources need. Install MSYS2 (winget install MSYS2.MSYS2) and its " + + "mingw-w64-x86_64-gcc package (pacman -S mingw-w64-x86_64-gcc), and/or set " + + "SCRIPTC_MINGW_ROOT to the mingw64 directory (its default install path is C:\\msys64\\mingw64).", + ); +} + +/** clang targeting MinGW still links against a few of GCC's OWN runtime + * support libraries (libgcc.a, libgcc_eh.a — software float/int helpers and + * the DWARF-2 unwinder MinGW's exception model uses), which live under a + * GCC-VERSION-specific subdirectory clang has no reason to already know + * (it is not the compiler that put them there) — glob for it rather than + * pinning a version this project doesn't control the upgrade schedule of. */ +function findMingwGccLibDir(mingwRoot: string): string | null { + const base = join(mingwRoot, "lib", "gcc", "x86_64-w64-mingw32"); + let versions: string[]; + try { + versions = readdirSync(base); + } catch { + return null; + } + versions.sort().reverse(); + for (const version of versions) { + const dir = join(base, version); + if (existsSync(join(dir, "libgcc.a"))) return dir; + } + return null; +} + /** Resolve native platform flags independently of the machine running tests, - * so the host-Linux contract remains pinned on every development host. */ -function nativePlatformArgs(platform: NodeJS.Platform): Pick { - return platform === "linux" - ? { targetArgs: ["-D_GNU_SOURCE"], linkArgs: ["-lm"] } - : { targetArgs: [], linkArgs: [] }; + * so the host-Linux contract remains pinned on every development host. + * `viaZig` distinguishes the two host-native drivers `resolveCc` can select + * (bare clang vs `zig cc`): on win32 they need OPPOSITE handling, unlike + * every other platform/driver combination here. Bare clang's own default + * target on Windows is the MSVC ABI, with none of the POSIX surface + * (dirent.h, unistd.h, ssize_t) this project's C sources need — it must be + * pointed at an external MinGW-w64 install via --target=x86_64-w64-mingw32. + * `zig cc`, by contrast, ships its OWN bundled mingw-w64 sysroot for its + * `x86_64-windows-gnu` target (see the module doc comment) and needs no + * external MinGW at all — worse, `--target=x86_64-w64-mingw32` is clang's + * LLVM triple spelling, not one of zig's own target-query spellings, and + * zig's `-target` parser hard-errors on it ("unable to parse target query + * 'x86_64-w64-mingw32': UnknownOperatingSystem"), confirmed against a real + * zig 0.16 install. Feeding it to zig doesn't just make findMingwRoot's + * external-install requirement pointless for zig users — it breaks the + * build outright. */ +function nativePlatformArgs( + platform: NodeJS.Platform, + env: NodeJS.ProcessEnv = process.env, + viaZig = false, +): Pick { + if (platform === "linux") return { targetArgs: ["-D_GNU_SOURCE"], linkArgs: ["-lm"] }; + if (platform === "win32" && !viaZig) { + // clang's own default target here is the MSVC ABI (this project never + // targeted Windows until now — its docs only ever named macOS/Linux + // toolchains). MinGW-w64's headers/libs are the POSIX-compatible + // surface clang already knows how to target via --target=x86_64-w64- + // mingw32 — the exact same shape as Linux's -D_GNU_SOURCE branch above: + // a target-specific flag set on the SAME compiler, not a different one. + const mingwRoot = findMingwRoot(env); + const gccLibDir = findMingwGccLibDir(mingwRoot); + return { + targetArgs: [ + "--target=x86_64-w64-mingw32", + `-isystem${mingwRoot}\\include`, + ], + linkArgs: [ + `-L${mingwRoot}\\lib`, + `-B${mingwRoot}\\bin`, + // libgcc.a/libgcc_eh.a (software helpers + the unwinder MinGW's + // exception model uses) — see findMingwGccLibDir. Absent only for a + // MinGW install with no GCC at all (clang-only toolchains exist), + // which nothing in this codebase's runtime C currently needs. + ...(gccLibDir !== null ? [`-L${gccLibDir}`] : []), + // winpthreads: MinGW's (pulled in by ) + // aliases clock_gettime/nanosleep to ITS OWN 64-bit-safe + // clock_gettime64/nanosleep64 (MinGW's own convention — unrelated + // to glibc's _TIME_BITS=64 Y2038 story, but the same idea); those + // symbols' actual definitions live in winpthreads, not the CRT. + "-lwinpthread", + ], + }; + } + return { targetArgs: [], linkArgs: [] }; } export function resolveCc( @@ -730,19 +826,32 @@ export function resolveCc( ): CcDriver { const cc = env["SCRIPTC_CC"] ?? ""; const target = env["SCRIPTC_TARGET"] ?? ""; - const hostArgs = nativePlatformArgs(hostPlatform); + // Computed lazily, only at the two return sites that actually use it + // (below): every cross-compile target (iOS, Android, wasm, ...) builds + // its own explicit targetArgs/linkArgs and never touches this. On + // win32, nativePlatformArgs THROWS when no MinGW-w64 install is found — + // eagerly computing this here would make every zig-cc cross-compile + // target refuse to resolve on a Windows host with no local MinGW, + // even though none of them need it. if (cc === "" || cc === "clang") { if (target !== "") { throw new Error( `SCRIPTC_TARGET=${target} requires SCRIPTC_CC=zigcc — the default clang path has no cross-target sysroots.`, ); } - return { argv: ["clang"], target: null, zigTarget: null, ...hostArgs }; + return { argv: ["clang"], target: null, zigTarget: null, ...nativePlatformArgs(hostPlatform, env) }; } if (cc !== "zigcc") { throw new Error(`unknown SCRIPTC_CC '${cc}' (supported: clang, zigcc)`); } - if (target === "") return { argv: ["zig", "cc"], target: null, zigTarget: null, ...hostArgs }; + if (target === "") { + return { + argv: ["zig", "cc"], + target: null, + zigTarget: null, + ...nativePlatformArgs(hostPlatform, env, /* viaZig */ true), + }; + } if (target.includes("wasi") && target !== "wasm32-wasi") { throw new Error(`unsupported WASI target '${target}' (supported: wasm32-wasi)`); }