@nimbus-sh/worker 0.2.0 → 0.2.2

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 (83) hide show
  1. package/dist/facets/manager.d.ts +47 -2
  2. package/dist/facets/manager.d.ts.map +1 -1
  3. package/dist/facets/manager.js +139 -15
  4. package/dist/loaders/generated-workers.d.ts +2 -2
  5. package/dist/loaders/generated-workers.d.ts.map +1 -1
  6. package/dist/loaders/generated-workers.js +3 -3
  7. package/dist/runtime/bash-repl.d.ts +1 -1
  8. package/dist/runtime/bash-repl.d.ts.map +1 -1
  9. package/dist/runtime/bash-repl.js +3 -2
  10. package/dist/runtime/cpython-resident.d.ts +27 -0
  11. package/dist/runtime/cpython-resident.d.ts.map +1 -0
  12. package/dist/runtime/cpython-resident.js +133 -0
  13. package/dist/runtime/facet-loader-host.d.ts +16 -1
  14. package/dist/runtime/facet-loader-host.d.ts.map +1 -1
  15. package/dist/runtime/facet-loader-host.js +42 -0
  16. package/dist/runtime/package-manager.d.ts +3 -58
  17. package/dist/runtime/package-manager.d.ts.map +1 -1
  18. package/dist/runtime/package-manager.js +6 -143
  19. package/dist/runtime/python-repl.d.ts +1 -1
  20. package/dist/runtime/python-repl.d.ts.map +1 -1
  21. package/dist/runtime/python-repl.js +2 -2
  22. package/dist/runtime/ruby-repl.js +1 -1
  23. package/dist/runtime/ruby-resident.d.ts +20 -0
  24. package/dist/runtime/ruby-resident.d.ts.map +1 -0
  25. package/dist/runtime/ruby-resident.js +278 -0
  26. package/dist/runtime/runtime-catalog.d.ts +4 -80
  27. package/dist/runtime/runtime-catalog.d.ts.map +1 -1
  28. package/dist/runtime/runtime-catalog.js +4 -67
  29. package/dist/session/init.d.ts +16 -7
  30. package/dist/session/init.d.ts.map +1 -1
  31. package/dist/session/init.js +210 -249
  32. package/dist/session/keys.d.ts +13 -0
  33. package/dist/session/keys.d.ts.map +1 -1
  34. package/dist/session/keys.js +13 -0
  35. package/dist/session/nimbus-session.d.ts +30 -2
  36. package/dist/session/nimbus-session.d.ts.map +1 -1
  37. package/dist/session/nimbus-session.js +49 -58
  38. package/dist/session/programmatic.d.ts +3 -3
  39. package/dist/session/programmatic.d.ts.map +1 -1
  40. package/dist/session/programmatic.js +3 -2
  41. package/dist/session/routes.js +1 -1
  42. package/dist/session/supervisor-rpc.d.ts +6 -13
  43. package/dist/session/supervisor-rpc.d.ts.map +1 -1
  44. package/dist/session/supervisor-rpc.js +7 -17
  45. package/package.json +2 -2
  46. package/scripts/bundle-facet-workers.mjs +9 -8
  47. package/scripts/bundle-runtime.mjs +276 -44
  48. package/dist/npm/tarball-stream.d.ts +0 -93
  49. package/dist/npm/tarball-stream.d.ts.map +0 -1
  50. package/dist/npm/tarball-stream.js +0 -235
  51. package/dist/npm/tarball.d.ts +0 -34
  52. package/dist/npm/tarball.d.ts.map +0 -1
  53. package/dist/npm/tarball.js +0 -97
  54. package/dist/runtime/bash-runner.d.ts +0 -63
  55. package/dist/runtime/bash-runner.d.ts.map +0 -1
  56. package/dist/runtime/bash-runner.generated.d.ts +0 -14
  57. package/dist/runtime/bash-runner.generated.d.ts.map +0 -1
  58. package/dist/runtime/bash-runner.generated.js +0 -13
  59. package/dist/runtime/bash-runner.js +0 -291
  60. package/dist/runtime/clang-runner.d.ts +0 -37
  61. package/dist/runtime/clang-runner.d.ts.map +0 -1
  62. package/dist/runtime/clang-runner.js +0 -865
  63. package/dist/runtime/cpython-runner.d.ts +0 -71
  64. package/dist/runtime/cpython-runner.d.ts.map +0 -1
  65. package/dist/runtime/cpython-runner.js +0 -535
  66. package/dist/runtime/python-pip.d.ts +0 -38
  67. package/dist/runtime/python-pip.d.ts.map +0 -1
  68. package/dist/runtime/python-pip.js +0 -1063
  69. package/dist/runtime/ruby-gems.d.ts +0 -30
  70. package/dist/runtime/ruby-gems.d.ts.map +0 -1
  71. package/dist/runtime/ruby-gems.js +0 -636
  72. package/dist/runtime/ruby-runner.d.ts +0 -75
  73. package/dist/runtime/ruby-runner.d.ts.map +0 -1
  74. package/dist/runtime/ruby-runner.js +0 -1577
  75. package/dist/runtime/runtime-registry.d.ts +0 -162
  76. package/dist/runtime/runtime-registry.d.ts.map +0 -1
  77. package/dist/runtime/runtime-registry.js +0 -364
  78. package/dist/runtime/virtual-socket-kernel.generated.d.ts +0 -14
  79. package/dist/runtime/virtual-socket-kernel.generated.d.ts.map +0 -1
  80. package/dist/runtime/virtual-socket-kernel.generated.js +0 -13
  81. package/dist/runtime/wasm-runner.d.ts +0 -70
  82. package/dist/runtime/wasm-runner.d.ts.map +0 -1
  83. package/dist/runtime/wasm-runner.js +0 -664
@@ -1,71 +0,0 @@
1
- /**
2
- * cpython-runner.ts — `python` / `python3` / `pip`, on CPython 3.13 built for
3
- * wasm32-wasi.
4
- *
5
- * This replaces the Pyodide runner, and the reason is not the interpreter: it
6
- * is the filesystem. Pyodide is CPython built with Emscripten, so it brings its
7
- * own MEMFS, and every invocation had to copy the session's files in and diff
8
- * them back out through vfs-snapshot.ts. That made Python the last runtime with
9
- * a private, parallel filesystem. This build talks to runtime/wasi/preamble.ts
10
- * like clang, bash and ruby do — `open()` in Python is the same syscall as
11
- * `open()` in C — so there is nothing to copy and nothing to diff.
12
- *
13
- * What follows from that:
14
- * - manifestVfs, not snapshotVfs: the facet is given sizes and modes and
15
- * demand-loads the handful of files the program actually opens.
16
- * - supervisorPid, not omitSupervisor: a pool without a supervisor can read
17
- * the seeded manifest and can never write anything back. It looks like it
18
- * works.
19
- * - No Python-level socket shim. CPython's _socket is real here, over
20
- * nimbus-net.c and the host's synthetic paths, so loopback is ordinary
21
- * socket code rather than a monkey-patch.
22
- *
23
- * The interpreter is a WASI reactor (see packages/worker/wasm/python), because
24
- * a command module's _start runs once and that only covers `python script.py`.
25
- *
26
- * FOUR THINGS THIS RUNTIME REDISCOVERED THE HARD WAY, ALL OF WHICH ruby-runner
27
- * ALREADY KNEW. Read the list rather than finding a fifth:
28
- * 1. Every entry into the VM goes through WebAssembly.promising, not only the
29
- * calls known to park — a Suspending import traps on an unpromised stack
30
- * even when it returns a plain integer.
31
- * 2. The supervisor is adopted AFTER __wasiInitFS, which clears it on purpose,
32
- * and the facet drains queued writes in a `finally`.
33
- * 3. modes are seeded `{ '': 7, tmp: 7, home: 7 }` ahead of the manifest,
34
- * because manifestVfs's walk skips the empty root — without it the preopen
35
- * at '/' is mode 0 and every traversal under it is EACCES.
36
- * 4. The loader pool is built per invocation, never cached: supervisorPid is
37
- * baked into the SUPERVISOR binding at construction, so a held pool hands
38
- * every later caller the first caller's write credential.
39
- *
40
- * One ordering in cpythonRunFacetFn is load-bearing and looks redundant: the
41
- * supervisor stub is PUBLISHED on globalThis and only then adopted, because
42
- * __wasiInitFS clears the adoption on purpose and the boot re-adopts it from
43
- * there afterwards. Adopting once at the entry and deleting the Reflect.set
44
- * leaves a guest that reads the seeded filesystem and silently writes nowhere —
45
- * every write queued, none landed, no error anywhere. Ruby carries the same
46
- * pair for the same reason. The drain in the `finally` is the other half: a
47
- * program that wrote a file and then raised still wrote the file.
48
- */
49
- import type { FacetManager } from '../facets/manager.js';
50
- import type { CommandContext } from '@nimbus-sh/core/substrate/lifo/commands/types.js';
51
- import type { SqliteVFS } from '@nimbus-sh/core/vfs/sqlite-vfs.js';
52
- import type { RuntimeManifest } from './runtime-catalog.js';
53
- /**
54
- * The one canonical facet preamble. Composed in exactly one place: a
55
- * hand-rolled second copy is how ruby-repl once drifted into booting a VM whose
56
- * language prelude was missing.
57
- */
58
- export declare function buildCPythonPreamble(): string;
59
- /**
60
- * The worker source for a resident Python process. Same shape as
61
- * buildRubySocketProcessWorker: the socket kernel and the interpreter live in a
62
- * DurableObject, startProcess runs the program until it stops, and inbound
63
- * requests arrive on handleHttpRequest and are dispatched into the server the
64
- * program registered before exiting.
65
- */
66
- export declare function buildCPythonSocketProcessWorker(preamble: string): string;
67
- export declare function makeCPythonRunnerFactory(deps: {
68
- facetMgr: FacetManager;
69
- vfs: SqliteVFS;
70
- }): (manifest: RuntimeManifest, installRoot: string, binName: string, binKind: string | undefined) => (ctx: CommandContext) => Promise<number>;
71
- //# sourceMappingURL=cpython-runner.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"cpython-runner.d.ts","sourceRoot":"","sources":["../../src/runtime/cpython-runner.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA+CG;AAEH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAEzD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,kDAAkD,CAAC;AAEvF,OAAO,KAAK,EAAmB,SAAS,EAAE,MAAM,mCAAmC,CAAC;AAcpF,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC;AAuB5D;;;;GAIG;AACH,wBAAgB,oBAAoB,IAAI,MAAM,CAM7C;AAkFD;;;;;;GAMG;AACH,wBAAgB,+BAA+B,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAwDxE;AA+GD,wBAAgB,wBAAwB,CAAC,IAAI,EAAE;IAC7C,QAAQ,EAAE,YAAY,CAAC;IACvB,GAAG,EAAE,SAAS,CAAC;CAChB,GAAG,CAAC,QAAQ,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,SAAS,KAC7F,CAAC,GAAG,EAAE,cAAc,KAAK,OAAO,CAAC,MAAM,CAAC,CAuP3C"}
@@ -1,535 +0,0 @@
1
- /**
2
- * cpython-runner.ts — `python` / `python3` / `pip`, on CPython 3.13 built for
3
- * wasm32-wasi.
4
- *
5
- * This replaces the Pyodide runner, and the reason is not the interpreter: it
6
- * is the filesystem. Pyodide is CPython built with Emscripten, so it brings its
7
- * own MEMFS, and every invocation had to copy the session's files in and diff
8
- * them back out through vfs-snapshot.ts. That made Python the last runtime with
9
- * a private, parallel filesystem. This build talks to runtime/wasi/preamble.ts
10
- * like clang, bash and ruby do — `open()` in Python is the same syscall as
11
- * `open()` in C — so there is nothing to copy and nothing to diff.
12
- *
13
- * What follows from that:
14
- * - manifestVfs, not snapshotVfs: the facet is given sizes and modes and
15
- * demand-loads the handful of files the program actually opens.
16
- * - supervisorPid, not omitSupervisor: a pool without a supervisor can read
17
- * the seeded manifest and can never write anything back. It looks like it
18
- * works.
19
- * - No Python-level socket shim. CPython's _socket is real here, over
20
- * nimbus-net.c and the host's synthetic paths, so loopback is ordinary
21
- * socket code rather than a monkey-patch.
22
- *
23
- * The interpreter is a WASI reactor (see packages/worker/wasm/python), because
24
- * a command module's _start runs once and that only covers `python script.py`.
25
- *
26
- * FOUR THINGS THIS RUNTIME REDISCOVERED THE HARD WAY, ALL OF WHICH ruby-runner
27
- * ALREADY KNEW. Read the list rather than finding a fifth:
28
- * 1. Every entry into the VM goes through WebAssembly.promising, not only the
29
- * calls known to park — a Suspending import traps on an unpromised stack
30
- * even when it returns a plain integer.
31
- * 2. The supervisor is adopted AFTER __wasiInitFS, which clears it on purpose,
32
- * and the facet drains queued writes in a `finally`.
33
- * 3. modes are seeded `{ '': 7, tmp: 7, home: 7 }` ahead of the manifest,
34
- * because manifestVfs's walk skips the empty root — without it the preopen
35
- * at '/' is mode 0 and every traversal under it is EACCES.
36
- * 4. The loader pool is built per invocation, never cached: supervisorPid is
37
- * baked into the SUPERVISOR binding at construction, so a held pool hands
38
- * every later caller the first caller's write credential.
39
- *
40
- * One ordering in cpythonRunFacetFn is load-bearing and looks redundant: the
41
- * supervisor stub is PUBLISHED on globalThis and only then adopted, because
42
- * __wasiInitFS clears the adoption on purpose and the boot re-adopts it from
43
- * there afterwards. Adopting once at the entry and deleting the Reflect.set
44
- * leaves a guest that reads the seeded filesystem and silently writes nowhere —
45
- * every write queued, none landed, no error anywhere. Ruby carries the same
46
- * pair for the same reason. The drain in the `finally` is the other half: a
47
- * program that wrote a file and then raised still wrote the file.
48
- */
49
- import { resolveVfsPath } from '@nimbus-sh/core/vfs/path.js';
50
- import { z } from 'zod/v4';
51
- import { hasLeadingCliFlag } from '@nimbus-sh/core/runtime/cli-flags.js';
52
- import { CPYTHON_PREAMBLE_TAIL } from '@nimbus-sh/core/runtime/cpython-preamble.js';
53
- import { PYTHON_SERVER_ADAPTER } from '@nimbus-sh/core/runtime/python-server-adapter.js';
54
- import { getFacetManagerLoaderHost } from './facet-loader-host.js';
55
- import { requireVfsCred } from '@nimbus-sh/core/runtime/os-contracts.js';
56
- import { buildPipInvocation, PYTHON_SITE_PACKAGES_ROOT, sessionUsesSciVariant, } from './python-pip.js';
57
- import { manifestVfs } from '@nimbus-sh/core/runtime/vfs-manifest.js';
58
- import { VIRTUAL_SOCKET_KERNEL_SRC } from './virtual-socket-kernel.generated.js';
59
- import { WASI_INSTANCE_PREAMBLE_SRC } from '@nimbus-sh/core/runtime/wasi-instance.js';
60
- const PYTHON_VERSION_FLAGS = new Set(['--version', '-V']);
61
- const PYTHON_HELP_FLAGS = new Set(['--help', '-h']);
62
- /** Where `nimbus install python` stages the interpreter inside the session. */
63
- const CPYTHON_WASM_REL = 'share/cpython/python.wasm';
64
- /**
65
- * The same interpreter with numpy and markupsafe's C speedups linked in, and
66
- * their Python half. wasm32-wasi has no dlopen, so a compiled package is either
67
- * in the binary or unavailable; EXTENSIONS.md has why that beat a runtime
68
- * linker. Chosen per invocation from what the session has installed, so a
69
- * session that installed none of it pays none of the 7.8 MiB.
70
- */
71
- const CPYTHON_SCI_WASM_REL = 'share/cpython/python-sci.wasm';
72
- const CPYTHON_SCI_PACKAGES_REL = 'lib/sci-packages.zip';
73
- const CPYTHON_STDLIB_REL = 'lib/python313.zip';
74
- const CPYTHON_CACERT_REL = 'etc/ssl/cert.pem';
75
- /**
76
- * The one canonical facet preamble. Composed in exactly one place: a
77
- * hand-rolled second copy is how ruby-repl once drifted into booting a VM whose
78
- * language prelude was missing.
79
- */
80
- export function buildCPythonPreamble() {
81
- return [
82
- VIRTUAL_SOCKET_KERNEL_SRC,
83
- WASI_INSTANCE_PREAMBLE_SRC,
84
- CPYTHON_PREAMBLE_TAIL,
85
- ].join('\n');
86
- }
87
- /**
88
- * Python's CLI, as far as a sandbox needs it. Flags that only affect an
89
- * interactive tty or bytecode caching are accepted and ignored; anything else
90
- * is refused by name rather than silently doing something different.
91
- */
92
- function parsePythonArgv(argv) {
93
- const fail = (error) => ({ mode: 'inline', inlineCode: '', scriptPath: '', scriptArgs: [], exitCode: 2, error });
94
- let i = 0;
95
- while (i < argv.length) {
96
- const a = argv[i];
97
- if (a === '-c') {
98
- const code = argv[i + 1];
99
- if (code === undefined)
100
- return fail('Argument expected for the -c option');
101
- return { mode: 'inline', inlineCode: code, scriptPath: '', scriptArgs: argv.slice(i + 2), exitCode: 0 };
102
- }
103
- if (a === '-m') {
104
- const mod = argv[i + 1];
105
- if (mod === undefined)
106
- return fail('Argument expected for the -m option');
107
- const rest = argv.slice(i + 2);
108
- const inlineCode = [
109
- 'import runpy, sys',
110
- `sys.argv = ${JSON.stringify([mod, ...rest])}`,
111
- `runpy.run_module(${JSON.stringify(mod)}, run_name='__main__', alter_sys=True)`,
112
- ].join('\n');
113
- return { mode: 'inline', inlineCode, scriptPath: '', scriptArgs: rest, exitCode: 0 };
114
- }
115
- if (a === '-') {
116
- return { mode: 'stdin', inlineCode: '', scriptPath: '-', scriptArgs: argv.slice(i + 1), exitCode: 0 };
117
- }
118
- if (!a.startsWith('-')) {
119
- return { mode: 'script', inlineCode: '', scriptPath: a, scriptArgs: argv.slice(i + 1), exitCode: 0 };
120
- }
121
- if (/^-[OBuEItcsx]+$/.test(a)) {
122
- i++;
123
- continue;
124
- }
125
- return fail(`unknown option: ${a}`);
126
- }
127
- // No mode argument. init.ts routes a bare `python` to the REPL before it gets
128
- // here, so reaching this point means flags without a program.
129
- return fail("no program given. Use 'python -c \"code\"', 'python -m <module>' or 'python script.py'.");
130
- }
131
- /**
132
- * Whether this invocation should get a resident process rather than a one-shot.
133
- * A script or `-m` can bind a port and keep serving; pip never does.
134
- */
135
- function shouldRunAsResidentProcess(argv, parsed, pipMode) {
136
- if (pipMode)
137
- return false;
138
- if (parsed.mode === 'script')
139
- return true;
140
- if (argv[0] === '-m' && argv[1] && argv[1] !== 'pip')
141
- return true;
142
- return false;
143
- }
144
- /** `python -m pip ...` is pip, reached the long way round. */
145
- async function buildPythonModulePipInvocation(argv, cwd, vfs, runtimeContext) {
146
- if (argv[0] !== '-m' || argv[1] !== 'pip')
147
- return { mode: 'none', code: '', exitCode: 0 };
148
- return await buildPipInvocation(argv.slice(2), 'pip', cwd, vfs, runtimeContext);
149
- }
150
- function toArrayBuffer(bytes) {
151
- return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
152
- }
153
- function errorMessage(error) {
154
- return error instanceof Error ? error.message : String(error);
155
- }
156
- /**
157
- * The worker source for a resident Python process. Same shape as
158
- * buildRubySocketProcessWorker: the socket kernel and the interpreter live in a
159
- * DurableObject, startProcess runs the program until it stops, and inbound
160
- * requests arrive on handleHttpRequest and are dispatched into the server the
161
- * program registered before exiting.
162
- */
163
- export function buildCPythonSocketProcessWorker(preamble) {
164
- return [
165
- 'import { DurableObject } from "cloudflare:workers";',
166
- // The module arrives by path (vfsWasmModules) and has to be published where
167
- // the preamble looks for it. Without this the facet boots and the first
168
- // thing it says is "python.wasm was not supplied to this facet".
169
- "import __NIMBUS_WASM_python from './python.wasm';",
170
- 'globalThis.__NIMBUS_WASM = globalThis.__NIMBUS_WASM || {};',
171
- "globalThis.__NIMBUS_WASM['python.wasm'] = __NIMBUS_WASM_python;",
172
- '',
173
- preamble,
174
- '',
175
- // Only adopt a real binding: routed fetch hops resolve the entrypoint
176
- // without a supervisor, and overwriting with undefined would drop the live
177
- // stub the process needs for its whole lifetime.
178
- 'function __nimbusAdoptPySupervisor(env) {',
179
- ' const supervisor = env && env.SUPERVISOR;',
180
- ' if (supervisor) globalThis.__nimbusPySupervisor = supervisor;',
181
- ' __wasiAdoptSupervisor(supervisor);',
182
- '}',
183
- // A resident process answers between requests, and that is the only moment
184
- // "durable while running" can be made true: by the time the caller holds a
185
- // response, everything the request wrote has reached the VFS.
186
- 'async function __nimbusParkPy(value) {',
187
- ' await __wasiDrainPersist();',
188
- ' await __wasiRevalidateFS();',
189
- ' return value;',
190
- '}',
191
- 'async function __nimbusStartPyProcess(args) {',
192
- ' const result = await globalThis.__cpythonStartProcess(args || {});',
193
- ' const ports = await globalThis.__cpythonListeningPorts();',
194
- ' const registrations = globalThis.__nimbusVirtualPortRegistrationPromises || [];',
195
- ' if (registrations.length > 0) await Promise.allSettled(registrations.splice(0));',
196
- ' if (ports.length > 0) {',
197
- ' return { state: "listening", port: ports[0], stdout: result.stdout, stderr: result.stderr };',
198
- ' }',
199
- ' return { state: "exited", result, stdout: result.stdout, stderr: result.stderr };',
200
- '}',
201
- 'export class NimbusProcess extends DurableObject {',
202
- ' async startProcess(args) {',
203
- ' __nimbusAdoptPySupervisor(this.env);',
204
- ' return __nimbusParkPy(await __nimbusStartPyProcess(args || {}));',
205
- ' }',
206
- ' async fetch(request) {',
207
- ' __nimbusAdoptPySupervisor(this.env);',
208
- ' return this.handleHttpRequest(request);',
209
- ' }',
210
- ' async handleHttpRequest(request) {',
211
- ' __nimbusAdoptPySupervisor(this.env);',
212
- ' const hinted = Number(request.headers.get("X-Nimbus-Port") || 0);',
213
- ' const port = hinted || Array.from(globalThis.__nimbusVirtualSockets.listeners.keys())[0];',
214
- ' if (!port) return new Response("Nimbus Python process has no listening virtual socket", { status: 502 });',
215
- ' return __nimbusParkPy(await globalThis.__nimbusVirtualSockets.handleHttpRequest(port, request));',
216
- ' }',
217
- '}',
218
- ].join('\n');
219
- }
220
- const CPythonBootSchema = z.object({
221
- state: z.string().optional(),
222
- port: z.number().optional(),
223
- stdout: z.string().optional(),
224
- stderr: z.string().optional(),
225
- result: z.object({ exitCode: z.number().optional() }).passthrough().optional(),
226
- }).passthrough();
227
- /**
228
- * Facet-side entry. Serialized with fn.toString(), so it captures nothing and
229
- * names no import: everything it needs is on globalThis, put there by the
230
- * preamble.
231
- */
232
- async function cpythonRunFacetFn(args, facetEnv) {
233
- const run = Reflect.get(globalThis, '__cpythonRun');
234
- if (typeof run !== 'function') {
235
- return {
236
- stdout: '', stderr: '', exitCode: 127,
237
- error: 'cpython preamble missing: __cpythonRun not in scope',
238
- };
239
- }
240
- const adopt = Reflect.get(globalThis, '__wasiAdoptSupervisor');
241
- const drain = Reflect.get(globalThis, '__wasiDrainPersist');
242
- const supervisor = facetEnv && facetEnv.SUPERVISOR;
243
- // Published where the boot re-adopts it after the mount: adopting only here
244
- // would be undone by __wasiInitFS, which clears it on purpose.
245
- if (supervisor)
246
- Reflect.set(globalThis, '__nimbusPySupervisor', supervisor);
247
- adopt?.(supervisor);
248
- try {
249
- return await run(args);
250
- }
251
- finally {
252
- // In `finally`, not on the success path: a program that wrote a file and
253
- // then raised still wrote the file, and those bytes are the user's.
254
- await drain?.();
255
- }
256
- }
257
- /**
258
- * Start a program that binds a port as its own process, so it outlives the
259
- * invocation that started it. Mirrors spawnRubySocketProcess.
260
- */
261
- async function spawnCPythonSocketProcess(facetMgr, args, command) {
262
- const workerCode = buildCPythonSocketProcessWorker(buildCPythonPreamble());
263
- const spawned = await facetMgr.spawnWorker(workerCode, command, args.cwd, {
264
- compatibilityFlags: ['nodejs_compat'],
265
- // By path, not by value: the interpreter is 10.6 MiB, more than a single
266
- // RPC value may carry, so whichever host runs this process reads it itself.
267
- vfsWasmModules: { 'python.wasm': args.wasmVfsPath },
268
- startArgs: args.startArgs,
269
- }).catch(() => null);
270
- if (!spawned)
271
- return { exitCode: 1, stdout: '', stderr: 'python process boot failed\n' };
272
- const boot = CPythonBootSchema.safeParse(spawned.boot);
273
- if (!boot.success) {
274
- facetMgr.finishProcess(spawned.pid, 1, 'python process boot failed');
275
- return { exitCode: 1, stdout: '', stderr: 'python process boot failed\n' };
276
- }
277
- const data = boot.data;
278
- if (data.state === 'listening' && typeof data.port === 'number' && data.port > 0) {
279
- facetMgr.registerPort(spawned.pid, data.port);
280
- const routeable = await facetMgr.waitForRouteablePorts(spawned.pid);
281
- const port = routeable.includes(data.port) ? data.port : routeable[0];
282
- if (!port) {
283
- facetMgr.kill(spawned.pid);
284
- return {
285
- exitCode: 1,
286
- stdout: data.stdout || '',
287
- stderr: `${data.stderr || ''}python: virtual socket port ${data.port} failed to attach a route handler\n`,
288
- };
289
- }
290
- return {
291
- exitCode: 0,
292
- stdout: `${data.stdout || ''}\x1b[2m[started (long-running): pid=${spawned.pid} cmd="${command}" port=${port}]\x1b[0m\n`,
293
- stderr: data.stderr || '',
294
- spawnedPid: spawned.pid,
295
- port,
296
- };
297
- }
298
- const result = data.result;
299
- const exitCode = typeof result?.exitCode === 'number' ? result.exitCode : 0;
300
- facetMgr.finishProcess(spawned.pid, exitCode, data.stderr || 'python process exited');
301
- return { exitCode, stdout: data.stdout || '', stderr: data.stderr || '' };
302
- }
303
- export function makeCPythonRunnerFactory(deps) {
304
- const { facetMgr } = deps;
305
- return function cpythonRunnerFactory(manifest, installRoot, binName, _binKind) {
306
- const findFile = (rel) => {
307
- const entry = manifest.files.find((f) => f.path === rel);
308
- return entry ? `${installRoot}/${entry.path}` : null;
309
- };
310
- const baseWasmVfs = findFile(CPYTHON_WASM_REL);
311
- const sciWasmVfs = findFile(CPYTHON_SCI_WASM_REL);
312
- const sciPackagesVfs = findFile(CPYTHON_SCI_PACKAGES_REL);
313
- const stdlibVfs = findFile(CPYTHON_STDLIB_REL);
314
- let manifestCache = null;
315
- return async function cpythonBinHandler(ctx) {
316
- const cred = requireVfsCred(ctx.cred, binName);
317
- const credKey = `${cred.uid}:${cred.gid}:${cred.groups.join(',')}`;
318
- const vfs = deps.vfs.as(cred);
319
- const argv = ctx.args || [];
320
- const cwd = ctx.cwd || '/home/user';
321
- const pipRuntimeContext = {
322
- // No Pyodide lockfile: there is no curated wheel index behind this
323
- // interpreter, so pip resolves against PyPI like anywhere else.
324
- pyodideLockfileText: null,
325
- runtimeArtifacts: manifest.runtime_artifacts || [],
326
- };
327
- const isPipBin = _binKind === 'pip' || binName === 'pip' || binName === 'pip3';
328
- const pipInvocation = isPipBin
329
- ? await buildPipInvocation(argv, binName, cwd, vfs, pipRuntimeContext)
330
- : await buildPythonModulePipInvocation(argv, cwd, vfs, pipRuntimeContext);
331
- if (pipInvocation.error) {
332
- ctx.stderr.write(`${binName}: ${pipInvocation.error}\n`);
333
- return pipInvocation.exitCode;
334
- }
335
- if (pipInvocation.mode !== 'pip' && hasLeadingCliFlag(argv, PYTHON_VERSION_FLAGS)) {
336
- ctx.stdout.write('Python 3.13.14 (CPython, wasm32-wasi, Nimbus runtime)\n');
337
- return 0;
338
- }
339
- if (pipInvocation.mode !== 'pip' && hasLeadingCliFlag(argv, PYTHON_HELP_FLAGS)) {
340
- ctx.stdout.write(`usage: ${binName} [option] ... [-c cmd | -m mod | file | -] [arg] ...\n`);
341
- ctx.stdout.write('Nimbus CPython 3.13 runtime (wasm32-wasi).\n');
342
- ctx.stdout.write('Supported: -c <code>, -m <module>, <file.py>, stdin via -, and the session filesystem directly.\n');
343
- ctx.stdout.write('zlib, lzma, bz2, hashlib, ssl, sqlite3 and sockets are built in; pure-Python wheels install with pip.\n');
344
- return 0;
345
- }
346
- // Which interpreter this invocation gets. The sci variant is chosen from
347
- // what the session has installed, never from a guess about what this
348
- // program will import — the whole point of keying on state is that it is
349
- // also right for `python -c` naming a module in a variable. Falling back
350
- // when the variant is absent keeps a session installed before the variant
351
- // shipped working, on the base interpreter, rather than failing to start.
352
- const wantsSci = sessionUsesSciVariant(vfs)
353
- && sciWasmVfs !== null && vfs.exists(sciWasmVfs);
354
- const wasmVfs = wantsSci ? sciWasmVfs : baseWasmVfs;
355
- const sciPackagesPath = wantsSci && sciPackagesVfs && vfs.exists(sciPackagesVfs)
356
- ? sciPackagesVfs
357
- : null;
358
- if (!wasmVfs || !vfs.exists(wasmVfs)) {
359
- ctx.stderr.write(`${binName}: python.wasm missing (re-run 'nimbus install python')\n`);
360
- return 127;
361
- }
362
- if (!stdlibVfs || !vfs.exists(stdlibVfs)) {
363
- ctx.stderr.write(`${binName}: python313.zip missing (re-run 'nimbus install python')\n`);
364
- return 127;
365
- }
366
- const parsed = pipInvocation.mode === 'pip'
367
- ? { mode: 'inline', inlineCode: pipInvocation.code, scriptPath: '', scriptArgs: [], exitCode: 0 }
368
- : parsePythonArgv(argv);
369
- if (parsed.error) {
370
- ctx.stderr.write(`${binName}: ${parsed.error}\n`);
371
- return parsed.exitCode;
372
- }
373
- let userCode = '';
374
- let progName = binName;
375
- let pyArgv = [binName];
376
- if (parsed.mode === 'inline') {
377
- userCode = parsed.inlineCode;
378
- pyArgv = ['-c', ...parsed.scriptArgs];
379
- }
380
- else if (parsed.mode === 'script') {
381
- const absPath = resolveVfsPath(parsed.scriptPath, cwd);
382
- try {
383
- if (!vfs.exists(absPath)) {
384
- ctx.stderr.write(`${binName}: can't open file '${parsed.scriptPath}': [Errno 2] No such file or directory\n`);
385
- return 2;
386
- }
387
- userCode = new TextDecoder('utf-8').decode(vfs.readFile(absPath));
388
- }
389
- catch (e) {
390
- ctx.stderr.write(`${binName}: ${parsed.scriptPath}: ${errorMessage(e)}\n`);
391
- return 1;
392
- }
393
- progName = parsed.scriptPath;
394
- pyArgv = [parsed.scriptPath, ...parsed.scriptArgs];
395
- }
396
- else {
397
- const stdinReader = ctx.stdin;
398
- userCode = (stdinReader && typeof stdinReader.read === 'function' ? await stdinReader.read() : '') ?? '';
399
- pyArgv = ['-', ...parsed.scriptArgs];
400
- }
401
- // sys.argv is set from Python rather than from WASI argv: the reactor has
402
- // no argv of its own, and this keeps the one place that decides what the
403
- // program sees in TypeScript.
404
- const prelude = [
405
- PYTHON_SERVER_ADAPTER,
406
- 'import sys',
407
- `sys.argv = ${JSON.stringify(pyArgv)}`,
408
- // The variant's own packages. zipimport reads them straight out of the
409
- // session filesystem, so they need no unpacking and no manifest entry
410
- // beyond the archive itself.
411
- ...(sciPackagesPath
412
- ? [`sys.path.insert(0, ${JSON.stringify(`/${sciPackagesPath.replace(/^\/+/, '')}`)})`]
413
- : []),
414
- `sys.path.insert(0, ${JSON.stringify(`/${PYTHON_SITE_PACKAGES_ROOT}`)})`,
415
- `sys.path.insert(0, ${JSON.stringify(cwd)})`,
416
- // WASI has no process cwd, so wasi-libc starts every guest at '/'.
417
- // Leaving it there silently reroutes every relative path a program
418
- // opens — the shell says the user is in /home/user and Python resolves
419
- // against the root.
420
- 'import os',
421
- 'try:',
422
- ` os.chdir(${JSON.stringify(cwd)})`,
423
- 'except OSError:',
424
- ' pass',
425
- ].join('\n');
426
- const cacertVfs = findFile(CPYTHON_CACERT_REL);
427
- const userEnv = { ...(ctx.env || {}) };
428
- if (!userEnv.HOME)
429
- userEnv.HOME = '/home/user';
430
- if (!userEnv.PYTHONUNBUFFERED)
431
- userEnv.PYTHONUNBUFFERED = '1';
432
- // Without this OpenSSL has no trust anchors at all — there is no
433
- // /etc/ssl on a Nimbus session — and every HTTPS request fails
434
- // verification with a message about a missing local issuer rather than
435
- // about a missing bundle.
436
- if (!userEnv.SSL_CERT_FILE && cacertVfs)
437
- userEnv.SSL_CERT_FILE = `/${cacertVfs.replace(/^\/+/, '')}`;
438
- // A manifest, not a copy: sizes and modes only, with the facet demand-
439
- // loading whatever the program opens. The stdlib zip is covered by it
440
- // like any other file, which is the whole point of not having a private
441
- // filesystem any more.
442
- const stdlibDir = stdlibVfs.replace(/\/[^/]+$/, '');
443
- // Every runtime file the interpreter is told about has to be a root of
444
- // its own. The trust store was reachable only while the cwd happened to
445
- // be an ancestor of the install — from ~ the walk swept the whole runtime
446
- // tree in — so `cd` into any subdirectory and SSL_CERT_FILE pointed at a
447
- // path the facet could not see, and every pip install failed
448
- // CERTIFICATE_VERIFY_FAILED with the bundle sitting right there.
449
- const cacertDir = cacertVfs ? cacertVfs.replace(/\/[^/]+$/, '') : null;
450
- const revision = Math.max(vfs.revision(cwd), vfs.revision(PYTHON_SITE_PACKAGES_ROOT), vfs.revision(stdlibVfs));
451
- let fsManifest = manifestCache && manifestCache.cred === credKey
452
- && manifestCache.cwd === cwd && manifestCache.revision === revision
453
- ? manifestCache.result
454
- : null;
455
- if (!fsManifest) {
456
- fsManifest = manifestVfs(vfs, cwd, {
457
- extraRoots: [PYTHON_SITE_PACKAGES_ROOT, stdlibDir, ...(cacertDir ? [cacertDir] : [])],
458
- revision,
459
- });
460
- manifestCache = { cred: credKey, cwd, revision, result: fsManifest };
461
- }
462
- if ('error' in fsManifest) {
463
- ctx.stderr.write(`${binName}: ${fsManifest.error}\n`);
464
- return 1;
465
- }
466
- const snapshot = fsManifest.snapshot;
467
- // Built per invocation, not cached: supervisorPid is baked into the
468
- // SUPERVISOR binding at construction, so a pool held across calls would
469
- // hand every later caller the first caller's write credential.
470
- const { NimbusLoaderPool } = await import('../loaders/loader-pool.js');
471
- const host = getFacetManagerLoaderHost(facetMgr);
472
- const pool = new NimbusLoaderPool(host.env, host.ctx, {
473
- // The variant is in the tag because the pool's constructor-time wasm
474
- // fingerprint is name:length:first-byte:last-byte, not a content hash.
475
- // Two variants differ by megabytes so they would not collide today, but
476
- // a warm slot serving the wrong interpreter is not a failure worth
477
- // leaving to a size coincidence.
478
- tag: wantsSci ? 'cpython-runner:sci' : 'cpython-runner',
479
- concurrency: 1,
480
- // NOT omitSupervisor. Without it the facet reads the seeded manifest
481
- // and can never write anything back — the program appears to run and
482
- // its output never reaches the session.
483
- supervisorPid: ctx.pid,
484
- preamble: buildCPythonPreamble(),
485
- wasmModules: { 'python.wasm': toArrayBuffer(vfs.readFile(wasmVfs)) },
486
- });
487
- const facetArgs = {
488
- userCode: `${prelude}\n${userCode}`,
489
- pyArgv,
490
- userEnv,
491
- progName,
492
- cwd,
493
- pythonHome: `/${installRoot.replace(/^\/+/, '')}`,
494
- supervisorPid: ctx.pid,
495
- fsSnapshot: snapshot,
496
- };
497
- // A script or `-m` can bind a port and keep serving, and such a program
498
- // is not finished when it stops producing output — it is finished when it
499
- // stops running. Pyodide gave those a dedicated socket process; that
500
- // spawn is not ported yet, so for now they get a one-shot facet with a
501
- // budget long enough not to cut a server off mid-request. Porting the
502
- // resident process is the last piece, and until it lands a server holds
503
- // its facet rather than being driven by inbound requests.
504
- const resident = shouldRunAsResidentProcess(argv, parsed, pipInvocation.mode === 'pip');
505
- if (resident) {
506
- const command = [binName, ...argv].map((part) => (/^[A-Za-z0-9_./:=@+-]+$/.test(part) ? part : JSON.stringify(part))).join(' ');
507
- const spawnResult = await spawnCPythonSocketProcess(facetMgr, { wasmVfsPath: wasmVfs, startArgs: facetArgs, cwd }, command);
508
- if (spawnResult.stdout)
509
- ctx.stdout.write(spawnResult.stdout);
510
- if (spawnResult.stderr)
511
- ctx.stderr.write(spawnResult.stderr);
512
- return spawnResult.exitCode;
513
- }
514
- let result;
515
- try {
516
- result = await pool.submit(cpythonRunFacetFn, facetArgs, {
517
- timeoutMs: 120_000,
518
- });
519
- }
520
- catch (e) {
521
- ctx.stderr.write(`${binName}: ${errorMessage(e)}\n`);
522
- return 1;
523
- }
524
- if (result.stdout)
525
- ctx.stdout.write(result.stdout);
526
- if (result.stderr)
527
- ctx.stderr.write(result.stderr);
528
- if (result.error) {
529
- ctx.stderr.write(`${binName}: ${result.error}\n`);
530
- return result.exitCode || 1;
531
- }
532
- return result.exitCode;
533
- };
534
- };
535
- }
@@ -1,38 +0,0 @@
1
- import { z } from 'zod/v4';
2
- import { type RuntimeArtifactMetadata, type RuntimePythonPackageArtifactMetadata } from './runtime-catalog.js';
3
- export declare const PYTHON_SITE_PACKAGES_ROOT = "home/user/.nimbus-python/site-packages";
4
- export declare const PYTHON_PYODIDE_PACKAGE_MANIFEST = "home/user/.nimbus-python/site-packages/.nimbus-pyodide-packages.json";
5
- interface PythonPipVfs {
6
- exists(path: string): boolean;
7
- readFile(path: string): Uint8Array;
8
- }
9
- /**
10
- * Whether this session has installed anything the sci interpreter variant
11
- * carries compiled code for.
12
- *
13
- * This reads installed state; it does not predict what a program might import.
14
- * The dist-info directory pip writes is the record, and a variant chosen from it
15
- * is right for `python -c` reading a module name out of a variable, which a
16
- * per-program classifier cannot be.
17
- */
18
- export declare function sessionUsesSciVariant(vfs: PythonPipVfs): boolean;
19
- export interface PythonPipRuntimeContext {
20
- pyodideLockfileText?: string | null;
21
- runtimeArtifacts?: RuntimeArtifactMetadata[];
22
- }
23
- export interface InstalledPyodidePackageManifest {
24
- version: 1;
25
- packages: RuntimePythonPackageArtifactMetadata[];
26
- }
27
- export interface PipInvocation {
28
- mode: 'pip' | 'none';
29
- code: string;
30
- error?: string;
31
- exitCode: number;
32
- pyodidePackages?: RuntimePythonPackageArtifactMetadata[];
33
- }
34
- export declare const InstalledPyodidePackageManifestSchema: z.ZodType<InstalledPyodidePackageManifest>;
35
- export declare function parseInstalledPyodidePackageManifest(text: string): InstalledPyodidePackageManifest;
36
- export declare function buildPipInvocation(argv: string[], binName: string, cwd: string, vfs: PythonPipVfs, runtimeContext?: PythonPipRuntimeContext): Promise<PipInvocation>;
37
- export {};
38
- //# sourceMappingURL=python-pip.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"python-pip.d.ts","sourceRoot":"","sources":["../../src/runtime/python-pip.ts"],"names":[],"mappings":"AAmBA,OAAO,EAAE,CAAC,EAAE,MAAM,QAAQ,CAAC;AAG3B,OAAO,EAGL,KAAK,uBAAuB,EAC5B,KAAK,oCAAoC,EAC1C,MAAM,sBAAsB,CAAC;AAE9B,eAAO,MAAM,yBAAyB,2CAA2C,CAAC;AAClF,eAAO,MAAM,+BAA+B,yEAA+D,CAAC;AAI5G,UAAU,YAAY;IACpB,MAAM,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC;IAC9B,QAAQ,CAAC,IAAI,EAAE,MAAM,GAAG,UAAU,CAAC;CACpC;AAuFD;;;;;;;;GAQG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,YAAY,GAAG,OAAO,CAGhE;AAkDD,MAAM,WAAW,uBAAuB;IACtC,mBAAmB,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,gBAAgB,CAAC,EAAE,uBAAuB,EAAE,CAAC;CAC9C;AAuDD,MAAM,WAAW,+BAA+B;IAC9C,OAAO,EAAE,CAAC,CAAC;IACX,QAAQ,EAAE,oCAAoC,EAAE,CAAC;CAClD;AAED,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,KAAK,GAAG,MAAM,CAAC;IACrB,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,CAAC,EAAE,oCAAoC,EAAE,CAAC;CAC1D;AAQD,eAAO,MAAM,qCAAqC,EAAE,CAAC,CAAC,OAAO,CAAC,+BAA+B,CAG3F,CAAC;AAEH,wBAAgB,oCAAoC,CAAC,IAAI,EAAE,MAAM,GAAG,+BAA+B,CAElG;AAED,wBAAsB,kBAAkB,CACtC,IAAI,EAAE,MAAM,EAAE,EACd,OAAO,EAAE,MAAM,EACf,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,YAAY,EACjB,cAAc,GAAE,uBAA4B,GAC3C,OAAO,CAAC,aAAa,CAAC,CA0CxB"}