Files
2026-08-27 21:09:14 +00:00

349 lines
16 KiB
TypeScript

/**
* Windows sysroot (xwin splat) handling for Windows cross-compiles.
*
* Cross-compiling for Windows needs the MSVC CRT/STL + Windows SDK + ATL
* headers and import libraries (see `Config.winsysroot`). Provisioned
* sysroots come from the agent image (.buildkite/Dockerfile /
* scripts/bootstrap.sh bake an xwin splat at /opt/winsysroot) or from a
* developer-created splat (docs/project/building-windows.mdx). When none is
* present, CI builds fetch one into the per-build cache dir at configure
* time — the build never depends on what the agent image happens to carry.
*
* The fetch is two steps, both pinned:
* 1. Download the xwin release binary for the build host (GitHub).
* 2. Run `xwin splat` — xwin downloads the CRT/SDK/ATL packages from
* Microsoft's CDN and lays them out like a Visual Studio install so a
* single `/winsysroot` flag works for clang-cl and lld-link.
* `--accept-license` accepts Microsoft's license terms for those
* components (the same terms the Windows CI images accept when
* installing VS Build Tools).
*
* Idempotent: a sentinel check (SDK include + lib trees with the target
* arch's kernel32 import lib, plus the ATL headers) makes re-runs a no-op,
* so calling this on every build only costs time when the sysroot is
* genuinely absent or incomplete.
*/
import { spawnSync } from "node:child_process";
import { copyFileSync, existsSync, mkdirSync, readdirSync, rmSync, symlinkSync } from "node:fs";
import { dirname, isAbsolute, join, resolve } from "node:path";
import type { Arch, Config } from "./config.ts";
import { downloadWithRetry, extractTarGz, extractZip } from "./download.ts";
import { BuildError } from "./error.ts";
/**
* Pinned xwin release — https://github.com/Jake-Shadle/xwin/releases
* Keep in sync with the baked splat in .buildkite/Dockerfile (ARG
* XWIN_VERSION) and scripts/bootstrap.sh (xwin_version).
*/
export const XWIN_VERSION = "0.9.0";
/**
* The Windows SDK and MSVC CRT versions the splat is pinned to. Passing them
* to xwin explicitly means a Visual Studio manifest update can't silently
* move the toolchain to a different SDK/CRT — the targeted Windows version
* and API surface stay put until these are bumped on purpose.
* Keep in sync with .buildkite/Dockerfile and scripts/bootstrap.sh.
*/
export const WINDOWS_SDK_VERSION = "10.0.26100";
export const MSVC_CRT_VERSION = "14.44.17.14";
/**
* Serviced Universal CRT static libraries, fetched from the official
* `Microsoft.Windows.SDK.CPP.<arch>` NuGet packages and laid over the xwin
* splat at link time (a /libpath: ahead of /winsysroot).
*
* Why: the "Universal CRT Headers Libraries and Sources" payload in the VS
* manifest (what xwin downloads, any xwin version) still carries an ancient
* arm64 build of the UCRT whose `__stdio_common_vsprintf` mis-formats on
* ARM64 — c-ares's discovered DNS servers came out as garbage and every
* printf-family call in the binary was suspect. The SDK NuGet ships the same
* SDK version's *serviced* libs (the ones a real Visual Studio install has),
* with a current-MSVC, pointer-auth-instrumented arm64 UCRT. Same SDK, same
* headers, same minimum Windows version — only the static lib binaries are
* newer.
*/
export const UCRT_SERVICING_VERSION = "10.0.26100.8249";
/**
* Resolve a directory entry whose on-disk casing varies. A real Visual
* Studio / Windows SDK copy uses title-case ("Include", "Lib",
* "kernel32.Lib"); an xwin splat in winsysroot-style mode writes lowercase
* and relies on symlink aliases for the rest (see ensureSdkCaseAliases).
*/
function joinIgnoreCase(parent: string, name: string): string | undefined {
for (const candidate of [name, name.toLowerCase()]) {
const p = join(parent, candidate);
if (existsSync(p)) return p;
}
return undefined;
}
function listDir(dir: string): string[] {
try {
return readdirSync(dir);
} catch {
return [];
}
}
/** MS arch notation used for per-arch lib dirs inside the SDK. */
function msArchName(arch: Arch): string {
return arch === "x64" ? "x64" : "arm64";
}
/**
* Does `dir` look like a winsysroot usable for an `arch` build? Checks the
* SDK include tree, the kernel32 import lib for the target arch, and the ATL
* headers so an interrupted or pre-ATL splat isn't treated as complete.
* Mirrors `detectWindowsSysroot()`'s sentinel (config.ts), with the extra
* lib/ATL checks. Case-tolerant: accepts both the SDK's title-case layout
* and xwin's lowercase winsysroot-style layout.
*/
export function isCompleteWindowsSysroot(dir: string, arch: Arch): boolean {
const sdkRoot = join(dir, "Windows Kits", "10");
const sdkInclude = joinIgnoreCase(sdkRoot, "Include");
const sdkLib = joinIgnoreCase(sdkRoot, "Lib");
if (sdkInclude === undefined || sdkLib === undefined) return false;
// The SDK ships the file as "kernel32.Lib"; xwin adds a lowercase symlink.
const hasKernel32 = listDir(sdkLib).some(ver =>
listDir(join(sdkLib, ver, "um", msArchName(arch))).some(f => f.toLowerCase() === "kernel32.lib"),
);
if (!hasKernel32) return false;
// ATL (<atlstr.h>, needed by src/jsc/bindings/windows/rescle.cpp): xwin's
// --include-atl merges the ATL headers into the VC include dir; a real
// Visual Studio copy keeps them under atlmfc/include.
const msvcRoot = join(dir, "VC", "Tools", "MSVC");
return listDir(msvcRoot).some(ver =>
[join(msvcRoot, ver, "include"), join(msvcRoot, ver, "atlmfc", "include")].some(incDir =>
listDir(incDir).some(f => f.toLowerCase() === "atlstr.h"),
),
);
}
/**
* clang-cl and lld-link compose SDK paths under /winsysroot with title-case
* "Include" and "Lib" (llvm/lib/WindowsDriver/MSVCPaths.cpp,
* lld/COFF/Driver.cpp), but xwin's winsysroot-style splat writes lowercase
* "include"/"lib" and only creates title-case aliases for its non-winsysroot
* layout. On a case-sensitive filesystem the toolchain would find nothing,
* so make sure both spellings resolve, whichever one the sysroot shipped
* with. No-op when the alias (or a real title-case dir) already exists.
*/
function ensureSdkCaseAliases(dir: string): void {
const sdkRoot = join(dir, "Windows Kits", "10");
if (!existsSync(sdkRoot)) return;
for (const [alias, real] of [
["Include", "include"],
["Lib", "lib"],
] as const) {
const aliasPath = join(sdkRoot, alias);
if (existsSync(aliasPath) || !existsSync(join(sdkRoot, real))) continue;
try {
symlinkSync(real, aliasPath);
} catch (error) {
// EEXIST: another configure raced us (or a dangling alias is present) —
// either way the path resolves or the compile error will say so.
if ((error as NodeJS.ErrnoException).code === "EEXIST") continue;
throw new BuildError(`Could not create the "${alias}" alias in ${sdkRoot}`, {
cause: error as Error,
hint: `clang-cl/lld-link look up Windows SDK paths as "${alias}". Create the alias manually: ln -s ${real} "${aliasPath}"`,
});
}
}
}
/** xwin release triple for the machine running the build. */
function xwinHostTriple(cfg: Config): string {
const arch = cfg.host.arch === "aarch64" ? "aarch64" : "x86_64";
switch (cfg.host.os) {
case "linux":
return `${arch}-unknown-linux-musl`;
case "darwin":
return `${arch}-apple-darwin`;
default:
throw new BuildError(`No xwin release for host ${cfg.host.os}-${cfg.host.arch}`, {
hint: "Provide a Windows sysroot via WINDOWS_SYSROOT / --winsysroot instead.",
});
}
}
/**
* Ensure `cfg.winsysroot` exists, is complete for the target arch, and has
* the case aliases the LLVM toolchain needs. Fetches the sysroot with xwin
* when it's missing — CI only; local builds get a clear error instead of a
* surprise multi-GB download into a directory they configured themselves.
* No-op for native Windows builds.
*/
export async function ensureWindowsSysroot(cfg: Config): Promise<void> {
if (!cfg.windows || cfg.host.os === "windows" || cfg.winsysroot === undefined) return;
const dest = cfg.winsysroot;
if (!isCompleteWindowsSysroot(dest, cfg.arch)) {
if (!cfg.ci && !cfg.buildkite) {
throw new BuildError(`Windows sysroot at ${dest} is missing the MSVC CRT / Windows SDK / ATL for ${cfg.arch}`, {
hint:
"Re-create it with xwin (see docs/project/building-windows.mdx):\n" +
` xwin --accept-license --arch x86_64,aarch64 --sdk-version ${WINDOWS_SDK_VERSION} --crt-version ${MSVC_CRT_VERSION} --include-atl splat --use-winsysroot-style --preserve-ms-arch-notation --include-debug-libs --output ${dest}`,
});
}
await fetchWindowsSysroot(cfg, dest);
}
ensureSdkCaseAliases(dest);
await ensureUcrtServicingOverlay(cfg);
}
/**
* Directory holding the serviced UCRT static libs for the target arch (see
* UCRT_SERVICING_VERSION). The link adds it as /libpath: ahead of the
* winsysroot so these win over the splat's stale copies. Undefined for
* native-Windows builds (they link the locally installed, already-serviced
* SDK).
*/
export function ucrtServicingLibDir(cfg: Config): string | undefined {
if (!cfg.windows || cfg.host.os === "windows") return undefined;
return join(cfg.cacheDir, `ucrt-servicing-${UCRT_SERVICING_VERSION}`, msArchName(cfg.arch));
}
/** Fetch the serviced UCRT libs from the Microsoft.Windows.SDK.CPP NuGet. */
async function ensureUcrtServicingOverlay(cfg: Config): Promise<void> {
const libDir = ucrtServicingLibDir(cfg);
if (libDir === undefined) return;
// libucrt.lib is the member that matters (static CRT); ucrt.lib comes along
// for /MD-style links of tooling. Presence of both = done.
if (existsSync(join(libDir, "libucrt.lib")) && existsSync(join(libDir, "ucrt.lib"))) {
return;
}
const arch = msArchName(cfg.arch);
const pkg = `microsoft.windows.sdk.cpp.${arch}`;
const url = `https://api.nuget.org/v3-flatcontainer/${pkg}/${UCRT_SERVICING_VERSION}/${pkg}.${UCRT_SERVICING_VERSION}.nupkg`;
const stagingDir = join(cfg.cacheDir, `ucrt-servicing-${UCRT_SERVICING_VERSION}`, `${arch}-staging`);
const nupkg = join(stagingDir, `${pkg}.nupkg`);
console.log(`fetching serviced UCRT libs (${pkg} ${UCRT_SERVICING_VERSION})`);
rmSync(stagingDir, { recursive: true, force: true });
mkdirSync(stagingDir, { recursive: true });
await downloadWithRetry(url, nupkg, "ucrt-servicing");
await extractZip(nupkg, stagingDir);
const extractedUcrt = join(stagingDir, "c", "ucrt", arch);
if (!existsSync(join(extractedUcrt, "libucrt.lib"))) {
throw new BuildError(`Serviced UCRT libs not found in ${pkg} ${UCRT_SERVICING_VERSION}`, {
hint: `Expected c/ucrt/${arch}/libucrt.lib inside the NuGet package — its layout may have changed.`,
});
}
rmSync(libDir, { recursive: true, force: true });
mkdirSync(libDir, { recursive: true });
for (const file of readdirSync(extractedUcrt)) {
if (file.toLowerCase().endsWith(".lib")) {
copyFileSync(join(extractedUcrt, file), join(libDir, file));
}
}
rmSync(stagingDir, { recursive: true, force: true });
}
/** Download xwin and splat the MSVC CRT + Windows SDK into `dest`. */
async function fetchWindowsSysroot(cfg: Config, dest: string): Promise<void> {
// ─── 1. xwin binary ───
const triple = xwinHostTriple(cfg);
const xwinDir = resolve(cfg.cacheDir, `xwin-${XWIN_VERSION}`);
const xwinExe = join(xwinDir, `xwin-${XWIN_VERSION}-${triple}`, "xwin");
if (!existsSync(xwinExe)) {
const url = `https://github.com/Jake-Shadle/xwin/releases/download/${XWIN_VERSION}/xwin-${XWIN_VERSION}-${triple}.tar.gz`;
const tarball = join(xwinDir, `xwin-${triple}.tar.gz`);
mkdirSync(xwinDir, { recursive: true });
console.log(`downloading ${url}`);
await downloadWithRetry(url, tarball, "xwin");
// Keep the release's top-level dir (strip=0) so the exe path is stable.
await extractTarGz(tarball, xwinDir, 0);
if (!existsSync(xwinExe)) {
throw new BuildError(`xwin binary not found after extraction: ${xwinExe}`);
}
}
// ─── 2. Splat the MSVC CRT + Windows SDK + ATL ───
// Both target arches in one splat; --include-debug-libs so /MTd (debug
// CRT) links work; --include-atl for <atlstr.h> (rescle.cpp);
// winsysroot-style + MS arch notation so clang-cl and lld-link resolve it
// with a single /winsysroot flag; symlinks stay ON (default) to fix
// include/lib casing on a case-sensitive filesystem.
//
// The incomplete previous attempt is wiped before re-splatting, but only
// when `dest` actually looks like a (partial) sysroot — a mistyped
// WINDOWS_SYSROOT / --winsysroot pointing at real data should error, not
// be deleted. dirname(dest) === dest catches "/" and drive roots.
if (!isAbsolute(dest) || dirname(dest) === dest) {
throw new BuildError(`Refusing to create a Windows sysroot at ${JSON.stringify(dest)}`, {
hint: "WINDOWS_SYSROOT / --winsysroot must be an absolute, non-root directory.",
});
}
if (existsSync(dest)) {
const looksLikeSysroot =
existsSync(join(dest, "Windows Kits")) || existsSync(join(dest, "VC")) || readdirSync(dest).length === 0;
if (!looksLikeSysroot) {
throw new BuildError(`Refusing to replace ${dest}: it exists but does not look like a Windows sysroot`, {
hint: "Point WINDOWS_SYSROOT / --winsysroot at an xwin splat (or an empty directory), or delete it manually if it should be re-created.",
});
}
}
console.log(`fetching MSVC CRT + Windows SDK into ${dest} (xwin splat)`);
const args = [
"--accept-license",
"--arch",
"x86_64,aarch64",
// Pin the SDK + CRT so a manifest refresh can't drift the toolchain.
"--sdk-version",
WINDOWS_SDK_VERSION,
"--crt-version",
MSVC_CRT_VERSION,
// Top-level option (payload selection), not a `splat` option.
"--include-atl",
"--cache-dir",
join(cfg.cacheDir, "xwin-dl"),
"splat",
"--use-winsysroot-style",
"--preserve-ms-arch-notation",
"--include-debug-libs",
"--output",
dest,
];
// Microsoft's CDN resets connections often enough that agents without a
// baked sysroot were failing real builds on it — retry the whole splat a
// couple of times before giving up (the package cache in cacheDir/xwin-dl
// makes retries cheap; the splat output dir is wiped each attempt so a
// partial extraction can't leak through).
const attempts = 3;
let result;
for (let attempt = 1; attempt <= attempts; attempt++) {
rmSync(dest, { recursive: true, force: true });
mkdirSync(dest, { recursive: true });
// xwin draws progress bars to stdout even when it isn't a terminal, which
// floods CI logs with megabytes of redraws. Keep stderr (real errors);
// only show the progress locally where it's actually a progress bar.
result = spawnSync(xwinExe, args, {
stdio: ["ignore", process.stdout.isTTY ? "inherit" : "ignore", "inherit"],
});
if (!result.error && result.status === 0) {
break;
}
if (attempt < attempts) {
console.warn(
`xwin splat failed${result.status !== null ? ` (exit ${result.status})` : ""}, retrying (${attempt}/${attempts - 1} retries used)`,
);
}
}
if (result!.error || result!.status !== 0) {
throw new BuildError(`xwin splat failed${result!.status !== null ? ` (exit ${result!.status})` : ""}`, {
cause: result!.error,
hint: "The MSVC CRT / Windows SDK download from Microsoft's CDN failed — check network access, or provide a sysroot via WINDOWS_SYSROOT / --winsysroot.",
});
}
if (!isCompleteWindowsSysroot(dest, cfg.arch)) {
throw new BuildError(`xwin splat finished but ${dest} is missing expected SDK files`, {
hint: "Delete the directory and retry, or provide a sysroot via WINDOWS_SYSROOT / --winsysroot.",
});
}
}