@nimbus-sh/worker 0.1.5 → 0.1.6

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 (135) hide show
  1. package/README.md +37 -0
  2. package/dist/_shared/crypto.d.ts +15 -0
  3. package/dist/_shared/crypto.d.ts.map +1 -0
  4. package/dist/_shared/crypto.js +122 -0
  5. package/dist/facets/manager.d.ts +24 -0
  6. package/dist/facets/manager.d.ts.map +1 -1
  7. package/dist/facets/manager.js +279 -70
  8. package/dist/facets/process.d.ts +11 -2
  9. package/dist/facets/process.d.ts.map +1 -1
  10. package/dist/facets/process.js +187 -21
  11. package/dist/facets/vite-dev-server.d.ts +2 -0
  12. package/dist/facets/vite-dev-server.d.ts.map +1 -1
  13. package/dist/facets/vite-dev-server.js +39 -12
  14. package/dist/index.d.ts +2 -0
  15. package/dist/index.d.ts.map +1 -1
  16. package/dist/index.js +2 -0
  17. package/dist/loaders/child-process/spawn-facet.d.ts +1 -3
  18. package/dist/loaders/child-process/spawn-facet.d.ts.map +1 -1
  19. package/dist/loaders/child-process/spawn-facet.js +1 -12
  20. package/dist/loaders/child-process/spawn-pool.d.ts +1 -1
  21. package/dist/loaders/child-process/spawn-pool.d.ts.map +1 -1
  22. package/dist/loaders/child-process/spawn-pool.js +1 -6
  23. package/dist/npm/installer.d.ts.map +1 -1
  24. package/dist/npm/installer.js +47 -27
  25. package/dist/npm/pre-bundle-facet.d.ts.map +1 -1
  26. package/dist/npm/pre-bundle-facet.js +2 -1
  27. package/dist/npm/resolve-one-facet.d.ts.map +1 -1
  28. package/dist/npm/resolve-one-facet.js +41 -13
  29. package/dist/npm/resolver.d.ts.map +1 -1
  30. package/dist/npm/resolver.js +70 -35
  31. package/dist/router/index.d.ts.map +1 -1
  32. package/dist/router/index.js +45 -21
  33. package/dist/router/remote-api.d.ts.map +1 -1
  34. package/dist/router/remote-api.js +29 -8
  35. package/dist/runtime/barrel-synthesizer.d.ts +7 -0
  36. package/dist/runtime/barrel-synthesizer.d.ts.map +1 -1
  37. package/dist/runtime/barrel-synthesizer.js +11 -0
  38. package/dist/runtime/clang-runner.d.ts.map +1 -1
  39. package/dist/runtime/clang-runner.js +1 -8
  40. package/dist/runtime/esbuild-service.d.ts +12 -5
  41. package/dist/runtime/esbuild-service.d.ts.map +1 -1
  42. package/dist/runtime/esbuild-service.js +28 -14
  43. package/dist/runtime/node-runner.d.ts +2 -0
  44. package/dist/runtime/node-runner.d.ts.map +1 -1
  45. package/dist/runtime/node-shims.d.ts +4 -2
  46. package/dist/runtime/node-shims.d.ts.map +1 -1
  47. package/dist/runtime/node-shims.js +404 -55
  48. package/dist/runtime/os-contracts.d.ts +128 -0
  49. package/dist/runtime/os-contracts.d.ts.map +1 -0
  50. package/dist/runtime/os-contracts.js +1 -0
  51. package/dist/runtime/package-manager.d.ts.map +1 -1
  52. package/dist/runtime/package-manager.js +32 -7
  53. package/dist/runtime/port-registry.d.ts +5 -0
  54. package/dist/runtime/port-registry.d.ts.map +1 -1
  55. package/dist/runtime/port-registry.js +22 -1
  56. package/dist/runtime/pyodide-runtime-assets.d.ts +19 -0
  57. package/dist/runtime/pyodide-runtime-assets.d.ts.map +1 -0
  58. package/dist/runtime/pyodide-runtime-assets.js +44 -0
  59. package/dist/runtime/python-repl.d.ts +3 -1
  60. package/dist/runtime/python-repl.d.ts.map +1 -1
  61. package/dist/runtime/python-repl.js +11 -29
  62. package/dist/runtime/python-runner.d.ts +8 -9
  63. package/dist/runtime/python-runner.d.ts.map +1 -1
  64. package/dist/runtime/python-runner.js +897 -61
  65. package/dist/runtime/python-socket-shim.d.ts +11 -0
  66. package/dist/runtime/python-socket-shim.d.ts.map +1 -0
  67. package/dist/runtime/python-socket-shim.js +526 -0
  68. package/dist/runtime/require-resolver.d.ts.map +1 -1
  69. package/dist/runtime/require-resolver.js +2 -11
  70. package/dist/runtime/ruby-gems.d.ts +25 -0
  71. package/dist/runtime/ruby-gems.d.ts.map +1 -0
  72. package/dist/runtime/ruby-gems.js +582 -0
  73. package/dist/runtime/ruby-runner.d.ts +2 -4
  74. package/dist/runtime/ruby-runner.d.ts.map +1 -1
  75. package/dist/runtime/ruby-runner.js +335 -34
  76. package/dist/runtime/runtime-catalog.d.ts +16 -4
  77. package/dist/runtime/runtime-catalog.d.ts.map +1 -1
  78. package/dist/runtime/runtime-catalog.js +21 -10
  79. package/dist/runtime/runtime-registry.d.ts +3 -0
  80. package/dist/runtime/runtime-registry.d.ts.map +1 -1
  81. package/dist/runtime/runtime-registry.js +4 -0
  82. package/dist/runtime/sqlite-runtime-fs-bridge.d.ts +43 -0
  83. package/dist/runtime/sqlite-runtime-fs-bridge.d.ts.map +1 -0
  84. package/dist/runtime/sqlite-runtime-fs-bridge.js +224 -0
  85. package/dist/runtime/static-server.d.ts +3 -0
  86. package/dist/runtime/static-server.d.ts.map +1 -0
  87. package/dist/runtime/static-server.js +121 -0
  88. package/dist/runtime/vfs-snapshot.d.ts +57 -0
  89. package/dist/runtime/vfs-snapshot.d.ts.map +1 -0
  90. package/dist/runtime/vfs-snapshot.js +141 -0
  91. package/dist/runtime/virtual-socket-kernel.d.ts +16 -0
  92. package/dist/runtime/virtual-socket-kernel.d.ts.map +1 -0
  93. package/dist/runtime/virtual-socket-kernel.js +360 -0
  94. package/dist/runtime/wasi-instance.d.ts +6 -3
  95. package/dist/runtime/wasi-instance.d.ts.map +1 -1
  96. package/dist/runtime/wasm-runner.d.ts +1 -25
  97. package/dist/runtime/wasm-runner.d.ts.map +1 -1
  98. package/dist/runtime/wasm-runner.js +1 -160
  99. package/dist/session/agent.d.ts +43 -0
  100. package/dist/session/agent.d.ts.map +1 -0
  101. package/dist/session/agent.js +1252 -0
  102. package/dist/session/init.d.ts.map +1 -1
  103. package/dist/session/init.js +15 -16
  104. package/dist/session/nimbus-session.d.ts +13 -2
  105. package/dist/session/nimbus-session.d.ts.map +1 -1
  106. package/dist/session/nimbus-session.js +55 -7
  107. package/dist/session/programmatic.d.ts +10 -0
  108. package/dist/session/programmatic.d.ts.map +1 -1
  109. package/dist/session/programmatic.js +121 -0
  110. package/dist/session/routes.d.ts.map +1 -1
  111. package/dist/session/routes.js +19 -1
  112. package/dist/session/rpc.d.ts +11 -0
  113. package/dist/session/rpc.d.ts.map +1 -1
  114. package/dist/session/rpc.js +56 -43
  115. package/dist/session/supervisor-rpc.d.ts +15 -0
  116. package/dist/session/supervisor-rpc.d.ts.map +1 -1
  117. package/dist/session/supervisor-rpc.js +32 -0
  118. package/dist/session/ws.d.ts +4 -3
  119. package/dist/session/ws.d.ts.map +1 -1
  120. package/dist/session/ws.js +11 -1
  121. package/dist/shell/unix-commands.js +12 -16
  122. package/dist/vfs/path.d.ts +8 -4
  123. package/dist/vfs/path.d.ts.map +1 -1
  124. package/dist/vfs/path.js +16 -4
  125. package/dist/vfs/sqlite-vfs.d.ts +2 -0
  126. package/dist/vfs/sqlite-vfs.d.ts.map +1 -1
  127. package/dist/vfs/sqlite-vfs.js +11 -0
  128. package/dist/vfs/symlink-registry.d.ts +7 -1
  129. package/dist/vfs/symlink-registry.d.ts.map +1 -1
  130. package/dist/vfs/symlink-registry.js +16 -11
  131. package/package.json +4 -1
  132. package/public/index.html +240 -0
  133. package/public/s/index.html +1172 -146
  134. package/runtime-contracts/pyodide-workerd-adapter.json +18 -0
  135. package/scripts/bundle-runtime.mjs +99 -24
@@ -8,24 +8,56 @@
8
8
  * - exit code via sys.exit(N) or unhandled exception → 1
9
9
  * - argv passed through to sys.argv
10
10
  *
11
- * Out of v1 (queued for v2/v3):
11
+ * Current limits:
12
12
  * - REPL mode (`python` with no args)
13
- * - File I/O beyond reading the entry script
14
- * - `pip install` / `loadPackage` / native-extension packages
13
+ * - Native Linux wheels, runtime-loaded extension modules, and extension builds
15
14
  * - Sync HTTP (urllib3 / requests blocked without JSPI)
16
15
  *
17
16
  * Architecture: SAME LOADER-modules transport as clang-runner/wasm-
18
17
  * runner. The Pyodide wasm bytes ship via the LOADER `modules` map
19
18
  * (CSP allows wasm code-gen at module-load time, not at request
20
- * time). The Pyodide.asm.js + stdlib zip ride via the loader-pool
21
- * `context` field (JSON-stringified into the inner worker.js at
22
- * module-load).
19
+ * time). The workerd-adapted Pyodide.asm.js artifact and stdlib zip ride via
20
+ * the loader-pool `context` field (JSON-stringified into the inner worker.js
21
+ * at module-load).
23
22
  *
24
23
  * Per wasm-csp/findings.md §4b: Pyodide.asm.wasm (10.1 MB on disk)
25
24
  * compiles in 314 ms via LOADER on PROD. With our v1 deployment of
26
25
  * 0.29.4 (8.25 MB asm.wasm), this is well under the empirical
27
26
  * ~32 MiB per-call ceiling.
28
27
  */
28
+ import { flushVfsDiff, snapshotVfs } from './vfs-snapshot.js';
29
+ import { createLiveStaticServerCode, parsePort } from './static-server.js';
30
+ import { parentVfsPath, resolveVfsPath } from '../vfs/path.js';
31
+ import { VIRTUAL_SOCKET_KERNEL_SRC } from './virtual-socket-kernel.js';
32
+ import { PYTHON_SOCKET_SHIM } from './python-socket-shim.js';
33
+ import { readPyodideRuntimeFiles } from './pyodide-runtime-assets.js';
34
+ const PYTHON_SITE_PACKAGES_ROOT = 'home/user/.nimbus-python/site-packages';
35
+ const PYTHON_VERSION_FLAGS = new Set(['--version', '-V']);
36
+ const PYTHON_HELP_FLAGS = new Set(['--help', '-h']);
37
+ const IGNORED_PIP_INSTALL_FLAGS = new Set([
38
+ '--upgrade',
39
+ '-U',
40
+ '--force-reinstall',
41
+ '--no-cache-dir',
42
+ '--user',
43
+ '--disable-pip-version-check',
44
+ '--prefer-binary',
45
+ '--only-binary=:all:',
46
+ ]);
47
+ const PIP_INSTALL_FLAGS_WITH_VALUE = new Set([
48
+ '-i',
49
+ '--index-url',
50
+ '--extra-index-url',
51
+ '-f',
52
+ '--find-links',
53
+ '--trusted-host',
54
+ '--timeout',
55
+ '--retries',
56
+ '--platform',
57
+ '--python-version',
58
+ '--implementation',
59
+ '--abi',
60
+ ]);
29
61
  /**
30
62
  * Build the python-runner factory. Called once at session init; the
31
63
  * returned factory binds the manifest + install root for each
@@ -33,7 +65,7 @@
33
65
  */
34
66
  export function makePythonRunnerFactory(deps) {
35
67
  const { facetMgr, vfs } = deps;
36
- return function pythonRunnerFactory(manifest, installRoot, binName, _binKind) {
68
+ return function pythonRunnerFactory(manifest, installRoot, binName, binKind) {
37
69
  const findFile = (rel) => {
38
70
  const entry = manifest.files.find((f) => f.path === rel);
39
71
  return entry ? `${installRoot}/${entry.path}` : null;
@@ -41,20 +73,38 @@ export function makePythonRunnerFactory(deps) {
41
73
  const asmWasmVfs = findFile('share/pyodide/pyodide.asm.wasm');
42
74
  const asmJsVfs = findFile('share/pyodide/pyodide.asm.js');
43
75
  const stdlibVfs = findFile('share/pyodide/python_stdlib.zip');
76
+ const lockfileVfs = findFile('share/pyodide/pyodide-lock.json');
44
77
  let runtimePromise = null;
78
+ let fsSnapshotCache = null;
45
79
  return async function pythonBinHandler(ctx) {
46
80
  const argv = ctx.args || [];
47
81
  const cwd = ctx.cwd || '/home/user';
82
+ const pipInvocation = binKind === 'pip' || binName === 'pip' || binName === 'pip3'
83
+ ? buildPipInvocation(argv, binName, cwd, vfs)
84
+ : buildPythonModulePipInvocation(argv, cwd, vfs);
85
+ if (pipInvocation.error) {
86
+ ctx.stderr.write(`${binName}: ${pipInvocation.error}\n`);
87
+ return pipInvocation.exitCode;
88
+ }
48
89
  // ── --version / --help fast paths (no wasm boot) ─────────────
49
- if (argv.includes('--version') || argv.includes('-V')) {
90
+ if (pipInvocation.mode !== 'pip' && hasLeadingCliFlag(argv, PYTHON_VERSION_FLAGS)) {
50
91
  ctx.stdout.write(`Python 3.13.2 (Pyodide 0.29.4, Nimbus runtime)\n`);
51
92
  return 0;
52
93
  }
53
- if (argv.includes('--help') || argv.includes('-h')) {
94
+ if (pipInvocation.mode !== 'pip' && hasLeadingCliFlag(argv, PYTHON_HELP_FLAGS)) {
54
95
  ctx.stdout.write(`usage: ${binName} [option] ... [-c cmd | -m mod | file | -] [arg] ...\n`);
55
96
  ctx.stdout.write(`Nimbus Pyodide 0.29.4 / Python 3.13 runtime.\n`);
56
- ctx.stdout.write(`Supported v1: -c <code>, <file.py>, stdin via -\n`);
57
- ctx.stdout.write(`Not supported: REPL (no args), pip, native extensions\n`);
97
+ ctx.stdout.write(`Supported: -c <code>, -m <module>, <file.py>, stdin via -, VFS-backed imports and file IO\n`);
98
+ ctx.stdout.write(`Package support is limited to pure Python wheels and packages with pure fallbacks; native Linux wheels and runtime-loaded extension modules are not executable in Nimbus.\n`);
99
+ return 0;
100
+ }
101
+ const staticServer = parsePythonHttpServer(argv, cwd);
102
+ if (staticServer) {
103
+ const code = createLiveStaticServerCode(staticServer.root);
104
+ const command = `${binName} -m http.server ${staticServer.port}`;
105
+ const spawned = facetMgr.spawn(code, command, staticServer.root, { port: staticServer.port });
106
+ ctx.stdout.write(`Serving HTTP on 0.0.0.0 port ${staticServer.port} from /${staticServer.root}\n`);
107
+ ctx.stdout.write(`[nimbus] started: pid=${spawned.pid} cmd="${command}" port=${staticServer.port}\n`);
58
108
  return 0;
59
109
  }
60
110
  // ── Resolve install bytes ────────────────────────────────────
@@ -76,7 +126,15 @@ export function makePythonRunnerFactory(deps) {
76
126
  // python <file.py> [args...] run script, args in sys.argv
77
127
  // python (no args) → not supported; REPL is v2
78
128
  // python - read code from stdin (advanced)
79
- const parsed = parsePythonArgv(argv);
129
+ const parsed = pipInvocation.mode === 'pip'
130
+ ? {
131
+ mode: 'inline',
132
+ inlineCode: pipInvocation.code,
133
+ scriptPath: '',
134
+ scriptArgs: [],
135
+ exitCode: 0,
136
+ }
137
+ : parsePythonArgv(argv);
80
138
  if (parsed.error) {
81
139
  ctx.stderr.write(`${binName}: ${parsed.error}\n`);
82
140
  return parsed.exitCode;
@@ -130,6 +188,46 @@ export function makePythonRunnerFactory(deps) {
130
188
  userEnv.HOME = '/home/pyodide';
131
189
  if (!userEnv.PYTHONUNBUFFERED)
132
190
  userEnv.PYTHONUNBUFFERED = '1';
191
+ if (userEnv.HOME === '/home/pyodide')
192
+ userEnv.HOME = '/home/user';
193
+ const revision = typeof vfs.revision === 'function' ? vfs.revision() : Date.now();
194
+ let fsSnapshot = fsSnapshotCache && fsSnapshotCache.cwd === cwd && fsSnapshotCache.revision === revision
195
+ ? fsSnapshotCache.result
196
+ : null;
197
+ if (!fsSnapshot) {
198
+ fsSnapshot = snapshotVfs(vfs, cwd, { extraRoots: [PYTHON_SITE_PACKAGES_ROOT] });
199
+ fsSnapshotCache = { cwd, revision, result: fsSnapshot };
200
+ }
201
+ if ('error' in fsSnapshot) {
202
+ ctx.stderr.write(`${binName}: ${fsSnapshot.error}\n`);
203
+ return 1;
204
+ }
205
+ if (shouldRunPythonAsSocketProcess(argv, parsed, pipInvocation.mode === 'pip')) {
206
+ const result = await spawnPythonSocketProcess(facetMgr, {
207
+ asmWasmVfs,
208
+ asmJsVfs,
209
+ stdlibVfs,
210
+ lockfileVfs,
211
+ manifest,
212
+ vfs,
213
+ }, {
214
+ userCode,
215
+ pyArgv,
216
+ userEnv,
217
+ progName,
218
+ cwd,
219
+ fsSnapshot: fsSnapshot.snapshot,
220
+ asyncRun: true,
221
+ pyodidePackages: [],
222
+ }, formatPythonCommand(binName, argv));
223
+ if (result.stdout)
224
+ ctx.stdout.write(result.stdout);
225
+ if (result.stderr)
226
+ ctx.stderr.write(result.stderr);
227
+ if (result.fsDiff)
228
+ flushVfsDiff(vfs, result.fsDiff);
229
+ return result.exitCode;
230
+ }
133
231
  // ── Dispatch the facet ───────────────────────────────────────
134
232
  let runtime;
135
233
  try {
@@ -138,6 +236,8 @@ export function makePythonRunnerFactory(deps) {
138
236
  asmWasmVfs,
139
237
  asmJsVfs,
140
238
  stdlibVfs,
239
+ lockfileVfs,
240
+ manifest,
141
241
  vfs,
142
242
  });
143
243
  }
@@ -153,11 +253,17 @@ export function makePythonRunnerFactory(deps) {
153
253
  pyArgv,
154
254
  userEnv,
155
255
  progName,
256
+ cwd,
257
+ fsSnapshot: fsSnapshot.snapshot,
258
+ asyncRun: pipInvocation.mode === 'pip',
259
+ pyodidePackages: pipInvocation.mode === 'pip' && /^import micropip/m.test(pipInvocation.code) ? ['micropip'] : [],
156
260
  });
157
261
  if (result.stdout)
158
262
  ctx.stdout.write(result.stdout);
159
263
  if (result.stderr)
160
264
  ctx.stderr.write(result.stderr);
265
+ if (result.fsDiff)
266
+ flushVfsDiff(vfs, result.fsDiff);
161
267
  if (result.error) {
162
268
  ctx.stderr.write(`${binName}: ${result.error}\n`);
163
269
  return 1;
@@ -166,6 +272,386 @@ export function makePythonRunnerFactory(deps) {
166
272
  };
167
273
  };
168
274
  }
275
+ function buildPythonModulePipInvocation(argv, cwd, vfs) {
276
+ if (argv[0] !== '-m' || argv[1] !== 'pip') {
277
+ return { mode: 'none', code: '', exitCode: 0 };
278
+ }
279
+ return buildPipInvocation(argv.slice(2), 'pip', cwd, vfs);
280
+ }
281
+ function buildPipInvocation(argv, binName, cwd, vfs) {
282
+ const wantsVersion = argv.includes('--version') || argv.includes('-V');
283
+ const wantsHelp = argv.length === 0 || argv.includes('--help') || argv.includes('-h');
284
+ if (wantsVersion) {
285
+ return {
286
+ mode: 'pip',
287
+ code: [
288
+ 'print("pip 24.3.1 (Nimbus Pyodide package bridge, Pyodide 0.29.4)")',
289
+ ].join('\n'),
290
+ exitCode: 0,
291
+ };
292
+ }
293
+ if (wantsHelp) {
294
+ return {
295
+ mode: 'pip',
296
+ code: [
297
+ `print(${JSON.stringify(`Usage: ${binName} install <package> [package...]`)})`,
298
+ 'print("Nimbus pip installs pure Python wheels and packages with pure fallbacks.")',
299
+ 'print("Native Linux wheels and runtime-loaded extension modules are not executable in Nimbus.")',
300
+ ].join('\n'),
301
+ exitCode: 0,
302
+ };
303
+ }
304
+ const command = argv[0];
305
+ if (command !== 'install') {
306
+ return {
307
+ mode: 'none',
308
+ code: '',
309
+ error: `pip subcommand '${command || '(none)'}' is not supported yet; supported: install, --version, --help`,
310
+ exitCode: 2,
311
+ };
312
+ }
313
+ const plan = buildPipInstallPlan(argv.slice(1), cwd, vfs);
314
+ if (plan.error) {
315
+ return { mode: 'none', code: '', error: plan.error, exitCode: plan.exitCode };
316
+ }
317
+ return {
318
+ mode: 'pip',
319
+ code: [
320
+ 'import micropip',
321
+ 'import os',
322
+ 'import shutil',
323
+ 'import sys',
324
+ `packages = ${JSON.stringify(plan.packages)}`,
325
+ `display_packages = ${JSON.stringify(plan.displayPackages)}`,
326
+ `install_deps = ${plan.deps ? 'True' : 'False'}`,
327
+ `target_site_packages = "/home/user/.nimbus-python/site-packages"`,
328
+ 'os.makedirs(target_site_packages, exist_ok=True)',
329
+ 'if target_site_packages not in sys.path:',
330
+ ' sys.path.insert(0, target_site_packages)',
331
+ 'source_site_packages = next((p for p in sys.path if isinstance(p, str) and p.startswith("/lib/python") and p.endswith("/site-packages") and os.path.isdir(p)), None)',
332
+ 'before_entries = set(os.listdir(source_site_packages)) if source_site_packages else set()',
333
+ 'def _nimbus_disable_unsupported_extensions():',
334
+ ' disabled = []',
335
+ ' roots = []',
336
+ ' for p in sys.path:',
337
+ ' if isinstance(p, str) and p and "site-packages" in p and os.path.isdir(p) and p not in roots:',
338
+ ' roots.append(p)',
339
+ ' for root in roots:',
340
+ ' for dirpath, _dirnames, filenames in os.walk(root):',
341
+ ' for name in filenames:',
342
+ ' if not name.endswith((".so", ".pyd", ".dylib")):',
343
+ ' continue',
344
+ ' path = os.path.join(dirpath, name)',
345
+ ' try:',
346
+ ' os.remove(path)',
347
+ ' disabled.append(path)',
348
+ ' except Exception:',
349
+ ' pass',
350
+ ' return disabled',
351
+ 'await micropip.install(packages, keep_going=False, deps=install_deps)',
352
+ 'if source_site_packages:',
353
+ ' for name in sorted(set(os.listdir(source_site_packages)) - before_entries):',
354
+ ' src = os.path.join(source_site_packages, name)',
355
+ ' dst = os.path.join(target_site_packages, name)',
356
+ ' if os.path.isdir(dst):',
357
+ ' shutil.rmtree(dst)',
358
+ ' elif os.path.exists(dst):',
359
+ ' os.remove(dst)',
360
+ ' if os.path.isdir(src):',
361
+ ' shutil.copytree(src, dst)',
362
+ ' else:',
363
+ ' shutil.copy2(src, dst)',
364
+ 'disabled_extensions = _nimbus_disable_unsupported_extensions()',
365
+ 'if disabled_extensions:',
366
+ ' names = ", ".join(os.path.basename(p) for p in disabled_extensions[:8])',
367
+ ' if len(disabled_extensions) > 8:',
368
+ ' names += " ..."',
369
+ ' print("Nimbus pip: disabled unsupported Python extension module(s): " + names)',
370
+ 'print("Successfully installed " + " ".join(display_packages))',
371
+ ].join('\n'),
372
+ exitCode: 0,
373
+ };
374
+ }
375
+ function buildPipInstallPlan(argv, cwd, vfs) {
376
+ const packages = [];
377
+ const displayPackages = [];
378
+ let deps = true;
379
+ const addSpec = (rawSpec, baseDir) => {
380
+ const normalized = normalizePipInstallSpec(rawSpec, baseDir, vfs);
381
+ if ('error' in normalized)
382
+ return normalized.error;
383
+ packages.push(normalized.installSpec);
384
+ displayPackages.push(normalized.displaySpec);
385
+ return null;
386
+ };
387
+ for (let i = 0; i < argv.length; i++) {
388
+ const arg = argv[i];
389
+ if (arg === '-r' || arg === '--requirement') {
390
+ const reqPath = argv[i + 1];
391
+ if (!reqPath)
392
+ return { packages, displayPackages, deps, error: `${arg}: missing requirements file`, exitCode: 2 };
393
+ const err = addRequirementsFile(reqPath, cwd, vfs, packages, displayPackages, new Set(), 0);
394
+ if (err)
395
+ return { packages, displayPackages, deps, error: err, exitCode: 1 };
396
+ i++;
397
+ continue;
398
+ }
399
+ if (arg.startsWith('--requirement=')) {
400
+ const reqPath = arg.slice('--requirement='.length);
401
+ const err = addRequirementsFile(reqPath, cwd, vfs, packages, displayPackages, new Set(), 0);
402
+ if (err)
403
+ return { packages, displayPackages, deps, error: err, exitCode: 1 };
404
+ continue;
405
+ }
406
+ if (arg === '--no-deps') {
407
+ deps = false;
408
+ continue;
409
+ }
410
+ if (isIgnoredPipInstallFlag(arg)) {
411
+ continue;
412
+ }
413
+ if (pipFlagTakesValue(arg)) {
414
+ if (!argv[i + 1])
415
+ return { packages, displayPackages, deps, error: `${arg}: missing value`, exitCode: 2 };
416
+ i++;
417
+ continue;
418
+ }
419
+ if (arg.startsWith('-')) {
420
+ return { packages, displayPackages, deps, error: `pip install option '${arg}' is not supported in Nimbus yet`, exitCode: 2 };
421
+ }
422
+ const err = addSpec(arg, cwd);
423
+ if (err)
424
+ return { packages, displayPackages, deps, error: err, exitCode: 1 };
425
+ }
426
+ if (packages.length === 0) {
427
+ return { packages, displayPackages, deps, error: 'pip install: missing package name', exitCode: 2 };
428
+ }
429
+ return { packages, displayPackages, deps, exitCode: 0 };
430
+ }
431
+ function addRequirementsFile(reqPath, baseDir, vfs, packages, displayPackages, seen, depth) {
432
+ if (depth > 8)
433
+ return 'requirements nesting exceeded 8 files';
434
+ const abs = resolveVfsPath(reqPath, baseDir);
435
+ if (seen.has(abs))
436
+ return null;
437
+ seen.add(abs);
438
+ if (!vfs.exists(abs))
439
+ return `requirements file not found: ${reqPath}`;
440
+ let text = '';
441
+ try {
442
+ text = new TextDecoder('utf-8').decode(vfs.readFile(abs));
443
+ }
444
+ catch (e) {
445
+ return `cannot read requirements file ${reqPath}: ${e?.message || e}`;
446
+ }
447
+ const nextBaseDir = parentVfsPath(abs);
448
+ for (const line of logicalRequirementLines(text)) {
449
+ const trimmed = trimRequirementComment(line);
450
+ if (!trimmed)
451
+ continue;
452
+ const tokens = splitRequirementArgs(trimmed);
453
+ if (tokens.length === 0)
454
+ continue;
455
+ const head = tokens[0];
456
+ if (head === '-r' || head === '--requirement') {
457
+ if (!tokens[1])
458
+ return `${reqPath}: ${head}: missing requirements file`;
459
+ const err = addRequirementsFile(tokens[1], nextBaseDir, vfs, packages, displayPackages, seen, depth + 1);
460
+ if (err)
461
+ return err;
462
+ continue;
463
+ }
464
+ if (head.startsWith('--requirement=')) {
465
+ const err = addRequirementsFile(head.slice('--requirement='.length), nextBaseDir, vfs, packages, displayPackages, seen, depth + 1);
466
+ if (err)
467
+ return err;
468
+ continue;
469
+ }
470
+ if (head === '-c' || head === '--constraint' || head.startsWith('--constraint=')) {
471
+ return `${reqPath}: constraints are not supported by Nimbus pip yet`;
472
+ }
473
+ if (head.startsWith('--hash='))
474
+ continue;
475
+ if (head.startsWith('-'))
476
+ continue;
477
+ const normalized = normalizePipInstallSpec(head, nextBaseDir, vfs);
478
+ if ('error' in normalized)
479
+ return normalized.error;
480
+ packages.push(normalized.installSpec);
481
+ displayPackages.push(normalized.displaySpec);
482
+ }
483
+ return null;
484
+ }
485
+ function logicalRequirementLines(text) {
486
+ const out = [];
487
+ let current = '';
488
+ for (const rawLine of text.replaceAll('\r\n', '\n').replaceAll('\r', '\n').split('\n')) {
489
+ const line = rawLine.trimEnd();
490
+ if (line.endsWith('\\')) {
491
+ current += line.slice(0, -1) + ' ';
492
+ continue;
493
+ }
494
+ out.push(current + line);
495
+ current = '';
496
+ }
497
+ if (current)
498
+ out.push(current);
499
+ return out;
500
+ }
501
+ function trimRequirementComment(line) {
502
+ let quote = '';
503
+ for (let i = 0; i < line.length; i++) {
504
+ const ch = line[i];
505
+ if (quote) {
506
+ if (ch === quote)
507
+ quote = '';
508
+ if (ch === '\\')
509
+ i++;
510
+ continue;
511
+ }
512
+ if (ch === '"' || ch === "'") {
513
+ quote = ch;
514
+ continue;
515
+ }
516
+ if (ch === '#') {
517
+ const prev = i === 0 ? ' ' : line[i - 1];
518
+ if (i === 0 || prev === ' ' || prev === '\t')
519
+ return line.slice(0, i).trim();
520
+ }
521
+ }
522
+ return line.trim();
523
+ }
524
+ function splitRequirementArgs(line) {
525
+ const out = [];
526
+ let cur = '';
527
+ let quote = '';
528
+ for (let i = 0; i < line.length; i++) {
529
+ const ch = line[i];
530
+ if (quote) {
531
+ if (ch === quote) {
532
+ quote = '';
533
+ }
534
+ else if (ch === '\\' && i + 1 < line.length) {
535
+ cur += line[++i];
536
+ }
537
+ else {
538
+ cur += ch;
539
+ }
540
+ continue;
541
+ }
542
+ if (ch === '"' || ch === "'") {
543
+ quote = ch;
544
+ continue;
545
+ }
546
+ if (ch === ' ' || ch === '\t') {
547
+ if (cur) {
548
+ out.push(cur);
549
+ cur = '';
550
+ }
551
+ continue;
552
+ }
553
+ cur += ch;
554
+ }
555
+ if (cur)
556
+ out.push(cur);
557
+ return out;
558
+ }
559
+ function normalizePipInstallSpec(rawSpec, baseDir, vfs) {
560
+ if (!rawSpec)
561
+ return { error: 'empty requirement' };
562
+ let spec = rawSpec;
563
+ if (spec.startsWith('file://'))
564
+ spec = spec.slice('file://'.length);
565
+ const looksLikePath = spec.startsWith('/') || spec.startsWith('./') || spec.startsWith('../') || spec.endsWith('.whl');
566
+ if (!looksLikePath)
567
+ return { installSpec: spec, displaySpec: spec };
568
+ const abs = resolveVfsPath(spec, baseDir);
569
+ if (!vfs.exists(abs))
570
+ return { error: `local wheel not found: ${rawSpec}` };
571
+ const fileName = abs.slice(abs.lastIndexOf('/') + 1);
572
+ if (!fileName.endsWith('.whl')) {
573
+ return { error: `local installs currently require a .whl file: ${rawSpec}` };
574
+ }
575
+ const wheelError = validatePyodideWheelFileName(fileName);
576
+ if (wheelError)
577
+ return { error: wheelError };
578
+ return {
579
+ installSpec: `emfs:/${abs}`,
580
+ displaySpec: fileName.slice(0, -'.whl'.length),
581
+ };
582
+ }
583
+ function validatePyodideWheelFileName(fileName) {
584
+ const stem = fileName.endsWith('.whl') ? fileName.slice(0, -4) : fileName;
585
+ const parts = stem.split('-');
586
+ if (parts.length < 5)
587
+ return `invalid wheel filename: ${fileName}`;
588
+ const pythonTag = parts[parts.length - 3].toLowerCase();
589
+ const abiTag = parts[parts.length - 2].toLowerCase();
590
+ const platformTag = parts[parts.length - 1].toLowerCase();
591
+ if (platformTag === 'any' && abiTag === 'none')
592
+ return null;
593
+ if (platformTag.includes('manylinux') ||
594
+ platformTag.includes('musllinux') ||
595
+ platformTag.includes('linux') ||
596
+ platformTag.includes('macosx') ||
597
+ platformTag.includes('win')) {
598
+ return `native Linux wheel '${fileName}' cannot run in Nimbus; install a pure wheel or a package with a pure Python fallback`;
599
+ }
600
+ if (platformTag.includes('emscripten') || platformTag.includes('wasm32')) {
601
+ return `Pyodide/Emscripten extension wheel '${fileName}' needs precompiled runtime-module support; install a pure wheel or a package with a pure Python fallback`;
602
+ }
603
+ if (pythonTag.startsWith('py') && platformTag === 'any')
604
+ return null;
605
+ return `wheel '${fileName}' targets unsupported platform '${platformTag}'; Nimbus pip supports pure Python wheels and packages with pure Python fallbacks`;
606
+ }
607
+ function isIgnoredPipInstallFlag(arg) {
608
+ return IGNORED_PIP_INSTALL_FLAGS.has(arg);
609
+ }
610
+ function pipFlagTakesValue(arg) {
611
+ if (arg.includes('='))
612
+ return false;
613
+ return PIP_INSTALL_FLAGS_WITH_VALUE.has(arg);
614
+ }
615
+ function hasLeadingCliFlag(argv, flags) {
616
+ for (let i = 0; i < argv.length; i++) {
617
+ const arg = argv[i];
618
+ if (flags.has(arg))
619
+ return true;
620
+ if (arg === '--')
621
+ return false;
622
+ if (arg === '-c' || arg === '-m' || arg === '-')
623
+ return false;
624
+ if (!arg.startsWith('-'))
625
+ return false;
626
+ }
627
+ return false;
628
+ }
629
+ function parsePythonHttpServer(argv, cwd) {
630
+ if (argv[0] !== '-m' || argv[1] !== 'http.server')
631
+ return null;
632
+ let port = 8000;
633
+ let directory = cwd;
634
+ for (let i = 2; i < argv.length; i++) {
635
+ const arg = argv[i];
636
+ if (arg === '--directory') {
637
+ directory = argv[i + 1] || directory;
638
+ i++;
639
+ continue;
640
+ }
641
+ if (arg.startsWith('--directory=')) {
642
+ directory = arg.slice('--directory='.length) || directory;
643
+ continue;
644
+ }
645
+ if (arg === '--bind' || arg === '-b') {
646
+ i++;
647
+ continue;
648
+ }
649
+ if (!arg.startsWith('-') && /^\d+$/.test(arg)) {
650
+ port = parsePort(arg, port);
651
+ }
652
+ }
653
+ return { port, root: resolveVfsPath(directory, cwd) };
654
+ }
169
655
  function parsePythonArgv(argv) {
170
656
  // Walk argv left-to-right looking for the first non-flag token OR
171
657
  // the -c / -m mode-switches. Python's CLI is more involved (-O,
@@ -230,21 +716,12 @@ function parsePythonArgv(argv) {
230
716
  return { mode: 'inline', inlineCode: '', scriptPath: '', scriptArgs: [], exitCode: 2,
231
717
  error: "REPL not supported in v1. Use 'python -c \"code\"' or 'python script.py'." };
232
718
  }
233
- function resolveVfsPath(rel, cwd) {
234
- const cwdN = cwd.replace(/^\/+/, '').replace(/\/+$/, '');
235
- if (rel.startsWith('/'))
236
- return rel.replace(/^\/+/, '');
237
- if (rel === '.')
238
- return cwdN;
239
- return `${cwdN}/${rel}`;
240
- }
241
- function uint8ToBase64(u8) {
242
- const CHUNK = 0x8000;
243
- let s = '';
244
- for (let i = 0; i < u8.length; i += CHUNK) {
245
- s += String.fromCharCode.apply(null, Array.from(u8.subarray(i, Math.min(i + CHUNK, u8.length))));
246
- }
247
- return btoa(s);
719
+ function buildPythonRuntimeAssets(args) {
720
+ const files = readPyodideRuntimeFiles(args);
721
+ return {
722
+ asmWasmBytes: files.asmWasmBytes,
723
+ preamble: buildPyodidePreamble(files.asmJsSrc, files.stdlibB64, files.lockfileText),
724
+ };
248
725
  }
249
726
  async function dispatchPythonFacet(runtime, args) {
250
727
  // v2 simplification: stdlibB64 and asmWasmMod are now embedded in the
@@ -261,6 +738,10 @@ async function dispatchPythonFacet(runtime, args) {
261
738
  pyArgv: inArgs.pyArgv,
262
739
  userEnv: inArgs.userEnv,
263
740
  progName: inArgs.progName,
741
+ cwd: inArgs.cwd,
742
+ fsSnapshot: inArgs.fsSnapshot,
743
+ asyncRun: !!inArgs.asyncRun,
744
+ pyodidePackages: inArgs.pyodidePackages || [],
264
745
  });
265
746
  };
266
747
  try {
@@ -269,6 +750,10 @@ async function dispatchPythonFacet(runtime, args) {
269
750
  pyArgv: args.pyArgv,
270
751
  userEnv: args.userEnv,
271
752
  progName: args.progName,
753
+ cwd: args.cwd,
754
+ fsSnapshot: args.fsSnapshot,
755
+ asyncRun: !!args.asyncRun,
756
+ pyodidePackages: args.pyodidePackages || [],
272
757
  }, {
273
758
  timeoutMs: 300_000,
274
759
  });
@@ -277,6 +762,7 @@ async function dispatchPythonFacet(runtime, args) {
277
762
  stdout: result.stdout || '',
278
763
  stderr: result.stderr || '',
279
764
  error: result.error,
765
+ fsDiff: result.fsDiff,
280
766
  };
281
767
  }
282
768
  catch (e) {
@@ -288,26 +774,202 @@ async function dispatchPythonFacet(runtime, args) {
288
774
  };
289
775
  }
290
776
  }
777
+ function shouldRunPythonAsSocketProcess(argv, parsed, pipMode) {
778
+ if (pipMode)
779
+ return false;
780
+ if (parsed.mode === 'script')
781
+ return true;
782
+ if (argv[0] === '-m' && argv[1] && argv[1] !== 'pip')
783
+ return true;
784
+ return false;
785
+ }
786
+ function formatPythonCommand(binName, argv) {
787
+ return [binName, ...argv].map((part) => {
788
+ if (/^[A-Za-z0-9_./:=@+-]+$/.test(part))
789
+ return part;
790
+ return JSON.stringify(part);
791
+ }).join(' ');
792
+ }
793
+ async function spawnPythonSocketProcess(facetMgr, assetPaths, args, command) {
794
+ const toAB = (u8) => u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength);
795
+ const assets = buildPythonRuntimeAssets(assetPaths);
796
+ const workerCode = buildPythonSocketProcessWorker(assets.preamble);
797
+ const spawned = facetMgr.spawnWorker(workerCode, command, args.cwd, {
798
+ compatibilityFlags: ['nodejs_compat'],
799
+ modules: {
800
+ 'pyodide.asm.wasm': { wasm: toAB(assets.asmWasmBytes) },
801
+ },
802
+ });
803
+ const bootResponse = await spawned.facetStub.fetch(new Request('http://nimbus.internal/__nimbus_start', {
804
+ method: 'POST',
805
+ headers: { 'Content-Type': 'application/json' },
806
+ body: JSON.stringify({
807
+ userCode: args.userCode,
808
+ pyArgv: args.pyArgv,
809
+ userEnv: args.userEnv,
810
+ progName: args.progName,
811
+ cwd: args.cwd,
812
+ fsSnapshot: args.fsSnapshot,
813
+ }),
814
+ }));
815
+ const boot = await bootResponse.json().catch(() => null);
816
+ if (!bootResponse.ok || !boot) {
817
+ facetMgr.finishProcess(spawned.pid, 1, 'python process boot failed');
818
+ return {
819
+ exitCode: 1,
820
+ stdout: '',
821
+ stderr: `python process boot failed: HTTP ${bootResponse.status}\n`,
822
+ };
823
+ }
824
+ if (boot.state === 'listening' && boot.port > 0) {
825
+ facetMgr.registerPort(spawned.pid, Number(boot.port), spawned.facetStub);
826
+ return {
827
+ exitCode: 0,
828
+ stdout: `${boot.stdout || ''}\x1b[2m[started (long-running): pid=${spawned.pid} cmd="${command}" port=${boot.port}]\x1b[0m\n`,
829
+ stderr: boot.stderr || '',
830
+ spawnedPid: spawned.pid,
831
+ port: Number(boot.port),
832
+ };
833
+ }
834
+ const reservedPorts = facetMgr.attachReservedPorts(spawned.pid, spawned.facetStub);
835
+ if (reservedPorts.length > 0) {
836
+ return {
837
+ exitCode: 0,
838
+ stdout: `${boot.stdout || ''}\x1b[2m[started (long-running): pid=${spawned.pid} cmd="${command}" port=${reservedPorts[0]}]\x1b[0m\n`,
839
+ stderr: boot.stderr || '',
840
+ spawnedPid: spawned.pid,
841
+ port: reservedPorts[0],
842
+ };
843
+ }
844
+ if (boot.state === 'exited') {
845
+ const result = boot.result || {};
846
+ facetMgr.finishProcess(spawned.pid, Number(result.exitCode || 0), result.stderr || 'python process exited');
847
+ return {
848
+ exitCode: Number(result.exitCode || 0),
849
+ stdout: result.stdout || '',
850
+ stderr: result.stderr || result.error || '',
851
+ fsDiff: result.fsDiff,
852
+ };
853
+ }
854
+ return {
855
+ exitCode: 0,
856
+ stdout: `${boot.stdout || ''}\x1b[2m[started (long-running): pid=${spawned.pid} cmd="${command}"]\x1b[0m\n`,
857
+ stderr: boot.stderr || '',
858
+ spawnedPid: spawned.pid,
859
+ };
860
+ }
861
+ function buildPythonSocketProcessWorker(preamble) {
862
+ return [
863
+ 'import { WorkerEntrypoint } from "cloudflare:workers";',
864
+ "import __NIMBUS_WASM_pyodide_asm_wasm from './pyodide.asm.wasm';",
865
+ 'globalThis.__NIMBUS_WASM = globalThis.__NIMBUS_WASM || {};',
866
+ "globalThis.__NIMBUS_WASM['pyodide.asm.wasm'] = __NIMBUS_WASM_pyodide_asm_wasm;",
867
+ '',
868
+ VIRTUAL_SOCKET_KERNEL_SRC,
869
+ '',
870
+ 'globalThis.__nimbusVirtualPortRegistrationPromises = globalThis.__nimbusVirtualPortRegistrationPromises || [];',
871
+ 'globalThis.__nimbusVirtualSocketDidListen = function __nimbusVirtualSocketDidListen(port) {',
872
+ ' const supervisor = globalThis.__nimbusPythonSupervisor;',
873
+ ' if (!supervisor || typeof supervisor.registerPort !== "function") return;',
874
+ ' try {',
875
+ ' const p = supervisor.registerPort(Number(port)).catch((e) => {',
876
+ ' const msg = e && e.message ? e.message : String(e);',
877
+ ' (globalThis.__nimbusPyStderr || (globalThis.__nimbusPyStderr = [])).push("[python-runner] port registration failed: " + msg + "\\n");',
878
+ ' });',
879
+ ' globalThis.__nimbusVirtualPortRegistrationPromises.push(p);',
880
+ ' } catch (e) {',
881
+ ' const msg = e && e.message ? e.message : String(e);',
882
+ ' (globalThis.__nimbusPyStderr || (globalThis.__nimbusPyStderr = [])).push("[python-runner] port registration failed: " + msg + "\\n");',
883
+ ' }',
884
+ '};',
885
+ '',
886
+ preamble,
887
+ '',
888
+ 'globalThis.__nimbusPythonSocketserverQueue = globalThis.__nimbusPythonSocketserverQueue || Promise.resolve();',
889
+ 'function __nimbusRunPythonSocketserverCall(fnName, port) {',
890
+ ' const run = async () => {',
891
+ ' const pyodide = globalThis.__nimbusPyodideInstance;',
892
+ ' if (!pyodide || typeof pyodide.runPythonAsync !== "function") return false;',
893
+ ' const n = Number(port);',
894
+ ' if (!Number.isInteger(n) || n <= 0 || n >= 65536) return false;',
895
+ ' try {',
896
+ ' await pyodide.runPythonAsync(fnName + "(" + n + ")");',
897
+ ' return true;',
898
+ ' } catch (e) {',
899
+ ' const msg = e && e.message ? e.message : String(e);',
900
+ ' (globalThis.__nimbusPyStderr || (globalThis.__nimbusPyStderr = [])).push("[python-runner] socketserver call failed: " + msg + "\\n");',
901
+ ' return false;',
902
+ ' }',
903
+ ' };',
904
+ ' const task = globalThis.__nimbusPythonSocketserverQueue.then(run, run);',
905
+ ' globalThis.__nimbusPythonSocketserverQueue = task.then(() => {}, () => {});',
906
+ ' return task;',
907
+ '}',
908
+ 'globalThis.__nimbusVirtualSocketEnsureListener = function __nimbusVirtualSocketEnsureListener(port) {',
909
+ ' return __nimbusRunPythonSocketserverCall("_nimbus_ensure_socketserver_listener", port);',
910
+ '};',
911
+ 'globalThis.__nimbusVirtualSocketRequestQueued = function __nimbusVirtualSocketRequestQueued(port) {',
912
+ ' return __nimbusRunPythonSocketserverCall("_nimbus_handle_socketserver_request", port);',
913
+ '};',
914
+ '',
915
+ 'async function __nimbusStartPythonProcess(args) {',
916
+ ' if (!globalThis.__nimbusPythonProcessPromise) {',
917
+ ' const stdoutStart = (globalThis.__nimbusPyStdout || []).length;',
918
+ ' const stderrStart = (globalThis.__nimbusPyStderr || []).length;',
919
+ ' globalThis.__nimbusPythonProcessOutputStart = { stdoutStart, stderrStart };',
920
+ ' globalThis.__nimbusPythonProcessPromise = (async () => {',
921
+ ' const result = await globalThis.__pyodideRun({',
922
+ ' userCode: args.userCode,',
923
+ ' pyArgv: args.pyArgv || [],',
924
+ ' userEnv: args.userEnv || {},',
925
+ ' progName: args.progName || "python",',
926
+ ' cwd: args.cwd || "/home/user",',
927
+ ' fsSnapshot: args.fsSnapshot,',
928
+ ' asyncRun: true,',
929
+ ' pyodidePackages: [],',
930
+ ' });',
931
+ ' globalThis.__nimbusPythonProcessResult = result;',
932
+ ' return result;',
933
+ ' })();',
934
+ ' }',
935
+ ' const started = globalThis.__nimbusPythonProcessOutputStart || { stdoutStart: 0, stderrStart: 0 };',
936
+ ' const listen = globalThis.__nimbusVirtualSockets.waitForListen(10_000).then((port) => ({ state: port ? "listening" : "pending", port }));',
937
+ ' const exit = globalThis.__nimbusPythonProcessPromise.then((result) => ({ state: "exited", result }));',
938
+ ' const first = await Promise.race([listen, exit]);',
939
+ ' const registrations = globalThis.__nimbusVirtualPortRegistrationPromises || [];',
940
+ ' if (registrations.length > 0) await Promise.allSettled(registrations.splice(0));',
941
+ ' const stdout = (globalThis.__nimbusPyStdout || []).slice(started.stdoutStart).join("");',
942
+ ' const stderr = (globalThis.__nimbusPyStderr || []).slice(started.stderrStart).join("");',
943
+ ' if (first.state === "listening") return { state: "listening", port: first.port, stdout, stderr };',
944
+ ' if (first.state === "exited") return { state: "exited", result: first.result, stdout, stderr };',
945
+ ' const currentPort = globalThis.__nimbusVirtualSockets.firstListeningPort();',
946
+ ' if (currentPort) return { state: "listening", port: currentPort, stdout, stderr };',
947
+ ' return { state: "running", stdout, stderr };',
948
+ '}',
949
+ '',
950
+ 'export default class NimbusPythonProcess extends WorkerEntrypoint {',
951
+ ' async fetch(request) {',
952
+ ' globalThis.__nimbusPythonSupervisor = this.env?.SUPERVISOR;',
953
+ ' const url = new URL(request.url);',
954
+ ' if (url.pathname === "/__nimbus_start") {',
955
+ ' const args = await request.json();',
956
+ ' const boot = await __nimbusStartPythonProcess(args);',
957
+ ' return Response.json(boot);',
958
+ ' }',
959
+ ' return this.handleHttpRequest(request);',
960
+ ' }',
961
+ ' async handleHttpRequest(request) {',
962
+ ' const hinted = Number(request.headers.get("X-Nimbus-Port") || 0);',
963
+ ' const port = hinted || Array.from(globalThis.__nimbusVirtualSockets.listeners.keys())[0];',
964
+ ' if (!port) return new Response("Nimbus Python process has no listening virtual socket", { status: 502 });',
965
+ ' return globalThis.__nimbusVirtualSockets.handleHttpRequest(port, request);',
966
+ ' }',
967
+ '}',
968
+ ].join('\n');
969
+ }
291
970
  async function createPythonFacetRuntime(facetMgr, args) {
292
971
  const toAB = (u8) => u8.buffer.slice(u8.byteOffset, u8.byteOffset + u8.byteLength);
293
- if (!args.asmWasmVfs || !args.asmJsVfs || !args.stdlibVfs) {
294
- throw new Error('installed Pyodide manifest is missing required files');
295
- }
296
- const asmWasmBytes = args.vfs.readFile(args.asmWasmVfs);
297
- const asmJsBytes = args.vfs.readFile(args.asmJsVfs);
298
- const stdlibBytes = args.vfs.readFile(args.stdlibVfs);
299
- let asmJsSrc = new TextDecoder('utf-8').decode(asmJsBytes);
300
- const PATCH_NEEDLE = 'else throw new Error("Cannot determine runtime environment")';
301
- const PATCH_REPLACE = '/* nimbus-patch: was: ' + PATCH_NEEDLE + ' */';
302
- if (asmJsSrc.includes(PATCH_NEEDLE)) {
303
- asmJsSrc = asmJsSrc.replace(PATCH_NEEDLE, PATCH_REPLACE);
304
- }
305
- const HEAD_NEEDLE = 'if(d.IN_BROWSER_MAIN_THREAD)';
306
- const HEAD_REPLACE = 'if(true||d.IN_BROWSER_MAIN_THREAD)';
307
- if (asmJsSrc.includes(HEAD_NEEDLE)) {
308
- asmJsSrc = asmJsSrc.replace(HEAD_NEEDLE, HEAD_REPLACE);
309
- }
310
- const preamble = buildPyodidePreamble(asmJsSrc, uint8ToBase64(stdlibBytes));
972
+ const assets = buildPythonRuntimeAssets(args);
311
973
  const { NimbusLoaderPool } = await import('../loaders/loader-pool.js');
312
974
  const env = facetMgr.env;
313
975
  const ctx = facetMgr.ctx;
@@ -315,16 +977,16 @@ async function createPythonFacetRuntime(facetMgr, args) {
315
977
  tag: 'python-runner',
316
978
  concurrency: 1,
317
979
  omitSupervisor: true,
318
- preamble,
980
+ preamble: assets.preamble,
319
981
  wasmModules: {
320
- 'pyodide.asm.wasm': toAB(asmWasmBytes),
982
+ 'pyodide.asm.wasm': toAB(assets.asmWasmBytes),
321
983
  },
322
984
  });
323
985
  return { pool };
324
986
  }
325
987
  /**
326
- * Compose the per-call preamble by splicing the pyodide.asm.js source
327
- * verbatim ahead of the __pyodideRun helper. Workerd compiles this
988
+ * Compose the per-call preamble by splicing the workerd-adapted
989
+ * pyodide.asm.js source ahead of the __pyodideRun helper. Workerd compiles this
328
990
  * blob as JS at module-load time (where `var` declarations + globals
329
991
  * assignment are allowed), then the asm.js's `var _createPyodideModule`
330
992
  * is hoisted onto globalThis.
@@ -333,7 +995,7 @@ async function createPythonFacetRuntime(facetMgr, args) {
333
995
  // reuse the canonical preamble verbatim. Additive change only —
334
996
  // internal callers are unchanged. The `export` keyword is the sole
335
997
  // modification to python-runner.ts in the REPL-W1 wave.
336
- export function buildPyodidePreamble(asmJsSrc, stdlibB64) {
998
+ export function buildPyodidePreamble(asmJsSrc, stdlibB64, lockfileContents = '{"packages":{}}') {
337
999
  return [
338
1000
  '// ── Pre-asm.js environment shims ───────────────────────────────',
339
1001
  '// Pyodide.asm.js detects its environment via heuristics. In',
@@ -420,7 +1082,7 @@ export function buildPyodidePreamble(asmJsSrc, stdlibB64) {
420
1082
  'globalThis.WorkerGlobalScope = __nimbusOrigWGS;',
421
1083
  '// ── END: pyodide.asm.js inline ──────────────────────────────────',
422
1084
  '',
423
- buildPreambleTail(stdlibB64),
1085
+ buildPreambleTail(stdlibB64, lockfileContents),
424
1086
  ].join('\n');
425
1087
  }
426
1088
  // ── Facet preamble ───────────────────────────────────────────────────
@@ -491,7 +1153,7 @@ export function buildPyodidePreamble(asmJsSrc, stdlibB64) {
491
1153
  // │ - runPython(userCode) → stdout/stderr/exitCode │
492
1154
  // └─────────────────────────────────────────────────────────────┘
493
1155
  //
494
- function buildPreambleTail(stdlibB64) {
1156
+ function buildPreambleTail(stdlibB64, lockfileContents) {
495
1157
  return `
496
1158
  // ── BEGIN: python-runner preamble tail (Pyodide 0.29.4, Nimbus v2) ──
497
1159
 
@@ -499,6 +1161,9 @@ function buildPreambleTail(stdlibB64) {
499
1161
  // base64 text — decoded once at module-init. Same content as
500
1162
  // share/pyodide/python_stdlib.zip in the runtime cache.
501
1163
  const __NIMBUS_STDLIB_B64 = ${JSON.stringify(stdlibB64)};
1164
+ const __NIMBUS_LOCKFILE_CONTENTS = ${JSON.stringify(lockfileContents)};
1165
+ const __NIMBUS_PERSISTENT_SITE_PACKAGES = '/home/user/.nimbus-python/site-packages';
1166
+ const __NIMBUS_PYTHON_SOCKET_SHIM = ${JSON.stringify(PYTHON_SOCKET_SHIM)};
502
1167
 
503
1168
  // Decode base64 → Uint8Array. Run at module-init time (synchronous).
504
1169
  const __nimbusStdlibBytes = (function decode(b64) {
@@ -538,6 +1203,134 @@ try {
538
1203
  globalThis.__nimbusPyStdout = globalThis.__nimbusPyStdout || [];
539
1204
  globalThis.__nimbusPyStderr = globalThis.__nimbusPyStderr || [];
540
1205
 
1206
+ if (!globalThis.__nimbusPythonFetchStripsIntegrity && typeof globalThis.fetch === 'function') {
1207
+ const __nimbusOrigFetch = globalThis.fetch.bind(globalThis);
1208
+ globalThis.fetch = function nimbusPythonFetch(input, init) {
1209
+ if (init && typeof init === 'object' && init.integrity) {
1210
+ const clean = Object.assign({}, init);
1211
+ delete clean.integrity;
1212
+ return __nimbusOrigFetch(input, clean);
1213
+ }
1214
+ return __nimbusOrigFetch(input, init);
1215
+ };
1216
+ globalThis.__nimbusPythonFetchStripsIntegrity = true;
1217
+ }
1218
+
1219
+ function __nimbusB64ToBytes(b64) {
1220
+ const bin = atob(b64);
1221
+ const out = new Uint8Array(bin.length);
1222
+ for (let i = 0; i < bin.length; i++) out[i] = bin.charCodeAt(i);
1223
+ return out;
1224
+ }
1225
+
1226
+ function __nimbusBytesToB64(bytes) {
1227
+ let s = '';
1228
+ for (let i = 0; i < bytes.length; i++) s += String.fromCharCode(bytes[i]);
1229
+ return btoa(s);
1230
+ }
1231
+
1232
+ function __nimbusInstallPythonSocketModules(pyodide) {
1233
+ if (!globalThis.__nimbusVirtualSockets || !pyodide || globalThis.__nimbusPythonSocketsInstalled) return;
1234
+ const kernel = globalThis.__nimbusVirtualSockets;
1235
+ try {
1236
+ pyodide.registerJsModule('nimbus_sockets', {
1237
+ listen: (port) => kernel.listen(port),
1238
+ close_listener: (port) => kernel.closeListener(port),
1239
+ accept: async (port) => await kernel.accept(port),
1240
+ accept_now: (port) => kernel.acceptNow(port),
1241
+ recv: (id, maxBytes) => kernel.recv(id, maxBytes),
1242
+ send: (id, bytes) => kernel.send(id, bytes),
1243
+ close: (id) => kernel.close(id),
1244
+ pending: (port) => kernel.pending(port),
1245
+ sleep: async (ms) => await new Promise((resolve) => setTimeout(resolve, Math.max(0, Number(ms) || 0))),
1246
+ wait_readable: async (ports, timeoutSeconds) => await kernel.waitReadable(Array.from(ports || []), timeoutSeconds),
1247
+ });
1248
+ } catch {}
1249
+ try {
1250
+ pyodide.runPython(__NIMBUS_PYTHON_SOCKET_SHIM);
1251
+ globalThis.__nimbusPythonSocketsInstalled = true;
1252
+ } catch (e) {
1253
+ globalThis.__nimbusPyStderr.push('[python-runner] virtual socket shim install failed: ' + (e && e.message) + '\\n');
1254
+ }
1255
+ }
1256
+
1257
+ function __nimbusNormPath(path) {
1258
+ const clean = String(path || '').replace(/^\\/+/, '').replace(/\\/+$/, '');
1259
+ return clean ? '/' + clean : '/';
1260
+ }
1261
+
1262
+ function __nimbusInstallFsSnapshot(M, snapshot) {
1263
+ if (!snapshot || !snapshot.root) return;
1264
+ const currentFiles = new Set(Object.keys(snapshot.files || {}));
1265
+ const previous = globalThis.__nimbusMountedPyFiles || new Set();
1266
+ for (const prev of Array.from(previous)) {
1267
+ if (currentFiles.has(prev)) continue;
1268
+ try {
1269
+ const abs = __nimbusNormPath(prev);
1270
+ if (M.FS.analyzePath(abs).exists) M.FS.unlink(abs);
1271
+ } catch {}
1272
+ }
1273
+ const dirs = Array.from(new Set(snapshot.dirs || [])).sort((a, b) => a.length - b.length);
1274
+ for (const dir of dirs) {
1275
+ try { M.FS.mkdirTree(__nimbusNormPath(dir)); } catch {}
1276
+ }
1277
+ for (const [path, b64] of Object.entries(snapshot.files || {})) {
1278
+ const abs = __nimbusNormPath(path);
1279
+ try {
1280
+ const parent = abs.slice(0, abs.lastIndexOf('/')) || '/';
1281
+ M.FS.mkdirTree(parent);
1282
+ M.FS.writeFile(abs, __nimbusB64ToBytes(b64));
1283
+ } catch (e) {
1284
+ globalThis.__nimbusPyStderr.push('[python-runner] VFS mount failed for ' + path + ': ' + (e && e.message) + '\\n');
1285
+ }
1286
+ }
1287
+ globalThis.__nimbusMountedPyFiles = currentFiles;
1288
+ }
1289
+
1290
+ function __nimbusWalkPyFs(M, absDir, out) {
1291
+ let entries;
1292
+ try { entries = M.FS.readdir(absDir); } catch { return; }
1293
+ for (const name of entries) {
1294
+ if (name === '.' || name === '..') continue;
1295
+ if (name === '.nimbus' || name === 'node_modules' || name === '.cache' || name === '.npm') continue;
1296
+ const abs = absDir === '/' ? '/' + name : absDir + '/' + name;
1297
+ const rel = abs.replace(/^\\/+/, '');
1298
+ let stat;
1299
+ try { stat = M.FS.stat(abs); } catch { continue; }
1300
+ if (M.FS.isDir(stat.mode)) {
1301
+ out.dirs.push(rel);
1302
+ __nimbusWalkPyFs(M, abs, out);
1303
+ continue;
1304
+ }
1305
+ if (M.FS.isFile(stat.mode)) {
1306
+ try { out.files[rel] = __nimbusBytesToB64(M.FS.readFile(abs)); } catch {}
1307
+ }
1308
+ }
1309
+ }
1310
+
1311
+ function __nimbusSnapshotPyDiff(M, before) {
1312
+ if (!before || !before.root) return null;
1313
+ const roots = Array.from(new Set(((before.roots && before.roots.length) ? before.roots : [before.root]).filter(Boolean)));
1314
+ const after = { dirs: roots.slice(), files: {} };
1315
+ for (const root of roots) {
1316
+ __nimbusWalkPyFs(M, __nimbusNormPath(root), after);
1317
+ }
1318
+ const beforeDirs = new Set(before.dirs || []);
1319
+ const afterDirs = new Set(after.dirs || []);
1320
+ const dirsCreated = after.dirs.filter((path) => !beforeDirs.has(path));
1321
+ const dirsDeleted = (before.dirs || []).filter((path) => !afterDirs.has(path)).sort((a, b) => b.length - a.length);
1322
+ const filesWritten = {};
1323
+ const filesDeleted = [];
1324
+ const beforeFiles = before.files || {};
1325
+ for (const [path, b64] of Object.entries(after.files)) {
1326
+ if (beforeFiles[path] !== b64) filesWritten[path] = b64;
1327
+ }
1328
+ for (const path of Object.keys(beforeFiles)) {
1329
+ if (!(path in after.files)) filesDeleted.push(path);
1330
+ }
1331
+ return { filesWritten, filesDeleted, dirsCreated, dirsDeleted };
1332
+ }
1333
+
541
1334
  // Bootstrap gate release fn — populated by gateRuntimeInit preRun hook
542
1335
  // during _createPyodideModule's synchronous preRun pass.
543
1336
  globalThis.__nimbusReleaseGate = null;
@@ -577,19 +1370,23 @@ globalThis.__pyodideBootstrap = (async function nimbusBootstrap() {
577
1370
  return { ok: false, error: '__NIMBUS_WASM missing pyodide.asm.wasm' };
578
1371
  }
579
1372
 
1373
+ const pyodidePackageBaseUrl = 'https://cdn.jsdelivr.net/pyodide/v0.29.4/full/';
1374
+
580
1375
  // Initial Pyodide config — note: args, env, progName come from the
581
1376
  // per-call args at request time. We use sensible defaults here so
582
1377
  // the bootstrap can complete; per-call __pyodideRun overrides via
583
1378
  // M.ENV / pyodide.runPython arguments.
584
1379
  const config = {
585
1380
  indexURL: '/pyodide/',
1381
+ cdnUrl: pyodidePackageBaseUrl,
1382
+ packageBaseUrl: pyodidePackageBaseUrl,
586
1383
  fullStdLib: false,
587
1384
  jsglobals: globalThis,
588
1385
  args: [],
589
1386
  env: { HOME: '/home/pyodide', PYTHONINSPECT: '1' },
590
1387
  packages: [],
591
- lockFileContents: '{"packages":{}}',
592
- packageCacheDir: undefined,
1388
+ lockFileContents: __NIMBUS_LOCKFILE_CONTENTS,
1389
+ packageCacheDir: pyodidePackageBaseUrl,
593
1390
  enableRunUntilComplete: true,
594
1391
  checkAPIVersion: false,
595
1392
  _sysExecutable: 'python',
@@ -610,7 +1407,11 @@ globalThis.__pyodideBootstrap = (async function nimbusBootstrap() {
610
1407
  onExit: function(code) { globalThis.__nimbusExitCode = code | 0; },
611
1408
  thisProgram: config._sysExecutable,
612
1409
  arguments: config.args,
613
- API: { config: config, runtimeEnv: { IN_NODE: false, IN_BROWSER: false, IN_SHELL: false } },
1410
+ API: {
1411
+ config: config,
1412
+ runtimeEnv: { IN_NODE: false, IN_BROWSER: false, IN_SHELL: false },
1413
+ lockFilePromise: Promise.resolve(__NIMBUS_LOCKFILE_CONTENTS),
1414
+ },
614
1415
  locateFile: function(path) { return '/pyodide/' + path; },
615
1416
  instantiateWasm: function(imports, successCallback) {
616
1417
  // Attach sentinel namespace synchronously — Pyodide expects
@@ -757,6 +1558,18 @@ globalThis.__pyodideRun = async function __pyodideRun(args) {
757
1558
  if (args.userEnv) {
758
1559
  try { Object.assign(pyodideMod.ENV, args.userEnv); } catch {}
759
1560
  }
1561
+ try {
1562
+ __nimbusInstallFsSnapshot(pyodideMod, args.fsSnapshot);
1563
+ try {
1564
+ pyodideMod.FS.mkdirTree(__NIMBUS_PERSISTENT_SITE_PACKAGES);
1565
+ pyodideMod.API.sitePackages = __NIMBUS_PERSISTENT_SITE_PACKAGES;
1566
+ } catch {}
1567
+ const cwd = __nimbusNormPath(args.cwd || (args.userEnv && args.userEnv.HOME) || '/home/user');
1568
+ try { pyodideMod.FS.mkdirTree(cwd); } catch {}
1569
+ try { pyodideMod.FS.chdir(cwd); } catch {}
1570
+ } catch (e) {
1571
+ globalThis.__nimbusPyStderr.push('[python-runner] VFS mount failed: ' + (e && e.message) + '\\n');
1572
+ }
760
1573
 
761
1574
  // finalizeBootstrap returns the public Pyodide JS API (the one with
762
1575
  // .runPython, .globals, .registerJsModule). It registers Python-side
@@ -808,10 +1621,20 @@ globalThis.__pyodideRun = async function __pyodideRun(args) {
808
1621
  userGlobals = null;
809
1622
  }
810
1623
 
1624
+ __nimbusInstallPythonSocketModules(pyodide);
1625
+
811
1626
  try {
812
1627
  pyodide.runPython(
813
- 'import sys\\nsys.argv = ' +
814
- JSON.stringify(effectiveArgv),
1628
+ 'import os, sys\\n' +
1629
+ 'sys.argv = ' + JSON.stringify(effectiveArgv) + '\\n' +
1630
+ 'cwd = ' + JSON.stringify(args.cwd || '/home/user') + '\\n' +
1631
+ 'site_packages = ' + JSON.stringify('/home/user/.nimbus-python/site-packages') + '\\n' +
1632
+ 'try:\\n os.chdir(cwd)\\nexcept Exception:\\n pass\\n' +
1633
+ 'try:\\n os.makedirs(site_packages, exist_ok=True)\\nexcept Exception:\\n pass\\n' +
1634
+ 'if site_packages not in sys.path:\\n sys.path.insert(0, site_packages)\\n' +
1635
+ 'try:\\n import site as _nimbus_site; _nimbus_site.addsitedir(site_packages)\\nexcept Exception:\\n pass\\n' +
1636
+ 'try:\\n import sysconfig as _nimbus_sysconfig\\n for _scheme in getattr(_nimbus_sysconfig, "_INSTALL_SCHEMES", {}).values():\\n _scheme["purelib"] = site_packages\\n _scheme["platlib"] = site_packages\\nexcept Exception:\\n pass\\n' +
1637
+ 'if "" not in sys.path:\\n sys.path.insert(0, "")\\n',
815
1638
  userGlobals ? { globals: userGlobals } : undefined
816
1639
  );
817
1640
  } catch { /* best-effort */ }
@@ -823,10 +1646,22 @@ globalThis.__pyodideRun = async function __pyodideRun(args) {
823
1646
  let exitCode = 0;
824
1647
  if (args.userCode) {
825
1648
  try {
826
- pyodide.runPython(
827
- args.userCode,
828
- userGlobals ? { globals: userGlobals } : undefined
829
- );
1649
+ if (Array.isArray(args.pyodidePackages) && args.pyodidePackages.length > 0) {
1650
+ for (const pkg of args.pyodidePackages) {
1651
+ await pyodide.loadPackage(pkg);
1652
+ }
1653
+ }
1654
+ if (args.asyncRun && typeof pyodide.runPythonAsync === 'function') {
1655
+ await pyodide.runPythonAsync(
1656
+ args.userCode,
1657
+ userGlobals ? { globals: userGlobals } : undefined
1658
+ );
1659
+ } else {
1660
+ pyodide.runPython(
1661
+ args.userCode,
1662
+ userGlobals ? { globals: userGlobals } : undefined
1663
+ );
1664
+ }
830
1665
  // If runPython returned normally, exit code is 0 unless onExit
831
1666
  // was invoked (which can happen if user called sys.exit but
832
1667
  // Pyodide handled it via ExitStatus before throwing).
@@ -864,6 +1699,7 @@ globalThis.__pyodideRun = async function __pyodideRun(args) {
864
1699
  exitCode: exitCode,
865
1700
  stdout: globalThis.__nimbusPyStdout.slice(stdoutStart).join(''),
866
1701
  stderr: globalThis.__nimbusPyStderr.slice(stderrStart).join(''),
1702
+ fsDiff: __nimbusSnapshotPyDiff(pyodideMod, args.fsSnapshot),
867
1703
  };
868
1704
  };
869
1705