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
@@ -49,15 +49,18 @@ import { spawnSync, type SpawnSyncReturns } from "node:child_process";
49
49
  import { dirname, join, resolve } from "node:path";
50
50
  import { fileURLToPath } from "node:url";
51
51
  import { platformSlug } from "../src/driver/paths.js";
52
+ import { applyMacosDeploymentTarget } from "../src/driver/platform-support.js";
52
53
  import { isMsvcTarget, probeClang, realRunner } from "../src/driver/toolchain.js";
53
54
  import { resolveToolchain } from "../src/driver/toolchain-provider.js";
54
55
  import {
55
56
  cmakeToolchainArgs,
57
+ cxxCompileFlags,
56
58
  HB_LEAN_DEFINES,
57
59
  LEAN_SDL_OPTIONS,
58
60
  SECTION_CMAKE_FLAGS,
59
61
  SECTION_FLAGS,
60
62
  } from "./gui-build-options.js";
63
+ import { ensureDeploymentTarget } from "./gui-build-cache.js";
61
64
 
62
65
  const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
63
66
  const runtimeDir = join(root, "runtime");
@@ -99,6 +102,10 @@ const isWindows = process.platform === "win32";
99
102
  const objectSuffix = isWindows ? ".obj" : ".o";
100
103
  const output = join(outDir, isWindows ? "gui.lib" : "gui.a");
101
104
 
105
+ /* The vendored CMake builds and the direct engine compiles must target the
106
+ * oldest supported macOS, not the build machine's version. */
107
+ applyMacosDeploymentTarget();
108
+
102
109
  const SDL_TAG = process.env.SDL3_TAG || "release-3.2.10";
103
110
  const FREETYPE_VERSION = process.env.FREETYPE_VERSION || "2.13.3";
104
111
  const HARFBUZZ_VERSION = process.env.HARFBUZZ_VERSION || "10.1.0";
@@ -204,19 +211,19 @@ function findArchiver(clangPath: string): string {
204
211
  * sibling clang++, and the matching llvm-ar. No `gcc`/`g++` fallbacks, and
205
212
  * Windows never falls back to a non-LLVM archiver — mixing another C++ runtime
206
213
  * or object format would break the final link. */
207
- function driverClang(): string {
214
+ function driverToolchain(): ReturnType<typeof resolveToolchain> {
208
215
  try {
209
- return resolveToolchain().clang;
216
+ return resolveToolchain();
210
217
  } catch (error) {
211
218
  fail(error instanceof Error ? error.message : String(error));
212
219
  }
213
220
  }
214
221
 
215
- const clang = driverClang();
222
+ const toolchain = driverToolchain();
223
+ const clang = toolchain.clang;
216
224
  const clangxx = findClangxx(clang);
217
225
  const llvmAr = findArchiver(clang);
218
226
 
219
-
220
227
  /** The compiler a previous CMake configure recorded for `buildDir`. */
221
228
  function cachedCompiler(buildDir: string, variable: string): string | undefined {
222
229
  const cache = join(buildDir, "CMakeCache.txt");
@@ -348,6 +355,7 @@ if (!existsSync(join(sdlSrc, "CMakeLists.txt"))) {
348
355
 
349
356
  /* -- 2. static SDL3 ------------------------------------------------------- */
350
357
 
358
+ ensureDeploymentTarget("SDL3", sdlBuild);
351
359
  const existingSdlArchive = sdlArchive();
352
360
  if (!existingSdlArchive) {
353
361
  /* Older trees configured SDL inside its source (`vendor/SDL/build`) and a
@@ -392,6 +400,7 @@ if (!existingSdlArchive) {
392
400
  ensureTarballSource("freetype", FREETYPE_URL, `freetype-${FREETYPE_VERSION}`);
393
401
  ensureTarballSource("harfbuzz", HARFBUZZ_URL, `harfbuzz-${HARFBUZZ_VERSION}`);
394
402
 
403
+ ensureDeploymentTarget("FreeType", freetypeBuild);
395
404
  if (!existsSync(freetypeArchive() ?? "")) {
396
405
  const cmake = configureCmake("FreeType", freetypeSrc, freetypeBuild, [
397
406
  "-DCMAKE_BUILD_TYPE=Release",
@@ -410,6 +419,7 @@ if (!existsSync(freetypeArchive() ?? "")) {
410
419
  console.log(`xbintsc: reusing existing ${freetypeArchive()}`);
411
420
  }
412
421
 
422
+ ensureDeploymentTarget("HarfBuzz", harfbuzzBuild);
413
423
  if (!existsSync(harfbuzzArchive() ?? "")) {
414
424
  const cmake = configureCmake("HarfBuzz", harfbuzzSrc, harfbuzzBuild, [
415
425
  "-DCMAKE_BUILD_TYPE=Release",
@@ -456,6 +466,7 @@ ensureTarballSource("glslang", GLSLANG_URL, `glslang-${GLSLANG_VERSION}`);
456
466
  ensureTarballSource(join("glslang", "External", "spirv-headers"), SPIRV_HEADERS_URL,
457
467
  `SPIRV-Headers-${SPIRV_HEADERS_VERSION}`);
458
468
 
469
+ ensureDeploymentTarget("glslang", glslangBuild);
459
470
  if (!existsSync(glslangArchive() ?? "")) {
460
471
  const cmake = configureCmake("glslang", glslangSrc, glslangBuild, [
461
472
  "-DCMAKE_BUILD_TYPE=Release",
@@ -484,6 +495,7 @@ const metalBackend = process.platform === "darwin";
484
495
  if (metalBackend) {
485
496
  ensureTarballSource("spirv-cross", SPIRV_CROSS_URL, `SPIRV-Cross-${SPIRV_CROSS_VERSION}`);
486
497
 
498
+ ensureDeploymentTarget("SPIRV-Cross", spirvCrossBuild);
487
499
  if (!existsSync(spirvCrossArchive("spirv-cross-msl") ?? "")) {
488
500
  const cmake = configureCmake("SPIRV-Cross", spirvCrossSrc, spirvCrossBuild, [
489
501
  "-DCMAKE_BUILD_TYPE=Release",
@@ -523,7 +535,7 @@ const sources = readdirSync(engineDir)
523
535
  .sort();
524
536
 
525
537
  const objects: string[] = [];
526
- const cxxFlags = ["-std=c++17", "-O2", "-Wall", "-Wextra", ...SECTION_FLAGS, ...includeFlags];
538
+ const cxxFlags = cxxCompileFlags(toolchain.sdkRoot, includeFlags);
527
539
  /* The engine is C++ and the MSVC C++ runtime must match the static C CRT that
528
540
  * clang links by default, so let every engine object record `libcpmt` as a
529
541
  * default library. The extension itself only contributes the DLL import library
@@ -48,8 +48,11 @@ import { spawnSync, type SpawnSyncReturns } from "node:child_process";
48
48
  import { dirname, join, resolve } from "node:path";
49
49
  import { fileURLToPath } from "node:url";
50
50
  import { platformSlug } from "../src/driver/paths.js";
51
+ import { applyMacosDeploymentTarget } from "../src/driver/platform-support.js";
51
52
  import { isMsvcTarget, probeClang, realRunner } from "../src/driver/toolchain.js";
52
53
  import { resolveToolchain } from "../src/driver/toolchain-provider.js";
54
+ import { downloadTarball } from "./fetch-tarball.js";
55
+ import { cxxCompileFlags } from "./gui-build-options.js";
53
56
 
54
57
  const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
55
58
  const runtimeDir = join(root, "runtime");
@@ -66,6 +69,10 @@ const objDir = join(root, "build", "media-obj");
66
69
  const outDir = join(runtimeDir, "lib", platformSlug());
67
70
  const output = join(outDir, isWindows ? "media.lib" : "media.a");
68
71
 
72
+ /* FFmpeg and the player object must target the oldest supported macOS. Clang's
73
+ * Darwin driver and FFmpeg's configure both read the environment variable. */
74
+ applyMacosDeploymentTarget();
75
+
69
76
  const FFMPEG_VERSION = process.env.FFMPEG_VERSION || "7.1";
70
77
  const FFMPEG_URL = `https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.xz`;
71
78
 
@@ -161,27 +168,49 @@ function findArchiver(clangPath: string): string {
161
168
  return archiver;
162
169
  }
163
170
 
164
- function driverClang(): string {
171
+ function driverToolchain(): ReturnType<typeof resolveToolchain> {
165
172
  try {
166
- return resolveToolchain().clang;
173
+ return resolveToolchain();
167
174
  } catch (error) {
168
175
  fail(error instanceof Error ? error.message : String(error));
169
176
  }
170
177
  }
171
178
 
172
- /** Download the FFmpeg release tarball and extract it under `vendor/ffmpeg`. */
179
+ /** Fetch and extract the FFmpeg release tarball under `vendor/ffmpeg`.
180
+ *
181
+ * The tarball is kept (`vendor/ffmpeg-<version>.tar.xz`) so CI can cache it by
182
+ * `FFMPEG_VERSION`, and `downloadTarball` retries the transient TLS failures the
183
+ * release host throws; the release marker re-extracts a tree of another release. */
173
184
  function ensureFfmpegSource(): void {
174
- if (existsSync(join(ffmpegSrc, "configure"))) return;
175
- const tarball = join(vendorDir, "ffmpeg.tar.xz");
176
- console.log(`xbintsc: fetching FFmpeg ${FFMPEG_VERSION} from ${FFMPEG_URL}`);
185
+ if (existsSync(join(ffmpegSrc, "configure")) && recordedRelease(ffmpegSrc) === FFMPEG_VERSION) return;
186
+ const tarball = join(vendorDir, `ffmpeg-${FFMPEG_VERSION}.tar.xz`);
177
187
  mkdirSync(vendorDir, { recursive: true });
178
- run("curl", ["-sL", "--fail", "-o", tarball, FFMPEG_URL]);
188
+ if (existsSync(tarball)) {
189
+ console.log(`xbintsc: extracting cached ${tarball}`);
190
+ } else {
191
+ try {
192
+ downloadTarball(tarball, FFMPEG_URL, `FFmpeg ${FFMPEG_VERSION}`);
193
+ } catch (error) {
194
+ fail(error instanceof Error ? error.message : String(error));
195
+ }
196
+ }
179
197
  run("tar", ["-xf", tarball, "-C", vendorDir]);
180
198
  const extracted = join(vendorDir, `ffmpeg-${FFMPEG_VERSION}`);
181
199
  if (!existsSync(extracted)) fail(`expected ffmpeg-${FFMPEG_VERSION} after extracting`);
182
200
  rmSync(ffmpegSrc, { recursive: true, force: true });
183
201
  renameSync(extracted, ffmpegSrc);
184
- rmSync(tarball, { force: true });
202
+ writeFileSync(releaseStamp(ffmpegSrc), `${FFMPEG_VERSION}\n`);
203
+ }
204
+
205
+ /** Marker recording the FFmpeg release a source tree or installed prefix holds. */
206
+ function releaseStamp(directory: string): string {
207
+ return join(directory, ".xbintsc-ffmpeg-version");
208
+ }
209
+
210
+ /** The release recorded in `directory`, or "" when it predates the marker. */
211
+ function recordedRelease(directory: string): string {
212
+ const stamp = releaseStamp(directory);
213
+ return existsSync(stamp) ? readFileSync(stamp, "utf8").trim() : "";
185
214
  }
186
215
 
187
216
  function ffmpegStaticLibraries(): string[] | undefined {
@@ -194,6 +223,44 @@ function ffmpegInstalled(): boolean {
194
223
  return ffmpegStaticLibraries() !== undefined && existsSync(join(ffmpegInstall, "include", "libavutil", "avutil.h"));
195
224
  }
196
225
 
226
+ /** Marker recording the deployment target `dist` was built for (macOS). */
227
+ function deploymentStamp(): string {
228
+ return join(ffmpegInstall, ".xbintsc-deployment-target");
229
+ }
230
+
231
+ /**
232
+ * Drop a cached FFmpeg build tree that was configured for another macOS
233
+ * deployment target. `dist` is reused when it already holds the libraries, and
234
+ * FFmpeg's Makefile does not notice a changed `MACOSX_DEPLOYMENT_TARGET`; a
235
+ * tree from an earlier checkout would keep 15.5 objects in `media.a`. A tree
236
+ * without the marker predates this check and is rebuilt once.
237
+ */
238
+ function ensureDeploymentTarget(): void {
239
+ if (process.platform !== "darwin") return;
240
+ const wanted = process.env.MACOSX_DEPLOYMENT_TARGET ?? "";
241
+ const stamp = deploymentStamp();
242
+ const recorded = existsSync(stamp) ? readFileSync(stamp, "utf8").trim() : "";
243
+ if (recorded === wanted) return;
244
+ if (existsSync(ffmpegBuild)) {
245
+ console.log(`xbintsc: FFmpeg was built for macOS '${recorded}'; rebuilding for '${wanted}'`);
246
+ rmSync(ffmpegBuild, { recursive: true, force: true });
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Drop a cached FFmpeg build tree from another release: neither the Makefile nor
252
+ * `dist` is keyed by `FFMPEG_VERSION`, so reusing it would link the previous
253
+ * release's libraries. A `dist` without the marker predates this check and is
254
+ * rebuilt once.
255
+ */
256
+ function ensureBuildRelease(): void {
257
+ if (!ffmpegInstalled()) return;
258
+ const recorded = recordedRelease(ffmpegInstall);
259
+ if (recorded === FFMPEG_VERSION) return;
260
+ console.log(`xbintsc: FFmpeg was built as '${recorded || "unknown"}'; rebuilding for '${FFMPEG_VERSION}'`);
261
+ rmSync(ffmpegBuild, { recursive: true, force: true });
262
+ }
263
+
197
264
  /** Arguments shared by the POSIX and Windows FFmpeg configure runs. */
198
265
  function ffmpegConfigureArgs(): string[] {
199
266
  const prefix = isWindows ? ffmpegInstall.replace(/\\/g, "/") : ffmpegInstall;
@@ -443,7 +510,8 @@ ensureFfmpegSource();
443
510
  /* One toolchain for the player and FFmpeg: the same clang the driver uses, its
444
511
  * sibling clang++, and the matching llvm-ar. Mixing another C++ runtime or
445
512
  * object format would break the final link. */
446
- const clang = driverClang();
513
+ const toolchain = driverToolchain();
514
+ const clang = toolchain.clang;
447
515
  const clangxx = findClangxx(clang);
448
516
  const archiver = findArchiver(clang);
449
517
  const windowsTools = isWindows
@@ -456,6 +524,8 @@ const windowsTools = isWindows
456
524
 
457
525
  /* -- 2. minimal static FFmpeg --------------------------------------------- */
458
526
 
527
+ ensureDeploymentTarget();
528
+ ensureBuildRelease();
459
529
  if (!ffmpegInstalled()) {
460
530
  if (windowsTools) {
461
531
  buildFfmpegWindows(windowsTools.shell, windowsTools.clangCl, windowsTools.msvcBin);
@@ -470,6 +540,12 @@ if (!ffmpegInstalled()) {
470
540
  } else {
471
541
  console.log(`xbintsc: reusing existing ${ffmpegInstall}/lib`);
472
542
  }
543
+ /* Record the target and release for the next run, built or reused. */
544
+ if (process.platform === "darwin") {
545
+ mkdirSync(ffmpegInstall, { recursive: true });
546
+ writeFileSync(deploymentStamp(), process.env.MACOSX_DEPLOYMENT_TARGET ?? "");
547
+ }
548
+ writeFileSync(releaseStamp(ffmpegInstall), `${FFMPEG_VERSION}\n`);
473
549
 
474
550
  /* -- 3. player object ------------------------------------------------------ */
475
551
 
@@ -482,7 +558,10 @@ mkdirSync(objDir, { recursive: true });
482
558
  * references every entry point, so each member is extracted on demand. */
483
559
  const playerSources = ["media.cpp", "decode_audio.cpp"];
484
560
  const playerObjects: string[] = [];
485
- const cxxFlags = ["-std=c++17", "-O2", "-Wall", "-Wextra", "-I", runtimeDir, "-I", join(ffmpegInstall, "include")];
561
+ const cxxFlags = cxxCompileFlags(toolchain.sdkRoot, [
562
+ "-I", runtimeDir,
563
+ "-I", join(ffmpegInstall, "include"),
564
+ ]);
486
565
  /* The engine and the player are C++ with the static CRT (`libcpmt`), matching
487
566
  * FFmpeg's clang-cl objects and the xbintsc runtime; see `scripts/build-gui.ts`. */
488
567
  if (isWindows) cxxFlags.push("-Xclang", "--dependent-lib=libcpmt");
@@ -26,8 +26,9 @@ import { basename, dirname, join, resolve } from "node:path";
26
26
  import { fileURLToPath } from "node:url";
27
27
  import { RUNTIME_SOURCES } from "../src/driver/compiler.js";
28
28
  import { platformSlug } from "../src/driver/paths.js";
29
+ import { applyMacosDeploymentTarget } from "../src/driver/platform-support.js";
29
30
  import { resolveToolchain } from "../src/driver/toolchain-provider.js";
30
- import { RUNTIME_CFLAGS } from "../src/driver/toolchain.js";
31
+ import { RUNTIME_CFLAGS, sysrootFlags } from "../src/driver/toolchain.js";
31
32
  import { nodeExtension } from "../src/extensions/node/index.js";
32
33
 
33
34
  const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
@@ -37,6 +38,10 @@ const objDir = join(root, "build", "runtime-obj");
37
38
  const isWindows = process.platform === "win32";
38
39
  const archiveSuffix = isWindows ? ".lib" : ".a";
39
40
 
41
+ /* Every archive targets the oldest supported macOS, not the build machine's
42
+ * version: a newer Xcode must not raise the deployment target of the objects. */
43
+ applyMacosDeploymentTarget();
44
+
40
45
  // Use the same toolchain the driver will.
41
46
  const toolchain = resolveToolchain();
42
47
  const clang = toolchain.clang;
@@ -78,7 +83,7 @@ function compile(sources: readonly string[], extension: string): string[] {
78
83
  const object = join(objDir, `${extension}_${index}_${basename(source, ".c")}.o`);
79
84
  const result = spawnSync(
80
85
  clang,
81
- [...RUNTIME_CFLAGS, "-c", source, "-o", object, `-I${runtimeDir}`],
86
+ [...RUNTIME_CFLAGS, ...sysrootFlags(toolchain.sdkRoot), "-c", source, "-o", object, `-I${runtimeDir}`],
82
87
  { stdio: "inherit", env: childEnv },
83
88
  );
84
89
  if (result.status !== 0) {
@@ -0,0 +1,71 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Fetch a pinned release tarball for the vendored toolchain.
4
+ *
5
+ * `scripts/build-media.ts` (FFmpeg) and `scripts/build-gui.ts` (SDL3, FreeType,
6
+ * HarfBuzz, glslang, SPIRV-Cross) build from release tarballs downloaded at
7
+ * build time. A single `curl` is not reliable enough for that: hosted CI
8
+ * runners and some networks intermittently fail the TLS handshake with the
9
+ * release hosts — `curl: (35) SSL connect error` — and the next attempt
10
+ * usually succeeds. CI also caches the tarball by version, so with a retry here
11
+ * the download happens at most once per release rather than once per run.
12
+ *
13
+ * The bytes are written to `<destination>.part` and renamed only after curl
14
+ * reports success: an interrupted transfer can then never be mistaken for a
15
+ * complete tarball, neither by this run nor by a later one that restores the
16
+ * directory from `actions/cache`.
17
+ */
18
+
19
+ import { spawnSync } from "node:child_process";
20
+ import { renameSync, rmSync } from "node:fs";
21
+
22
+ /** curl exit codes of transfers that may succeed when repeated: name
23
+ * resolution (6), connect timeout (28), TLS handshake (35), empty reply (52),
24
+ * send failure (55) and connection reset (56). Everything else — a 404, a
25
+ * permission error — is reported immediately. */
26
+ const TRANSIENT_STATUSES = new Set([6, 28, 35, 52, 55, 56]);
27
+
28
+ export interface DownloadOptions {
29
+ /** Total attempts, including the first. */
30
+ attempts?: number;
31
+ /** Base backoff in milliseconds; attempt n waits `retryDelayMs * n`. */
32
+ retryDelayMs?: number;
33
+ }
34
+
35
+ /** Block the single-threaded build long enough for a fresh TLS handshake. */
36
+ function pause(ms: number): void {
37
+ if (ms <= 0) return;
38
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
39
+ }
40
+
41
+ /**
42
+ * Download `url` to `destination`, retrying the transient failures above.
43
+ * Throws when the transfer still fails, leaving no `.part` file behind.
44
+ */
45
+ export function downloadTarball(
46
+ destination: string,
47
+ url: string,
48
+ label: string,
49
+ options: DownloadOptions = {},
50
+ ): void {
51
+ const attempts = options.attempts ?? 6;
52
+ const retryDelayMs = options.retryDelayMs ?? 3_000;
53
+ const partial = `${destination}.part`;
54
+ rmSync(partial, { force: true });
55
+ for (let attempt = 1; ; attempt += 1) {
56
+ const note = attempt === 1 ? "" : ` (attempt ${attempt}/${attempts})`;
57
+ console.log(`xbintsc: fetching ${label} from ${url}${note}`);
58
+ const { status } = spawnSync("curl", ["-sL", "--fail", "--connect-timeout", "30", "-o", partial, url], {
59
+ stdio: ["ignore", "inherit", "inherit"],
60
+ });
61
+ if (status === 0) {
62
+ renameSync(partial, destination);
63
+ return;
64
+ }
65
+ if (status === null || !TRANSIENT_STATUSES.has(status) || attempt >= attempts) {
66
+ rmSync(partial, { force: true });
67
+ throw new Error(`${label}: curl ${url} failed with status ${status ?? "?"}`);
68
+ }
69
+ pause(retryDelayMs * attempt);
70
+ }
71
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Cache invalidation for the vendored GUI build trees.
3
+ *
4
+ * `build-gui.ts` reuses `vendor/*-build-lean` when the archive it produced is
5
+ * already there, which is what makes a second `npm run gui` fast. CMake records
6
+ * the compiler in its cache, but the macOS deployment target is only a cache
7
+ * entry: a tree configured by an earlier checkout (or before the baseline was
8
+ * pinned) keeps its old objects inside an archive that is supposed to target
9
+ * the oldest supported macOS. This drops such a tree once, so the next
10
+ * configure builds it for the current target.
11
+ */
12
+
13
+ import { existsSync, readFileSync, rmSync } from "node:fs";
14
+ import { join } from "node:path";
15
+
16
+ /** Remove `buildDir` when its CMake cache targets a different macOS version. */
17
+ export function ensureDeploymentTarget(name: string, buildDir: string): void {
18
+ if (process.platform !== "darwin") return;
19
+ const cache = join(buildDir, "CMakeCache.txt");
20
+ if (!existsSync(cache)) return;
21
+ const configured = /^CMAKE_OSX_DEPLOYMENT_TARGET:STRING=(.*)$/m.exec(readFileSync(cache, "utf8"))?.[1]?.trim();
22
+ const wanted = process.env.MACOSX_DEPLOYMENT_TARGET ?? "";
23
+ if ((configured ?? "") === wanted) return;
24
+ console.log(
25
+ `xbintsc: ${name} was built for macOS '${configured ?? ""}'; rebuilding for '${wanted}'`,
26
+ );
27
+ rmSync(buildDir, { recursive: true, force: true });
28
+ }
@@ -1,11 +1,14 @@
1
1
  /**
2
- * Size options shared by the GUI archive build.
2
+ * Build options shared by the GUI and media archive builds.
3
3
  *
4
4
  * Kept apart from `build-gui.ts` so the orchestration stays under the
5
5
  * repository's per-file line budget and the "what do we compile out?" policy is
6
6
  * reviewable in one place.
7
7
  */
8
8
 
9
+ import { MACOS_MINIMUM_VERSION } from "../src/driver/platform-support.js";
10
+ import { sysrootFlags } from "../src/driver/toolchain.js";
11
+
9
12
  /**
10
13
  * CMake generator and ABI settings used for every vendored library. Ninja is
11
14
  * selected on Windows because the Visual Studio generator imports the
@@ -22,6 +25,10 @@
22
25
  * `cmake_minimum_required` below 3.15, which defaults the policy to OLD and
23
26
  * silently ignores it — hence `CMAKE_POLICY_DEFAULT_CMP0091`.
24
27
  *
28
+ * On macOS every vendored library targets the oldest supported deployment
29
+ * target (`MACOSX_DEPLOYMENT_TARGET`, default 11.0), so a newer SDK on the
30
+ * build machine cannot raise the floor of the archive.
31
+ *
25
32
  * The tool paths are absolute and come from the same clang the driver resolves
26
33
  * (on Windows, the LLVM bundled with Visual Studio when no newer one is on
27
34
  * `PATH`), so every object merged into the archive shares one ABI and one
@@ -38,7 +45,11 @@ export function cmakeToolchainArgs(toolchain: {
38
45
  `-DCMAKE_C_COMPILER=${toolchain.clang}`,
39
46
  `-DCMAKE_CXX_COMPILER=${toolchain.clangxx}`,
40
47
  ];
41
- if (!toolchain.isWindows) return compilers;
48
+ const macos =
49
+ process.platform === "darwin"
50
+ ? [`-DCMAKE_OSX_DEPLOYMENT_TARGET=${process.env.CMAKE_OSX_DEPLOYMENT_TARGET ?? MACOS_MINIMUM_VERSION}`]
51
+ : [];
52
+ if (!toolchain.isWindows) return [...compilers, ...macos];
42
53
  return [
43
54
  "-G", "Ninja",
44
55
  "-DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded",
@@ -52,6 +63,19 @@ export function cmakeToolchainArgs(toolchain: {
52
63
  * never references (`--gc-sections`, `-dead_strip`, link.exe `/OPT:REF`). */
53
64
  export const SECTION_FLAGS = ["-ffunction-sections", "-fdata-sections"] as const;
54
65
 
66
+ /**
67
+ * Flags for the C++ translation units merged into the GUI and media archives.
68
+ *
69
+ * `sdkRoot` must be passed as `-isysroot`: an Apple clang invoked by absolute
70
+ * path never infers its sysroot, and macOS keeps every standard header —
71
+ * libc++'s `<string>` included — inside the SDK rather than in `/usr/include`
72
+ * or the toolchain. Without it the first translation unit fails with
73
+ * `'string' file not found`.
74
+ */
75
+ export function cxxCompileFlags(sdkRoot: string | undefined, includeFlags: readonly string[]): string[] {
76
+ return ["-std=c++17", "-O2", "-Wall", "-Wextra", ...SECTION_FLAGS, ...sysrootFlags(sdkRoot), ...includeFlags];
77
+ }
78
+
55
79
  /** The same flags as CMake cache entries. */
56
80
  export const SECTION_CMAKE_FLAGS = [
57
81
  `-DCMAKE_C_FLAGS=${SECTION_FLAGS.join(" ")}`,
@@ -101,6 +101,10 @@ if (process.platform !== "win32") chmodSync(stagedBinary, 0o755);
101
101
  console.log(`xbintsc: packaged binary ${basename(binary)} (${statSync(binary).size} bytes)`);
102
102
 
103
103
  // 2. The C runtime (sources fallback + prebuilt archives) next to the binary.
104
+ // The copy is bracketed with its own messages so a stalled packaging step can be
105
+ // attributed: this tree carries the freshly built gui/media archives, and a copy
106
+ // blocked on one of them looks exactly like a hang in the `tar` call below.
107
+ console.log("xbintsc: copying runtime/ into the staging tree");
104
108
  cpSync(findRuntimeDir(), join(stage, "runtime"), { recursive: true });
105
109
  console.log("xbintsc: packaged runtime/");
106
110
 
@@ -116,6 +120,10 @@ const extension = releaseArchiveExtension(hasZstd);
116
120
  const archive = join(releaseRoot, `${archiveBase}${extension}`);
117
121
  rmSync(archive, { force: true });
118
122
  const tarArgs = ["-c", hasZstd ? "--zstd" : "-z", "-f", archive, "-C", stageRoot, base];
123
+ // Log the exact command before running it: `tar` prints nothing on success, so
124
+ // this line is what tells a stuck step apart from a stuck filesystem copy above,
125
+ // and it records which compression filter this runner chose (`--zstd` vs `-z`).
126
+ console.log(`xbintsc: running tar ${tarArgs.join(" ")}`);
119
127
  const tar = spawnSync("tar", tarArgs, { stdio: "inherit" });
120
128
  if (tar.error) fail(`failed to run tar: ${tar.error.message}`);
121
129
  if (tar.status !== 0) fail(`tar exited with ${tar.status}`);
package/src/cli/hints.ts CHANGED
@@ -36,10 +36,27 @@ const REQUIREMENT_HINT: DocumentationPointer = {
36
36
  path: DOC_REQUIREMENTS,
37
37
  };
38
38
 
39
- /** Hint for a clang failure: a missing or too-old clang has a specific answer. */
40
- export function toolchainPointer(message: string): DocumentationPointer {
39
+ /**
40
+ * Hint for a clang failure: a missing or too-old clang has a specific answer.
41
+ *
42
+ * `platform` is a parameter so the macOS branch can be tested from any host.
43
+ */
44
+ export function toolchainPointer(
45
+ message: string,
46
+ platform: NodeJS.Platform = process.platform,
47
+ ): DocumentationPointer {
41
48
  const usesTypedPointers = /defined with type '\[.*i8\]\*' but expected 'i8\*'/.test(message);
42
49
  if (usesTypedPointers || /No C compiler found/i.test(message)) return REQUIREMENT_HINT;
50
+ /* On macOS a C standard header lives in the SDK, so "'<header>' file not
51
+ * found" means the driver never got a sysroot — the header itself is fine.
52
+ * The driver resolves the SDK with `xcrun` and passes `-isysroot`, which
53
+ * makes the selected Xcode the thing to check. */
54
+ if (platform === "darwin" && /fatal error: '.+' file not found/.test(message)) {
55
+ return {
56
+ text: "clang cannot see the macOS SDK headers; `xcrun --show-sdk-path` must resolve for the selected Xcode, or set SDKROOT yourself",
57
+ path: DOC_TROUBLESHOOTING,
58
+ };
59
+ }
43
60
  return {
44
61
  text: "read the clang output above, then the toolchain section of the troubleshooting guide",
45
62
  path: DOC_TROUBLESHOOTING,
package/src/cli/main.ts CHANGED
@@ -20,9 +20,11 @@ import { formatDiagnostic, type Diagnostic } from "../diagnostics/diagnostic.js"
20
20
  import { SourceFile } from "../diagnostics/source.js";
21
21
  import { build, compileEntry, COMPILER_VERSION, type EmitKind, type PhaseTiming } from "../driver/compiler.js";
22
22
  import { findRuntimeDir, platformSlug } from "../driver/paths.js";
23
+ import { supportFloor } from "../driver/platform-support.js";
23
24
  import { findRuntimeLibrary, runtimeLibDir } from "../driver/runtime-lib.js";
24
25
  import { resolveToolchain, type ResolvedToolchain } from "../driver/toolchain-provider.js";
25
26
  import { MINIMUM_CLANG_MAJOR, probeClang, realRunner, ToolchainError } from "../driver/toolchain.js";
27
+ import { macosSdkRoot } from "../driver/macos-sdk.js";
26
28
  import {
27
29
  DOC_CLI,
28
30
  DOC_TROUBLESHOOTING,
@@ -277,6 +279,15 @@ function doctor(io: CliIo): number {
277
279
  io.stdout("runtime lib: unavailable\n");
278
280
  }
279
281
 
282
+ io.stdout(`support : ${supportFloor()} (release artifacts target this floor)\n`);
283
+
284
+ if (process.platform === "darwin") {
285
+ /* macOS keeps the C headers in the SDK; without one, no header the runtime
286
+ * includes resolves. Report the sysroot the driver hands to clang. */
287
+ const sdk = toolchain?.sdkRoot ?? process.env.SDKROOT ?? macosSdkRoot(realRunner);
288
+ io.stdout(`sdk : ${sdk ?? "unresolved (xcrun --show-sdk-path failed)"}\n`);
289
+ }
290
+
280
291
  try {
281
292
  if (!toolchain) throw new Error("no toolchain");
282
293
  const resource = resolveResourceCompiler(realRunner, toolchain.clang);
@@ -22,6 +22,8 @@ export interface AppArtifactRequest {
22
22
  readonly app: AppConfig;
23
23
  readonly icon: IconInfo | undefined;
24
24
  readonly env: Record<string, string>;
25
+ /** macOS SDK for `-isysroot`; see `sysrootFlags`. */
26
+ readonly sdkRoot?: string;
25
27
  readonly verbose: boolean | undefined;
26
28
  }
27
29
 
@@ -46,11 +48,11 @@ export interface AppArtifacts {
46
48
  * no equivalent.
47
49
  */
48
50
  export function ensureAppArtifacts(request: AppArtifactRequest): AppArtifacts {
49
- const { runner, clang, cacheDir, registry, app, icon, env, verbose } = request;
51
+ const { runner, clang, cacheDir, registry, app, icon, env, sdkRoot, verbose } = request;
50
52
  const windowsSubsystem = process.platform === "win32" && registry.has("gui") && app.console !== true;
51
53
  const linkerFlags = windowsSubsystem ? ["-Wl,/SUBSYSTEM:WINDOWS"] : [];
52
54
  const entryObject = windowsSubsystem
53
- ? ensureWindowsEntryObject(runner, clang, cacheDir, env)
55
+ ? ensureWindowsEntryObject(runner, clang, cacheDir, env, sdkRoot)
54
56
  : undefined;
55
57
 
56
58
  const iconObjects: string[] = [];
@@ -58,7 +60,7 @@ export function ensureAppArtifacts(request: AppArtifactRequest): AppArtifacts {
58
60
  // program gets an (empty) icon object even with no icon configured.
59
61
  if (icon !== undefined || registry.has("gui")) {
60
62
  try {
61
- iconObjects.push(ensureIconObject(runner, clang, cacheDir, icon ?? EMPTY_ICON, env));
63
+ iconObjects.push(ensureIconObject(runner, clang, cacheDir, icon ?? EMPTY_ICON, env, sdkRoot));
62
64
  if (icon && process.platform === "win32") {
63
65
  const resourceCompiler = resolveResourceCompiler(runner, clang);
64
66
  if (resourceCompiler) {