xbintsc 0.5.52 → 0.6.8

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.
Files changed (156) hide show
  1. package/README.md +8 -2
  2. package/README.zh-CN.md +9 -3
  3. package/dist/scripts/fetch-tarball.d.ts +28 -0
  4. package/dist/scripts/fetch-tarball.js +57 -0
  5. package/dist/scripts/fetch-tarball.js.map +1 -0
  6. package/dist/scripts/gui-build-options.d.ts +77 -0
  7. package/dist/scripts/gui-build-options.js +134 -0
  8. package/dist/scripts/gui-build-options.js.map +1 -0
  9. package/dist/src/cli/help.d.ts +1 -1
  10. package/dist/src/cli/hints.d.ts +6 -2
  11. package/dist/src/cli/hints.js +16 -2
  12. package/dist/src/cli/hints.js.map +1 -1
  13. package/dist/src/cli/main.js +9 -0
  14. package/dist/src/cli/main.js.map +1 -1
  15. package/dist/src/driver/app-artifacts.d.ts +2 -0
  16. package/dist/src/driver/app-artifacts.js +3 -3
  17. package/dist/src/driver/app-artifacts.js.map +1 -1
  18. package/dist/src/driver/compiler.js +147 -52
  19. package/dist/src/driver/compiler.js.map +1 -1
  20. package/dist/src/driver/icon.d.ts +1 -1
  21. package/dist/src/driver/icon.js +3 -3
  22. package/dist/src/driver/icon.js.map +1 -1
  23. package/dist/src/driver/macos-sdk.d.ts +32 -0
  24. package/dist/src/driver/macos-sdk.js +50 -0
  25. package/dist/src/driver/macos-sdk.js.map +1 -0
  26. package/dist/src/driver/options.d.ts +6 -0
  27. package/dist/src/driver/platform-support.d.ts +45 -0
  28. package/dist/src/driver/platform-support.js +66 -0
  29. package/dist/src/driver/platform-support.js.map +1 -0
  30. package/dist/src/driver/prebuilt.d.ts +26 -0
  31. package/dist/src/driver/prebuilt.js +58 -0
  32. package/dist/src/driver/prebuilt.js.map +1 -0
  33. package/dist/src/driver/runtime-objects.d.ts +11 -0
  34. package/dist/src/driver/runtime-objects.js +14 -5
  35. package/dist/src/driver/runtime-objects.js.map +1 -1
  36. package/dist/src/driver/toolchain-provider.d.ts +8 -0
  37. package/dist/src/driver/toolchain-provider.js +8 -0
  38. package/dist/src/driver/toolchain-provider.js.map +1 -1
  39. package/dist/src/driver/toolchain.d.ts +20 -1
  40. package/dist/src/driver/toolchain.js +19 -0
  41. package/dist/src/driver/toolchain.js.map +1 -1
  42. package/dist/src/driver/version.d.ts +1 -1
  43. package/dist/src/driver/version.js +1 -1
  44. package/dist/src/driver/version.js.map +1 -1
  45. package/dist/src/driver/win-entry.d.ts +1 -1
  46. package/dist/src/driver/win-entry.js +4 -4
  47. package/dist/src/driver/win-entry.js.map +1 -1
  48. package/dist/src/extensions/gui/html.d.ts +10 -7
  49. package/dist/src/extensions/gui/html.js +53 -29
  50. package/dist/src/extensions/gui/html.js.map +1 -1
  51. package/dist/src/extensions/node/child_process/index.d.ts +4 -3
  52. package/dist/src/extensions/node/child_process/index.js +4 -3
  53. package/dist/src/extensions/node/child_process/index.js.map +1 -1
  54. package/dist/tests/cli/hints.test.js +9 -1
  55. package/dist/tests/cli/hints.test.js.map +1 -1
  56. package/dist/tests/cli/main.test.js +9 -0
  57. package/dist/tests/cli/main.test.js.map +1 -1
  58. package/dist/tests/driver/compiler.test.js +107 -0
  59. package/dist/tests/driver/compiler.test.js.map +1 -1
  60. package/dist/tests/driver/cxx-compile-flags.test.d.ts +1 -0
  61. package/dist/tests/driver/cxx-compile-flags.test.js +24 -0
  62. package/dist/tests/driver/cxx-compile-flags.test.js.map +1 -0
  63. package/dist/tests/driver/macos-sdk.test.d.ts +1 -0
  64. package/dist/tests/driver/macos-sdk.test.js +83 -0
  65. package/dist/tests/driver/macos-sdk.test.js.map +1 -0
  66. package/dist/tests/driver/platform-support.test.d.ts +1 -0
  67. package/dist/tests/driver/platform-support.test.js +41 -0
  68. package/dist/tests/driver/platform-support.test.js.map +1 -0
  69. package/dist/tests/driver/prebuilt.test.d.ts +1 -0
  70. package/dist/tests/driver/prebuilt.test.js +44 -0
  71. package/dist/tests/driver/prebuilt.test.js.map +1 -0
  72. package/dist/tests/driver/toolchain-provider.test.js +24 -0
  73. package/dist/tests/driver/toolchain-provider.test.js.map +1 -1
  74. package/dist/tests/driver/toolchain.test.js +23 -0
  75. package/dist/tests/driver/toolchain.test.js.map +1 -1
  76. package/dist/tests/e2e/async-await.test.js +4 -2
  77. package/dist/tests/e2e/async-await.test.js.map +1 -1
  78. package/dist/tests/e2e/compile-node.test.js +21 -0
  79. package/dist/tests/e2e/compile-node.test.js.map +1 -1
  80. package/dist/tests/e2e/fetch.test.js +34 -6
  81. package/dist/tests/e2e/fetch.test.js.map +1 -1
  82. package/dist/tests/e2e/gui-example-pelican3d.test.d.ts +10 -0
  83. package/dist/tests/e2e/gui-example-pelican3d.test.js +94 -0
  84. package/dist/tests/e2e/gui-example-pelican3d.test.js.map +1 -0
  85. package/dist/tests/e2e/gui-script.test.js +48 -0
  86. package/dist/tests/e2e/gui-script.test.js.map +1 -1
  87. package/dist/tests/e2e/unhandled-rejection.test.d.ts +14 -0
  88. package/dist/tests/e2e/unhandled-rejection.test.js +107 -0
  89. package/dist/tests/e2e/unhandled-rejection.test.js.map +1 -0
  90. package/dist/tests/extensions/gui.test.js +44 -0
  91. package/dist/tests/extensions/gui.test.js.map +1 -1
  92. package/dist/tests/scripts/fetch-tarball.test.d.ts +1 -0
  93. package/dist/tests/scripts/fetch-tarball.test.js +70 -0
  94. package/dist/tests/scripts/fetch-tarball.test.js.map +1 -0
  95. package/doc/ai/cli.md +6 -0
  96. package/doc/ai/language-support.md +1 -0
  97. package/doc/ai/troubleshooting.md +32 -2
  98. package/doc/ai/zh-CN/cli.md +5 -0
  99. package/doc/ai/zh-CN/language-support.md +1 -0
  100. package/doc/ai/zh-CN/troubleshooting.md +27 -2
  101. package/doc/gui/build.md +7 -3
  102. package/doc/gui/dom.md +3 -1
  103. package/doc/gui/webgl.md +4 -0
  104. package/doc/gui-scripts.md +33 -17
  105. package/doc/gui.md +17 -1
  106. package/doc/implemented/async.md +1 -0
  107. package/doc/implemented/modules.md +9 -6
  108. package/doc/implemented/node/child-process.md +12 -4
  109. package/doc/implemented/runtime-core.md +1 -1
  110. package/doc/npm-package.md +23 -2
  111. package/doc/post/make-js-great-again.md +30 -0
  112. package/doc/requirements.md +31 -0
  113. package/doc/unimplemented/deviations.md +1 -0
  114. package/doc/zh-CN/gui/build.md +4 -2
  115. package/doc/zh-CN/gui/dom.md +3 -1
  116. package/doc/zh-CN/gui/webgl.md +1 -0
  117. package/doc/zh-CN/gui-scripts.md +23 -11
  118. package/doc/zh-CN/gui.md +11 -1
  119. package/doc/zh-CN/implemented/async.md +1 -0
  120. package/doc/zh-CN/implemented/modules.md +8 -4
  121. package/doc/zh-CN/implemented/node/child-process.md +10 -3
  122. package/doc/zh-CN/implemented/runtime-core.md +1 -1
  123. package/doc/zh-CN/npm-package.md +18 -1
  124. package/doc/zh-CN/post/make-js-great-again.md +32 -0
  125. package/doc/zh-CN/requirements.md +25 -0
  126. package/doc/zh-CN/unimplemented/deviations.md +1 -0
  127. package/package.json +7 -7
  128. package/runtime/ext_node/child_process/child_process.c +149 -7
  129. package/runtime/rt.h +4 -0
  130. package/runtime/xt_async.c +4 -0
  131. package/runtime/xt_generator.c +2 -0
  132. package/runtime/xt_loop.c +8 -4
  133. package/runtime/xt_promise.c +103 -0
  134. package/scripts/build-gui.ts +17 -5
  135. package/scripts/build-media.ts +89 -10
  136. package/scripts/build-runtime.ts +7 -2
  137. package/scripts/fetch-tarball.ts +71 -0
  138. package/scripts/gui-build-cache.ts +28 -0
  139. package/scripts/gui-build-options.ts +26 -2
  140. package/scripts/package-release.ts +8 -0
  141. package/src/cli/hints.ts +19 -2
  142. package/src/cli/main.ts +11 -0
  143. package/src/driver/app-artifacts.ts +5 -3
  144. package/src/driver/compiler.ts +167 -56
  145. package/src/driver/icon.ts +3 -2
  146. package/src/driver/macos-sdk.ts +52 -0
  147. package/src/driver/options.ts +6 -0
  148. package/src/driver/platform-support.ts +69 -0
  149. package/src/driver/prebuilt.ts +61 -0
  150. package/src/driver/runtime-objects.ts +26 -5
  151. package/src/driver/toolchain-provider.ts +11 -0
  152. package/src/driver/toolchain.ts +26 -1
  153. package/src/driver/version.ts +1 -1
  154. package/src/driver/win-entry.ts +4 -3
  155. package/src/extensions/gui/html.ts +61 -31
  156. package/src/extensions/node/child_process/index.ts +4 -3
@@ -18,18 +18,20 @@ import { DiagnosticBag, DiagnosticCode } from "../diagnostics/diagnostic.js";
18
18
  import { generate } from "../codegen/llvm.js";
19
19
  import { createDefaultRegistry } from "../extensions/registry.js";
20
20
  import { BuildCache } from "./cache.js";
21
- import { findRuntimeDir } from "./paths.js";
22
- import { resolveToolchain } from "./toolchain-provider.js";
21
+ import { findRuntimeDir, platformSlug } from "./paths.js";
22
+ import { resolveToolchain, type ResolvedToolchain } from "./toolchain-provider.js";
23
23
  import { readIcon, IconError, type IconInfo } from "./icon.js";
24
24
  import { bundlePathFor, packageMacApp } from "./mac-bundle.js";
25
25
  import type { AppConfig } from "./config.js";
26
26
  import { compileIr, link, mergeProfileData, realRunner, ToolchainError } from "./toolchain.js";
27
27
  import { parseEntry } from "./frontend.js";
28
- import { ensureRuntimeObjects } from "./runtime-objects.js";
28
+ import { ensureRuntimeObjects, type RuntimeObjects } from "./runtime-objects.js";
29
29
  import { ensureAppArtifacts } from "./app-artifacts.js";
30
30
  import { buildCacheKey } from "./build-key.js";
31
31
  import { collectUsage } from "./usage.js";
32
32
  import { resolveLinkFeatures, type ResolvedLinkFeature } from "./link-features.js";
33
+ import { isPrebuiltArchivePath, firstFailureLine, looksLikePrebuiltLinkFailure } from "./prebuilt.js";
34
+ import { runtimeLibDirs as defaultRuntimeLibDirs } from "./runtime-lib.js";
33
35
  import type { BuildOptions, BuildResult, EmitKind, PhaseTiming } from "./options.js";
34
36
 
35
37
  export { COMPILER_VERSION } from "./version.js";
@@ -68,6 +70,36 @@ function executableName(entry: string, outDir: string, explicit: string | undefi
68
70
  return resolve(outDir, base + suffix);
69
71
  }
70
72
 
73
+ /**
74
+ * Explain a retry that still fails while prebuilt `gui`/`media` archives are in
75
+ * the link. Unlike `core`/`ext_node`, those bundle SDL3/FFmpeg/glslang and
76
+ * their sources are not part of the npm package, so the source fallback cannot
77
+ * replace them; only a release/toolchain match can.
78
+ */
79
+ function prebuiltFallbackError(
80
+ error: unknown,
81
+ runtimeObjects: RuntimeObjects,
82
+ dirs: readonly string[] | undefined,
83
+ ): unknown {
84
+ if (!(error instanceof ToolchainError) || !looksLikePrebuiltLinkFailure(error.stderr)) return error;
85
+ const runtimeDirs = dirs ?? defaultRuntimeLibDirs();
86
+ const fixed = [...runtimeObjects.featureObjects, ...runtimeObjects.nativeObjects].filter((object) =>
87
+ isPrebuiltArchivePath(object, runtimeDirs),
88
+ );
89
+ if (fixed.length === 0) return error;
90
+ const archives = [...new Set(fixed.map((object) => basename(object)))].join(", ");
91
+ return new ToolchainError(
92
+ error.command,
93
+ error.status,
94
+ `${error.stderr}\n` +
95
+ `The remaining prebuilt archives (${archives}) come from the @xbintsc/${platformSlug()} ` +
96
+ "package. Unlike the C runtime, they bundle SDL3/FFmpeg, whose sources are not shipped " +
97
+ "in the npm package, so they cannot be rebuilt here.\n" +
98
+ "Build with the Xcode Command Line Tools / Visual Studio C++ toolset the release was built " +
99
+ "with, or use a release archive built for this machine.",
100
+ );
101
+ }
102
+
71
103
  export function build(entryPath: string, options: BuildOptions = {}): BuildResult {
72
104
  const runner = options.runner ?? realRunner;
73
105
  const registry = options.extensions ?? createDefaultRegistry();
@@ -220,8 +252,8 @@ export function build(entryPath: string, options: BuildOptions = {}): BuildResul
220
252
 
221
253
  // -- toolchain -----------------------------------------------------------
222
254
  const clangStarted = Date.now();
223
- const toolchain = options.clang
224
- ? { clang: options.clang, linkerArgs: [] as string[], env: {} as Record<string, string> }
255
+ const toolchain: Pick<ResolvedToolchain, "clang" | "linkerArgs" | "env" | "sdkRoot"> = options.clang
256
+ ? { clang: options.clang, linkerArgs: [], env: {} }
225
257
  : resolveToolchain(runner);
226
258
  const clang = toolchain.clang;
227
259
  if (pgo === "use") {
@@ -244,6 +276,7 @@ export function build(entryPath: string, options: BuildOptions = {}): BuildResul
244
276
  objectPath,
245
277
  optimize,
246
278
  env: toolchain.env,
279
+ sdkRoot: toolchain.sdkRoot,
247
280
  debug: options.debug === true,
248
281
  lto,
249
282
  ...(pgo ? { pgo, pgoDir } : {}),
@@ -256,21 +289,32 @@ export function build(entryPath: string, options: BuildOptions = {}): BuildResul
256
289
  return { outputPath, irPath, cached: false, diagnostics: [], ir, timings };
257
290
  }
258
291
 
259
- const runtimeStarted = Date.now();
260
- const { runtimeObjects, extensionObjects, featureObjects, nativeObjects } = ensureRuntimeObjects({
261
- runner,
262
- clang,
263
- runtimeDir,
264
- cacheDir,
265
- registry,
266
- preferPrebuilt: resolvePreferPrebuilt(options.preferPrebuilt) && !lto && pgo === undefined,
267
- env: toolchain.env,
268
- lto,
269
- pgo,
270
- pgoDir,
271
- features: linkFeatures,
272
- });
273
- timings.push({ phase: "runtime", ms: Date.now() - runtimeStarted });
292
+ /* The runtime is assembled before the app artifacts so a failed prebuilt
293
+ * link can be retried with the same artifact inputs. */
294
+ const runtimeLibDirs = options.runtimeLibDirs;
295
+ const preferPrebuilt = resolvePreferPrebuilt(options.preferPrebuilt) && !lto && pgo === undefined;
296
+ const assembleRuntime = (usePrebuilt: boolean): RuntimeObjects => {
297
+ const started = Date.now();
298
+ const assembled = ensureRuntimeObjects({
299
+ runner,
300
+ clang,
301
+ runtimeDir,
302
+ cacheDir,
303
+ registry,
304
+ preferPrebuilt: usePrebuilt,
305
+ env: toolchain.env,
306
+ sdkRoot: toolchain.sdkRoot,
307
+ lto,
308
+ pgo,
309
+ pgoDir,
310
+ features: linkFeatures,
311
+ ...(runtimeLibDirs !== undefined ? { runtimeLibDirs } : {}),
312
+ });
313
+ timings.push({ phase: "runtime", ms: Date.now() - started });
314
+ return assembled;
315
+ };
316
+
317
+ let assembled = assembleRuntime(preferPrebuilt);
274
318
 
275
319
  const artifacts = ensureAppArtifacts({
276
320
  runner,
@@ -280,6 +324,7 @@ export function build(entryPath: string, options: BuildOptions = {}): BuildResul
280
324
  app,
281
325
  icon,
282
326
  env: toolchain.env,
327
+ sdkRoot: toolchain.sdkRoot,
283
328
  verbose: options.verbose,
284
329
  });
285
330
  if (artifacts.iconError !== undefined) {
@@ -287,42 +332,108 @@ export function build(entryPath: string, options: BuildOptions = {}): BuildResul
287
332
  return { outputPath, irPath, cached: false, diagnostics: diagnostics.diagnostics, ir };
288
333
  }
289
334
 
290
- const linkStarted = Date.now();
291
- link(runner, {
292
- clang,
293
- objectPaths: [
294
- objectPath,
295
- /* Feature archives come after their bridges (which reference the
296
- * implementation) and before the extensions' own archives (`gui.a`
297
- * provides symbols the bridges call back into). `Set` keeps that order
298
- * and drops an archive two features or an extension share. */
299
- ...new Set([
300
- ...runtimeObjects,
301
- ...extensionObjects,
302
- ...featureObjects,
303
- ...nativeObjects,
304
- ...(artifacts.entryObject !== undefined ? [artifacts.entryObject] : []),
305
- ...artifacts.iconObjects,
306
- ]),
307
- ],
308
- outputPath,
309
- linkerFlags: [
310
- ...toolchain.linkerArgs,
311
- ...(process.platform === "win32" ? ["-lws2_32"] : ["-lm", "-pthread"]),
312
- ...registry.linkerFlags(),
313
- ...linkFeatures.flatMap((feature) => feature.linkerFlags),
314
- /* A GUI program is a Windows-subsystem executable: no console window is
315
- * created when it is launched. Console output (stdout/stderr) is lost in
316
- * that subsystem, so a program that wants to report things opts back in
317
- * with `app.console: true`. */
318
- ...artifacts.linkerFlags,
319
- ],
320
- optimize,
321
- env: toolchain.env,
322
- lto,
323
- ...(pgo ? { pgo, pgoDir } : {}),
324
- });
325
- timings.push({ phase: "link", ms: Date.now() - linkStarted });
335
+ const linkAssembled = (runtimeObjects: RuntimeObjects): string => {
336
+ const started = Date.now();
337
+ const result = link(runner, {
338
+ clang,
339
+ objectPaths: [
340
+ objectPath,
341
+ /* Feature archives come after their bridges (which reference the
342
+ * implementation) and before the extensions' own archives (`gui.a`
343
+ * provides symbols the bridges call back into). `Set` keeps that order
344
+ * and drops an archive two features or an extension share. */
345
+ ...new Set([
346
+ ...runtimeObjects.runtimeObjects,
347
+ ...runtimeObjects.extensionObjects,
348
+ ...runtimeObjects.featureObjects,
349
+ ...runtimeObjects.nativeObjects,
350
+ ...(artifacts.entryObject !== undefined ? [artifacts.entryObject] : []),
351
+ ...artifacts.iconObjects,
352
+ ]),
353
+ ],
354
+ outputPath,
355
+ linkerFlags: [
356
+ ...toolchain.linkerArgs,
357
+ ...(process.platform === "win32" ? ["-lws2_32"] : ["-lm", "-pthread"]),
358
+ ...registry.linkerFlags(),
359
+ ...linkFeatures.flatMap((feature) => feature.linkerFlags),
360
+ /* A GUI program is a Windows-subsystem executable: no console window is
361
+ * created when it is launched. Console output (stdout/stderr) is lost in
362
+ * that subsystem, so a program that wants to report things opts back in
363
+ * with `app.console: true`. */
364
+ ...artifacts.linkerFlags,
365
+ ],
366
+ optimize,
367
+ env: toolchain.env,
368
+ sdkRoot: toolchain.sdkRoot,
369
+ lto,
370
+ ...(pgo ? { pgo, pgoDir } : {}),
371
+ });
372
+ timings.push({ phase: "link", ms: Date.now() - started });
373
+ return result.stderr;
374
+ };
375
+
376
+ /**
377
+ * Retry once with the C runtime compiled from the sources shipped in the
378
+ * package. The failed attempt is not cached: a later toolchain upgrade can
379
+ * use the archive again.
380
+ */
381
+ const retryFromSource = (stderr: string): void => {
382
+ const reason = firstFailureLine(stderr);
383
+ process.stderr.write(
384
+ "xbintsc: the prebuilt runtime archive does not match this toolchain" +
385
+ (reason ? ` (${reason})` : "") +
386
+ "; compiling the C runtime from source\n",
387
+ );
388
+ assembled = assembleRuntime(false);
389
+ try {
390
+ linkAssembled(assembled);
391
+ } catch (retryError) {
392
+ throw prebuiltFallbackError(retryError, assembled, runtimeLibDirs);
393
+ }
394
+ };
395
+ const prebuiltInUse = (): boolean =>
396
+ preferPrebuilt && assembled.prebuiltArchives.length > 0;
397
+ /** Whether the linker talked about an archive the source fallback can replace. */
398
+ const mentionsRebuildableArchive = (stderr: string): boolean =>
399
+ assembled.prebuiltArchives.some((archive) => stderr.includes(basename(archive)));
400
+
401
+ /* One attempt, captured instead of thrown so the retry below is not itself
402
+ * caught by this decision block. `failed` is explicit because a thrown
403
+ * `undefined` must not look like a successful link. */
404
+ let failed = false;
405
+ let firstError: unknown;
406
+ let firstStderr = "";
407
+ try {
408
+ firstStderr = linkAssembled(assembled);
409
+ } catch (error) {
410
+ failed = true;
411
+ firstError = error;
412
+ }
413
+
414
+ if (failed) {
415
+ const error = firstError;
416
+ /* The release runner's Xcode/SDK or Visual Studio toolset can be newer
417
+ * than this machine's, and then the prebuilt archive cannot be linked
418
+ * (`LNK2038`, `LNK2019`, `Undefined symbols`, ...). The C runtime sources
419
+ * ship with the package, so retry once with them instead of failing on a
420
+ * linker message the user cannot act on. */
421
+ if (!prebuiltInUse() || !(error instanceof ToolchainError) || !looksLikePrebuiltLinkFailure(error.stderr)) {
422
+ throw error;
423
+ }
424
+ retryFromSource(error.stderr);
425
+ } else if (
426
+ prebuiltInUse() &&
427
+ mentionsRebuildableArchive(firstStderr) &&
428
+ looksLikePrebuiltLinkFailure(firstStderr)
429
+ ) {
430
+ /* A successful link can still warn that the archive was built for a newer
431
+ * macOS than the one being targeted: the binary would then claim the local
432
+ * deployment target while calling newer SDK symbols at run time, so rebuild
433
+ * it from source just like an outright failure. Only `core`/`ext_node` can
434
+ * be replaced this way; a warning about `gui.a`/`media.a` is left alone. */
435
+ retryFromSource(firstStderr);
436
+ }
326
437
 
327
438
  const bundlePath =
328
439
  bundle !== undefined
@@ -16,7 +16,7 @@
16
16
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
17
17
  import { join, resolve } from "node:path";
18
18
  import { hashParts } from "./cache.js";
19
- import { compileC, type Runner } from "./toolchain.js";
19
+ import { compileC, sysrootFlags, type Runner } from "./toolchain.js";
20
20
 
21
21
  export type IconFormat = "png" | "ico" | "icns";
22
22
 
@@ -144,6 +144,7 @@ export function ensureIconObject(
144
144
  cacheDir: string,
145
145
  icon: IconInfo,
146
146
  env?: Record<string, string>,
147
+ sdkRoot?: string,
147
148
  ): string {
148
149
  const key = hashParts(["icon", process.platform, Buffer.from(icon.bytes).toString("base64")]);
149
150
  const sourcePath = join(cacheDir, `icon-${key}.c`);
@@ -151,7 +152,7 @@ export function ensureIconObject(
151
152
  if (!existsSync(objectPath)) {
152
153
  mkdirSync(cacheDir, { recursive: true });
153
154
  writeFileSync(sourcePath, iconSource(icon));
154
- compileC(runner, clang, sourcePath, objectPath, cacheDir, env);
155
+ compileC(runner, clang, sourcePath, objectPath, cacheDir, env, sysrootFlags(sdkRoot));
155
156
  }
156
157
  return objectPath;
157
158
  }
@@ -0,0 +1,52 @@
1
+ /**
2
+ * macOS SDK discovery.
3
+ *
4
+ * macOS has no `/usr/include`: every C standard header (`stdio.h`,
5
+ * `setjmp.h`, …) ships inside the SDK, and the linker's `libm`/`libSystem`
6
+ * stubs live there too. clang only searches the SDK when it has a sysroot,
7
+ * which comes from the `/usr/bin/clang` shim, an explicit `-isysroot`, or the
8
+ * `SDKROOT` environment variable.
9
+ *
10
+ * xbintsc invokes clang by absolute path (CI pins it to the oldest installed
11
+ * Xcode so released archives stay linkable by newer ones), which bypasses the
12
+ * shim. Without a sysroot that driver sees neither the SDK nor `/usr/include`,
13
+ * and the first runtime include fails with `'setjmp.h' file not found` — the
14
+ * header is irrelevant, any system header would do. The same gap makes the
15
+ * link fail later with `ld: library 'm' not found`.
16
+ *
17
+ * The SDK is therefore resolved once, with `xcrun`, and handed to clang as
18
+ * `-isysroot` on the command line rather than as `SDKROOT` in the environment:
19
+ * a self-hosted xbintsc binary runs the same driver, and its compiled
20
+ * `spawnSync` ignores `options.env`, so the variable would be lost exactly
21
+ * where the compiler rebuilds itself. An explicit `SDKROOT` in the environment
22
+ * always wins, so a developer can select an older SDK deliberately.
23
+ */
24
+
25
+ import { existsSync } from "node:fs";
26
+ import type { CommandResult, Runner } from "./toolchain.js";
27
+
28
+ /**
29
+ * The macOS SDK `xcrun` reports, or `undefined` when the host is not macOS,
30
+ * the environment already names one, or no usable SDK can be resolved.
31
+ *
32
+ * A missing SDK is not an error here: the build still runs, and clang reports
33
+ * the real problem (or the driver never needed the SDK in the first place).
34
+ */
35
+ export function macosSdkRoot(
36
+ runner: Runner,
37
+ env: NodeJS.ProcessEnv = process.env,
38
+ platform: NodeJS.Platform = process.platform,
39
+ ): string | undefined {
40
+ if (platform !== "darwin") return undefined;
41
+ if (env.SDKROOT) return undefined;
42
+ let result: CommandResult;
43
+ try {
44
+ result = runner.run("xcrun", ["--show-sdk-path"]);
45
+ } catch {
46
+ /* A host without xcrun keeps the previous behaviour: let clang decide. */
47
+ return undefined;
48
+ }
49
+ if (result.status !== 0) return undefined;
50
+ const sdk = result.stdout.trim();
51
+ return sdk.length > 0 && existsSync(sdk) ? sdk : undefined;
52
+ }
@@ -28,6 +28,12 @@ export interface BuildOptions {
28
28
  readonly clang?: string;
29
29
  /** Prefer a prebuilt `runtime/lib/<os>-<arch>/*.a` archive when present (default true). */
30
30
  readonly preferPrebuilt?: boolean;
31
+ /**
32
+ * Folders searched for the prebuilt `core`/`ext_<name>` archives, replacing
33
+ * the default (`runtime/lib/<os>-<arch>` plus the installed platform
34
+ * package). Mostly useful for tests and embedding tools.
35
+ */
36
+ readonly runtimeLibDirs?: readonly string[];
31
37
  readonly write?: boolean;
32
38
  /** Application icon embedded into the binary (shorthand for `app.icon`). */
33
39
  readonly icon?: string;
@@ -0,0 +1,69 @@
1
+ /**
2
+ * Oldest platform versions xbintsc supports, and the baseline every released
3
+ * artifact is built against.
4
+ *
5
+ * The prebuilt runtime archives (`core`, `ext_node`, `gui`, `media`) and the
6
+ * self-hosted compiler are built by CI with the **oldest toolchain each host
7
+ * still offers**, targeting the floors below. A newer Xcode/Visual Studio on a
8
+ * user's machine can link those older objects, but the reverse is what fails
9
+ * (`LNK2038`, `LNK2019`, `Undefined symbols`, `was built for newer macOS
10
+ * version`), which is why the direction matters.
11
+ *
12
+ * These values are the single source of truth for the archive build scripts,
13
+ * `xbintsc doctor` and `doc/requirements.md`.
14
+ */
15
+
16
+ /** Deployment target for every macOS artifact (also the Apple Silicon floor). */
17
+ export const MACOS_MINIMUM_VERSION = "11.0";
18
+
19
+ /**
20
+ * Oldest Windows version the prebuilt objects and the compiler target.
21
+ *
22
+ * Windows 10 was the floor while the release jobs ran on `windows-2022`
23
+ * (VS 2022 / MSVC v143). Those images cannot assemble a release: their bundled
24
+ * `tar` has no built-in zstd, so `tar --zstd` hands compression to an external
25
+ * program the step waits on forever. The release jobs therefore run on
26
+ * `windows-2025`, whose toolset targets Windows 11, and the supported floor
27
+ * moved with them.
28
+ */
29
+ export const WINDOWS_MINIMUM_VERSION = "11";
30
+
31
+ /**
32
+ * glibc the release archives are built and tested against: the GitHub
33
+ * `ubuntu-24.04` image. Older distributions may link the npm package's
34
+ * archives, but only this floor is supported.
35
+ */
36
+ export const LINUX_MINIMUM_GLIBC = "2.39";
37
+
38
+ /** Human-readable support floor for the host running the compiler. */
39
+ export function supportFloor(platform: NodeJS.Platform = process.platform): string {
40
+ switch (platform) {
41
+ case "darwin":
42
+ return `macOS ${MACOS_MINIMUM_VERSION}+`;
43
+ case "win32":
44
+ return `Windows ${WINDOWS_MINIMUM_VERSION}+`;
45
+ case "linux":
46
+ return `glibc ${LINUX_MINIMUM_GLIBC}+`;
47
+ default:
48
+ return `${platform} (unverified)`;
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Make a macOS archive build target {@link MACOS_MINIMUM_VERSION}.
54
+ *
55
+ * Clang's Darwin driver reads `MACOSX_DEPLOYMENT_TARGET`, and CMake reads it as
56
+ * the default for `CMAKE_OSX_DEPLOYMENT_TARGET`; setting both keeps the direct
57
+ * engine compiles, the vendored CMake builds and FFmpeg's configure on the same
58
+ * floor. An explicit value in the environment still wins, so a developer can
59
+ * target an even older macOS deliberately. No-op on the other hosts.
60
+ */
61
+ export function applyMacosDeploymentTarget(platform: NodeJS.Platform = process.platform): void {
62
+ if (platform !== "darwin") return;
63
+ if (!process.env.MACOSX_DEPLOYMENT_TARGET) {
64
+ process.env.MACOSX_DEPLOYMENT_TARGET = MACOS_MINIMUM_VERSION;
65
+ }
66
+ if (!process.env.CMAKE_OSX_DEPLOYMENT_TARGET) {
67
+ process.env.CMAKE_OSX_DEPLOYMENT_TARGET = MACOS_MINIMUM_VERSION;
68
+ }
69
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * Detecting a prebuilt runtime archive that this machine's toolchain cannot
3
+ * link.
4
+ *
5
+ * The optional `@xbintsc/<os>-<arch>` package carries archives compiled on the
6
+ * release runner (`core.a`, `ext_node.a`, `gui.a`, `media.a`). Its Xcode
7
+ * Command Line Tools or Visual Studio C++ toolset can be **newer** than the
8
+ * one on the machine that runs the build: a linker can then reject the archive
9
+ * with unresolved symbols (`LNK2038`/`LNK2019`, `Undefined symbols`,
10
+ * `undefined reference`) or `was built for newer macOS version`.
11
+ *
12
+ * The driver answers that with one automatic retry that compiles the runtime
13
+ * from the sources shipped in the npm package. Its decision is based on the
14
+ * linker's own output rather than a version comparison, because an archive does
15
+ * not record the toolset that produced it portably: Mach-O objects carry
16
+ * `LC_BUILD_VERSION`, COFF objects do not carry an MSVC toolset version.
17
+ *
18
+ * `gui.a`/`media.a` are exempt: they bundle SDL3/FFmpeg/glslang, whose sources
19
+ * are not part of the npm package, so they cannot be rebuilt from it.
20
+ */
21
+
22
+ import { dirname, resolve } from "node:path";
23
+
24
+ const ARCHIVE_SUFFIXES = [".a", ".lib"] as const;
25
+
26
+ /** True when `candidate` is a static archive that sits in one of `dirs`. */
27
+ export function isPrebuiltArchivePath(candidate: string, dirs: readonly string[]): boolean {
28
+ if (!ARCHIVE_SUFFIXES.some((suffix) => candidate.endsWith(suffix))) return false;
29
+ const parent = dirname(resolve(candidate));
30
+ return dirs.some((dir) => resolve(dir) === parent);
31
+ }
32
+
33
+ /**
34
+ * Linker messages that mean "these objects were produced by a different
35
+ * toolchain/SDK". Kept deliberately narrow: a missing file, a bad flag or a
36
+ * permission error must not trigger the (slow) source rebuild.
37
+ */
38
+ const MISMATCH_PATTERNS = [
39
+ /LNK2038/i, // mismatch detected for 'RuntimeLibrary'/'_MSC_VER'
40
+ /LNK20(?:01|19)/i, // unresolved external symbol
41
+ /undefined symbols?/i, // ld64
42
+ /undefined reference/i, // GNU/LLD
43
+ /symbol\(s\) not found/i, // ld64 summary line
44
+ /was built for newer (?:macOS|iOS|tvOS|watchOS|visionOS)/i, // ld64 warning
45
+ /but linking in object file built for/i, // ld64 deployment-target error
46
+ /has an incompatible (?:architecture|platform)/i,
47
+ ] as const;
48
+
49
+ /** Whether a failed link's stderr looks like a toolchain/archive mismatch. */
50
+ export function looksLikePrebuiltLinkFailure(stderr: string): boolean {
51
+ return MISMATCH_PATTERNS.some((pattern) => pattern.test(stderr));
52
+ }
53
+
54
+ /** First non-empty line of a command's stderr, for a one-line build notice. */
55
+ export function firstFailureLine(stderr: string): string {
56
+ for (const line of stderr.split(/\r?\n/)) {
57
+ const trimmed = line.trim();
58
+ if (trimmed.length > 0) return trimmed.length > 160 ? `${trimmed.slice(0, 157)}...` : trimmed;
59
+ }
60
+ return "";
61
+ }
@@ -12,7 +12,7 @@ import type { ExtensionRegistry } from "../extensions/registry.js";
12
12
  import type { ResolvedLinkFeature } from "./link-features.js";
13
13
  import { hashParts } from "./cache.js";
14
14
  import { findRuntimeLibrary } from "./runtime-lib.js";
15
- import { compileC, optimizationFlags, RUNTIME_CFLAGS, type PgoMode, type Runner } from "./toolchain.js";
15
+ import { compileC, optimizationFlags, RUNTIME_CFLAGS, sysrootFlags, type PgoMode, type Runner } from "./toolchain.js";
16
16
  import { COMPILER_VERSION } from "./version.js";
17
17
 
18
18
  /** Core runtime translation units (each compiled and cached independently). */
@@ -51,6 +51,12 @@ export interface RuntimeObjects {
51
51
  readonly featureObjects: readonly string[];
52
52
  /** Pre-built C++/Rust objects and archives contributed by native extensions. */
53
53
  readonly nativeObjects: readonly string[];
54
+ /**
55
+ * The prebuilt `core`/`ext_<name>` archives this link used. Empty when the
56
+ * runtime was compiled from source. The driver retries a failed link without
57
+ * them, so it has to know whether the first attempt used any.
58
+ */
59
+ readonly prebuiltArchives: readonly string[];
54
60
  }
55
61
 
56
62
  export interface RuntimeObjectsOptions {
@@ -61,11 +67,16 @@ export interface RuntimeObjectsOptions {
61
67
  readonly registry: ExtensionRegistry;
62
68
  readonly preferPrebuilt: boolean;
63
69
  readonly env: Record<string, string>;
70
+ /** macOS SDK for `-isysroot`; part of the object cache key. */
71
+ readonly sdkRoot?: string;
64
72
  readonly lto: boolean;
65
73
  readonly pgo: PgoMode | undefined;
66
74
  readonly pgoDir: string;
67
75
  /** Optional link features selected for this program. */
68
76
  readonly features?: readonly ResolvedLinkFeature[];
77
+ /** Override the folders searched for prebuilt archives (defaults to the
78
+ * runtime's `lib/<os>-<arch>` plus the installed platform package). */
79
+ readonly runtimeLibDirs?: readonly string[];
69
80
  }
70
81
 
71
82
  /**
@@ -79,6 +90,9 @@ export interface RuntimeObjectsOptions {
79
90
  export function ensureRuntimeObjects(options: RuntimeObjectsOptions): RuntimeObjects {
80
91
  const { runner, clang, runtimeDir, cacheDir, registry, preferPrebuilt, env, lto, pgo, pgoDir } = options;
81
92
  const optimization = optimizationFlags(lto, pgo, pgoDir);
93
+ /* The sysroot joins the extra flags (and therefore the cache key): objects
94
+ compiled against a different SDK are a different artifact. */
95
+ const compileFlags = [...optimization, ...sysrootFlags(options.sdkRoot)];
82
96
  /* Runtime objects depend on the shared headers (rt.h/rt_internal.h/...) and
83
97
  the `#include`d implementation fragments (`.inc`), so any change to them
84
98
  must invalidate every cached object. Hash them all. */
@@ -99,7 +113,7 @@ export function ensureRuntimeObjects(options: RuntimeObjectsOptions): RuntimeObj
99
113
  "runtime",
100
114
  COMPILER_VERSION,
101
115
  RUNTIME_CFLAGS.join(" "),
102
- optimization.join(" "),
116
+ compileFlags.join(" "),
103
117
  tag,
104
118
  headerStamp,
105
119
  body,
@@ -108,24 +122,30 @@ export function ensureRuntimeObjects(options: RuntimeObjectsOptions): RuntimeObj
108
122
  ]);
109
123
  const objectPath = join(cacheDir, `${tag}-${key}.o`);
110
124
  if (!existsSync(objectPath)) {
111
- compileC(runner, clang, sourcePath, objectPath, runtimeDir, env, optimization);
125
+ compileC(runner, clang, sourcePath, objectPath, runtimeDir, env, compileFlags);
112
126
  }
113
127
  return objectPath;
114
128
  };
115
129
 
116
- const coreArchive = preferPrebuilt ? findRuntimeLibrary("core") : undefined;
130
+ const coreArchive = preferPrebuilt ? findRuntimeLibrary("core", options.runtimeLibDirs) : undefined;
117
131
  const runtimeObjects = coreArchive
118
132
  ? [coreArchive]
119
133
  : RUNTIME_SOURCES.map((source) => compileOne(join(runtimeDir, source), basename(source, ".c")));
120
134
 
135
+ /** Prebuilt archives this link uses; the driver retries without them. */
136
+ const prebuiltArchives: string[] = coreArchive ? [coreArchive] : [];
137
+
121
138
  const extensionObjects: string[] = [];
122
139
  const seen = new Set<string>();
123
140
  for (const extension of registry.all()) {
124
141
  const sources = extension.runtimeSources ? extension.runtimeSources() : [];
125
142
  if (sources.length === 0) continue;
126
- const archive = preferPrebuilt ? findRuntimeLibrary(`ext_${extension.name}`) : undefined;
143
+ const archive = preferPrebuilt
144
+ ? findRuntimeLibrary(`ext_${extension.name}`, options.runtimeLibDirs)
145
+ : undefined;
127
146
  if (archive) {
128
147
  extensionObjects.push(archive);
148
+ prebuiltArchives.push(archive);
129
149
  continue;
130
150
  }
131
151
  for (const sourcePath of sources) {
@@ -156,5 +176,6 @@ export function ensureRuntimeObjects(options: RuntimeObjectsOptions): RuntimeObj
156
176
  extensionObjects,
157
177
  featureObjects: [...new Set(featureObjects)],
158
178
  nativeObjects: [...new Set(registry.nativeObjects())],
179
+ prebuiltArchives,
159
180
  };
160
181
  }
@@ -22,11 +22,17 @@
22
22
  * skipped (or rejected, when named explicitly) instead of producing objects
23
23
  * the linker cannot mix.
24
24
  *
25
+ * On macOS the resolved driver also carries the SDK as `sdkRoot`, which the
26
+ * compile and link steps pass as `-isysroot` (see `macos-sdk.ts`): an Apple
27
+ * clang invoked by absolute path does not infer the SDK by itself, and without
28
+ * one not even `<stdio.h>` — nor the linker's `libm` stub — is found.
29
+ *
25
30
  * Extra linker arguments (for example `-fuse-ld=lld`) can be supplied through
26
31
  * `xbintsc_LINKER_ARGS`.
27
32
  */
28
33
 
29
34
  import { locateClang, realRunner, type ClangVersion, type Runner } from "./toolchain.js";
35
+ import { macosSdkRoot } from "./macos-sdk.js";
30
36
 
31
37
  export type ToolchainSource = "env" | "system";
32
38
 
@@ -42,6 +48,9 @@ export interface ResolvedToolchain {
42
48
  /** Extra environment for the toolchain subprocesses, merged over the current
43
49
  * environment. */
44
50
  readonly env: Record<string, string>;
51
+ /** macOS SDK to pass as `-isysroot`; lost off macOS or when `SDKROOT` is
52
+ * already set. See `macos-sdk.ts`. */
53
+ readonly sdkRoot?: string;
45
54
  }
46
55
 
47
56
  function linkerArgs(fallback: readonly string[]): string[] {
@@ -53,11 +62,13 @@ function linkerArgs(fallback: readonly string[]): string[] {
53
62
  /** Resolve the toolchain that `build` should use. See the module docblock. */
54
63
  export function resolveToolchain(runner: Runner = realRunner): ResolvedToolchain {
55
64
  const location = locateClang(runner);
65
+ const sdkRoot = macosSdkRoot(runner);
56
66
  return {
57
67
  clang: location.command,
58
68
  source: location.source,
59
69
  ...(location.version ? { version: location.version } : {}),
60
70
  linkerArgs: linkerArgs([]),
61
71
  env: {},
72
+ ...(sdkRoot === undefined ? {} : { sdkRoot }),
62
73
  };
63
74
  }