@nimbus-sh/core 0.3.0 → 0.5.0

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 (155) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +140 -0
  3. package/dist/_shared/error-text.d.ts +15 -0
  4. package/dist/_shared/error-text.d.ts.map +1 -0
  5. package/dist/_shared/error-text.js +21 -0
  6. package/dist/_shared/exports-resolver.d.ts +19 -7
  7. package/dist/_shared/exports-resolver.d.ts.map +1 -1
  8. package/dist/_shared/exports-resolver.js +1 -1
  9. package/dist/_shared/retry.d.ts.map +1 -1
  10. package/dist/_shared/retry.js +3 -1
  11. package/dist/_shared/tarball-stream.d.ts +93 -0
  12. package/dist/_shared/tarball-stream.d.ts.map +1 -0
  13. package/dist/_shared/tarball-stream.js +235 -0
  14. package/dist/_shared/tarball.d.ts +17 -0
  15. package/dist/_shared/tarball.d.ts.map +1 -0
  16. package/dist/_shared/tarball.js +39 -0
  17. package/dist/_shared/w7-frame.js +2 -1
  18. package/dist/constants.d.ts.map +1 -1
  19. package/dist/constants.js +30 -16
  20. package/dist/observability/oom-classify.d.ts +1 -1
  21. package/dist/observability/oom-classify.d.ts.map +1 -1
  22. package/dist/observability/oom-classify.js +7 -0
  23. package/dist/observability/oom-discriminator.d.ts +1 -1
  24. package/dist/runtime/clang-runner.d.ts +38 -0
  25. package/dist/runtime/clang-runner.d.ts.map +1 -0
  26. package/dist/runtime/clang-runner.js +866 -0
  27. package/dist/runtime/esbuild-service.d.ts +4 -3
  28. package/dist/runtime/esbuild-service.d.ts.map +1 -1
  29. package/dist/runtime/esbuild-service.js +9 -4
  30. package/dist/runtime/facet-host.d.ts +12 -0
  31. package/dist/runtime/facet-host.d.ts.map +1 -1
  32. package/dist/runtime/installed-runtimes.d.ts +4 -3
  33. package/dist/runtime/installed-runtimes.d.ts.map +1 -1
  34. package/dist/runtime/local-facet-host.d.ts.map +1 -1
  35. package/dist/runtime/local-facet-host.js +29 -9
  36. package/dist/runtime/long-running-handle.d.ts +3 -3
  37. package/dist/runtime/long-running-handle.d.ts.map +1 -1
  38. package/dist/runtime/os-contracts.d.ts +7 -5
  39. package/dist/runtime/os-contracts.d.ts.map +1 -1
  40. package/dist/runtime/port-registry.d.ts +28 -0
  41. package/dist/runtime/port-registry.d.ts.map +1 -1
  42. package/dist/runtime/port-registry.js +79 -9
  43. package/dist/runtime/process-logs.d.ts +9 -1
  44. package/dist/runtime/process-logs.d.ts.map +1 -1
  45. package/dist/runtime/project-detect.d.ts +6 -1
  46. package/dist/runtime/project-detect.d.ts.map +1 -1
  47. package/dist/runtime/require-resolver.d.ts.map +1 -1
  48. package/dist/runtime/require-resolver.js +3 -4
  49. package/dist/runtime/ruby-gems.d.ts +30 -0
  50. package/dist/runtime/ruby-gems.d.ts.map +1 -0
  51. package/dist/runtime/ruby-gems.js +636 -0
  52. package/dist/runtime/ruby-runner.d.ts +127 -0
  53. package/dist/runtime/ruby-runner.d.ts.map +1 -0
  54. package/dist/runtime/ruby-runner.js +1357 -0
  55. package/dist/runtime/runtime-registry.d.ts +4 -3
  56. package/dist/runtime/runtime-registry.d.ts.map +1 -1
  57. package/dist/runtime/runtime-registry.js +2 -1
  58. package/dist/runtime/session-process-supervisor.d.ts +15 -0
  59. package/dist/runtime/session-process-supervisor.d.ts.map +1 -1
  60. package/dist/runtime/session-process-supervisor.js +30 -0
  61. package/dist/runtime/sqlite-runtime-fs-bridge.js +1 -5
  62. package/dist/runtime/virtual-socket-kernel.d.ts.map +1 -1
  63. package/dist/runtime/virtual-socket-kernel.generated.d.ts +1 -1
  64. package/dist/runtime/virtual-socket-kernel.generated.d.ts.map +1 -1
  65. package/dist/runtime/virtual-socket-kernel.generated.js +2 -2
  66. package/dist/runtime/virtual-socket-kernel.js +10 -5
  67. package/dist/runtime/wasm-runner.d.ts.map +1 -1
  68. package/dist/runtime/wasm-runner.js +22 -14
  69. package/dist/shell/features.d.ts.map +1 -1
  70. package/dist/shell/features.js +5 -4
  71. package/dist/shell/unix-commands.d.ts +14 -1
  72. package/dist/shell/unix-commands.d.ts.map +1 -1
  73. package/dist/shell/unix-commands.js +115 -69
  74. package/dist/substrate/lifo/commands/net/ip.d.ts.map +1 -1
  75. package/dist/substrate/lifo/commands/system/ps.d.ts +2 -1
  76. package/dist/substrate/lifo/commands/system/ps.d.ts.map +1 -1
  77. package/dist/substrate/lifo/kernel/ServiceManager.d.ts.map +1 -1
  78. package/dist/substrate/lifo/kernel/ServiceManager.js +0 -2
  79. package/dist/substrate/lifo/kernel/network/NetworkStack.d.ts +22 -11
  80. package/dist/substrate/lifo/kernel/network/NetworkStack.d.ts.map +1 -1
  81. package/dist/substrate/lifo/kernel/network/NetworkStack.js +0 -2
  82. package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.d.ts +2 -1
  83. package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.d.ts.map +1 -1
  84. package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.js +3 -4
  85. package/dist/substrate/lifo/kernel/vfs/VFS.d.ts +17 -0
  86. package/dist/substrate/lifo/kernel/vfs/VFS.d.ts.map +1 -1
  87. package/dist/substrate/lifo/kernel/vfs/VFS.js +33 -0
  88. package/dist/substrate/lifo/node-compat/esbuild.d.ts.map +1 -1
  89. package/dist/substrate/lifo/node-compat/esbuild.js +2 -4
  90. package/dist/substrate/lifo/node-compat/index.d.ts.map +1 -1
  91. package/dist/substrate/lifo/node-compat/index.js +3 -2
  92. package/dist/substrate/lifo/node-compat/loose-equality.d.ts +19 -0
  93. package/dist/substrate/lifo/node-compat/loose-equality.d.ts.map +1 -0
  94. package/dist/substrate/lifo/node-compat/loose-equality.js +91 -0
  95. package/dist/substrate/lifo/node-compat/process.d.ts.map +1 -1
  96. package/dist/substrate/lifo/node-compat/process.js +6 -3
  97. package/dist/substrate/lifo/shell/Shell.d.ts +19 -0
  98. package/dist/substrate/lifo/shell/Shell.d.ts.map +1 -1
  99. package/dist/substrate/lifo/shell/Shell.js +86 -75
  100. package/dist/substrate/lifo/shell/lexer.d.ts +9 -0
  101. package/dist/substrate/lifo/shell/lexer.d.ts.map +1 -1
  102. package/dist/substrate/lifo/shell/lexer.js +40 -0
  103. package/dist/vfs/seed-project.d.ts.map +1 -1
  104. package/dist/vfs/seed-project.js +8 -2
  105. package/dist/vfs/sqlite-vfs.d.ts +61 -1
  106. package/dist/vfs/sqlite-vfs.d.ts.map +1 -1
  107. package/dist/vfs/sqlite-vfs.js +199 -35
  108. package/dist/workspace/nimbus-workspace.d.ts.map +1 -1
  109. package/dist/workspace/nimbus-workspace.js +9 -2
  110. package/package.json +4 -2
  111. package/src/_shared/error-text.ts +21 -0
  112. package/src/_shared/exports-resolver.ts +24 -4
  113. package/src/_shared/retry.ts +4 -2
  114. package/src/_shared/tarball-stream.ts +263 -0
  115. package/src/_shared/tarball.ts +46 -0
  116. package/src/_shared/w7-frame.ts +2 -1
  117. package/src/constants.ts +30 -16
  118. package/src/observability/oom-classify.ts +7 -0
  119. package/src/observability/oom-discriminator.ts +1 -1
  120. package/src/runtime/clang-runner.ts +924 -0
  121. package/src/runtime/esbuild-service.ts +16 -11
  122. package/src/runtime/facet-host.ts +12 -0
  123. package/src/runtime/installed-runtimes.ts +4 -3
  124. package/src/runtime/local-facet-host.ts +28 -8
  125. package/src/runtime/long-running-handle.ts +3 -3
  126. package/src/runtime/os-contracts.ts +7 -5
  127. package/src/runtime/port-registry.ts +97 -9
  128. package/src/runtime/process-logs.ts +12 -3
  129. package/src/runtime/project-detect.ts +7 -1
  130. package/src/runtime/require-resolver.ts +7 -7
  131. package/src/runtime/ruby-gems.ts +682 -0
  132. package/src/runtime/ruby-runner.ts +1484 -0
  133. package/src/runtime/runtime-registry.ts +9 -7
  134. package/src/runtime/session-process-supervisor.ts +27 -0
  135. package/src/runtime/sqlite-runtime-fs-bridge.ts +9 -6
  136. package/src/runtime/virtual-socket-kernel.generated.ts +2 -2
  137. package/src/runtime/virtual-socket-kernel.ts +10 -5
  138. package/src/runtime/wasm-runner.ts +55 -29
  139. package/src/shell/features.ts +5 -4
  140. package/src/shell/unix-commands.ts +273 -128
  141. package/src/substrate/lifo/commands/net/ip.ts +15 -15
  142. package/src/substrate/lifo/commands/system/ps.ts +2 -1
  143. package/src/substrate/lifo/kernel/ServiceManager.ts +0 -2
  144. package/src/substrate/lifo/kernel/network/NetworkStack.ts +24 -17
  145. package/src/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.ts +37 -12
  146. package/src/substrate/lifo/kernel/vfs/VFS.ts +34 -0
  147. package/src/substrate/lifo/node-compat/esbuild.ts +16 -8
  148. package/src/substrate/lifo/node-compat/index.ts +3 -2
  149. package/src/substrate/lifo/node-compat/loose-equality.ts +81 -0
  150. package/src/substrate/lifo/node-compat/process.ts +5 -2
  151. package/src/substrate/lifo/shell/Shell.ts +92 -70
  152. package/src/substrate/lifo/shell/lexer.ts +38 -0
  153. package/src/vfs/seed-project.ts +10 -3
  154. package/src/vfs/sqlite-vfs.ts +209 -36
  155. package/src/workspace/nimbus-workspace.ts +10 -1
@@ -0,0 +1,924 @@
1
+ /**
2
+ * clang-runner.ts — compile, link, and execute C programs for Nimbus WASI.
3
+ *
4
+ * Architecture (compile-link-run, two facet calls):
5
+ *
6
+ * compile : clang.wasm + sysroot subset for C includes + user .c
7
+ * → produces .o bytes (returned to supervisor).
8
+ * link : lld.wasm + sysroot subset for link (crt1.o + libc.a)
9
+ * + .o from compile → produces .wasm executable.
10
+ * write : final .wasm flushed to user VFS at the requested path.
11
+ *
12
+ * The filesystem both halves see is the one WASI layer every other
13
+ * non-node runtime uses (wasi-instance.ts), seeded and sealed.
14
+ *
15
+ * Splitting compile and link into separate facet calls keeps each
16
+ * call under the empirical payload ceiling. Each ships:
17
+ *
18
+ * - compile: 31 MiB clang.wasm + ~1.3 MiB sysroot subset (C includes).
19
+ * - link : 19 MiB lld.wasm + ~0.75 MiB libs + tiny .o.
20
+ *
21
+ * Sysroot subset extraction happens supervisor-side via a small ustar
22
+ * parser. The full sysroot.tar is parsed once when the clang runtime
23
+ * warms for a session; compile/link calls reuse the filtered subsets.
24
+ *
25
+ * Dispatch stays direct: no sleeps, no caller-side retries, and no
26
+ * catch-and-continue around loader failures.
27
+ */
28
+
29
+ import type { RuntimeManifest } from './runtime-manifest.js';
30
+ import type { CredentialedVfs, SqliteVFS } from '../vfs/sqlite-vfs.js';
31
+ import type { Command, CommandContext } from '../substrate/lifo/commands/types.js';
32
+ import type { Facet, FacetHost } from './facet-host.js';
33
+ import { CRED_KERNEL, requireVfsCred, WASM32_WASI_NIMBUS_ABI } from './os-contracts.js';
34
+ import { resolveVfsPath } from '../vfs/path.js';
35
+ import { hasLeadingCliFlag } from './cli-flags.js';
36
+ import { WASI_ABI_NAMESPACE, WASI_INSTANCE_PREAMBLE_SRC } from './wasi-instance.js';
37
+
38
+ const CLANG_VERSION_FLAGS = new Set(['--version', '-v']);
39
+
40
+ /** Build the runner factory. Closes over the facet host + vfs. */
41
+ export function makeClangRunnerFactory(deps: {
42
+ facets: FacetHost;
43
+ vfs: SqliteVFS;
44
+ }): (manifest: RuntimeManifest, installRoot: string, binName: string, binKind: string | undefined) =>
45
+ Command {
46
+ const runtimeVfs = deps.vfs.as(CRED_KERNEL);
47
+
48
+ return function clangRunnerFactory(manifest, installRoot, binName, binKind) {
49
+ const findFile = (rel: string): string | null => {
50
+ const entry = manifest.files.find((f) => f.path === rel);
51
+ return entry ? `${installRoot}/${entry.path}` : null;
52
+ };
53
+
54
+ const clangVfsPath = findFile('bin/clang');
55
+ const lldVfsPath = findFile('bin/wasm-ld');
56
+ const sysrootVfsPath = findFile('share/clang/sysroot.tar');
57
+ let runtimePromise: Promise<ClangFacetRuntime> | null = null;
58
+
59
+ return async function clangBinHandler(ctx: CommandContext): Promise<number> {
60
+ const vfs = deps.vfs.as(requireVfsCred(ctx.cred, binName));
61
+ const argv: string[] = ctx.args || [];
62
+ const cwd: string = ctx.cwd || '/home/user';
63
+
64
+ // Fast paths — no wasm boot.
65
+ if (hasLeadingCliFlag(argv, CLANG_VERSION_FLAGS)) {
66
+ ctx.stdout.write(`Nimbus wasm-clang (binji-2020, LLVM 8.0.1)\n`);
67
+ ctx.stdout.write(`Target: ${WASM32_WASI_NIMBUS_ABI.id} (via wasm-ld linker)\n`);
68
+ return 0;
69
+ }
70
+ if (argv.includes('--help') || argv.includes('-h')) {
71
+ ctx.stdout.write(`usage: ${binName} [options] <source.c> -o <output>\n`);
72
+ ctx.stdout.write(`Wasm-compiled clang/wasm-ld bundle for Nimbus.\n`);
73
+ ctx.stdout.write(`Target: ${WASM32_WASI_NIMBUS_ABI.id}\n`);
74
+ ctx.stdout.write(`Supported: C compilation + linking to wasm.\n`);
75
+ return 0;
76
+ }
77
+
78
+ const isLinker = binKind === 'linker' || binName === 'wasm-ld';
79
+
80
+ // Resolve bundle paths.
81
+ if (!sysrootVfsPath || !runtimeVfs.exists(sysrootVfsPath)) {
82
+ ctx.stderr.write(`${binName}: sysroot.tar missing from install\n`);
83
+ return 127;
84
+ }
85
+ if (!clangVfsPath || !lldVfsPath) {
86
+ ctx.stderr.write(`${binName}: clang/wasm-ld missing from install\n`);
87
+ return 127;
88
+ }
89
+
90
+ // Parse argv: find input .c + output path.
91
+ const parsed = parseUserArgv(argv);
92
+ if (parsed.error) {
93
+ ctx.stderr.write(`${binName}: ${parsed.error}\n`);
94
+ return parsed.exitCode;
95
+ }
96
+ if (isLinker) {
97
+ // Direct wasm-ld invocation: pass argv through (advanced
98
+ // users only). Not on the hello-world path.
99
+ ctx.stderr.write(`${binName}: direct wasm-ld invocation not yet wired (v1.2)\n`);
100
+ return 2;
101
+ }
102
+
103
+ // Validate all user-supplied source inputs exist in the user's
104
+ // session VFS. Collect their bytes to seed the filesystem.
105
+ const userSourceFiles: Record<string, Uint8Array> = {};
106
+ // Pre-built objects/archives the user passed (e.g. extra.o, libfoo.a)
107
+ // — shipped to the LINK step only (not compile).
108
+ const preBuiltLinkInputs: string[] = [];
109
+ const sourceInputs: string[] = [];
110
+ for (const input of parsed.inputPaths) {
111
+ const inputAbs = resolveVfsPath(input, cwd);
112
+ try {
113
+ if (!vfs.exists(inputAbs)) {
114
+ ctx.stderr.write(`${binName}: ${input}: No such file or directory\n`);
115
+ return 1;
116
+ }
117
+ userSourceFiles[input] = vfs.readFile(inputAbs);
118
+ } catch (error) {
119
+ ctx.stderr.write(`${binName}: ${input}: ${errorMessage(error)}\n`);
120
+ return 1;
121
+ }
122
+ if (isSourceExt(input)) {
123
+ sourceInputs.push(input);
124
+ } else {
125
+ // .o / .a — pass through to link as a pre-built input. The
126
+ // path in the seeded filesystem is the user-supplied relative path.
127
+ preBuiltLinkInputs.push(input);
128
+ }
129
+ }
130
+ if (sourceInputs.length === 0 && preBuiltLinkInputs.length === 0) {
131
+ ctx.stderr.write(`${binName}: no compilable / linkable inputs\n`);
132
+ return 1;
133
+ }
134
+
135
+ let runtime: ClangFacetRuntime;
136
+ try {
137
+ if (!runtimePromise) {
138
+ runtimePromise = createClangFacetRuntime(deps.facets, {
139
+ clangVfsPath,
140
+ lldVfsPath,
141
+ sysrootVfsPath,
142
+ vfs: runtimeVfs,
143
+ });
144
+ }
145
+ runtime = await runtimePromise;
146
+ } catch (e: unknown) {
147
+ runtimePromise = null;
148
+ ctx.stderr.write(`${binName}: clang runtime warm-up failed: ${errorMessage(e)}\n`);
149
+ return 1;
150
+ }
151
+
152
+ // Walk the user's cwd to gather headers (.h/.hpp/.hxx/.inc/...)
153
+ // and any sibling headers users typically expect to be visible
154
+ // to #include "..." resolution. Real clang/gcc auto-search the
155
+ // dir of the including source for quote-form includes; we ship
156
+ // those files at their relative-to-cwd paths so the seeded
157
+ // filesystem reproduces the user's working tree.
158
+ //
159
+ // Size-capped (4 MiB, 200 files, depth 8) so accidental
160
+ // huge-projects don't OOM the facet. Real C-tutorial projects
161
+ // are vastly under the cap.
162
+ const userIncludeBundle = collectIncludeBundle(vfs, cwd.replace(/^\/+/, ''));
163
+
164
+ // ── COMPILE PHASE ────────────────────────────────────────────
165
+ // Reuse the warm clang pool and ship only the C-include
166
+ // subset plus user source/header files for this invocation.
167
+
168
+ // Build -I flag list. We pass each user -I path verbatim AND add
169
+ // an implicit '.' (cwd) for quote-form lookup. wasm-clang's -cc1
170
+ // mode does NOT add cwd to the quote search list by default
171
+ // (the driver normally does that for "foo.h" includes), so we
172
+ // wire it ourselves. This is what makes
173
+ // clang main.c (main.c does #include "greet.h", greet.h
174
+ // next to main.c)
175
+ // succeed without an explicit -I from the user.
176
+ const userIncludeFlags: string[] = [];
177
+ for (const ip of parsed.includePaths) {
178
+ userIncludeFlags.push('-I', ip);
179
+ }
180
+ userIncludeFlags.push('-I', '.');
181
+
182
+ // Compile each source to its own .o. Object file naming: replace
183
+ // the source extension with .o. Collisions across cwd subdirs
184
+ // (e.g. src/foo.c and lib/foo.c both → foo.o) are avoided by
185
+ // keeping the directory component (the seed preserves user layout).
186
+ const objPaths: string[] = [];
187
+ const objBytesMap: Record<string, Uint8Array> = {};
188
+ for (const src of sourceInputs) {
189
+ const objPath = src.replace(/\.(c|cc|cpp|cxx|c\+\+|C)$/, '.o');
190
+ // For C++ inputs use -x c++; default -x c.
191
+ const isCpp = /\.(cc|cpp|cxx|c\+\+|C)$/.test(src);
192
+ const compileArgv = [
193
+ 'clang', '-cc1', '-emit-obj',
194
+ '-disable-free',
195
+ '-isysroot', '/',
196
+ '-internal-isystem', '/include/c++/v1',
197
+ '-internal-isystem', '/include',
198
+ '-internal-isystem', '/lib/clang/8.0.1/include',
199
+ '-ferror-limit', '19',
200
+ '-fmessage-length', '80',
201
+ '-fcolor-diagnostics',
202
+ '-O2',
203
+ ...userIncludeFlags,
204
+ '-o', objPath,
205
+ '-x', isCpp ? 'c++' : 'c',
206
+ src,
207
+ ];
208
+ // Per compile we ship: the current source file + the user's
209
+ // header bundle. Multi-TU is handled at link time, not compile,
210
+ // so sibling sources stay out of the seed (smaller payload, no
211
+ // surface for unintended cross-TU textual inclusion via -I.).
212
+ const oneSourceFile: Record<string, Uint8Array> = { [src]: userSourceFiles[src] };
213
+ const compileResult = await dispatchClangFacet(runtime.compile, {
214
+ sysrootFiles: {
215
+ ...runtime.compile.sysrootFiles,
216
+ ...oneSourceFile,
217
+ // User's headers from cwd tree (so quote-form #include
218
+ // resolves; this is the primary clang-include-fix payload).
219
+ ...userIncludeBundle,
220
+ },
221
+ argv: compileArgv,
222
+ outputPaths: [objPath],
223
+ });
224
+
225
+ if (compileResult.stdout) ctx.stdout.write(compileResult.stdout);
226
+ if (compileResult.stderr) ctx.stderr.write(compileResult.stderr);
227
+ if (compileResult.error) {
228
+ ctx.stderr.write(`${binName}: ${compileResult.error}\n`);
229
+ return 1;
230
+ }
231
+ if (compileResult.exitCode !== 0) {
232
+ return compileResult.exitCode;
233
+ }
234
+ const objBytes = compileResult.outputFiles[objPath];
235
+ if (!objBytes || objBytes.length === 0) {
236
+ ctx.stderr.write(`${binName}: compile produced no ${objPath} (internal error)\n`);
237
+ return 1;
238
+ }
239
+ objPaths.push(objPath);
240
+ objBytesMap[objPath] = objBytes;
241
+ }
242
+
243
+ // -c (compile-only): flush each .o to the user VFS, no link.
244
+ if (parsed.compileOnly) {
245
+ for (const objPath of objPaths) {
246
+ const objVfsPath = resolveVfsPath(objPath, cwd);
247
+ const parent = objVfsPath.replace(/\/[^/]+$/, '');
248
+ if (parent && parent !== objVfsPath && !vfs.exists(parent)) {
249
+ vfs.mkdir(parent, { recursive: true });
250
+ }
251
+ vfs.writeFile(objVfsPath, objBytesMap[objPath]);
252
+ }
253
+ // Honor user's -o for single-input compile-only: rename the one
254
+ // .o to the requested output if -o was passed.
255
+ if (sourceInputs.length === 1 && parsed.outputPath !== 'a.out') {
256
+ const fromVfs = resolveVfsPath(objPaths[0], cwd);
257
+ const toVfs = resolveVfsPath(parsed.outputPath, cwd);
258
+ if (fromVfs !== toVfs) {
259
+ try {
260
+ vfs.writeFile(toVfs, vfs.readFile(fromVfs));
261
+ vfs.unlink(fromVfs);
262
+ } catch { /* best-effort */ }
263
+ }
264
+ }
265
+ return 0;
266
+ }
267
+
268
+ // Add pre-built .o / .a inputs (the user passed them on argv
269
+ // alongside .c sources, e.g. `clang main.c extra.o -o out`).
270
+ const preBuiltBytesMap: Record<string, Uint8Array> = {};
271
+ for (const lp of preBuiltLinkInputs) {
272
+ preBuiltBytesMap[lp] = userSourceFiles[lp];
273
+ }
274
+
275
+ // ── LINK PHASE ───────────────────────────────────────────────
276
+ // Reuse the warm wasm-ld pool and ship only the link
277
+ // sysroot subset plus object/archive inputs for this invocation.
278
+ const stackSize = 1024 * 1024;
279
+ // User-supplied -L paths and -l libraries flow through. The user
280
+ // -L paths point at user-VFS dirs; we currently don't ship user
281
+ // libraries (they'd need their own collect step), so -l<name>
282
+ // works only against the sysroot's -L paths today. -L user-side
283
+ // would no-op silently in v1 — out of scope for this wave.
284
+ const userLinkFlags: string[] = [];
285
+ for (const lp of parsed.libraryPaths) {
286
+ userLinkFlags.push('-L', lp);
287
+ }
288
+ const linkArgv = [
289
+ 'wasm-ld',
290
+ '--no-threads',
291
+ '--export-dynamic',
292
+ '-z', `stack-size=${stackSize}`,
293
+ '-L/lib/wasm32-wasi',
294
+ // Stream-C: modern wasi-libc references __muloti4 / __divti3
295
+ // (128-bit math from utimensat's timespec arithmetic) — these
296
+ // live in compiler-rt's libclang_rt.builtins-wasm32.a at the
297
+ // clang resource dir. binji-2020's libc.a self-bundled them;
298
+ // modern doesn't, so we link compiler-rt explicitly. wasm-ld
299
+ // dead-strips unused builtins, so binji binaries are unaffected.
300
+ '-L/lib/clang/8.0.1/lib/wasi',
301
+ ...userLinkFlags,
302
+ '/lib/wasm32-wasi/crt1.o',
303
+ ...objPaths,
304
+ ...preBuiltLinkInputs,
305
+ '-lc',
306
+ ...parsed.libraries.map((l) => '-l' + l),
307
+ '-lclang_rt.builtins-wasm32',
308
+ '-o', parsed.outputPath,
309
+ ];
310
+ const linkResult = await dispatchClangFacet(runtime.link, {
311
+ sysrootFiles: { ...runtime.link.sysrootFiles, ...objBytesMap, ...preBuiltBytesMap },
312
+ argv: linkArgv,
313
+ outputPaths: [parsed.outputPath],
314
+ });
315
+
316
+ if (linkResult.stdout) ctx.stdout.write(linkResult.stdout);
317
+ if (linkResult.stderr) ctx.stderr.write(linkResult.stderr);
318
+ if (linkResult.error) {
319
+ ctx.stderr.write(`${binName}: ${linkResult.error}\n`);
320
+ return 1;
321
+ }
322
+ if (linkResult.exitCode !== 0) {
323
+ return linkResult.exitCode;
324
+ }
325
+ const wasmBytes = linkResult.outputFiles[parsed.outputPath];
326
+ if (!wasmBytes || wasmBytes.length === 0) {
327
+ ctx.stderr.write(`${binName}: link produced no ${parsed.outputPath} (internal error)\n`);
328
+ return 1;
329
+ }
330
+
331
+ // ── FLUSH OUTPUT ─────────────────────────────────────────────
332
+ const outVfsPath = resolveVfsPath(parsed.outputPath, cwd);
333
+ try {
334
+ const parent = outVfsPath.replace(/\/[^/]+$/, '');
335
+ if (parent && parent !== outVfsPath && !vfs.exists(parent)) {
336
+ vfs.mkdir(parent, { recursive: true });
337
+ }
338
+ vfs.writeFile(outVfsPath, wasmBytes);
339
+ vfs.chmod(outVfsPath, 0o755);
340
+ } catch (error) {
341
+ ctx.stderr.write(`${binName}: ${parsed.outputPath}: ${errorMessage(error)}\n`);
342
+ return 1;
343
+ }
344
+ // Real linkers chmod their output executable (+x even after a
345
+ // prior chmod -x) — so `./a.out` runs with no manual chmod.
346
+
347
+ return 0;
348
+ };
349
+ };
350
+ }
351
+
352
+ // ── argv parser ──────────────────────────────────────────────────────
353
+
354
+ interface ParsedArgv {
355
+ /** All input source files (.c/.cpp/.cc/.cxx) in user-supplied order. */
356
+ inputPaths: string[];
357
+ /** -I include directories (cwd-relative or absolute) passed by user. */
358
+ includePaths: string[];
359
+ /** -L library search directories (cwd-relative or absolute). */
360
+ libraryPaths: string[];
361
+ /** -l library names (without 'lib' prefix or '.a' suffix). */
362
+ libraries: string[];
363
+ /** Output path from -o. Defaults to 'a.out'. */
364
+ outputPath: string;
365
+ /** If true, user passed -c (compile-only, no link). */
366
+ compileOnly: boolean;
367
+ error?: string;
368
+ exitCode: number;
369
+ }
370
+
371
+ /** Recognized C / C++ source extensions for input classification. */
372
+ function isSourceExt(p: string): boolean {
373
+ return /\.(c|cc|cpp|cxx|c\+\+|C)$/.test(p);
374
+ }
375
+
376
+ /**
377
+ * Nimbus RUNS threaded wasm — see runtime/wasi-threads.ts — but this compiler
378
+ * cannot BUILD it. The bundled toolchain is LLVM 8 over a wasi-sdk-19 sysroot
379
+ * that ships one target directory, `lib/wasm32-wasi`, with no threads variant:
380
+ * no atomics-and-bulk-memory libc, no `libpthread.a`, and a fixed link line
381
+ * with no `--shared-memory`.
382
+ *
383
+ * Measured on the shipped sysroot, `-pthread` fell through parseUserArgv's
384
+ * catch-all for unrecognised flags, and what the user saw depended on their
385
+ * includes: `clang -pthread prog.c` on a program that does not include
386
+ * <pthread.h> built and ran with exit 0 and the flag quietly ignored, while a
387
+ * real threaded program died at `'pthread.h' file not found` — a diagnosis
388
+ * that names a missing header rather than a toolchain that has no threads at
389
+ * all, and points nowhere. Refuse at the front door instead, and say where the
390
+ * working path is, because Nimbus does run these programs once they are built
391
+ * correctly.
392
+ */
393
+ function threadedBuildRefusal(argv: string[]): string | null {
394
+ const flag = argv.find((a) =>
395
+ a === '-pthread' || a === '-mthread-model' || a === '--pthread'
396
+ || /^(-target|--target)=.*threads$/.test(a));
397
+ const target = argv.findIndex((a) => a === '-target' || a === '--target');
398
+ const targetsThreads = target >= 0 && /threads$/.test(argv[target + 1] || '');
399
+ if (!flag && !targetsThreads) return null;
400
+ return `${flag ?? `${argv[target]} ${argv[target + 1]}`}: this toolchain cannot build threaded wasm.\n`
401
+ + ` The bundled sysroot is wasm32-wasi only — it has no wasm32-wasip1-threads libc.\n`
402
+ + ` Nimbus RUNS pthread programs (mutex, condvar, join, TLS, barrier, semaphore),\n`
403
+ + ` but they must be built with a full wasi-sdk and linked against the futex shim:\n`
404
+ + ` clang --target=wasm32-wasip1-threads --sysroot=$WASI_SYSROOT -pthread \\\n`
405
+ + ` -Wl,--import-memory,--shared-memory,--max-memory=67108864 \\\n`
406
+ + ` -o prog.wasm prog.c nimbus-threads.c\n`
407
+ + ` See docs/wasi-threads.md for nimbus-threads.c and why the shim is required.`;
408
+ }
409
+
410
+ function parseUserArgv(argv: string[]): ParsedArgv {
411
+ const inputPaths: string[] = [];
412
+ const includePaths: string[] = [];
413
+ const libraryPaths: string[] = [];
414
+ const libraries: string[] = [];
415
+ let outputPath = 'a.out';
416
+ let compileOnly = false;
417
+ // Flags that take a separate argv slot for their value.
418
+ const takesArg = new Set(['-o', '-x', '-isystem', '-include', '-isysroot',
419
+ '-target', '--target', '-std', '-MF', '-MT', '-MQ']);
420
+ for (let i = 0; i < argv.length; i++) {
421
+ const a = argv[i];
422
+ if (a === '-o' && i + 1 < argv.length) { outputPath = argv[i + 1]; i++; continue; }
423
+ if (a === '-c') { compileOnly = true; continue; }
424
+ // -I<path> or -I <path>
425
+ if (a === '-I' && i + 1 < argv.length) { includePaths.push(argv[i + 1]); i++; continue; }
426
+ if (a.startsWith('-I')) { includePaths.push(a.substring(2)); continue; }
427
+ // -L<path> or -L <path>
428
+ if (a === '-L' && i + 1 < argv.length) { libraryPaths.push(argv[i + 1]); i++; continue; }
429
+ if (a.startsWith('-L')) { libraryPaths.push(a.substring(2)); continue; }
430
+ // -l<name> or -l <name>
431
+ if (a === '-l' && i + 1 < argv.length) { libraries.push(argv[i + 1]); i++; continue; }
432
+ if (a.startsWith('-l')) { libraries.push(a.substring(2)); continue; }
433
+ // Skip recognised takes-arg flags we don't yet interpret.
434
+ if (takesArg.has(a) && i + 1 < argv.length) { i++; continue; }
435
+ // Any other -flag is opaque to the parser; the user passes them
436
+ // through (we don't currently relay arbitrary flags to clang-cc1,
437
+ // see compileArgv construction).
438
+ if (a.startsWith('-')) continue;
439
+ // Positional. If it looks like a source file, take it; else ignore.
440
+ if (isSourceExt(a)) {
441
+ inputPaths.push(a);
442
+ } else if (a.endsWith('.o') || a.endsWith('.a')) {
443
+ // Pre-built objects/archives — treat as link-only inputs. We
444
+ // surface them as inputs so the link step picks them up; the
445
+ // compile step skips them (it only walks .c/.cc/.cpp).
446
+ inputPaths.push(a);
447
+ }
448
+ // else: drop silently (e.g. typos). clang would warn; we don't yet.
449
+ }
450
+ const threaded = threadedBuildRefusal(argv);
451
+ if (threaded) {
452
+ return {
453
+ inputPaths: [], includePaths, libraryPaths, libraries,
454
+ outputPath: '', compileOnly, exitCode: 1, error: threaded,
455
+ };
456
+ }
457
+ if (inputPaths.length === 0) {
458
+ return {
459
+ inputPaths: [], includePaths, libraryPaths, libraries,
460
+ outputPath: '', compileOnly, exitCode: 2, error: 'no input files',
461
+ };
462
+ }
463
+ return {
464
+ inputPaths, includePaths, libraryPaths, libraries,
465
+ outputPath, compileOnly, exitCode: 0,
466
+ };
467
+ }
468
+
469
+ /**
470
+ * Recognise headers / inline-include files. The compile facet ships
471
+ * these alongside the .c sources so `#include "foo.h"` (quote-form)
472
+ * resolves against the directory of the including source file — which
473
+ * is how clang / gcc behave on real Unix.
474
+ */
475
+ function isHeaderExt(name: string): boolean {
476
+ return /\.(h|hh|hpp|hxx|H|inc|ipp|tcc)$/.test(name);
477
+ }
478
+
479
+ /**
480
+ * Walk a VFS directory recursively to collect headers + (optionally)
481
+ * source files, with bounded depth and total size cap, returning a
482
+ * map of root-relative-path → bytes.
483
+ *
484
+ * Layout convention: paths returned are RELATIVE TO `rootVfsPath`, so
485
+ * a header at `home/user/sub/foo.h` (when rootVfsPath is `home/user`)
486
+ * comes out as `sub/foo.h`. This matches the layout the user passes
487
+ * on argv (e.g. `clang sub/lib.c -o out`, with `lib.c` including
488
+ * `"helpers.h"` next to itself).
489
+ *
490
+ * `extra` extensions can be added (used to include `.c/.cpp/.o/.a` when
491
+ * looking under -L / sibling source dirs). Empty by default.
492
+ *
493
+ * Anti-DoS:
494
+ * - MAX_FILES = 200 (covers realistic user projects without ballooning
495
+ * the facet payload).
496
+ * - MAX_BYTES = 4 MiB.
497
+ * - MAX_DEPTH = 8 (deep enough for typical "src/", "include/", "lib/" trees).
498
+ * - skipDirs prunes obvious non-source directories.
499
+ */
500
+ function collectIncludeBundle(
501
+ vfs: Pick<CredentialedVfs, 'exists' | 'isDirectory' | 'readFile' | 'readdir'>,
502
+ rootVfsPath: string,
503
+ opts: { extraExts?: RegExp; maxFiles?: number; maxBytes?: number; maxDepth?: number } = {},
504
+ ): Record<string, Uint8Array> {
505
+ const extraExts = opts.extraExts ?? null;
506
+ const MAX_FILES = opts.maxFiles ?? 200;
507
+ const MAX_BYTES = opts.maxBytes ?? 4 * 1024 * 1024;
508
+ const MAX_DEPTH = opts.maxDepth ?? 8;
509
+ const out: Record<string, Uint8Array> = {};
510
+ if (!vfs.exists(rootVfsPath) || !vfs.isDirectory(rootVfsPath)) return out;
511
+ // Directories pruned regardless of depth — these never contain user
512
+ // headers and would balloon the payload if traversed.
513
+ const skipDirs = new Set([
514
+ '.nimbus', 'node_modules', '.cache', '.npm', '.git',
515
+ 'dist', 'build', '.next', '.nuxt', '.svelte-kit', 'coverage',
516
+ ]);
517
+ const root = rootVfsPath.replace(/^\/+/, '').replace(/\/+$/, '');
518
+ let totalBytes = 0;
519
+ let fileCount = 0;
520
+ const stack: { dir: string; depth: number }[] = [{ dir: root, depth: 0 }];
521
+ while (stack.length > 0) {
522
+ const { dir, depth } = stack.pop()!;
523
+ if (depth > MAX_DEPTH) continue;
524
+ let entries: { name: string; type: string }[];
525
+ try { entries = vfs.readdir(dir); } catch { continue; }
526
+ for (const e of entries) {
527
+ const childAbs = dir + '/' + e.name;
528
+ const rel = childAbs.startsWith(root + '/') ? childAbs.substring(root.length + 1) : childAbs;
529
+ if (e.type === 'directory') {
530
+ if (skipDirs.has(e.name)) continue;
531
+ stack.push({ dir: childAbs, depth: depth + 1 });
532
+ continue;
533
+ }
534
+ const isHeader = isHeaderExt(e.name);
535
+ const isExtra = extraExts && extraExts.test(e.name);
536
+ if (!isHeader && !isExtra) continue;
537
+ let bytes: Uint8Array;
538
+ try { bytes = vfs.readFile(childAbs); } catch { continue; }
539
+ totalBytes += bytes.length;
540
+ fileCount++;
541
+ if (totalBytes > MAX_BYTES || fileCount > MAX_FILES) {
542
+ // Cap reached — stop walking but return what we have.
543
+ return out;
544
+ }
545
+ out[rel] = bytes;
546
+ }
547
+ }
548
+ return out;
549
+ }
550
+
551
+ function errorMessage(error: unknown): string {
552
+ return error instanceof Error ? error.message : String(error);
553
+ }
554
+
555
+ // ── ustar parser (supervisor-side) ───────────────────────────────────
556
+
557
+ /**
558
+ * Parse a POSIX ustar archive into a path→bytes map. Trims the
559
+ * leading "/" from paths so they are seen as "include/stdio.h"
560
+ * (not "/include/stdio.h"). Directories are NOT recorded — only
561
+ * regular file entries.
562
+ */
563
+ function parseUstar(tarBytes: Uint8Array): Map<string, Uint8Array> {
564
+ const files = new Map<string, Uint8Array>();
565
+ let off = 0;
566
+ while (off + 512 <= tarBytes.length) {
567
+ let nameEnd = off;
568
+ while (nameEnd < off + 100 && tarBytes[nameEnd] !== 0) nameEnd++;
569
+ let name = '';
570
+ for (let i = off; i < nameEnd; i++) name += String.fromCharCode(tarBytes[i]);
571
+ if (!name) break;
572
+ const typeflag = tarBytes[off + 156];
573
+ let sizeStr = '';
574
+ for (let i = off + 124; i < off + 124 + 11; i++) {
575
+ const c = tarBytes[i];
576
+ if (c >= 0x30 && c <= 0x37) sizeStr += String.fromCharCode(c);
577
+ }
578
+ const size = parseInt(sizeStr || '0', 8);
579
+ let prefixEnd = off + 345;
580
+ while (prefixEnd < off + 345 + 155 && tarBytes[prefixEnd] !== 0) prefixEnd++;
581
+ let prefix = '';
582
+ for (let i = off + 345; i < prefixEnd; i++) prefix += String.fromCharCode(tarBytes[i]);
583
+ const fullName = prefix ? `${prefix}/${name}` : name;
584
+ off += 512;
585
+ const isRegular = typeflag === 0 || typeflag === 0x30; // '0'
586
+ const isDir = typeflag === 0x35 || fullName.endsWith('/'); // '5'
587
+ if (isRegular && !isDir) {
588
+ const bytes = tarBytes.slice(off, off + size);
589
+ files.set(fullName.replace(/\/$/, ''), bytes);
590
+ }
591
+ off += Math.ceil(size / 512) * 512;
592
+ }
593
+ return files;
594
+ }
595
+
596
+ /**
597
+ * Filter the sysroot to just what the compile step needs:
598
+ * - include/ (minus include/c++/) — C system headers
599
+ * - lib/clang/8.0.1/include/ — clang intrinsic headers
600
+ *
601
+ * Excludes the C++ standard library headers (libc++/v1) which alone
602
+ * are ~4 MiB and aren't needed for plain C compilation.
603
+ */
604
+ function filterSysrootForCompile(all: Map<string, Uint8Array>): Record<string, Uint8Array> {
605
+ const out: Record<string, Uint8Array> = {};
606
+ for (const [path, bytes] of all.entries()) {
607
+ if (path.startsWith('include/c++/')) continue;
608
+ if (path.startsWith('include/')) { out[path] = bytes; continue; }
609
+ if (path.startsWith('lib/clang/')) { out[path] = bytes; continue; }
610
+ }
611
+ return out;
612
+ }
613
+
614
+ /**
615
+ * Filter the sysroot to just what the link step needs for a C program:
616
+ * - lib/wasm32-wasi/crt1.o — the entry-point start file
617
+ * - lib/wasm32-wasi/libc.a — libc archive (printf etc.)
618
+ * - lib/wasm32-wasi/libc.imports — WASI symbol allow-list. Without
619
+ * this, wasm-ld treats `__wasi_fd_close` etc. as undefined
620
+ * symbols (the symbols are SUPPOSED to be unresolved imports,
621
+ * not errors); the .imports file tells lld "these names are
622
+ * external WASI imports, not link errors."
623
+ * - lib/clang/8.0.1/lib/wasi/libclang_rt.builtins-wasm32.a — compiler-rt
624
+ * builtins (e.g. __muloti4 for 128-bit math). Modern wasi-libc's
625
+ * utimensat.o references __muloti4; binji-2020 self-bundled it
626
+ * into libc.a, the modern build expects compiler-rt to provide.
627
+ * wasm-ld dead-strips, so trivial mains pay zero cost.
628
+ *
629
+ * Excludes libc++/libc++abi (C++-only) and the WASI emulated-mman /
630
+ * pthread / canvas variants we don't drive in v1.1.
631
+ */
632
+ function filterSysrootForLink(all: Map<string, Uint8Array>): Record<string, Uint8Array> {
633
+ const out: Record<string, Uint8Array> = {};
634
+ for (const [path, bytes] of all.entries()) {
635
+ if (path === 'lib/wasm32-wasi/crt1.o') { out[path] = bytes; continue; }
636
+ if (path === 'lib/wasm32-wasi/libc.a') { out[path] = bytes; continue; }
637
+ if (path === 'lib/wasm32-wasi/libc.imports') { out[path] = bytes; continue; }
638
+ if (path === 'lib/clang/8.0.1/lib/wasi/libclang_rt.builtins-wasm32.a') { out[path] = bytes; continue; }
639
+ }
640
+ return out;
641
+ }
642
+
643
+ // ── Facet dispatch ───────────────────────────────────────────────────
644
+
645
+ interface ClangFacetArgs {
646
+ /** Path → bytes seeded into the WASI filesystem before _start. */
647
+ sysrootFiles: Record<string, Uint8Array>;
648
+ argv: string[];
649
+ /** Paths read back out of the filesystem into outputFiles. */
650
+ outputPaths: string[];
651
+ }
652
+
653
+ interface ClangFacetTarget {
654
+ primaryName: 'clang' | 'wasm-ld';
655
+ facet: Facet;
656
+ sysrootFiles: Record<string, Uint8Array>;
657
+ }
658
+
659
+ interface ClangFacetRuntime {
660
+ compile: ClangFacetTarget;
661
+ link: ClangFacetTarget;
662
+ }
663
+
664
+ /** What the facet answers with: outputs still base64, as they crossed. */
665
+ interface ClangFacetCallResult {
666
+ exitCode: number;
667
+ stdout: string;
668
+ stderr: string;
669
+ outputFiles: Record<string, string>;
670
+ error?: string;
671
+ }
672
+
673
+ interface ClangFacetResult {
674
+ exitCode: number;
675
+ stdout: string;
676
+ stderr: string;
677
+ outputFiles: Record<string, Uint8Array>;
678
+ error?: string;
679
+ }
680
+
681
+ async function createClangFacetRuntime(
682
+ facets: FacetHost,
683
+ args: {
684
+ clangVfsPath: string | null;
685
+ lldVfsPath: string | null;
686
+ sysrootVfsPath: string | null;
687
+ vfs: CredentialedVfs;
688
+ },
689
+ ): Promise<ClangFacetRuntime> {
690
+ if (!args.clangVfsPath || !args.lldVfsPath || !args.sysrootVfsPath) {
691
+ throw new Error('installed clang manifest is missing required files');
692
+ }
693
+
694
+ // Hand the file's own backing buffer to the loader when the Uint8Array
695
+ // spans it exactly (the uncached reads below always allocate a fresh
696
+ // whole buffer) — avoids a second 31 MiB copy of clang.wasm in the DO
697
+ // heap during warm-up. Falls back to a slice for sub-views.
698
+ const toAB = (u8: Uint8Array): ArrayBuffer =>
699
+ (u8.byteOffset === 0 && u8.byteLength === u8.buffer.byteLength
700
+ ? u8.buffer
701
+ : u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength)) as ArrayBuffer;
702
+
703
+ // Uncached reads: these are one-shot bulk reads of large runtime blobs
704
+ // (clang 31 MiB, wasm-ld 18.5 MiB, sysroot 9.3 MiB). Routing them
705
+ // through the LRU content cache would evict the user's hot working set
706
+ // and pin ~32 MiB of clang chunks resident in the DO heap for the whole
707
+ // session — a primary cause of supervisor-DO memory pressure that tips
708
+ // heavy sessions into an OOM reset mid-compile.
709
+ const clangBytes = args.vfs.readFileUncached(args.clangVfsPath);
710
+ const lldBytes = args.vfs.readFileUncached(args.lldVfsPath);
711
+ const sysroot = parseUstar(args.vfs.readFileUncached(args.sysrootVfsPath));
712
+
713
+ const makeTarget = (
714
+ primaryName: 'clang' | 'wasm-ld',
715
+ primaryBytes: Uint8Array,
716
+ sysrootFiles: Record<string, Uint8Array>,
717
+ ): ClangFacetTarget => ({
718
+ primaryName,
719
+ sysrootFiles,
720
+ facet: facets.open({
721
+ tag: `clang-runner-${primaryName}`,
722
+ concurrency: 1,
723
+ // No `syscalls`: the toolchain is sealed. It sees the sysroot subset and
724
+ // the translation unit it was handed, and the outputs are read back out
725
+ // of that filesystem — a compile cannot reach the session at all.
726
+ //
727
+ // Which is also why one warm facet may serve every tenant: with nothing
728
+ // of the session in it and nothing kept between calls, `clang main.c` is
729
+ // the same compile whoever asks.
730
+ reuse: 'global',
731
+ preamble: CLANG_RUNNER_PREAMBLE,
732
+ wasmModules: { 'primary.wasm': toAB(primaryBytes) },
733
+ }),
734
+ });
735
+
736
+ return {
737
+ compile: makeTarget('clang', clangBytes, filterSysrootForCompile(sysroot)),
738
+ link: makeTarget('wasm-ld', lldBytes, filterSysrootForLink(sysroot)),
739
+ };
740
+ }
741
+
742
+ async function dispatchClangFacet(
743
+ target: ClangFacetTarget,
744
+ args: ClangFacetArgs,
745
+ ): Promise<ClangFacetResult> {
746
+ // Encode sysroot files as base64 for facet transport. We do this on
747
+ // the supervisor to keep the facet preamble small and CPU-light.
748
+ const filesB64: Record<string, string> = {};
749
+ for (const [path, bytes] of Object.entries(args.sysrootFiles)) {
750
+ filesB64[path] = uint8ToBase64(bytes);
751
+ }
752
+
753
+ const facetFn = async function clangFacetCall(
754
+ inArgs: {
755
+ primaryName: string;
756
+ argv: string[];
757
+ filesB64: Record<string, string>;
758
+ outputPaths: string[];
759
+ },
760
+ ): Promise<ClangFacetCallResult> {
761
+ const wasm = Reflect.get(globalThis, '__NIMBUS_WASM') as Record<string, unknown> | undefined;
762
+ const primaryMod = wasm?.['primary.wasm'];
763
+ if (!primaryMod) {
764
+ return {
765
+ exitCode: 127, stdout: '', stderr: '',
766
+ outputFiles: {},
767
+ error: 'clang-runner: __NIMBUS_WASM missing primary.wasm',
768
+ };
769
+ }
770
+ const fn = Reflect.get(globalThis, '__clangRun') as
771
+ ((a: unknown) => Promise<ClangFacetCallResult>) | undefined;
772
+ if (typeof fn !== 'function') {
773
+ return {
774
+ exitCode: 127, stdout: '', stderr: '',
775
+ outputFiles: {},
776
+ error: 'clang-runner preamble missing: __clangRun not in scope',
777
+ };
778
+ }
779
+ return await fn({
780
+ primaryName: inArgs.primaryName,
781
+ argv: inArgs.argv,
782
+ filesB64: inArgs.filesB64,
783
+ outputPaths: inArgs.outputPaths,
784
+ primaryMod,
785
+ });
786
+ };
787
+
788
+ try {
789
+ const result = await target.facet.submit(facetFn, {
790
+ primaryName: target.primaryName,
791
+ argv: args.argv,
792
+ filesB64,
793
+ outputPaths: args.outputPaths,
794
+ }, {
795
+ timeoutMs: 300_000,
796
+ });
797
+ // Decode outputFiles from base64 → Uint8Array.
798
+ const outputFiles: Record<string, Uint8Array> = {};
799
+ for (const [path, b64] of Object.entries(result.outputFiles || {})) {
800
+ const bin = atob(b64);
801
+ const u8 = new Uint8Array(bin.length);
802
+ for (let i = 0; i < bin.length; i++) u8[i] = bin.charCodeAt(i);
803
+ outputFiles[path] = u8;
804
+ }
805
+ return {
806
+ exitCode: result.exitCode,
807
+ stdout: result.stdout || '',
808
+ stderr: result.stderr || '',
809
+ outputFiles,
810
+ error: result.error,
811
+ };
812
+ } catch (e: unknown) {
813
+ return {
814
+ exitCode: 1,
815
+ stdout: '',
816
+ stderr: '',
817
+ outputFiles: {},
818
+ error: `clang-runner dispatch failed: ${errorMessage(e)}`,
819
+ };
820
+ }
821
+ }
822
+
823
+ function uint8ToBase64(u8: Uint8Array): string {
824
+ // Chunked to avoid String.fromCharCode call-stack limits on big arrays.
825
+ const CHUNK = 0x8000;
826
+ let s = '';
827
+ for (let i = 0; i < u8.length; i += CHUNK) {
828
+ s += String.fromCharCode.apply(
829
+ null,
830
+ Array.from(u8.subarray(i, Math.min(i + CHUNK, u8.length))),
831
+ );
832
+ }
833
+ return btoa(s);
834
+ }
835
+
836
+ // ── Facet preamble ───────────────────────────────────────────────────
837
+
838
+ const CLANG_RUNNER_PREAMBLE_TAIL = `
839
+ // ── BEGIN: clang-runner preamble ──────────────────────────────────────
840
+ //
841
+ // The toolchain is a plain wasi_unstable (preview0) guest: clang.wasm
842
+ // declares 27 imports and wasm-ld 25, every one of them in that namespace
843
+ // and every one of them implemented by the WASI layer above. Its filesystem
844
+ // is that layer's, seeded with the sysroot subset and the translation unit
845
+ // and sealed — no supervisor is bound, so a compile cannot reach or disturb
846
+ // the session VFS, and the named outputs are read back out at the end.
847
+
848
+ globalThis.__clangRun = async function __clangRun(args) {
849
+ const stdout = [];
850
+ const stderr = [];
851
+
852
+ // Everything the seed carries is readable; directories are traversable.
853
+ // The layer denies by default for a mapped inode with no mode, and the
854
+ // producer here is a tar, which has no cred to project.
855
+ const modes = {};
856
+ const dirs = new Set();
857
+ for (const path of Object.keys(args.filesB64 || {})) {
858
+ const canon = path.replace(/^\\/+/, '');
859
+ modes[canon] = 6;
860
+ const parts = canon.split('/');
861
+ for (let i = 1; i < parts.length; i++) {
862
+ const dir = parts.slice(0, i).join('/');
863
+ dirs.add(dir);
864
+ modes[dir] = 7;
865
+ }
866
+ }
867
+ modes[''] = 7;
868
+
869
+ __wasiInitFS({
870
+ root: '',
871
+ // One preopen with the EMPTY name. That is what makes a bare relative
872
+ // input resolve: this toolchain's wasi-libc predates cwd support, so a
873
+ // relative path is matched only against a zero-length preopen name, and
874
+ // an absolute one ('-isysroot /' puts the sysroot at /include) against
875
+ // the same entry with the leading slash stripped. Naming it '/' serves
876
+ // the absolute paths and silently loses every relative one — the input
877
+ // file then fails to open with no path_open ever reaching this layer.
878
+ preopens: [{ wasiPath: '', vfsPath: '' }],
879
+ files: args.filesB64 || {},
880
+ dirs: Array.from(dirs).filter(Boolean),
881
+ modes,
882
+ });
883
+
884
+ let memory = null;
885
+ const wasi = __wasiMakeImports({
886
+ abi: 'preview0',
887
+ argv: args.argv || [],
888
+ env: { USER: 'user', HOME: '/', PWD: '/' },
889
+ getMemory: () => memory,
890
+ stdoutWrite: (s) => { stdout.push(s); },
891
+ stderrWrite: (s) => { stderr.push(s); },
892
+ });
893
+
894
+ let instance;
895
+ try {
896
+ const r = await WebAssembly.instantiate(args.primaryMod, {
897
+ ${WASI_ABI_NAMESPACE.preview0}: wasi.wasiImport,
898
+ });
899
+ instance = (r instanceof WebAssembly.Instance ? r : r.instance);
900
+ } catch (e) {
901
+ return {
902
+ exitCode: 1, stdout: stdout.join(''), stderr: stderr.join(''), outputFiles: {},
903
+ error: 'primary (' + args.primaryName + ') instantiate failed: ' + (e && e.message),
904
+ };
905
+ }
906
+ memory = instance.exports.memory;
907
+
908
+ const run = await __wasiRunStartAsync(instance, { memory });
909
+ if (run.error) {
910
+ stderr.push('[clang-runner] ' + args.primaryName + ' trapped: ' + run.error + '\\n');
911
+ }
912
+
913
+ return {
914
+ exitCode: run.exitCode,
915
+ stdout: stdout.join(''),
916
+ stderr: stderr.join(''),
917
+ outputFiles: __wasiReadFilesB64(args.outputPaths || []),
918
+ };
919
+ };
920
+
921
+ // ── END: clang-runner preamble ────────────────────────────────────────
922
+ `;
923
+
924
+ export const CLANG_RUNNER_PREAMBLE = `${WASI_INSTANCE_PREAMBLE_SRC}\n${CLANG_RUNNER_PREAMBLE_TAIL}`;