@scriptc/compiler 0.0.27 → 0.0.29

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -208,11 +208,31 @@ export interface CcDriver {
208
208
  argv: string[];
209
209
  /** The SCRIPTC_TARGET triple, or null for a host-native build. */
210
210
  target: string | null;
211
+ /** The spelling handed to `zig cc -target` (and the relocatable-merge
212
+ * link). Identical to `target` except for the mobile triples, whose
213
+ * canonical LLVM spellings map to zig's own (`aarch64-apple-ios` →
214
+ * `aarch64-ios.15.0`), pinning the minimum OS version in the same
215
+ * breath. Null for a host-native build. */
216
+ zigTarget: string | null;
211
217
  /** Extra compile args the produced platform demands. */
212
218
  targetArgs: string[];
213
219
  /** Platform libraries appended after every object/archive input. */
214
220
  linkArgs: string[];
215
221
  }
222
+ export declare const IPHONEOS_MIN_VERSION = "15.0";
223
+ export declare const ANDROID_MIN_API = 26;
224
+ export declare function isIosTarget(target: string | null): boolean;
225
+ export declare function isAndroidTarget(target: string | null): boolean;
226
+ export declare function isMobileTarget(target: string | null): boolean;
227
+ /** The canonical mobile triple SCRIPTC_TARGET selects, or null when the
228
+ * environment names none. Pure string inspection — safe to consult before
229
+ * any toolchain discovery runs. */
230
+ export declare function mobileLibraryTarget(env?: NodeJS.ProcessEnv): string | null;
231
+ /** The admission verdict for a mobile-family triple: null when the spelling
232
+ * and host pairing are supported, otherwise the refusal text (the same text
233
+ * resolveCc throws and compileLibrary reports as SC3002). Pure string/host
234
+ * inspection — no discovery, no subprocess. */
235
+ export declare function mobileTargetRefusal(target: string, hostPlatform?: NodeJS.Platform): string | null;
216
236
  export declare function resolveCc(env?: NodeJS.ProcessEnv, hostPlatform?: NodeJS.Platform): CcDriver;
217
237
  /** Musl intentionally has no predefined libc macro. The explicit Zig target
218
238
  * is therefore the source of truth for selecting its small runtime shim. */
@@ -255,9 +275,13 @@ export interface LibArchiveOptions {
255
275
  * symbol. Toolchain sanitizer ABI remains external as required,
256
276
  * so N archives built under pairwise-distinct prefixes link into one
257
277
  * process with no symbol collisions and no shared mutable runtime state.
258
- * Undefined references (libc/libm, plus sanitizer ABI in instrumented
259
- * builds) keep their global binding. Omitted = the classic archive,
260
- * byte-for-byte. Host-native builds only. */
278
+ * Undefined references (the target C/math runtime and system APIs, plus
279
+ * sanitizer ABI in instrumented builds) keep their global binding. Windows
280
+ * embedders additionally link advapi32, iphlpapi, and ws2_32. Omitted = the
281
+ * classic archive, byte-for-byte. Admitted for darwin/linux/win32 native
282
+ * hosts, linux/android/windows cross triples from any host, and macos/ios
283
+ * cross triples from a darwin host (compileLibrary owns the refusal
284
+ * fence). */
261
285
  localizeSymbols?: readonly string[];
262
286
  /** Thread-instanced state (the profile's abi.instance_per_thread): every
263
287
  * TU of the archive compiles with -DSCR_THREAD_INSTANCES, moving the
@@ -1,11 +1,12 @@
1
- import { execFile } from "node:child_process";
1
+ import { execFile, spawnSync } from "node:child_process";
2
2
  import { createHash } from "node:crypto";
3
- import { constants as fsConstants } from "node:fs";
3
+ import { constants as fsConstants, existsSync, readdirSync } from "node:fs";
4
4
  import { createRequire } from "node:module";
5
5
  import { access, chmod, copyFile, link, mkdir, mkdtemp, readdir, readFile, realpath, rename, rm, stat, unlink, utimes, writeFile } from "node:fs/promises";
6
6
  import { availableParallelism, homedir, tmpdir } from "node:os";
7
7
  import { basename, delimiter, dirname, extname, isAbsolute, join, resolve } from "node:path";
8
8
  import { promisify } from "node:util";
9
+ import { localizeElfObject, mergeAndLocalizeCoffObjects } from "./object-localize.js";
9
10
  const execFileAsync = promisify(execFile);
10
11
  /** Test lanes run against one immutable checkout and one immutable toolchain
11
12
  * for the lifetime of each Vitest worker. Production deliberately rediscovers
@@ -175,6 +176,181 @@ export function runtimeSrcDir() {
175
176
  const require = createRequire(import.meta.url);
176
177
  return join(dirname(require.resolve("@scriptc/runtime/package.json")), "src");
177
178
  }
179
+ /* ── mobile targets (library mode) ─────────────────────────────────────────
180
+ * Three mobile triples are admitted, and only for LIBRARY-MODE archive
181
+ * builds — the consuming pattern is an embedding app linking the archive,
182
+ * never a standalone executable (compile()/compileC refuse the executable
183
+ * lane with the pointer to --lib):
184
+ *
185
+ * aarch64-apple-ios device archives; zig `aarch64-ios.15.0`
186
+ * aarch64-apple-ios-simulator simulator archives; zig
187
+ * `aarch64-ios.15.0-simulator`
188
+ * aarch64-linux-android zig `aarch64-linux-android.26`
189
+ *
190
+ * The minimum-version floors are part of the target contract: iOS archives
191
+ * build for iOS 15.0 (IPHONEOS_MIN_VERSION), Android archives for API level
192
+ * 26 (ANDROID_MIN_API) — LC_BUILD_VERSION minos and the bionic stub level
193
+ * both come from the pinned zig spelling above, so an embedder's deployment
194
+ * target at or above the floor links cleanly.
195
+ *
196
+ * Zig bundles no Apple or bionic libc, so both families compile against an
197
+ * explicit sysroot discovered here and spelled into targetArgs (where every
198
+ * cache tier already keys it):
199
+ *
200
+ * iOS darwin hosts only — `xcrun --show-sdk-path` selects the
201
+ * iPhoneOS/iPhoneSimulator SDK; compiles add `-isysroot <sdk>`
202
+ * plus `-isystem <sdk>/usr/include` (zig's driver manages libc
203
+ * header search itself and would otherwise find no headers).
204
+ * Android any host with an NDK — ANDROID_NDK_ROOT/ANDROID_NDK_HOME, or
205
+ * the newest ndk/<version> under ANDROID_HOME/ANDROID_SDK_ROOT
206
+ * or the platform-default SDK location; compiles add the NDK
207
+ * sysroot's generic and per-triple include directories.
208
+ *
209
+ * Library archives never link, so the sysroot's LIBRARIES are the
210
+ * embedder's side of the contract: Xcode links iOS archives against the
211
+ * selected SDK, and Gradle/NDK builds link Android archives against the
212
+ * API-26+ bionic stubs. */
213
+ export const IPHONEOS_MIN_VERSION = "15.0";
214
+ export const ANDROID_MIN_API = 26;
215
+ const MOBILE_LIBRARY_TARGETS = [
216
+ "aarch64-apple-ios",
217
+ "aarch64-apple-ios-simulator",
218
+ "aarch64-linux-android",
219
+ ];
220
+ export function isIosTarget(target) {
221
+ return target === "aarch64-apple-ios" || target === "aarch64-apple-ios-simulator";
222
+ }
223
+ export function isAndroidTarget(target) {
224
+ return target === "aarch64-linux-android";
225
+ }
226
+ export function isMobileTarget(target) {
227
+ return isIosTarget(target) || isAndroidTarget(target);
228
+ }
229
+ /** The canonical mobile triple SCRIPTC_TARGET selects, or null when the
230
+ * environment names none. Pure string inspection — safe to consult before
231
+ * any toolchain discovery runs. */
232
+ export function mobileLibraryTarget(env = process.env) {
233
+ const target = env["SCRIPTC_TARGET"] ?? "";
234
+ return isMobileTarget(target) ? target : null;
235
+ }
236
+ /** The admission verdict for a mobile-family triple: null when the spelling
237
+ * and host pairing are supported, otherwise the refusal text (the same text
238
+ * resolveCc throws and compileLibrary reports as SC3002). Pure string/host
239
+ * inspection — no discovery, no subprocess. */
240
+ export function mobileTargetRefusal(target, hostPlatform = process.platform) {
241
+ if (isIosTarget(target)) {
242
+ return hostPlatform === "darwin"
243
+ ? null
244
+ : `${target} library archives build on macOS hosts only (the Apple iOS SDK sysroot and Mach-O symbol localization live there); this host is ${hostPlatform}`;
245
+ }
246
+ if (isAndroidTarget(target))
247
+ return null;
248
+ // A near-miss mobile spelling must refuse with the supported set named,
249
+ // never reach zig with no sysroot wired (the compile would fail on the
250
+ // first libc header) or produce an artifact for an unverified device
251
+ // class.
252
+ if (/(?:^|-)(?:ios|tvos|watchos|visionos|android)/.test(target)) {
253
+ return `unsupported mobile target '${target}' (supported: ${MOBILE_LIBRARY_TARGETS.join(", ")})`;
254
+ }
255
+ return null;
256
+ }
257
+ /** The Apple SDK root for one mobile platform, discovered through xcrun the
258
+ * way Xcode's own build system selects it. Memoized per SDK name and
259
+ * selection environment: production rediscovers per process, and the two
260
+ * selection variables (DEVELOPER_DIR/SDKROOT) are already mutable-input
261
+ * keys that disable persistent caching. */
262
+ const appleSdkMemos = new Map();
263
+ function appleSdkRoot(sdk, env) {
264
+ const key = [sdk, env["PATH"] ?? "", env["DEVELOPER_DIR"] ?? "", env["SDKROOT"] ?? ""].join("\0");
265
+ const memo = appleSdkMemos.get(key);
266
+ if (memo !== undefined)
267
+ return memo;
268
+ const probe = spawnSync("xcrun", ["--sdk", sdk, "--show-sdk-path"], {
269
+ encoding: "utf8",
270
+ env,
271
+ });
272
+ const path = probe.status === 0 ? (probe.stdout ?? "").trim() : "";
273
+ if (path === "" || !existsSync(join(path, "usr", "include"))) {
274
+ throw new Error(`the ${sdk} SDK was not found (xcrun --sdk ${sdk} --show-sdk-path failed) — ` +
275
+ `iOS targets need Xcode with the ${sdk === "iphoneos" ? "iPhoneOS" : "iPhoneSimulator"} SDK installed`);
276
+ }
277
+ appleSdkMemos.set(key, path);
278
+ return path;
279
+ }
280
+ /** Compare dotted-numeric NDK version directory names, newest first. */
281
+ function compareNdkVersionsDesc(a, b) {
282
+ const as = a.split(".").map((s) => Number.parseInt(s, 10));
283
+ const bs = b.split(".").map((s) => Number.parseInt(s, 10));
284
+ for (let i = 0; i < Math.max(as.length, bs.length); i++) {
285
+ const d = (bs[i] ?? 0) - (as[i] ?? 0);
286
+ if (d !== 0 && Number.isFinite(d))
287
+ return d;
288
+ }
289
+ return a < b ? 1 : a > b ? -1 : 0;
290
+ }
291
+ /** The Android NDK sysroot for the selected environment: an explicit
292
+ * ANDROID_NDK_ROOT/ANDROID_NDK_HOME wins; otherwise the newest ndk/<version>
293
+ * under ANDROID_HOME, ANDROID_SDK_ROOT, or the platform-default SDK
294
+ * location. The prebuilt host directory is discovered rather than guessed —
295
+ * the NDK ships exactly one per host OS. Memoized per selection
296
+ * environment. */
297
+ const ndkSysrootMemos = new Map();
298
+ function androidNdkSysroot(env) {
299
+ const key = ["ANDROID_NDK_ROOT", "ANDROID_NDK_HOME", "ANDROID_HOME", "ANDROID_SDK_ROOT"]
300
+ .map((name) => env[name] ?? "")
301
+ .join("\0");
302
+ const memo = ndkSysrootMemos.get(key);
303
+ if (memo !== undefined)
304
+ return memo;
305
+ const ndkRoots = [];
306
+ const explicit = [env["ANDROID_NDK_ROOT"], env["ANDROID_NDK_HOME"]]
307
+ .find((root) => root !== undefined && root !== "");
308
+ if (explicit !== undefined) {
309
+ ndkRoots.push(explicit);
310
+ }
311
+ else {
312
+ const sdkRoots = [
313
+ env["ANDROID_HOME"],
314
+ env["ANDROID_SDK_ROOT"],
315
+ process.platform === "darwin"
316
+ ? join(homedir(), "Library", "Android", "sdk")
317
+ : process.platform === "win32"
318
+ ? join(env["LOCALAPPDATA"] ?? join(homedir(), "AppData", "Local"), "Android", "Sdk")
319
+ : join(homedir(), "Android", "Sdk"),
320
+ ].filter((root) => root !== undefined && root !== "");
321
+ for (const root of sdkRoots) {
322
+ let versions = [];
323
+ try {
324
+ versions = readdirSync(join(root, "ndk")).filter((name) => /^\d/.test(name));
325
+ }
326
+ catch {
327
+ continue;
328
+ }
329
+ versions.sort(compareNdkVersionsDesc);
330
+ ndkRoots.push(...versions.map((version) => join(root, "ndk", version)));
331
+ }
332
+ }
333
+ for (const ndk of ndkRoots) {
334
+ const prebuilt = join(ndk, "toolchains", "llvm", "prebuilt");
335
+ let hosts = [];
336
+ try {
337
+ hosts = readdirSync(prebuilt).sort();
338
+ }
339
+ catch {
340
+ continue;
341
+ }
342
+ for (const host of hosts) {
343
+ const sysroot = join(prebuilt, host, "sysroot");
344
+ if (existsSync(join(sysroot, "usr", "include", "aarch64-linux-android"))) {
345
+ ndkSysrootMemos.set(key, sysroot);
346
+ return sysroot;
347
+ }
348
+ }
349
+ }
350
+ throw new Error("no Android NDK sysroot was found — install an NDK (sdkmanager 'ndk;<version>') and/or set " +
351
+ "ANDROID_NDK_ROOT to it (ANDROID_HOME with an ndk/ directory also works). " +
352
+ "aarch64-linux-android compiles against the NDK's bionic headers.");
353
+ }
178
354
  /** Resolve native platform flags independently of the machine running tests,
179
355
  * so the host-Linux contract remains pinned on every development host. */
180
356
  function nativePlatformArgs(platform) {
@@ -190,22 +366,70 @@ export function resolveCc(env = process.env, hostPlatform = process.platform) {
190
366
  if (target !== "") {
191
367
  throw new Error(`SCRIPTC_TARGET=${target} requires SCRIPTC_CC=zigcc — the default clang path has no cross-target sysroots.`);
192
368
  }
193
- return { argv: ["clang"], target: null, ...hostArgs };
369
+ return { argv: ["clang"], target: null, zigTarget: null, ...hostArgs };
194
370
  }
195
371
  if (cc !== "zigcc") {
196
372
  throw new Error(`unknown SCRIPTC_CC '${cc}' (supported: clang, zigcc)`);
197
373
  }
198
374
  if (target === "")
199
- return { argv: ["zig", "cc"], target: null, ...hostArgs };
375
+ return { argv: ["zig", "cc"], target: null, zigTarget: null, ...hostArgs };
200
376
  if (target.includes("wasi") && target !== "wasm32-wasi") {
201
377
  throw new Error(`unsupported WASI target '${target}' (supported: wasm32-wasi)`);
202
378
  }
379
+ const mobileRefusal = mobileTargetRefusal(target, hostPlatform);
380
+ if (mobileRefusal !== null)
381
+ throw new Error(mobileRefusal);
382
+ if (isIosTarget(target)) {
383
+ // Library-mode-only target (compile()/compileC own the executable-lane
384
+ // refusal). Zig bundles no Apple libc: the compile rides the selected
385
+ // SDK sysroot, with the libc header directory spelled explicitly
386
+ // because zig's driver manages libc search itself and consults no
387
+ // -isysroot for it. The zig spelling pins the iOS 15.0 floor into
388
+ // every object's LC_BUILD_VERSION minos.
389
+ const simulator = target === "aarch64-apple-ios-simulator";
390
+ const sdk = appleSdkRoot(simulator ? "iphonesimulator" : "iphoneos", env);
391
+ const zigTarget = `aarch64-ios.${IPHONEOS_MIN_VERSION}${simulator ? "-simulator" : ""}`;
392
+ return {
393
+ argv: ["zig", "cc"],
394
+ target,
395
+ zigTarget,
396
+ targetArgs: ["-target", zigTarget, "-isysroot", sdk, "-isystem", join(sdk, "usr", "include")],
397
+ linkArgs: [],
398
+ };
399
+ }
400
+ if (isAndroidTarget(target)) {
401
+ // Library-mode-only target. Zig bundles no bionic: compiles ride the
402
+ // NDK sysroot's generic and per-triple include directories. Bionic
403
+ // supports _GNU_SOURCE like glibc (and hides some POSIX declarations
404
+ // without it); the zig spelling pins the API 26 floor, which the NDK's
405
+ // versioned stub libraries enforce at the embedder's link. API 26
406
+ // bionic carries everything the library-lane units call (arc4random_buf
407
+ // included), so no shim TU joins the archive.
408
+ const sysroot = androidNdkSysroot(env);
409
+ const zigTarget = `aarch64-linux-android.${ANDROID_MIN_API}`;
410
+ return {
411
+ argv: ["zig", "cc"],
412
+ target,
413
+ zigTarget,
414
+ targetArgs: [
415
+ "-target",
416
+ zigTarget,
417
+ "-D_GNU_SOURCE",
418
+ "-isystem",
419
+ join(sysroot, "usr", "include"),
420
+ "-isystem",
421
+ join(sysroot, "usr", "include", "aarch64-linux-android"),
422
+ ],
423
+ linkArgs: ["-lm"],
424
+ };
425
+ }
203
426
  const linux = target.includes("linux");
204
427
  const wasi = target.includes("wasi");
205
428
  const musl = target.includes("linux-musl");
206
429
  return {
207
430
  argv: ["zig", "cc"],
208
431
  target,
432
+ zigTarget: target,
209
433
  targetArgs: [
210
434
  "-target",
211
435
  target,
@@ -236,6 +460,11 @@ export function targetPlatform(driver) {
236
460
  return process.platform;
237
461
  if (driver.target.includes("wasi"))
238
462
  return "wasi";
463
+ // iOS is a darwin-family target: Mach-O objects, ld64 localization,
464
+ // POSIX path/EOL semantics. Android falls to the linux arm below —
465
+ // bionic is a linux libc and its archives are ordinary ELF.
466
+ if (isIosTarget(driver.target))
467
+ return "darwin";
239
468
  if (driver.target.includes("linux"))
240
469
  return "linux";
241
470
  if (driver.target.includes("windows"))
@@ -712,9 +941,12 @@ async function ensureTlsArchive(sanitize, driver, buildIdentity, cacheRoot = ven
712
941
  * units a library may reach are the pure-data ones (regex + the vendored matcher, assert,
713
942
  * inspect, symbol, searchParams, emitter+dyn_handle, zlib); every
714
943
  * loop-hooked or ambient unit was refused at SC4005 before emission.
715
- * External-symbol contract: undefined references only to libc/libm — which
716
- * is why zlib rides the VENDORED per-flavor objects here even on hosts
717
- * (the executable lane's system `-lz` cannot ride inside an archive). */
944
+ * External-symbol contract: undefined references only to the target's C/math
945
+ * runtime and system APIs. Windows embedders additionally link advapi32,
946
+ * iphlpapi, and ws2_32; the platform driver supplies its ordinary CRT and
947
+ * kernel imports. Zlib rides the VENDORED per-flavor objects here even on
948
+ * hosts because the executable lane's system `-lz` cannot ride inside an
949
+ * archive. */
718
950
  /** The library base: the executable lane's unconditional sources minus the
719
951
  * fiber/loop and child-process units, plus the library-mode TU. */
720
952
  const LIB_RUNTIME_SOURCES = [
@@ -817,9 +1049,10 @@ export async function compileLibArchive(opts) {
817
1049
  }
818
1050
  const vendorBuildIdentity = await currentVendorCacheBuildIdentity(driver, `${toolchainEnv}\0${implicitToolchain ?? "<uncached>"}`);
819
1051
  // Runtime-localized archives skip the completed-archive tier: their bytes
820
- // additionally depend on the host's ld/objcopy identities, which the
821
- // archive key does not fingerprint. The runtime-object tier still serves
822
- // them (localization consumes the same per-flavor objects).
1052
+ // additionally depend on the localization toolchain's identity (host ld/
1053
+ // objcopy or the cross driver's lld), which the archive key does not
1054
+ // fingerprint. The runtime-object tier still serves them (localization
1055
+ // consumes the same per-flavor objects).
823
1056
  const cacheCompleteArchive = opts.localizeSymbols === undefined &&
824
1057
  root !== null && await archiverSupportsPersistentCache(arArgv, driver);
825
1058
  let cachedArchive = null;
@@ -1054,9 +1287,11 @@ export async function compileLibArchive(opts) {
1054
1287
  * renamed apart — they stop being visible to the embedder's linker at all,
1055
1288
  * so a second archive built under a different prefix brings its own private
1056
1289
  * copy of the whole runtime (allocator, collector, arena, panic sink) into
1057
- * the same process. Undefined references (libc/libm, plus sanitizer ABI in
1058
- * instrumented builds) keep their global binding: the C library and
1059
- * sanitizer are the embedder's, shared by design.
1290
+ * the same process. Undefined references (the target C/math runtime and
1291
+ * system APIs, plus sanitizer ABI in instrumented builds) keep their global
1292
+ * binding: those platform services and the sanitizer are the embedder's,
1293
+ * shared by design. Windows embedders additionally link advapi32, iphlpapi,
1294
+ * and ws2_32.
1060
1295
  *
1061
1296
  * Member selection matters: a classic archive's unused members (and their
1062
1297
  * undefined references to units library mode excludes, like the
@@ -1064,26 +1299,40 @@ export async function compileLibArchive(opts) {
1064
1299
  * blind merge of every object would carry those references into the one
1065
1300
  * combined member. Staging the support objects into an intermediate
1066
1301
  * archive keeps the linker's own member semantics: `ld -r` pulls only the
1067
- * members the program object transitively needs.
1302
+ * members the program object transitively needs (the COFF arm implements
1303
+ * the same member semantics in process).
1068
1304
  *
1069
- * darwin — one ld64 invocation: -r merges program + needed members,
1305
+ * Mach-O — one host-ld64 invocation: -r merges program + needed members,
1070
1306
  * -exported_symbols_list demotes every unlisted global to
1071
1307
  * private extern, and -r without -keep_private_externs writes
1072
1308
  * private externs out as non-external symbols. Apple ASan's
1073
1309
  * image-registration COMMON remains shared so the final Mach-O
1074
- * image registers its globals once.
1075
- * linux — ld -r merges with --force-group-allocation (ASan's
1076
- * instrumented globals ride ELF section groups whose signatures
1077
- * repeat across archives sharing runtime objects; resolving the
1078
- * groups into the combined member keeps a later multi-archive
1079
- * link from discarding one archive's copies), then binutils
1080
- * objcopy --keep-global-symbols localizes every other DEFINED
1081
- * global (objcopy leaves undefined symbols global by its own
1082
- * rule).
1083
- *
1084
- * Host-native only: compileLibrary refuses localization for cross targets
1085
- * before emission (this step runs the host's ld/objcopy over host-format
1086
- * objects). */
1310
+ * image registers its globals once. ld64 reads every Apple
1311
+ * platform's objects — macOS architectures and the iOS/
1312
+ * iOS-simulator triples alike (LC_BUILD_VERSION platform and
1313
+ * minos survive the merge) — so darwin-host macos AND ios
1314
+ * targets ride the same arm; compileLibrary refuses those
1315
+ * targets elsewhere.
1316
+ * ELF, native linux host — ld -r merges with --force-group-allocation
1317
+ * (ASan's instrumented globals ride ELF section groups whose
1318
+ * signatures repeat across archives sharing runtime objects;
1319
+ * resolving the groups into the combined member keeps a later
1320
+ * multi-archive link from discarding one archive's copies),
1321
+ * then binutils objcopy --keep-global-symbols localizes every
1322
+ * other DEFINED global (objcopy leaves undefined symbols
1323
+ * global by its own rule). The exact historical recipe.
1324
+ * ELF, cross triples from any host — `zig cc -target <triple> -r`
1325
+ * merges (zig is already the cross driver's hard requirement),
1326
+ * then localizeElfObject demotes in process and resolves
1327
+ * section groups the way --force-group-allocation does, with
1328
+ * no host binutils/llvm-objcopy dependency.
1329
+ * COFF, native win32 hosts and windows cross triples from any host —
1330
+ * mergeAndLocalizeCoffObjects performs member selection, the
1331
+ * merge, cross-object symbol resolution, and demotion in
1332
+ * process: no linker offers a COFF relocatable mode (lld-link
1333
+ * mirrors MSVC link.exe; zig's COFF driver refuses multi-object
1334
+ * merges) and llvm-objcopy rejects symbol-scope flags for
1335
+ * COFF, so no tool pairing exists to shell out to. */
1087
1336
  async function localizeLibraryObjects(driver, arArgv, buildDir, programObject, supportObjects, keepSymbols, stem) {
1088
1337
  const platform = targetPlatform(driver);
1089
1338
  const combined = join(buildDir, `${stem}.localized.o`);
@@ -1096,21 +1345,58 @@ async function localizeLibraryObjects(driver, arArgv, buildDir, programObject, s
1096
1345
  catch (err) {
1097
1346
  const stderr = err.stderr ?? String(err);
1098
1347
  throw new Error(`${argv[0]} failed while localizing the library archive's runtime symbols (abi.localize_runtime).\n` +
1099
- `Runtime symbol localization needs the host toolchain's ${platform === "darwin" ? "ld" : "ld and objcopy"} beside the C compiler.\n\n${stderr}`);
1348
+ `Runtime symbol localization needs the ${platform === "darwin" ? "host toolchain's ld" : driver.target === null ? "host toolchain's ld and objcopy" : "cross driver's relocatable link"} beside the C compiler.\n\n${stderr}`);
1100
1349
  }
1101
1350
  };
1351
+ if (platform === "win32") {
1352
+ // COFF has no relocatable-link tool to stage through; the member
1353
+ // selection and combine+demote happen in process over the object bytes.
1354
+ const [program, ...support] = await Promise.all([programObject, ...supportObjects].map((path) => readFile(path)));
1355
+ try {
1356
+ await writeFile(combined, mergeAndLocalizeCoffObjects(program, support, new Set(keepSymbols), {
1357
+ program: basename(programObject),
1358
+ support: supportObjects.map((path) => basename(path)),
1359
+ }));
1360
+ }
1361
+ catch (err) {
1362
+ throw new Error(`COFF symbol localization failed while localizing the library archive's runtime symbols (abi.localize_runtime).\n\n${err.message}`);
1363
+ }
1364
+ return combined;
1365
+ }
1102
1366
  await run([arArgv[0] ?? "ar", ...arArgv.slice(1), "rcs", staging, ...supportObjects]);
1103
1367
  if (platform === "darwin") {
1104
1368
  await writeFile(keepFile, keepSymbols.map((s) => `_${s}\n`).join(""));
1105
1369
  await run(["ld", "-r", programObject, staging, "-o", combined, "-exported_symbols_list", keepFile]);
1106
1370
  }
1107
- else if (platform === "linux") {
1371
+ else if (platform === "linux" && driver.target === null) {
1108
1372
  await writeFile(keepFile, keepSymbols.map((s) => `${s}\n`).join(""));
1109
1373
  await run(["ld", "-r", "--force-group-allocation", programObject, staging, "-o", combined]);
1110
1374
  await run(["objcopy", `--keep-global-symbols=${keepFile}`, combined]);
1111
1375
  }
1376
+ else if (platform === "linux") {
1377
+ // Cross ELF: the cross driver's own lld performs the relocatable merge
1378
+ // (with the staging archive's member semantics); demotion and section-
1379
+ // group resolution happen in process. -nostdlib keeps zig from feeding
1380
+ // libc/compiler-rt inputs into the merge. Android rides this arm
1381
+ // unchanged: bionic archives are ordinary aarch64 ELF64, and the merge
1382
+ // uses the driver's zig spelling (which pins the API floor).
1383
+ await run([
1384
+ driver.argv[0] ?? "zig",
1385
+ ...driver.argv.slice(1),
1386
+ "-target", driver.zigTarget ?? driver.target,
1387
+ "-nostdlib",
1388
+ "-r", programObject, staging,
1389
+ "-o", combined,
1390
+ ]);
1391
+ try {
1392
+ await writeFile(combined, localizeElfObject(await readFile(combined), new Set(keepSymbols)));
1393
+ }
1394
+ catch (err) {
1395
+ throw new Error(`ELF symbol localization failed while localizing the library archive's runtime symbols (abi.localize_runtime).\n\n${err.message}`);
1396
+ }
1397
+ }
1112
1398
  else {
1113
- throw new Error(`runtime symbol localization (abi.localize_runtime) supports darwin and linux host builds; this build targets ${platform}`);
1399
+ throw new Error(`runtime symbol localization (abi.localize_runtime) has no ${platform} arm; compileLibrary admits darwin, linux, and win32 builds only`);
1114
1400
  }
1115
1401
  return combined;
1116
1402
  }
@@ -2277,6 +2563,14 @@ export async function compileC(opts) {
2277
2563
  const http = (opts.http ?? false) || nativeFetch || netIsland;
2278
2564
  const tls = (opts.tls ?? false) || nativeFetch || netIsland;
2279
2565
  const driver = resolveCc();
2566
+ // Mobile triples produce library archives, never standalone executables:
2567
+ // the executable-lane runtime (event loop, sockets, child processes) is
2568
+ // not verified on those device classes. compile() reports the SC3002
2569
+ // diagnostic first; this is the backstop for direct compileC callers.
2570
+ if (isMobileTarget(driver.target)) {
2571
+ throw new Error(`SCRIPTC_TARGET=${driver.target} builds library-mode static archives only — ` +
2572
+ `compile with a library profile (SCRIPTC_CC=zigcc scriptc build --lib --profile <profile.json>) and link the archive from the app project.`);
2573
+ }
2280
2574
  const runtimeSources = targetPlatform(driver) === "wasi"
2281
2575
  ? RUNTIME_SOURCES.filter((source) => source !== "scr_child.c")
2282
2576
  : RUNTIME_SOURCES;