xbintsc 0.5.32 → 0.5.37

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 (222) hide show
  1. package/README.md +10 -7
  2. package/README.zh-CN.md +10 -8
  3. package/dist/scripts/check-version.d.ts +27 -0
  4. package/dist/scripts/check-version.js +71 -0
  5. package/dist/scripts/check-version.js.map +1 -0
  6. package/dist/scripts/package-platform.d.ts +58 -0
  7. package/dist/scripts/package-platform.js +142 -0
  8. package/dist/scripts/package-platform.js.map +1 -0
  9. package/dist/scripts/verify-platform-package.d.ts +34 -0
  10. package/dist/scripts/verify-platform-package.js +161 -0
  11. package/dist/scripts/verify-platform-package.js.map +1 -0
  12. package/dist/src/cli/help.d.ts +1 -1
  13. package/dist/src/cli/main.js +3 -1
  14. package/dist/src/cli/main.js.map +1 -1
  15. package/dist/src/driver/runtime-lib.d.ts +29 -4
  16. package/dist/src/driver/runtime-lib.js +68 -10
  17. package/dist/src/driver/runtime-lib.js.map +1 -1
  18. package/dist/src/driver/version.d.ts +1 -1
  19. package/dist/src/driver/version.js +1 -1
  20. package/dist/src/extensions/gui/index.js +18 -5
  21. package/dist/src/extensions/gui/index.js.map +1 -1
  22. package/dist/src/extensions/media/index.js +4 -3
  23. package/dist/src/extensions/media/index.js.map +1 -1
  24. package/dist/src/parser/context.d.ts +41 -0
  25. package/dist/src/parser/context.js +89 -4
  26. package/dist/src/parser/context.js.map +1 -1
  27. package/dist/src/parser/literals.js +6 -12
  28. package/dist/src/parser/literals.js.map +1 -1
  29. package/dist/tests/driver/platform-package.test.d.ts +1 -0
  30. package/dist/tests/driver/platform-package.test.js +49 -0
  31. package/dist/tests/driver/platform-package.test.js.map +1 -0
  32. package/dist/tests/driver/runtime-lib.test.js +51 -4
  33. package/dist/tests/driver/runtime-lib.test.js.map +1 -1
  34. package/dist/tests/e2e/gui-audio.test.d.ts +14 -0
  35. package/dist/tests/e2e/gui-audio.test.js +287 -0
  36. package/dist/tests/e2e/gui-audio.test.js.map +1 -0
  37. package/dist/tests/e2e/gui-browser.test.js +65 -0
  38. package/dist/tests/e2e/gui-browser.test.js.map +1 -1
  39. package/dist/tests/e2e/gui-canvas-args.test.d.ts +6 -0
  40. package/dist/tests/e2e/gui-canvas-args.test.js +217 -0
  41. package/dist/tests/e2e/gui-canvas-args.test.js.map +1 -0
  42. package/dist/tests/e2e/gui-canvas.test.js.map +1 -1
  43. package/dist/tests/e2e/gui-dom.test.js +215 -0
  44. package/dist/tests/e2e/gui-dom.test.js.map +1 -1
  45. package/dist/tests/e2e/gui-events.test.js +112 -0
  46. package/dist/tests/e2e/gui-events.test.js.map +1 -1
  47. package/dist/tests/e2e/gui-helpers.d.ts +7 -0
  48. package/dist/tests/e2e/gui-helpers.js +1 -0
  49. package/dist/tests/e2e/gui-helpers.js.map +1 -1
  50. package/dist/tests/e2e/gui-media.test.js +78 -0
  51. package/dist/tests/e2e/gui-media.test.js.map +1 -1
  52. package/dist/tests/e2e/gui-style.test.js +55 -0
  53. package/dist/tests/e2e/gui-style.test.js.map +1 -1
  54. package/dist/tests/e2e/gui-webgl.test.js +258 -0
  55. package/dist/tests/e2e/gui-webgl.test.js.map +1 -1
  56. package/dist/tests/e2e/gui-webgl2-3d.test.d.ts +4 -0
  57. package/dist/tests/e2e/gui-webgl2-3d.test.js +264 -0
  58. package/dist/tests/e2e/gui-webgl2-3d.test.js.map +1 -0
  59. package/dist/tests/e2e/gui-webgl2-blit.test.d.ts +10 -0
  60. package/dist/tests/e2e/gui-webgl2-blit.test.js +262 -0
  61. package/dist/tests/e2e/gui-webgl2-blit.test.js.map +1 -0
  62. package/dist/tests/e2e/gui-webgl2-formats.test.d.ts +6 -0
  63. package/dist/tests/e2e/gui-webgl2-formats.test.js +395 -0
  64. package/dist/tests/e2e/gui-webgl2-formats.test.js.map +1 -0
  65. package/dist/tests/e2e/gui-webgl2-mrt.test.d.ts +4 -0
  66. package/dist/tests/e2e/gui-webgl2-mrt.test.js +105 -0
  67. package/dist/tests/e2e/gui-webgl2-mrt.test.js.map +1 -0
  68. package/dist/tests/e2e/gui-webgl2-ubo.test.d.ts +8 -0
  69. package/dist/tests/e2e/gui-webgl2-ubo.test.js +112 -0
  70. package/dist/tests/e2e/gui-webgl2-ubo.test.js.map +1 -0
  71. package/dist/tests/e2e/gui-webgl2.test.js +45 -1
  72. package/dist/tests/e2e/gui-webgl2.test.js.map +1 -1
  73. package/dist/tests/extensions/gui-index.test.js +1 -0
  74. package/dist/tests/extensions/gui-index.test.js.map +1 -1
  75. package/dist/tests/extensions/media.test.js +1 -0
  76. package/dist/tests/extensions/media.test.js.map +1 -1
  77. package/dist/tests/parser/expressions.test.js +30 -0
  78. package/dist/tests/parser/expressions.test.js.map +1 -1
  79. package/dist/tests/platform-tarball.test.d.ts +1 -0
  80. package/dist/tests/platform-tarball.test.js +111 -0
  81. package/dist/tests/platform-tarball.test.js.map +1 -0
  82. package/dist/tests/version.test.js +6 -0
  83. package/dist/tests/version.test.js.map +1 -1
  84. package/doc/ai/contributing.md +11 -0
  85. package/doc/ai/extensions.md +7 -4
  86. package/doc/ai/troubleshooting.md +7 -5
  87. package/doc/ai/zh-CN/contributing.md +9 -0
  88. package/doc/ai/zh-CN/extensions.md +4 -2
  89. package/doc/ai/zh-CN/troubleshooting.md +5 -3
  90. package/doc/gui/api.md +99 -22
  91. package/doc/gui/build.md +11 -4
  92. package/doc/gui/canvas.md +49 -18
  93. package/doc/gui/dom.md +57 -7
  94. package/doc/gui/media.md +4 -0
  95. package/doc/gui/webgl.md +105 -46
  96. package/doc/gui.md +7 -3
  97. package/doc/npm-package.md +55 -27
  98. package/doc/requirements.md +6 -2
  99. package/doc/zh-CN/gui/api.md +84 -14
  100. package/doc/zh-CN/gui/build.md +7 -2
  101. package/doc/zh-CN/gui/canvas.md +44 -19
  102. package/doc/zh-CN/gui/dom.md +48 -4
  103. package/doc/zh-CN/gui/media.md +3 -0
  104. package/doc/zh-CN/gui/webgl.md +22 -16
  105. package/doc/zh-CN/gui.md +3 -2
  106. package/doc/zh-CN/npm-package.md +44 -23
  107. package/doc/zh-CN/requirements.md +4 -1
  108. package/package.json +17 -1
  109. package/runtime/ext_gui/audio.cpp +365 -153
  110. package/runtime/ext_gui/audio_buffer.cpp +166 -0
  111. package/runtime/ext_gui/audio_buffer.h +37 -0
  112. package/runtime/ext_gui/audio_decode.cpp +219 -0
  113. package/runtime/ext_gui/audio_decode.h +39 -0
  114. package/runtime/ext_gui/audio_device.cpp +87 -0
  115. package/runtime/ext_gui/audio_effects.cpp +163 -0
  116. package/runtime/ext_gui/audio_effects.h +25 -0
  117. package/runtime/ext_gui/audio_internal.h +30 -0
  118. package/runtime/ext_gui/audio_mixer.cpp +494 -0
  119. package/runtime/ext_gui/audio_mixer.h +238 -0
  120. package/runtime/ext_gui/audio_param.cpp +104 -0
  121. package/runtime/ext_gui/audio_param_object.cpp +155 -0
  122. package/runtime/ext_gui/browser.cpp +45 -50
  123. package/runtime/ext_gui/browser_abort.cpp +249 -0
  124. package/runtime/ext_gui/browser_dialogs.cpp +76 -0
  125. package/runtime/ext_gui/browser_media.cpp +176 -0
  126. package/runtime/ext_gui/browser_navigation.cpp +130 -0
  127. package/runtime/ext_gui/canvas.cpp +88 -30
  128. package/runtime/ext_gui/canvas.h +74 -9
  129. package/runtime/ext_gui/canvas_image_data.cpp +143 -0
  130. package/runtime/ext_gui/canvas_internal.h +8 -5
  131. package/runtime/ext_gui/canvas_pattern.cpp +90 -0
  132. package/runtime/ext_gui/canvas_raster.cpp +31 -9
  133. package/runtime/ext_gui/canvas_style.cpp +4 -2
  134. package/runtime/ext_gui/css_selector.cpp +21 -3
  135. package/runtime/ext_gui/dom.h +4 -0
  136. package/runtime/ext_gui/dom_api.cpp +1 -0
  137. package/runtime/ext_gui/dom_api_canvas.cpp +120 -114
  138. package/runtime/ext_gui/dom_api_canvas_extra.cpp +57 -2
  139. package/runtime/ext_gui/dom_api_canvas_image.cpp +239 -0
  140. package/runtime/ext_gui/dom_api_canvas_paths.cpp +179 -0
  141. package/runtime/ext_gui/dom_api_computed_style.cpp +124 -0
  142. package/runtime/ext_gui/dom_api_dataset.cpp +154 -0
  143. package/runtime/ext_gui/dom_api_document.cpp +58 -5
  144. package/runtime/ext_gui/dom_api_events.cpp +93 -17
  145. package/runtime/ext_gui/dom_api_internal.h +71 -2
  146. package/runtime/ext_gui/dom_api_mutation.cpp +159 -0
  147. package/runtime/ext_gui/dom_api_node.cpp +40 -19
  148. package/runtime/ext_gui/dom_api_node_read.cpp +37 -1
  149. package/runtime/ext_gui/dom_api_path2d.cpp +358 -0
  150. package/runtime/ext_gui/dom_api_proto.cpp +35 -0
  151. package/runtime/ext_gui/dom_api_style.cpp +223 -13
  152. package/runtime/ext_gui/dom_api_webgl.cpp +7 -0
  153. package/runtime/ext_gui/dom_api_webgl2.cpp +151 -36
  154. package/runtime/ext_gui/dom_api_webgl2_draw.cpp +3 -3
  155. package/runtime/ext_gui/dom_api_webgl_buffer.cpp +6 -2
  156. package/runtime/ext_gui/dom_api_webgl_compressed.cpp +167 -0
  157. package/runtime/ext_gui/dom_api_webgl_framebuffer.cpp +160 -0
  158. package/runtime/ext_gui/dom_api_webgl_shader.cpp +4 -4
  159. package/runtime/ext_gui/dom_api_webgl_source.cpp +161 -0
  160. package/runtime/ext_gui/dom_api_webgl_state.cpp +169 -23
  161. package/runtime/ext_gui/dom_api_webgl_texture.cpp +276 -236
  162. package/runtime/ext_gui/dom_api_webgl_ubo.cpp +81 -0
  163. package/runtime/ext_gui/gui.cpp +103 -14
  164. package/runtime/ext_gui/gui_engine.h +56 -4
  165. package/runtime/ext_gui/gui_events.cpp +80 -9
  166. package/runtime/ext_gui/image_bitmap.cpp +28 -0
  167. package/runtime/ext_gui/image_bitmap.h +26 -0
  168. package/runtime/ext_gui/image_bitmap_browser.cpp +136 -0
  169. package/runtime/ext_gui/listener_options.h +43 -0
  170. package/runtime/ext_gui/media.cpp +32 -0
  171. package/runtime/ext_gui/media.h +13 -0
  172. package/runtime/ext_gui/media_api.h +23 -0
  173. package/runtime/ext_gui/paint.cpp +28 -0
  174. package/runtime/ext_gui/paint.h +4 -0
  175. package/runtime/ext_gui/webgl.h +152 -18
  176. package/runtime/ext_gui/webgl_blit.cpp +95 -0
  177. package/runtime/ext_gui/webgl_buffer.cpp +178 -263
  178. package/runtime/ext_gui/webgl_buffer_objects.cpp +270 -0
  179. package/runtime/ext_gui/webgl_clear.cpp +142 -0
  180. package/runtime/ext_gui/webgl_compressed.cpp +180 -0
  181. package/runtime/ext_gui/webgl_context.cpp +241 -156
  182. package/runtime/ext_gui/webgl_draw.cpp +68 -3
  183. package/runtime/ext_gui/webgl_draw_buffers.cpp +75 -0
  184. package/runtime/ext_gui/webgl_glsl.h +40 -0
  185. package/runtime/ext_gui/webgl_glsl_compile.cpp +6 -0
  186. package/runtime/ext_gui/webgl_glsl_rewrite.cpp +18 -0
  187. package/runtime/ext_gui/webgl_glsl_scan.cpp +20 -11
  188. package/runtime/ext_gui/webgl_glsl_scan_block.cpp +126 -0
  189. package/runtime/ext_gui/webgl_glsl_tokens.h +28 -0
  190. package/runtime/ext_gui/webgl_pipeline.cpp +71 -19
  191. package/runtime/ext_gui/webgl_program_blocks.cpp +58 -0
  192. package/runtime/ext_gui/webgl_program_internal.h +6 -0
  193. package/runtime/ext_gui/webgl_shader.cpp +13 -1
  194. package/runtime/ext_gui/webgl_state.cpp +228 -0
  195. package/runtime/ext_gui/webgl_texture.cpp +144 -17
  196. package/runtime/ext_gui/webgl_texture_3d.cpp +215 -0
  197. package/runtime/ext_gui/webgl_uniform_block.h +31 -0
  198. package/runtime/ext_gui/webgl_webgl2.cpp +58 -5
  199. package/runtime/ext_gui/window.cpp +14 -65
  200. package/runtime/ext_gui/window_events.cpp +125 -0
  201. package/runtime/ext_media/decode_audio.cpp +308 -0
  202. package/runtime/ext_media/media_bridge.c +2 -0
  203. package/runtime/rt.h +43 -0
  204. package/runtime/rt_internal.h +4 -0
  205. package/runtime/xt_array_buffer.c +30 -0
  206. package/runtime/xt_builtins.c +0 -21
  207. package/runtime/xt_containers/access.inc +94 -2
  208. package/runtime/xt_containers/objects.inc +29 -31
  209. package/runtime/xt_typed_array/elements.inc +13 -0
  210. package/scripts/build-media.ts +14 -6
  211. package/scripts/build-runtime.ts +21 -16
  212. package/scripts/check-version.ts +39 -9
  213. package/scripts/package-platform.ts +183 -0
  214. package/scripts/set-platform-version.ts +62 -0
  215. package/scripts/verify-platform-package.ts +182 -0
  216. package/src/cli/main.ts +5 -1
  217. package/src/driver/runtime-lib.ts +73 -9
  218. package/src/driver/version.ts +1 -1
  219. package/src/extensions/gui/index.ts +18 -5
  220. package/src/extensions/media/index.ts +4 -3
  221. package/src/parser/context.ts +99 -4
  222. package/src/parser/literals.ts +6 -12
@@ -58,6 +58,19 @@ const char *xt_typed_array_name_of(int kind) {
58
58
  return xt_ta_names[kind];
59
59
  }
60
60
 
61
+ /* Public wrappers for extensions (ext_gui's audio decoder reads a Float32Array's
62
+ * bytes). `.buffer`/`.byteOffset` are accessors on the typed-array prototypes,
63
+ * so a raw `xt_object_get_cstr` cannot reach them. */
64
+ int xt_is_typed_array(xt_value value) { return xt_typed_array_kind_of(value) >= 0; }
65
+
66
+ xt_value xt_typed_array_backing_buffer(xt_value value) {
67
+ return xt_typed_array_kind_of(value) >= 0 ? xt_typed_array_buffer_of(value) : XT_UNDEFINED;
68
+ }
69
+
70
+ uint32_t xt_typed_array_view_offset(xt_value value) {
71
+ return xt_typed_array_kind_of(value) >= 0 ? xt_typed_array_byte_offset_of(value) : 0;
72
+ }
73
+
61
74
  /* -- buffer-backed views -------------------------------------------------- */
62
75
 
63
76
  /* The backing ArrayBuffer and byte offset live under private symbols, so a
@@ -65,7 +65,6 @@ const ffmpegInstall = join(ffmpegBuild, "dist");
65
65
  const objDir = join(root, "build", "media-obj");
66
66
  const outDir = join(runtimeDir, "lib", platformSlug());
67
67
  const output = join(outDir, isWindows ? "media.lib" : "media.a");
68
- const object = join(objDir, isWindows ? "media.obj" : "media.o");
69
68
 
70
69
  const FFMPEG_VERSION = process.env.FFMPEG_VERSION || "7.1";
71
70
  const FFMPEG_URL = `https://ffmpeg.org/releases/ffmpeg-${FFMPEG_VERSION}.tar.xz`;
@@ -478,28 +477,37 @@ if (!ffmpegInstalled()) {
478
477
  * because the *source* root contains a file literally named `version` that
479
478
  * would shadow the C++ standard header for any `.cpp` that includes it. */
480
479
  mkdirSync(objDir, { recursive: true });
481
- const playerSource = join(sourceDir, "media.cpp");
482
- console.log("xbintsc: compiling media.cpp");
480
+ /* Two translation units: the file-oriented player and the in-memory decoder
481
+ * `decodeAudioData` uses. Both are merged into the archive; the bridge object
482
+ * references every entry point, so each member is extracted on demand. */
483
+ const playerSources = ["media.cpp", "decode_audio.cpp"];
484
+ const playerObjects: string[] = [];
483
485
  const cxxFlags = ["-std=c++17", "-O2", "-Wall", "-Wextra", "-I", runtimeDir, "-I", join(ffmpegInstall, "include")];
484
486
  /* The engine and the player are C++ with the static CRT (`libcpmt`), matching
485
487
  * FFmpeg's clang-cl objects and the xbintsc runtime; see `scripts/build-gui.ts`. */
486
488
  if (isWindows) cxxFlags.push("-Xclang", "--dependent-lib=libcpmt");
487
489
  else cxxFlags.push("-fPIC");
488
- run(clangxx, [...cxxFlags, "-c", playerSource, "-o", object]);
490
+ for (const name of playerSources) {
491
+ const source = join(sourceDir, name);
492
+ const compiled = join(objDir, isWindows ? name.replace(/\.cpp$/, ".obj") : name.replace(/\.cpp$/, ".o"));
493
+ console.log(`xbintsc: compiling ${name}`);
494
+ run(clangxx, [...cxxFlags, "-c", source, "-o", compiled]);
495
+ playerObjects.push(compiled);
496
+ }
489
497
 
490
498
  /* -- 4. merge into the archive -------------------------------------------- */
491
499
 
492
500
  mkdirSync(outDir, { recursive: true });
493
501
  rmSync(output, { force: true });
494
502
  if (process.platform === "darwin") {
495
- run("libtool", ["-static", "-o", output, object, ...ffmpegStaticLibraries()!], { quietStderr: true });
503
+ run("libtool", ["-static", "-o", output, ...playerObjects, ...ffmpegStaticLibraries()!], { quietStderr: true });
496
504
  } else {
497
505
  /* `ar -M` (GNU/LLVM) merges archives through an MRI script. Windows paths are
498
506
  * normalised to `/` so the MRI parser does not read `\` as an escape. */
499
507
  const mri = (path: string): string => path.replace(/\\/g, "/");
500
508
  const script = [
501
509
  `create ${mri(output)}`,
502
- `addmod ${mri(object)}`,
510
+ ...playerObjects.map((object) => `addmod ${mri(object)}`),
503
511
  ...ffmpegStaticLibraries()!.map((library) => `addlib ${mri(library)}`),
504
512
  "save",
505
513
  "end",
@@ -5,13 +5,19 @@
5
5
  * runtime/lib/<os>-<arch>/core.a compiled from RUNTIME_SOURCES
6
6
  * runtime/lib/<os>-<arch>/ext_node.a compiled from nodeExtension.runtimeSources()
7
7
  *
8
+ * On Windows the same archives are written as MSVC-compatible COFF libraries
9
+ * (`core.lib`, `ext_node.lib`): the objects are compiled by the MSVC-targeting
10
+ * clang the driver uses and merged by `llvm-ar`, exactly like `gui.lib` and
11
+ * `media.lib`.
12
+ *
8
13
  * The driver prefers these archives (see `src/driver/runtime-lib.ts`) and falls
9
14
  * back to compiling the C sources on demand when they are missing. Run this in
10
- * CI (or locally) after the runtime sources change.
15
+ * CI (or locally) after the runtime sources change; `scripts/package-platform.ts`
16
+ * then places the result in the optional `@xbintsc/<os>-<arch>` npm package.
11
17
  *
12
18
  * Environment:
13
19
  * xbintsc_CLANG C compiler override (otherwise the resolved toolchain is used)
14
- * xbintsc_AR archiver override (otherwise `llvm-ar`, then `ar`)
20
+ * xbintsc_AR archiver override (otherwise `llvm-ar`, then `ar` on POSIX)
15
21
  */
16
22
 
17
23
  import { mkdirSync, rmSync } from "node:fs";
@@ -28,15 +34,8 @@ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
28
34
  const runtimeDir = join(root, "runtime");
29
35
  const outDir = join(runtimeDir, "lib", platformSlug());
30
36
  const objDir = join(root, "build", "runtime-obj");
31
-
32
- if (process.platform === "win32") {
33
- // The driver links with the MSVC ABI on Windows, where the prebuilt archives
34
- // are unnecessary: `build` compiles the runtime C sources on demand with the
35
- // user's clang. (Producing MSVC-compatible COFF `.lib` archives is possible
36
- // but not worth the extra step.)
37
- console.log("xbintsc: skipping runtime archives on Windows; the driver compiles the C sources on demand");
38
- process.exit(0);
39
- }
37
+ const isWindows = process.platform === "win32";
38
+ const archiveSuffix = isWindows ? ".lib" : ".a";
40
39
 
41
40
  // Use the same toolchain the driver will.
42
41
  const toolchain = resolveToolchain();
@@ -51,18 +50,24 @@ function isRunnable(candidate: string): boolean {
51
50
 
52
51
  function findArchiver(clangPath: string): string {
53
52
  const dir = dirname(clangPath);
54
- const exe = process.platform === "win32" ? ".exe" : "";
53
+ const exe = isWindows ? ".exe" : "";
55
54
  const candidates = [
56
55
  process.env.xbintsc_AR,
57
56
  // A clang install usually ships `llvm-ar` next to the driver.
58
57
  dir === "." ? undefined : join(dir, `llvm-ar${exe}`),
59
58
  "llvm-ar",
60
- "ar",
59
+ // On Windows only LLVM's archiver is known to write the COFF flavour the
60
+ // MSVC linker accepts; never fall back to a GNU `ar`.
61
+ isWindows ? undefined : "ar",
61
62
  ].filter((candidate): candidate is string => Boolean(candidate));
62
63
  for (const candidate of candidates) {
63
64
  if (isRunnable(candidate)) return candidate;
64
65
  }
65
- console.error("xbintsc: no archiver found; set xbintsc_AR to llvm-ar or ar");
66
+ console.error(
67
+ isWindows
68
+ ? "xbintsc: no archiver found; install llvm-ar or set xbintsc_AR to it"
69
+ : "xbintsc: no archiver found; set xbintsc_AR to llvm-ar or ar",
70
+ );
66
71
  process.exit(1);
67
72
  }
68
73
 
@@ -101,9 +106,9 @@ console.log(`xbintsc: runtime toolchain ${clang} (${toolchain.source})`);
101
106
  const coreObjects = compile(RUNTIME_SOURCES.map((source) => join(runtimeDir, source)), "core");
102
107
 
103
108
  const ar = findArchiver(clang);
104
- archive("core.a", coreObjects, ar);
109
+ archive(`core${archiveSuffix}`, coreObjects, ar);
105
110
 
106
111
  const nodeSources = nodeExtension.runtimeSources?.() ?? [];
107
112
  if (nodeSources.length > 0) {
108
- archive("ext_node.a", compile(nodeSources, "node"), ar);
113
+ archive(`ext_node${archiveSuffix}`, compile(nodeSources, "node"), ar);
109
114
  }
@@ -1,18 +1,25 @@
1
1
  #!/usr/bin/env node
2
2
  /**
3
- * Verify that `COMPILER_VERSION` (reported by `xbintsc version` / `doctor` and
4
- * embedded in the incremental cache key) matches the version in `package.json`
5
- * (which names the release archives).
3
+ * Verify the release bookkeeping that must agree before a tag is pushed:
6
4
  *
7
- * Release automation runs this before tagging. A mismatch means an upgraded
8
- * compiler can reuse binaries produced by the previous one while presenting the
9
- * wrong version to bug reports.
5
+ * 1. `COMPILER_VERSION` (reported by `xbintsc version` / `doctor` and embedded
6
+ * in the incremental cache key) matches the version in `package.json`;
7
+ * 2. every `@xbintsc/<os>-<arch>` optional dependency is either the current
8
+ * version (a release checkout) or the published `0.0.0` placeholder (the
9
+ * committed state; release automation rewrites it before `npm publish`).
10
+ *
11
+ * A mismatch means an upgraded compiler can reuse binaries produced by the
12
+ * previous one while presenting the wrong version to bug reports, or that an
13
+ * install would ask npm for a platform package that does not exist.
10
14
  */
11
15
 
12
16
  import { readFileSync } from "node:fs";
13
17
  import { dirname, join, resolve } from "node:path";
14
18
  import { fileURLToPath, pathToFileURL } from "node:url";
15
19
 
20
+ /** The version of the empty `@xbintsc/*` packages created before a release. */
21
+ export const PLATFORM_PLACEHOLDER_VERSION = "0.0.0";
22
+
16
23
  /** Read the version literal from the compiler source. */
17
24
  export function compilerVersionOf(root: string): string {
18
25
  const text = readFileSync(join(root, "src", "driver", "version.ts"), "utf8");
@@ -39,14 +46,37 @@ export function checkCompilerVersion(root: string): string | undefined {
39
46
  );
40
47
  }
41
48
 
49
+ /**
50
+ * Returns a message when a platform optional dependency names a version that is
51
+ * neither the release version nor the placeholder, or `undefined` when valid.
52
+ */
53
+ export function checkPlatformVersions(root: string): string | undefined {
54
+ const parsed = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as {
55
+ version: string;
56
+ optionalDependencies?: Record<string, string>;
57
+ };
58
+ for (const [name, spec] of Object.entries(parsed.optionalDependencies ?? {})) {
59
+ if (!name.startsWith("@xbintsc/")) continue;
60
+ if (spec === parsed.version || spec === PLATFORM_PLACEHOLDER_VERSION) continue;
61
+ return (
62
+ `optional dependency ${name} is "${spec}", but package.json is "${parsed.version}".\n` +
63
+ `Use "${PLATFORM_PLACEHOLDER_VERSION}" in the committed manifest (release automation rewrites it) ` +
64
+ `or "${parsed.version}" in a release checkout.`
65
+ );
66
+ }
67
+ return undefined;
68
+ }
69
+
42
70
  const invokedDirectly =
43
71
  process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;
44
72
 
45
73
  if (invokedDirectly) {
46
74
  const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
47
- const mismatch = checkCompilerVersion(root);
48
- if (mismatch) {
49
- process.stderr.write(`xbintsc: version mismatch\n${mismatch}\n`);
75
+ const problems = [checkCompilerVersion(root), checkPlatformVersions(root)].filter(
76
+ (problem): problem is string => problem !== undefined,
77
+ );
78
+ if (problems.length > 0) {
79
+ process.stderr.write(`xbintsc: version mismatch\n${problems.join("\n")}\n`);
50
80
  process.exit(1);
51
81
  }
52
82
  process.stdout.write(`xbintsc: version ${compilerVersionOf(root)} matches package.json\n`);
@@ -0,0 +1,183 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Assemble the optional `@xbintsc/<os>-<arch>` npm package for the current host.
4
+ *
5
+ * The package carries the prebuilt archives in the same layout the compiler
6
+ * already searches:
7
+ *
8
+ * package/runtime/lib/<os>-<arch>/{core,ext_node}.{a,lib}
9
+ * package/runtime/lib/<os>-<arch>/gui.{a,lib} (when the release ships it)
10
+ * package/runtime/lib/<os>-<arch>/media.{a,lib} (when the release ships it)
11
+ *
12
+ * `xbintsc` lists these packages in `optionalDependencies`, so npm installs the
13
+ * one matching the host and the driver links its archives instead of compiling
14
+ * the C runtime. Release automation publishes the tarball produced here; see
15
+ * `.github/workflows/release.yml`.
16
+ *
17
+ * Output: `dist/npm-platform/xbintsc-<os>-<arch>-<version>.tgz`.
18
+ */
19
+
20
+ import { spawnSync } from "node:child_process";
21
+ import { cpSync, existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
22
+ import { dirname, join, resolve } from "node:path";
23
+ import { fileURLToPath, pathToFileURL } from "node:url";
24
+ import { platformSlug } from "../src/driver/paths.js";
25
+
26
+ export interface PackageRepository {
27
+ readonly type: string;
28
+ readonly url: string;
29
+ }
30
+
31
+ export interface PlatformPackageOptions {
32
+ readonly version: string;
33
+ readonly slug: string;
34
+ readonly repository?: PackageRepository;
35
+ }
36
+
37
+ export interface PlatformPackageManifest {
38
+ readonly name: string;
39
+ readonly version: string;
40
+ readonly description: string;
41
+ readonly license: string;
42
+ readonly repository?: PackageRepository;
43
+ readonly os: readonly string[];
44
+ readonly cpu: readonly string[];
45
+ readonly files: readonly string[];
46
+ readonly publishConfig: { readonly access: "public" };
47
+ }
48
+
49
+ /** Every host a release publishes a platform package for. */
50
+ export const PLATFORM_SLUGS = [
51
+ "linux-x64",
52
+ "linux-arm64",
53
+ "darwin-x64",
54
+ "darwin-arm64",
55
+ "win32-x64",
56
+ "win32-arm64",
57
+ ] as const;
58
+
59
+ /** npm name of the platform package, e.g. `@xbintsc/linux-x64`. */
60
+ export function platformPackageName(slug: string): string {
61
+ return `@xbintsc/${slug}`;
62
+ }
63
+
64
+ /**
65
+ * File name `npm pack` gives the platform package, e.g.
66
+ * `xbintsc-linux-x64-1.2.3.tgz` (npm drops the scope from the file name).
67
+ */
68
+ export function platformPackageFileName(slug: string, version: string): string {
69
+ return `xbintsc-${slug}-${version}.tgz`;
70
+ }
71
+
72
+ /** Split `linux-x64` into its npm `os` and `cpu` constraints. */
73
+ function splitSlug(slug: string): { os: string; arch: string } {
74
+ const dash = slug.lastIndexOf("-");
75
+ if (dash <= 0 || dash === slug.length - 1) {
76
+ throw new Error(`invalid platform slug "${slug}" (expected <os>-<arch>)`);
77
+ }
78
+ return { os: slug.slice(0, dash), arch: slug.slice(dash + 1) };
79
+ }
80
+
81
+ /** Archives every platform package must contain. */
82
+ export function requiredArchives(slug: string): string[] {
83
+ const suffix = slug.startsWith("win32-") ? ".lib" : ".a";
84
+ return [`core${suffix}`, `ext_node${suffix}`];
85
+ }
86
+
87
+ /** The package manifest written into the staging tree and packed by npm. */
88
+ export function platformPackageManifest(options: PlatformPackageOptions): PlatformPackageManifest {
89
+ const { os, arch } = splitSlug(options.slug);
90
+ const name = platformPackageName(options.slug);
91
+ return {
92
+ name,
93
+ version: options.version,
94
+ description: `Prebuilt xbintsc C runtime archives for ${options.slug}. Installed automatically by xbintsc.`,
95
+ license: "MIT",
96
+ ...(options.repository ? { repository: options.repository } : {}),
97
+ os: [os],
98
+ cpu: [arch],
99
+ files: ["runtime"],
100
+ publishConfig: { access: "public" },
101
+ };
102
+ }
103
+
104
+ /** Run npm through the same CLI that invoked this script, when available. */
105
+ function runNpm(args: readonly string[], cwd: string): { stdout: string } {
106
+ const execPath = process.env.npm_execpath;
107
+ const useCli = execPath !== undefined && /\.(cjs|mjs|js)$/.test(execPath);
108
+ const command = useCli ? process.execPath : process.platform === "win32" ? "npm.cmd" : "npm";
109
+ const commandArgs = useCli ? [execPath, ...args] : [...args];
110
+ const result = spawnSync(command, commandArgs, {
111
+ cwd,
112
+ encoding: "utf8",
113
+ shell: !useCli && process.platform === "win32",
114
+ });
115
+ if (result.error) throw result.error;
116
+ if (result.status !== 0) {
117
+ throw new Error(`npm ${args.join(" ")} failed (${result.status}):\n${result.stderr}`);
118
+ }
119
+ return { stdout: result.stdout };
120
+ }
121
+
122
+ /**
123
+ * Copy the host's archives into a staging tree, write the manifest and run
124
+ * `npm pack`. Returns the absolute path of the produced `.tgz`.
125
+ */
126
+ export function packagePlatform(root: string, slug: string = platformSlug()): string {
127
+ const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as {
128
+ version: string;
129
+ repository?: PackageRepository;
130
+ };
131
+ const libDir = join(root, "runtime", "lib", slug);
132
+ const missing = requiredArchives(slug).filter((name) => !existsSync(join(libDir, name)));
133
+ if (missing.length > 0) {
134
+ throw new Error(`missing ${missing.join(", ")} in ${libDir}; run \`npm run runtime\` first`);
135
+ }
136
+
137
+ const stageRoot = join(root, "build", "npm-platform-stage");
138
+ const stage = join(stageRoot, "package");
139
+ rmSync(stageRoot, { recursive: true, force: true });
140
+ mkdirSync(stage, { recursive: true });
141
+ cpSync(libDir, join(stage, "runtime", "lib", slug), { recursive: true });
142
+
143
+ const manifest = platformPackageManifest({
144
+ version: pkg.version,
145
+ slug,
146
+ ...(pkg.repository ? { repository: pkg.repository } : {}),
147
+ });
148
+ writeFileSync(join(stage, "package.json"), `${JSON.stringify(manifest, null, 2)}\n`);
149
+ writeFileSync(
150
+ join(stage, "README.md"),
151
+ `# ${manifest.name}\n\n` +
152
+ `Prebuilt xbintsc runtime archives for \`${slug}\`.\n\n` +
153
+ `This package is installed automatically as an optional dependency of\n` +
154
+ `[\`xbintsc\`](https://www.npmjs.com/package/xbintsc) and is not meant to be\n` +
155
+ `used on its own. The compiler links \`runtime/lib/${slug}/\` from this\n` +
156
+ `package instead of compiling its C runtime on the first build.\n`,
157
+ );
158
+ const license = join(root, "LICENSE");
159
+ if (existsSync(license)) cpSync(license, join(stage, "LICENSE"));
160
+
161
+ const outDir = join(root, "dist", "npm-platform");
162
+ rmSync(outDir, { recursive: true, force: true });
163
+ mkdirSync(outDir, { recursive: true });
164
+ const packed = JSON.parse(runNpm(["pack", "--json", "--pack-destination", outDir], stage).stdout) as Array<{
165
+ filename?: string;
166
+ }>;
167
+ const filename = packed[0]?.filename;
168
+ if (!filename) throw new Error("npm pack did not report a tarball name");
169
+ return join(outDir, filename);
170
+ }
171
+
172
+ const invokedDirectly =
173
+ process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;
174
+
175
+ if (invokedDirectly) {
176
+ try {
177
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
178
+ console.log(`xbintsc: wrote ${packagePlatform(root)}`);
179
+ } catch (error) {
180
+ console.error(`xbintsc: ${error instanceof Error ? error.message : String(error)}`);
181
+ process.exit(1);
182
+ }
183
+ }
@@ -0,0 +1,62 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Point `optionalDependencies` at the version being released.
4
+ *
5
+ * The repository keeps every `@xbintsc/<os>-<arch>` entry on the `0.0.0`
6
+ * placeholder: those packages exist on npm (an empty first publish), while a
7
+ * release version does not exist until this workflow publishes it. Committing a
8
+ * not-yet-published version would make `npm ci` resolve a missing tarball.
9
+ *
10
+ * Release automation runs this after the platform tarballs have been validated
11
+ * and published, immediately before `npm publish`, so the published `xbintsc`
12
+ * manifest depends on the platform packages built from the same commit.
13
+ *
14
+ * Usage: `npm run platform-versions -- --version <x.y.z>` (defaults to the
15
+ * version in package.json).
16
+ */
17
+
18
+ import { readFileSync, writeFileSync } from "node:fs";
19
+ import { dirname, join, resolve } from "node:path";
20
+ import { fileURLToPath, pathToFileURL } from "node:url";
21
+
22
+ /** Rewrite every platform optional dependency; returns the names changed. */
23
+ export function setPlatformVersions(root: string, version: string): string[] {
24
+ const manifestPath = join(root, "package.json");
25
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8")) as {
26
+ optionalDependencies?: Record<string, string>;
27
+ };
28
+ const dependencies = manifest.optionalDependencies;
29
+ if (!dependencies) throw new Error("package.json has no optionalDependencies");
30
+ const changed: string[] = [];
31
+ for (const name of Object.keys(dependencies)) {
32
+ if (!name.startsWith("@xbintsc/")) continue;
33
+ if (dependencies[name] === version) continue;
34
+ dependencies[name] = version;
35
+ changed.push(name);
36
+ }
37
+ writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
38
+ return changed;
39
+ }
40
+
41
+ const invokedDirectly =
42
+ process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;
43
+
44
+ if (invokedDirectly) {
45
+ try {
46
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
47
+ const flag = process.argv.indexOf("--version");
48
+ const version =
49
+ flag >= 0 && process.argv[flag + 1] !== undefined
50
+ ? process.argv[flag + 1]!
51
+ : (JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as { version: string }).version;
52
+ const changed = setPlatformVersions(root, version);
53
+ console.log(
54
+ changed.length > 0
55
+ ? `xbintsc: platform optionalDependencies -> ${version} (${changed.length} packages)`
56
+ : `xbintsc: platform optionalDependencies already at ${version}`,
57
+ );
58
+ } catch (error) {
59
+ console.error(`xbintsc: ${error instanceof Error ? error.message : String(error)}`);
60
+ process.exit(1);
61
+ }
62
+ }
@@ -0,0 +1,182 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Verify the `@xbintsc/<os>-<arch>` npm tarballs staged for a release.
4
+ *
5
+ * `release.yml` publishes one platform package per host before `xbintsc` and
6
+ * then points the main manifest's `optionalDependencies` at exactly those
7
+ * versions, so every tarball has to prove it was packed from this release: its
8
+ * manifest must name `@xbintsc/<slug>@<version>` and it must carry the runtime
9
+ * archives the driver links (`core` and `ext_node`, see `requiredArchives`).
10
+ *
11
+ * This runs in Node on purpose. The check used to be a shell pipeline,
12
+ * `tar -tf x.tgz | grep -q <pattern>`, under `set -o pipefail`: `grep -q` closes
13
+ * the pipe on its first match, so `tar` — still decompressing the tens of
14
+ * megabytes that follow `core.a` — dies of SIGPIPE, and the pipeline reports a
15
+ * perfectly good tarball as "has no core runtime archive". Reading the gzip
16
+ * stream here has no such race, needs no external `tar` and behaves the same on
17
+ * every host.
18
+ *
19
+ * Usage: `npm run verify:platform -- --dir platform-packages [--version x.y.z]`
20
+ */
21
+
22
+ import { existsSync, readFileSync } from "node:fs";
23
+ import { basename, dirname, join, resolve } from "node:path";
24
+ import { fileURLToPath, pathToFileURL } from "node:url";
25
+ import { gunzipSync } from "node:zlib";
26
+ import {
27
+ PLATFORM_SLUGS,
28
+ platformPackageFileName,
29
+ platformPackageName,
30
+ requiredArchives,
31
+ } from "./package-platform.js";
32
+
33
+ /** One member of a tar archive; `data` is a view into the decompressed stream. */
34
+ export interface TarEntry {
35
+ readonly path: string;
36
+ readonly data: Buffer;
37
+ }
38
+
39
+ const BLOCK = 512;
40
+
41
+ /** Read a header field, stopping at its NUL padding. */
42
+ function readString(block: Buffer, offset: number, length: number): string {
43
+ const field = block.subarray(offset, offset + length);
44
+ const end = field.indexOf(0);
45
+ return field.subarray(0, end === -1 ? field.length : end).toString("utf8");
46
+ }
47
+
48
+ /** Decode a numeric header field (octal, or base-256 for very large values). */
49
+ function readNumber(block: Buffer, offset: number, length: number): number {
50
+ if ((block[offset]! & 0x80) !== 0) {
51
+ let value = block[offset]! & 0x7f;
52
+ for (let index = 1; index < length; index += 1) value = value * 256 + block[offset + index]!;
53
+ return value;
54
+ }
55
+ const text = readString(block, offset, length).trim();
56
+ return text === "" ? 0 : Number.parseInt(text, 8);
57
+ }
58
+
59
+ /** The `path=` record of a pax extended header, when it carries one. */
60
+ function paxPath(data: Buffer): string | undefined {
61
+ // Records are `<byte length> <key>=<value>\n`; only the path matters here.
62
+ const match = /(?:^|\n)\d+ path=([^\n]*)\n/.exec(data.toString("utf8"));
63
+ return match?.[1];
64
+ }
65
+
66
+ /** List the regular files of a gzip-compressed tar archive, in archive order. */
67
+ export function tarballEntries(tgz: Buffer, source = "tarball"): TarEntry[] {
68
+ let tar: Buffer;
69
+ try {
70
+ tar = gunzipSync(tgz);
71
+ } catch (error) {
72
+ const reason = error instanceof Error ? error.message : String(error);
73
+ throw new Error(`${source} is not a gzip-compressed tar archive (${reason})`);
74
+ }
75
+ const entries: TarEntry[] = [];
76
+ let offset = 0;
77
+ let override: string | undefined;
78
+ while (offset + BLOCK <= tar.length) {
79
+ const header = tar.subarray(offset, offset + BLOCK);
80
+ if (header.every((byte) => byte === 0)) break;
81
+ const size = readNumber(header, 124, 12);
82
+ const type = header[156] === 0 ? "0" : String.fromCharCode(header[156]!);
83
+ const dataStart = offset + BLOCK;
84
+ if (dataStart + size > tar.length) throw new Error(`${source} is truncated`);
85
+ const data = tar.subarray(dataStart, dataStart + size);
86
+ offset = dataStart + Math.ceil(size / BLOCK) * BLOCK;
87
+
88
+ // A long path arrives in its own preceding header: GNU `L` stores it
89
+ // verbatim, pax `x` records it as `path=`. `K` only renames a link target.
90
+ if (type === "L") {
91
+ override = readString(data, 0, data.length);
92
+ continue;
93
+ }
94
+ if (type === "x") {
95
+ override = paxPath(data) ?? override;
96
+ continue;
97
+ }
98
+ if (type === "K") continue;
99
+
100
+ const prefix = readString(header, 345, 155);
101
+ const name = readString(header, 0, 100);
102
+ const path = override ?? (prefix === "" ? name : `${prefix}/${name}`);
103
+ override = undefined;
104
+ // Only regular files can be the archives checked below; directories and
105
+ // links are skipped instead of being reported by their trailing slash.
106
+ if (type === "0" || type === "7") entries.push({ path, data });
107
+ }
108
+ return entries;
109
+ }
110
+
111
+ /** Read the manifest out of an already-parsed platform tarball. */
112
+ function manifestOf(entries: readonly TarEntry[], source: string): { name?: string; version?: string } {
113
+ const manifest = entries.find((entry) => entry.path === "package/package.json");
114
+ if (!manifest) throw new Error(`${source} has no package/package.json`);
115
+ try {
116
+ return JSON.parse(manifest.data.toString("utf8")) as { name?: string; version?: string };
117
+ } catch {
118
+ throw new Error(`${source} has an unreadable package/package.json`);
119
+ }
120
+ }
121
+
122
+ /**
123
+ * Check one platform tarball's manifest identity and runtime archives.
124
+ * `source` names the tarball in error messages (its path, or a fixture name).
125
+ */
126
+ export function verifyPlatformEntries(
127
+ entries: readonly TarEntry[],
128
+ slug: string,
129
+ version: string,
130
+ source = "platform package",
131
+ ): void {
132
+ const parsed = manifestOf(entries, source);
133
+ const expectedName = platformPackageName(slug);
134
+ if (parsed.name !== expectedName || parsed.version !== version) {
135
+ throw new Error(`${source} contains ${parsed.name}@${parsed.version}, expected ${expectedName}@${version}`);
136
+ }
137
+
138
+ const paths = new Set(entries.map((entry) => entry.path));
139
+ for (const archive of requiredArchives(slug)) {
140
+ const expected = `package/runtime/lib/${slug}/${archive}`;
141
+ if (paths.has(expected)) continue;
142
+ // Keep the historical wording: `core.a`/`core.lib` is "the core archive".
143
+ const label = archive.replace(/\.(a|lib)$/, "");
144
+ throw new Error(`${source} has no ${label} runtime archive (expected ${expected})`);
145
+ }
146
+ }
147
+
148
+ /** Read `tgzPath` and verify it as the platform package for `slug`/`version`. */
149
+ export function verifyPlatformPackage(tgzPath: string, slug: string, version: string): TarEntry[] {
150
+ const entries = tarballEntries(readFileSync(tgzPath), tgzPath);
151
+ verifyPlatformEntries(entries, slug, version, tgzPath);
152
+ return entries;
153
+ }
154
+
155
+ /** Value of `--name <value>`, or `undefined` when the flag is absent. */
156
+ function argument(name: string): string | undefined {
157
+ const index = process.argv.indexOf(name);
158
+ return index >= 0 ? process.argv[index + 1] : undefined;
159
+ }
160
+
161
+ const invokedDirectly =
162
+ process.argv[1] !== undefined && import.meta.url === pathToFileURL(process.argv[1]).href;
163
+
164
+ if (invokedDirectly) {
165
+ try {
166
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
167
+ const dir = argument("--dir") ?? join(root, "platform-packages");
168
+ const version =
169
+ argument("--version") ??
170
+ (JSON.parse(readFileSync(join(root, "package.json"), "utf8")) as { version: string }).version;
171
+ for (const slug of PLATFORM_SLUGS) {
172
+ const file = join(dir, platformPackageFileName(slug, version));
173
+ if (!existsSync(file)) throw new Error(`Missing npm platform package ${file}.`);
174
+ const entries = verifyPlatformPackage(file, slug, version);
175
+ console.log(`xbintsc: ${basename(file)} carries ${entries.length} files`);
176
+ }
177
+ console.log(`xbintsc: verified ${PLATFORM_SLUGS.length} platform packages at ${version}`);
178
+ } catch (error) {
179
+ console.error(`xbintsc: ${error instanceof Error ? error.message : String(error)}`);
180
+ process.exit(1);
181
+ }
182
+ }
package/src/cli/main.ts CHANGED
@@ -20,7 +20,7 @@ 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 { runtimeLibDir } from "../driver/runtime-lib.js";
23
+ import { findRuntimeLibrary, runtimeLibDir } from "../driver/runtime-lib.js";
24
24
  import { resolveToolchain, type ResolvedToolchain } from "../driver/toolchain-provider.js";
25
25
  import { MINIMUM_CLANG_MAJOR, probeClang, realRunner, ToolchainError } from "../driver/toolchain.js";
26
26
  import {
@@ -290,6 +290,10 @@ function doctor(io: CliIo): number {
290
290
 
291
291
  try {
292
292
  io.stdout(`runtime lib: ${runtimeLibDir()}\n`);
293
+ const archives = ["core", "ext_node", "gui", "media"].filter((name) => findRuntimeLibrary(name) !== undefined);
294
+ io.stdout(
295
+ `archives : ${archives.length > 0 ? archives.join(", ") : "none (C runtime compiled on demand)"}\n`,
296
+ );
293
297
  } catch {
294
298
  io.stdout("runtime lib: unavailable\n");
295
299
  }