@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.
- package/README.md +37 -0
- package/dist/_shared/crypto.d.ts +15 -0
- package/dist/_shared/crypto.d.ts.map +1 -0
- package/dist/_shared/crypto.js +122 -0
- package/dist/facets/manager.d.ts +24 -0
- package/dist/facets/manager.d.ts.map +1 -1
- package/dist/facets/manager.js +279 -70
- package/dist/facets/process.d.ts +11 -2
- package/dist/facets/process.d.ts.map +1 -1
- package/dist/facets/process.js +187 -21
- package/dist/facets/vite-dev-server.d.ts +2 -0
- package/dist/facets/vite-dev-server.d.ts.map +1 -1
- package/dist/facets/vite-dev-server.js +39 -12
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/loaders/child-process/spawn-facet.d.ts +1 -3
- package/dist/loaders/child-process/spawn-facet.d.ts.map +1 -1
- package/dist/loaders/child-process/spawn-facet.js +1 -12
- package/dist/loaders/child-process/spawn-pool.d.ts +1 -1
- package/dist/loaders/child-process/spawn-pool.d.ts.map +1 -1
- package/dist/loaders/child-process/spawn-pool.js +1 -6
- package/dist/npm/installer.d.ts.map +1 -1
- package/dist/npm/installer.js +47 -27
- package/dist/npm/pre-bundle-facet.d.ts.map +1 -1
- package/dist/npm/pre-bundle-facet.js +2 -1
- package/dist/npm/resolve-one-facet.d.ts.map +1 -1
- package/dist/npm/resolve-one-facet.js +41 -13
- package/dist/npm/resolver.d.ts.map +1 -1
- package/dist/npm/resolver.js +70 -35
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +45 -21
- package/dist/router/remote-api.d.ts.map +1 -1
- package/dist/router/remote-api.js +29 -8
- package/dist/runtime/barrel-synthesizer.d.ts +7 -0
- package/dist/runtime/barrel-synthesizer.d.ts.map +1 -1
- package/dist/runtime/barrel-synthesizer.js +11 -0
- package/dist/runtime/clang-runner.d.ts.map +1 -1
- package/dist/runtime/clang-runner.js +1 -8
- package/dist/runtime/esbuild-service.d.ts +12 -5
- package/dist/runtime/esbuild-service.d.ts.map +1 -1
- package/dist/runtime/esbuild-service.js +28 -14
- package/dist/runtime/node-runner.d.ts +2 -0
- package/dist/runtime/node-runner.d.ts.map +1 -1
- package/dist/runtime/node-shims.d.ts +4 -2
- package/dist/runtime/node-shims.d.ts.map +1 -1
- package/dist/runtime/node-shims.js +404 -55
- package/dist/runtime/os-contracts.d.ts +128 -0
- package/dist/runtime/os-contracts.d.ts.map +1 -0
- package/dist/runtime/os-contracts.js +1 -0
- package/dist/runtime/package-manager.d.ts.map +1 -1
- package/dist/runtime/package-manager.js +32 -7
- package/dist/runtime/port-registry.d.ts +5 -0
- package/dist/runtime/port-registry.d.ts.map +1 -1
- package/dist/runtime/port-registry.js +22 -1
- package/dist/runtime/pyodide-runtime-assets.d.ts +19 -0
- package/dist/runtime/pyodide-runtime-assets.d.ts.map +1 -0
- package/dist/runtime/pyodide-runtime-assets.js +44 -0
- package/dist/runtime/python-repl.d.ts +3 -1
- package/dist/runtime/python-repl.d.ts.map +1 -1
- package/dist/runtime/python-repl.js +11 -29
- package/dist/runtime/python-runner.d.ts +8 -9
- package/dist/runtime/python-runner.d.ts.map +1 -1
- package/dist/runtime/python-runner.js +897 -61
- package/dist/runtime/python-socket-shim.d.ts +11 -0
- package/dist/runtime/python-socket-shim.d.ts.map +1 -0
- package/dist/runtime/python-socket-shim.js +526 -0
- package/dist/runtime/require-resolver.d.ts.map +1 -1
- package/dist/runtime/require-resolver.js +2 -11
- package/dist/runtime/ruby-gems.d.ts +25 -0
- package/dist/runtime/ruby-gems.d.ts.map +1 -0
- package/dist/runtime/ruby-gems.js +582 -0
- package/dist/runtime/ruby-runner.d.ts +2 -4
- package/dist/runtime/ruby-runner.d.ts.map +1 -1
- package/dist/runtime/ruby-runner.js +335 -34
- package/dist/runtime/runtime-catalog.d.ts +16 -4
- package/dist/runtime/runtime-catalog.d.ts.map +1 -1
- package/dist/runtime/runtime-catalog.js +21 -10
- package/dist/runtime/runtime-registry.d.ts +3 -0
- package/dist/runtime/runtime-registry.d.ts.map +1 -1
- package/dist/runtime/runtime-registry.js +4 -0
- package/dist/runtime/sqlite-runtime-fs-bridge.d.ts +43 -0
- package/dist/runtime/sqlite-runtime-fs-bridge.d.ts.map +1 -0
- package/dist/runtime/sqlite-runtime-fs-bridge.js +224 -0
- package/dist/runtime/static-server.d.ts +3 -0
- package/dist/runtime/static-server.d.ts.map +1 -0
- package/dist/runtime/static-server.js +121 -0
- package/dist/runtime/vfs-snapshot.d.ts +57 -0
- package/dist/runtime/vfs-snapshot.d.ts.map +1 -0
- package/dist/runtime/vfs-snapshot.js +141 -0
- package/dist/runtime/virtual-socket-kernel.d.ts +16 -0
- package/dist/runtime/virtual-socket-kernel.d.ts.map +1 -0
- package/dist/runtime/virtual-socket-kernel.js +360 -0
- package/dist/runtime/wasi-instance.d.ts +6 -3
- package/dist/runtime/wasi-instance.d.ts.map +1 -1
- package/dist/runtime/wasm-runner.d.ts +1 -25
- package/dist/runtime/wasm-runner.d.ts.map +1 -1
- package/dist/runtime/wasm-runner.js +1 -160
- package/dist/session/agent.d.ts +43 -0
- package/dist/session/agent.d.ts.map +1 -0
- package/dist/session/agent.js +1252 -0
- package/dist/session/init.d.ts.map +1 -1
- package/dist/session/init.js +15 -16
- package/dist/session/nimbus-session.d.ts +13 -2
- package/dist/session/nimbus-session.d.ts.map +1 -1
- package/dist/session/nimbus-session.js +55 -7
- package/dist/session/programmatic.d.ts +10 -0
- package/dist/session/programmatic.d.ts.map +1 -1
- package/dist/session/programmatic.js +121 -0
- package/dist/session/routes.d.ts.map +1 -1
- package/dist/session/routes.js +19 -1
- package/dist/session/rpc.d.ts +11 -0
- package/dist/session/rpc.d.ts.map +1 -1
- package/dist/session/rpc.js +56 -43
- package/dist/session/supervisor-rpc.d.ts +15 -0
- package/dist/session/supervisor-rpc.d.ts.map +1 -1
- package/dist/session/supervisor-rpc.js +32 -0
- package/dist/session/ws.d.ts +4 -3
- package/dist/session/ws.d.ts.map +1 -1
- package/dist/session/ws.js +11 -1
- package/dist/shell/unix-commands.js +12 -16
- package/dist/vfs/path.d.ts +8 -4
- package/dist/vfs/path.d.ts.map +1 -1
- package/dist/vfs/path.js +16 -4
- package/dist/vfs/sqlite-vfs.d.ts +2 -0
- package/dist/vfs/sqlite-vfs.d.ts.map +1 -1
- package/dist/vfs/sqlite-vfs.js +11 -0
- package/dist/vfs/symlink-registry.d.ts +7 -1
- package/dist/vfs/symlink-registry.d.ts.map +1 -1
- package/dist/vfs/symlink-registry.js +16 -11
- package/package.json +4 -1
- package/public/index.html +240 -0
- package/public/s/index.html +1172 -146
- package/runtime-contracts/pyodide-workerd-adapter.json +18 -0
- 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
|
-
*
|
|
11
|
+
* Current limits:
|
|
12
12
|
* - REPL mode (`python` with no args)
|
|
13
|
-
* -
|
|
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
|
|
21
|
-
* `context` field (JSON-stringified into the inner worker.js
|
|
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,
|
|
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 (
|
|
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 (
|
|
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
|
|
57
|
-
ctx.stdout.write(`
|
|
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 =
|
|
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
|
|
234
|
-
const
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
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
|
|
327
|
-
*
|
|
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:
|
|
592
|
-
packageCacheDir:
|
|
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: {
|
|
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\\
|
|
814
|
-
|
|
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
|
-
|
|
827
|
-
args.
|
|
828
|
-
|
|
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
|
|