@nimbus-sh/core 0.1.0 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/dist/index.d.ts +5 -0
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +2 -0
  4. package/dist/runtime/bash-runner.d.ts +63 -0
  5. package/dist/runtime/bash-runner.d.ts.map +1 -0
  6. package/dist/runtime/bash-runner.generated.d.ts +14 -0
  7. package/dist/runtime/bash-runner.generated.d.ts.map +1 -0
  8. package/dist/runtime/bash-runner.generated.js +13 -0
  9. package/dist/runtime/bash-runner.js +290 -0
  10. package/dist/runtime/cpython-runner.d.ts +86 -0
  11. package/dist/runtime/cpython-runner.d.ts.map +1 -0
  12. package/dist/runtime/cpython-runner.js +425 -0
  13. package/dist/runtime/facet-host.d.ts +159 -0
  14. package/dist/runtime/facet-host.d.ts.map +1 -0
  15. package/dist/runtime/facet-host.js +22 -0
  16. package/dist/runtime/installed-runtimes.d.ts +99 -0
  17. package/dist/runtime/installed-runtimes.d.ts.map +1 -0
  18. package/dist/runtime/installed-runtimes.js +162 -0
  19. package/dist/runtime/local-facet-host.d.ts +38 -0
  20. package/dist/runtime/local-facet-host.d.ts.map +1 -0
  21. package/dist/runtime/local-facet-host.js +171 -0
  22. package/dist/runtime/python-pip.d.ts +38 -0
  23. package/dist/runtime/python-pip.d.ts.map +1 -0
  24. package/dist/runtime/python-pip.js +1063 -0
  25. package/dist/runtime/runtime-manifest.d.ts +86 -0
  26. package/dist/runtime/runtime-manifest.d.ts.map +1 -0
  27. package/dist/runtime/runtime-manifest.js +72 -0
  28. package/dist/runtime/runtime-package.d.ts +63 -0
  29. package/dist/runtime/runtime-package.d.ts.map +1 -0
  30. package/dist/runtime/runtime-package.js +66 -0
  31. package/dist/runtime/runtime-registry.d.ts +162 -0
  32. package/dist/runtime/runtime-registry.d.ts.map +1 -0
  33. package/dist/runtime/runtime-registry.js +363 -0
  34. package/dist/runtime/vfs-snapshot.d.ts.map +1 -1
  35. package/dist/runtime/vfs-snapshot.js +15 -1
  36. package/dist/runtime/vfs-supervisor.d.ts +22 -0
  37. package/dist/runtime/vfs-supervisor.d.ts.map +1 -0
  38. package/dist/runtime/vfs-supervisor.js +65 -0
  39. package/dist/runtime/virtual-socket-kernel.generated.d.ts +14 -0
  40. package/dist/runtime/virtual-socket-kernel.generated.d.ts.map +1 -0
  41. package/dist/runtime/virtual-socket-kernel.generated.js +13 -0
  42. package/dist/runtime/wasm-runner.d.ts +80 -0
  43. package/dist/runtime/wasm-runner.d.ts.map +1 -0
  44. package/dist/runtime/wasm-runner.js +686 -0
  45. package/dist/workspace/nimbus-workspace.d.ts +116 -20
  46. package/dist/workspace/nimbus-workspace.d.ts.map +1 -1
  47. package/dist/workspace/nimbus-workspace.js +238 -45
  48. package/package.json +4 -2
  49. package/src/index.ts +16 -0
  50. package/src/runtime/bash-runner.generated.ts +14 -0
  51. package/src/runtime/bash-runner.ts +347 -0
  52. package/src/runtime/cpython-runner.ts +504 -0
  53. package/src/runtime/facet-host.ts +170 -0
  54. package/src/runtime/installed-runtimes.ts +235 -0
  55. package/src/runtime/local-facet-host.ts +205 -0
  56. package/src/runtime/python-pip.ts +1211 -0
  57. package/src/runtime/runtime-manifest.ts +155 -0
  58. package/src/runtime/runtime-package.ts +114 -0
  59. package/src/runtime/runtime-registry.ts +511 -0
  60. package/src/runtime/vfs-snapshot.ts +15 -1
  61. package/src/runtime/vfs-supervisor.ts +67 -0
  62. package/src/runtime/virtual-socket-kernel.generated.ts +14 -0
  63. package/src/runtime/wasm-runner.ts +835 -0
  64. package/src/workspace/nimbus-workspace.ts +349 -54
@@ -0,0 +1,1211 @@
1
+ import {
2
+ maxSatisfying,
3
+ satisfies as pep440Satisfies,
4
+ valid as validPep440Version,
5
+ validRange as validPep440Range,
6
+ } from '@renovatebot/pep440';
7
+ import {
8
+ parsePipRequirementsFile,
9
+ parsePipRequirementsLine,
10
+ RequirementsSyntaxError,
11
+ } from 'pip-requirements-js';
12
+ import type {
13
+ EnvironmentMarker,
14
+ EnvironmentMarkerLeaf,
15
+ EnvironmentMarkerNode,
16
+ ProjectNameRequirement,
17
+ Requirement,
18
+ VersionSpec,
19
+ } from 'pip-requirements-js';
20
+ import { z } from 'zod/v4';
21
+ import { parentVfsPath, resolveVfsPath } from '../vfs/path.js';
22
+ import { PYODIDE_PACKAGE_ABI } from './os-contracts.js';
23
+ import {
24
+ isRuntimePythonPackageArtifactMetadata,
25
+ RuntimePythonPackageArtifactMetadataSchema,
26
+ type RuntimeArtifactMetadata,
27
+ type RuntimePythonPackageArtifactMetadata,
28
+ } from './runtime-manifest.js';
29
+
30
+ export const PYTHON_SITE_PACKAGES_ROOT = 'home/user/.nimbus-python/site-packages';
31
+ export const PYTHON_PYODIDE_PACKAGE_MANIFEST = `${PYTHON_SITE_PACKAGES_ROOT}/.nimbus-pyodide-packages.json`;
32
+
33
+ const PYPI_API = 'https://pypi.org/pypi';
34
+
35
+ interface PythonPipVfs {
36
+ exists(path: string): boolean;
37
+ readFile(path: string): Uint8Array;
38
+ }
39
+
40
+ const IGNORED_PIP_INSTALL_FLAGS = new Set([
41
+ '--upgrade',
42
+ '-U',
43
+ '--force-reinstall',
44
+ '--no-cache-dir',
45
+ '--user',
46
+ '--disable-pip-version-check',
47
+ '--prefer-binary',
48
+ '--only-binary=:all:',
49
+ ]);
50
+
51
+ const PIP_INSTALL_FLAGS_WITH_VALUE = new Set([
52
+ '-i',
53
+ '--index-url',
54
+ '--extra-index-url',
55
+ '-f',
56
+ '--find-links',
57
+ '--trusted-host',
58
+ '--timeout',
59
+ '--retries',
60
+ '--platform',
61
+ '--python-version',
62
+ '--implementation',
63
+ '--abi',
64
+ '--only-binary',
65
+ ]);
66
+
67
+ const PurePythonSourcePackageSchema = z.object({
68
+ canonicalName: z.string().min(1),
69
+ importName: z.string().min(1),
70
+ version: z.string().min(1),
71
+ sourceUrl: z.url(),
72
+ sha256: z.string().regex(/^[a-f0-9]{64}$/),
73
+ sourcePackageDir: z.string().min(1),
74
+ });
75
+
76
+ type PurePythonSourcePackage = z.infer<typeof PurePythonSourcePackageSchema>;
77
+
78
+ const PIP_SOURCE_PACKAGES = z.record(z.string(), PurePythonSourcePackageSchema).parse({
79
+ // Pinned to the release build-python.sh compiles _speedups.c from. The two
80
+ // halves of this package are built apart and only meet at import, so a skew
81
+ // between them is a C extension paired with an __init__.py it never saw.
82
+ markupsafe: {
83
+ canonicalName: 'markupsafe',
84
+ importName: 'markupsafe',
85
+ version: '3.0.3',
86
+ sourceUrl: 'https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz',
87
+ sha256: '722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698',
88
+ sourcePackageDir: 'markupsafe-3.0.3/src/markupsafe',
89
+ },
90
+ });
91
+
92
+ const VariantPackageSchema = z.object({
93
+ canonicalName: z.string().min(1),
94
+ version: z.string().min(1),
95
+ });
96
+
97
+ /**
98
+ * Packages the sci interpreter variant already contains, whole.
99
+ *
100
+ * wasm32-wasi has no dlopen, so numpy's thirteen extension modules are linked
101
+ * into python-sci.wasm and its Python half ships beside it in sci-packages.zip
102
+ * (EXTENSIONS.md). There is nothing for pip to fetch, so installing one only
103
+ * records it — and that record is what makes the next `python` choose the
104
+ * variant that has it, which is why it is written even though no bytes move.
105
+ *
106
+ * markupsafe is deliberately not here: only its _speedups module is compiled, so
107
+ * its Python half installs from source like any other package, and the variant
108
+ * supplies the C half to a session that has selected it.
109
+ */
110
+ const PIP_VARIANT_PACKAGES = z.record(z.string(), VariantPackageSchema).parse({
111
+ numpy: { canonicalName: 'numpy', version: '2.4.3' },
112
+ });
113
+
114
+ /**
115
+ * The dist-info directories that mean "this session needs the sci variant".
116
+ *
117
+ * Derived from the pins above rather than written out, so a version bump cannot
118
+ * leave the selector looking for a directory pip no longer writes.
119
+ */
120
+ const SCI_VARIANT_DIST_INFO: readonly string[] = Object.freeze([
121
+ ...Object.values(PIP_VARIANT_PACKAGES).map((p) => `${p.canonicalName}-${p.version}.dist-info`),
122
+ `${PIP_SOURCE_PACKAGES.markupsafe.canonicalName}-${PIP_SOURCE_PACKAGES.markupsafe.version}.dist-info`,
123
+ ]);
124
+
125
+ /**
126
+ * Whether this session has installed anything the sci interpreter variant
127
+ * carries compiled code for.
128
+ *
129
+ * This reads installed state; it does not predict what a program might import.
130
+ * The dist-info directory pip writes is the record, and a variant chosen from it
131
+ * is right for `python -c` reading a module name out of a variable, which a
132
+ * per-program classifier cannot be.
133
+ */
134
+ export function sessionUsesSciVariant(vfs: PythonPipVfs): boolean {
135
+ return SCI_VARIANT_DIST_INFO.some((dir) =>
136
+ vfs.exists(`${PYTHON_SITE_PACKAGES_ROOT}/${dir}`));
137
+ }
138
+
139
+ const PypiFileSchema = z.object({
140
+ filename: z.string(),
141
+ packagetype: z.string().optional(),
142
+ url: z.url(),
143
+ digests: z.object({
144
+ sha256: z.string().regex(/^[a-f0-9]{64}$/).optional(),
145
+ }).optional(),
146
+ yanked: z.union([z.boolean(), z.string()]).optional(),
147
+ });
148
+
149
+ type PypiFile = z.infer<typeof PypiFileSchema>;
150
+
151
+ const PypiJsonSchema = z.object({
152
+ info: z.object({
153
+ name: z.string(),
154
+ version: z.string(),
155
+ requires_dist: z.array(z.string()).nullable().optional(),
156
+ }),
157
+ releases: z.record(z.string(), z.array(PypiFileSchema)).optional(),
158
+ urls: z.array(PypiFileSchema).optional(),
159
+ });
160
+
161
+ type PypiJson = z.infer<typeof PypiJsonSchema>;
162
+
163
+ const PyodideLockPackageSchema = z.object({
164
+ depends: z.array(z.string()).default([]),
165
+ file_name: z.string().min(1),
166
+ imports: z.array(z.string()).default([]),
167
+ install_dir: z.string().optional(),
168
+ name: z.string().min(1),
169
+ package_type: z.string().optional(),
170
+ sha256: z.string().regex(/^[a-f0-9]{64}$/),
171
+ version: z.string().min(1),
172
+ });
173
+
174
+ const PyodideLockfileSchema = z.object({
175
+ info: z.object({
176
+ abi_version: z.string().min(1),
177
+ arch: z.string().min(1),
178
+ platform: z.string().min(1),
179
+ python: z.string().min(1),
180
+ }),
181
+ packages: z.record(z.string(), PyodideLockPackageSchema),
182
+ });
183
+
184
+ type PyodideLockPackage = z.infer<typeof PyodideLockPackageSchema>;
185
+ type PyodideLockfile = z.infer<typeof PyodideLockfileSchema>;
186
+
187
+ export interface PythonPipRuntimeContext {
188
+ pyodideLockfileText?: string | null;
189
+ runtimeArtifacts?: RuntimeArtifactMetadata[];
190
+ }
191
+
192
+ interface PackageRequirement {
193
+ name: string;
194
+ specs: string[];
195
+ extras: string[];
196
+ }
197
+
198
+ interface ResolvedPackage {
199
+ name: string;
200
+ version: string;
201
+ artifact: RemoteWheelArtifact | SourceArtifact | PyodidePackageArtifact | VariantArtifact;
202
+ }
203
+
204
+ interface RemoteWheelArtifact {
205
+ kind: 'remote-wheel';
206
+ canonicalName: string;
207
+ version: string;
208
+ wheelUrl: string;
209
+ sha256: string;
210
+ }
211
+
212
+ interface SourceArtifact extends PurePythonSourcePackage {
213
+ kind: 'source';
214
+ }
215
+
216
+ interface VariantArtifact {
217
+ kind: 'variant';
218
+ canonicalName: string;
219
+ version: string;
220
+ }
221
+
222
+ interface PyodidePackageArtifact {
223
+ kind: 'pyodide-package';
224
+ packageName: string;
225
+ version: string;
226
+ artifact: RuntimePythonPackageArtifactMetadata;
227
+ }
228
+
229
+ interface LocalWheelArtifact {
230
+ path: string;
231
+ displayName: string;
232
+ }
233
+
234
+ interface PipInstallPlan {
235
+ remoteWheels: RemoteWheelArtifact[];
236
+ sourcePackages: SourceArtifact[];
237
+ variantPackages: VariantArtifact[];
238
+ pyodidePackages: RuntimePythonPackageArtifactMetadata[];
239
+ localWheels: LocalWheelArtifact[];
240
+ displayPackages: string[];
241
+ error?: string;
242
+ exitCode: number;
243
+ }
244
+
245
+ export interface InstalledPyodidePackageManifest {
246
+ version: 1;
247
+ packages: RuntimePythonPackageArtifactMetadata[];
248
+ }
249
+
250
+ export interface PipInvocation {
251
+ mode: 'pip' | 'none';
252
+ code: string;
253
+ error?: string;
254
+ exitCode: number;
255
+ pyodidePackages?: RuntimePythonPackageArtifactMetadata[];
256
+ }
257
+
258
+ interface PypiCacheEntry {
259
+ promise: Promise<PypiJson>;
260
+ }
261
+
262
+ const pypiCache = new Map<string, PypiCacheEntry>();
263
+
264
+ export const InstalledPyodidePackageManifestSchema: z.ZodType<InstalledPyodidePackageManifest> = z.object({
265
+ version: z.literal(1),
266
+ packages: z.array(RuntimePythonPackageArtifactMetadataSchema),
267
+ });
268
+
269
+ export function parseInstalledPyodidePackageManifest(text: string): InstalledPyodidePackageManifest {
270
+ return InstalledPyodidePackageManifestSchema.parse(JSON.parse(text));
271
+ }
272
+
273
+ export async function buildPipInvocation(
274
+ argv: string[],
275
+ binName: string,
276
+ cwd: string,
277
+ vfs: PythonPipVfs,
278
+ runtimeContext: PythonPipRuntimeContext = {},
279
+ ): Promise<PipInvocation> {
280
+ const wantsVersion = argv.includes('--version') || argv.includes('-V');
281
+ const wantsHelp = argv.length === 0 || argv.includes('--help') || argv.includes('-h');
282
+ if (wantsVersion) {
283
+ return {
284
+ mode: 'pip',
285
+ code: 'print("pip 24.3.1 (Nimbus package bridge for CPython 3.13, wasm32-wasi)")',
286
+ exitCode: 0,
287
+ };
288
+ }
289
+ if (wantsHelp) {
290
+ return {
291
+ mode: 'pip',
292
+ code: [
293
+ `print(${JSON.stringify(`Usage: ${binName} install <package> [package...]`)})`,
294
+ 'print("Nimbus pip installs PyPI pure wheels, curated pure source artifacts, and local pure wheels.")',
295
+ 'print("Compiled Pyodide wheels require startup-loaded Nimbus package artifacts.")',
296
+ 'print("Native Linux wheels and request-time extension modules are rejected before install.")',
297
+ ].join('\n'),
298
+ exitCode: 0,
299
+ };
300
+ }
301
+ const command = argv[0];
302
+ if (command !== 'install') {
303
+ return {
304
+ mode: 'none',
305
+ code: '',
306
+ error: `pip subcommand '${command || '(none)'}' is not supported yet; supported: install, --version, --help`,
307
+ exitCode: 2,
308
+ };
309
+ }
310
+
311
+ const plan = await buildPipInstallPlan(argv.slice(1), cwd, vfs, runtimeContext);
312
+ if (plan.error) {
313
+ return { mode: 'none', code: '', error: plan.error, exitCode: plan.exitCode };
314
+ }
315
+ return {
316
+ mode: 'pip',
317
+ code: buildPipInstallCode(plan),
318
+ exitCode: 0,
319
+ pyodidePackages: plan.pyodidePackages,
320
+ };
321
+ }
322
+
323
+ async function buildPipInstallPlan(
324
+ argv: string[],
325
+ cwd: string,
326
+ vfs: PythonPipVfs,
327
+ runtimeContext: PythonPipRuntimeContext,
328
+ ): Promise<PipInstallPlan> {
329
+ const roots: PackageRequirement[] = [];
330
+ const constraints = new Map<string, string[]>();
331
+ const localWheels: LocalWheelArtifact[] = [];
332
+ const displayPackages: string[] = [];
333
+ let includeDependencies = true;
334
+
335
+ const addRequirement = (requirement: Requirement, baseDir: string, source: string): string | null => {
336
+ if (requirement.type === 'RequirementsFile') {
337
+ return addRequirementsFile(requirement.path, baseDir, vfs, roots, constraints, displayPackages, 0);
338
+ }
339
+ if (requirement.type === 'ConstraintsFile') {
340
+ return addConstraintsFile(requirement.path, baseDir, vfs, constraints, 0);
341
+ }
342
+ if (requirement.type === 'ProjectURL') {
343
+ const local = localWheelArtifact(requirement.url, baseDir, vfs);
344
+ if ('error' in local) return local.error;
345
+ if (local.artifact) {
346
+ localWheels.push({
347
+ ...local.artifact,
348
+ displayName: canonicalPackageName(requirement.name),
349
+ });
350
+ displayPackages.push(canonicalPackageName(requirement.name));
351
+ return null;
352
+ }
353
+ return `${source}: URL requirements need a local pure wheel path`;
354
+ }
355
+ if (!markerApplies(requirement.environmentMarkerTree, requirement.extras || [])) return null;
356
+ roots.push(projectRequirementToPackageRequirement(requirement));
357
+ displayPackages.push(formatDisplayRequirement(requirement));
358
+ return null;
359
+ };
360
+
361
+ for (let i = 0; i < argv.length; i++) {
362
+ const arg = argv[i];
363
+ if (arg === '-r' || arg === '--requirement') {
364
+ const reqPath = argv[i + 1];
365
+ if (!reqPath) return failedPlan(`${arg}: missing requirements file`, 2);
366
+ const err = addRequirementsFile(reqPath, cwd, vfs, roots, constraints, displayPackages, 0);
367
+ if (err) return failedPlan(err, 1);
368
+ i++;
369
+ continue;
370
+ }
371
+ if (arg.startsWith('--requirement=')) {
372
+ const err = addRequirementsFile(arg.slice('--requirement='.length), cwd, vfs, roots, constraints, displayPackages, 0);
373
+ if (err) return failedPlan(err, 1);
374
+ continue;
375
+ }
376
+ if (arg === '-c' || arg === '--constraint') {
377
+ const constraintPath = argv[i + 1];
378
+ if (!constraintPath) return failedPlan(`${arg}: missing constraints file`, 2);
379
+ const err = addConstraintsFile(constraintPath, cwd, vfs, constraints, 0);
380
+ if (err) return failedPlan(err, 1);
381
+ i++;
382
+ continue;
383
+ }
384
+ if (arg.startsWith('--constraint=')) {
385
+ const err = addConstraintsFile(arg.slice('--constraint='.length), cwd, vfs, constraints, 0);
386
+ if (err) return failedPlan(err, 1);
387
+ continue;
388
+ }
389
+ if (isIgnoredPipInstallFlag(arg)) continue;
390
+ if (pipFlagTakesValue(arg)) {
391
+ if (!argv[i + 1]) return failedPlan(`${arg}: missing value`, 2);
392
+ i++;
393
+ continue;
394
+ }
395
+ if (arg === '--no-deps') {
396
+ includeDependencies = false;
397
+ continue;
398
+ }
399
+ if (arg.startsWith('-')) {
400
+ return failedPlan(`pip install option '${arg}' is not supported in Nimbus yet`, 2);
401
+ }
402
+ const local = localWheelArtifact(arg, cwd, vfs);
403
+ if ('error' in local) return failedPlan(local.error, 1);
404
+ if (local.artifact) {
405
+ localWheels.push(local.artifact);
406
+ displayPackages.push(local.artifact.displayName);
407
+ continue;
408
+ }
409
+ let parsed: Requirement | null;
410
+ try {
411
+ parsed = parsePipRequirementsLine(arg);
412
+ } catch (e) {
413
+ return failedPlan(e instanceof RequirementsSyntaxError ? e.message : `invalid requirement '${arg}'`, 1);
414
+ }
415
+ if (!parsed) continue;
416
+ const err = addRequirement(parsed, cwd, arg);
417
+ if (err) return failedPlan(err, 1);
418
+ }
419
+
420
+ if (roots.length === 0 && localWheels.length === 0) {
421
+ return failedPlan('pip install: missing package name', 2);
422
+ }
423
+
424
+ const resolved = await resolveRequirements(roots, constraints, includeDependencies, runtimeContext);
425
+ if ('error' in resolved) return failedPlan(resolved.error, 1);
426
+
427
+ const remoteWheels: RemoteWheelArtifact[] = [];
428
+ const sourcePackages: SourceArtifact[] = [];
429
+ const pyodidePackages: RuntimePythonPackageArtifactMetadata[] = [];
430
+ const variantPackages: VariantArtifact[] = [];
431
+ for (const pkg of resolved.packages.values()) {
432
+ if (pkg.artifact.kind === 'remote-wheel') {
433
+ remoteWheels.push(pkg.artifact);
434
+ } else if (pkg.artifact.kind === 'source') {
435
+ sourcePackages.push(pkg.artifact);
436
+ } else if (pkg.artifact.kind === 'variant') {
437
+ variantPackages.push(pkg.artifact);
438
+ } else {
439
+ pyodidePackages.push(pkg.artifact.artifact);
440
+ }
441
+ }
442
+
443
+ const installLabels = [
444
+ ...remoteWheels.map((wheel) => wheel.canonicalName),
445
+ ...sourcePackages.map((source) => source.canonicalName),
446
+ ...variantPackages.map((variant) => variant.canonicalName),
447
+ ...pyodidePackages.map((artifact) => artifact.packageName),
448
+ ...localWheels.map((wheel) => wheel.displayName),
449
+ ];
450
+
451
+ return {
452
+ remoteWheels,
453
+ sourcePackages,
454
+ variantPackages,
455
+ pyodidePackages,
456
+ localWheels,
457
+ displayPackages: installLabels.length > 0 ? installLabels : displayPackages,
458
+ exitCode: 0,
459
+ };
460
+ }
461
+
462
+ function failedPlan(error: string, exitCode: number): PipInstallPlan {
463
+ return {
464
+ remoteWheels: [], sourcePackages: [], variantPackages: [], pyodidePackages: [],
465
+ localWheels: [], displayPackages: [], error, exitCode,
466
+ };
467
+ }
468
+
469
+ function addRequirementsFile(
470
+ reqPath: string,
471
+ baseDir: string,
472
+ vfs: PythonPipVfs,
473
+ requirements: PackageRequirement[],
474
+ constraints: Map<string, string[]>,
475
+ displayPackages: string[],
476
+ depth: number,
477
+ ): string | null {
478
+ if (depth > 8) return 'requirements nesting exceeded 8 files';
479
+ const abs = resolveVfsPath(reqPath, baseDir);
480
+ const probe = probeVfsPath(vfs, abs);
481
+ if ('error' in probe) return `cannot read requirements file ${reqPath}: ${probe.error}`;
482
+ if (!probe.exists) return `requirements file not found: ${reqPath}`;
483
+ const text = readVfsText(vfs, abs);
484
+ if ('error' in text) return `cannot read requirements file ${reqPath}: ${text.error}`;
485
+
486
+ let parsed: Requirement[];
487
+ try {
488
+ parsed = parsePipRequirementsFile(text.text);
489
+ } catch (e) {
490
+ return e instanceof RequirementsSyntaxError ? e.message : `invalid requirements file: ${reqPath}`;
491
+ }
492
+
493
+ const nextBaseDir = parentVfsPath(abs);
494
+ for (const requirement of parsed) {
495
+ if (requirement.type === 'RequirementsFile') {
496
+ const err = addRequirementsFile(requirement.path, nextBaseDir, vfs, requirements, constraints, displayPackages, depth + 1);
497
+ if (err) return err;
498
+ continue;
499
+ }
500
+ if (requirement.type === 'ConstraintsFile') {
501
+ const err = addConstraintsFile(requirement.path, nextBaseDir, vfs, constraints, depth + 1);
502
+ if (err) return err;
503
+ continue;
504
+ }
505
+ if (requirement.type === 'ProjectURL') {
506
+ return `${reqPath}: URL requirements need a Nimbus wheel artifact or local pure wheel path`;
507
+ }
508
+ if (!markerApplies(requirement.environmentMarkerTree, requirement.extras || [])) continue;
509
+ requirements.push(projectRequirementToPackageRequirement(requirement));
510
+ displayPackages.push(formatDisplayRequirement(requirement));
511
+ }
512
+ return null;
513
+ }
514
+
515
+ function addConstraintsFile(
516
+ reqPath: string,
517
+ baseDir: string,
518
+ vfs: PythonPipVfs,
519
+ constraints: Map<string, string[]>,
520
+ depth: number,
521
+ ): string | null {
522
+ if (depth > 8) return 'constraints nesting exceeded 8 files';
523
+ const abs = resolveVfsPath(reqPath, baseDir);
524
+ const probe = probeVfsPath(vfs, abs);
525
+ if ('error' in probe) return `cannot read constraints file ${reqPath}: ${probe.error}`;
526
+ if (!probe.exists) return `constraints file not found: ${reqPath}`;
527
+ const text = readVfsText(vfs, abs);
528
+ if ('error' in text) return `cannot read constraints file ${reqPath}: ${text.error}`;
529
+
530
+ let parsed: Requirement[];
531
+ try {
532
+ parsed = parsePipRequirementsFile(text.text);
533
+ } catch (e) {
534
+ return e instanceof RequirementsSyntaxError ? e.message : `invalid constraints file: ${reqPath}`;
535
+ }
536
+
537
+ const nextBaseDir = parentVfsPath(abs);
538
+ for (const requirement of parsed) {
539
+ if (requirement.type === 'RequirementsFile' || requirement.type === 'ConstraintsFile') {
540
+ const err = addConstraintsFile(requirement.path, nextBaseDir, vfs, constraints, depth + 1);
541
+ if (err) return err;
542
+ continue;
543
+ }
544
+ if (requirement.type === 'ProjectURL') return `${reqPath}: URL constraints are not supported`;
545
+ if (!markerApplies(requirement.environmentMarkerTree, requirement.extras || [])) continue;
546
+ const name = canonicalPackageName(requirement.name);
547
+ constraints.set(name, [...(constraints.get(name) || []), ...versionSpecifiers(requirement.versionSpec || [])]);
548
+ }
549
+ return null;
550
+ }
551
+
552
+ function readVfsText(vfs: PythonPipVfs, path: string): { text: string } | { error: string } {
553
+ try {
554
+ return { text: new TextDecoder('utf-8').decode(vfs.readFile(path)) };
555
+ } catch (e) {
556
+ return { error: errorMessage(e) };
557
+ }
558
+ }
559
+
560
+ function probeVfsPath(vfs: PythonPipVfs, path: string): { exists: boolean } | { error: string } {
561
+ try {
562
+ return { exists: vfs.exists(path) };
563
+ } catch (e) {
564
+ return { error: errorMessage(e) };
565
+ }
566
+ }
567
+
568
+ function errorMessage(error: unknown): string {
569
+ return error instanceof Error ? error.message : String(error);
570
+ }
571
+
572
+ async function resolveRequirements(
573
+ roots: PackageRequirement[],
574
+ constraints: Map<string, string[]>,
575
+ includeDependencies: boolean,
576
+ runtimeContext: PythonPipRuntimeContext,
577
+ ): Promise<{ packages: Map<string, ResolvedPackage> } | { error: string }> {
578
+ const requirements = new Map<string, PackageRequirement>();
579
+ const queue: string[] = [];
580
+
581
+ const add = (req: PackageRequirement): void => {
582
+ const name = canonicalPackageName(req.name);
583
+ const existing = requirements.get(name);
584
+ if (existing) {
585
+ existing.specs = uniqueStrings([...existing.specs, ...req.specs]);
586
+ existing.extras = uniqueStrings([...existing.extras, ...req.extras]);
587
+ } else {
588
+ requirements.set(name, { name, specs: uniqueStrings(req.specs), extras: uniqueStrings(req.extras) });
589
+ }
590
+ if (!queue.includes(name)) queue.push(name);
591
+ };
592
+
593
+ for (const root of roots) add(root);
594
+
595
+ const resolved = new Map<string, ResolvedPackage>();
596
+ while (queue.length > 0) {
597
+ const name = queue.shift()!;
598
+ const req = requirements.get(name)!;
599
+ const resolvedPkg = await resolveOneRequirement({
600
+ ...req,
601
+ specs: uniqueStrings([...req.specs, ...(constraints.get(name) || [])]),
602
+ }, runtimeContext);
603
+ if ('error' in resolvedPkg) return resolvedPkg;
604
+ const previous = resolved.get(name);
605
+ resolved.set(name, resolvedPkg.package);
606
+ if (previous && previous.version === resolvedPkg.package.version) continue;
607
+
608
+ if (!includeDependencies) continue;
609
+
610
+ const dependencyLines = resolvedPkg.package.artifact.kind === 'pyodide-package'
611
+ ? resolvedPkg.package.artifact.artifact.dependencies
612
+ : null;
613
+ const metadata = dependencyLines
614
+ ? null
615
+ : await fetchPypiJson(name, resolvedPkg.package.version);
616
+ if (metadata && 'error' in metadata) return metadata;
617
+ for (const depLine of dependencyLines ?? metadata?.data.info.requires_dist ?? []) {
618
+ let dep: Requirement | null;
619
+ try {
620
+ dep = parsePipRequirementsLine(depLine);
621
+ } catch {
622
+ return { error: `${name} dependency '${depLine}' is not a supported PEP 508 requirement` };
623
+ }
624
+ if (!dep) continue;
625
+ if (dep.type !== 'ProjectName') {
626
+ return { error: `${name} dependency '${depLine}' needs a Nimbus package artifact` };
627
+ }
628
+ if (!markerApplies(dep.environmentMarkerTree, req.extras)) continue;
629
+ add(projectRequirementToPackageRequirement(dep));
630
+ }
631
+ }
632
+ return { packages: resolved };
633
+ }
634
+
635
+ async function resolveOneRequirement(
636
+ req: PackageRequirement,
637
+ runtimeContext: PythonPipRuntimeContext,
638
+ ): Promise<{ package: ResolvedPackage } | { error: string }> {
639
+ const pyodidePackage = findPyodideCompiledPackage(req, runtimeContext);
640
+ if (pyodidePackage && 'error' in pyodidePackage) return pyodidePackage;
641
+ if (pyodidePackage) {
642
+ const runtimeArtifact = findRuntimePythonPackageArtifact(pyodidePackage, runtimeContext.runtimeArtifacts || []);
643
+ if (runtimeArtifact) {
644
+ return {
645
+ package: {
646
+ name: req.name,
647
+ version: runtimeArtifact.version,
648
+ artifact: {
649
+ kind: 'pyodide-package',
650
+ packageName: runtimeArtifact.packageName,
651
+ version: runtimeArtifact.version,
652
+ artifact: runtimeArtifact,
653
+ },
654
+ },
655
+ };
656
+ }
657
+ const sourcePolicy = PIP_SOURCE_PACKAGES[req.name];
658
+ if (!sourcePolicy) return { error: pyodideCompiledPackageDiagnostic(pyodidePackage) };
659
+ return resolveSourcePolicy(req, sourcePolicy);
660
+ }
661
+
662
+ const variantPolicy = PIP_VARIANT_PACKAGES[canonicalPackageName(req.name)];
663
+ if (variantPolicy) {
664
+ const range = specifierRange(req.specs);
665
+ if (range && !pep440Satisfies(variantPolicy.version, range)) {
666
+ return {
667
+ error: `only ${variantPolicy.canonicalName} ${variantPolicy.version} is available here `
668
+ + `(it is compiled into the interpreter, not fetched), and it does not satisfy ${range}`,
669
+ };
670
+ }
671
+ return {
672
+ package: {
673
+ name: variantPolicy.canonicalName,
674
+ version: variantPolicy.version,
675
+ artifact: { kind: 'variant', ...variantPolicy },
676
+ },
677
+ };
678
+ }
679
+
680
+ const sourcePolicy = PIP_SOURCE_PACKAGES[req.name];
681
+ if (sourcePolicy) return resolveSourcePolicy(req, sourcePolicy);
682
+
683
+ const metadata = await fetchPypiJson(req.name);
684
+ if ('error' in metadata) return metadata;
685
+ const releases = metadata.data.releases || {};
686
+ const versions = Object.keys(releases).filter((version) =>
687
+ validPep440Version(version) && releases[version]?.some((file) => !file.yanked));
688
+ const range = specifierRange(req.specs);
689
+ const version = findBestVersion(versions, range || '>=0');
690
+ if (!version) {
691
+ return { error: `no PyPI release of ${req.name} satisfies ${range || '>=0'}` };
692
+ }
693
+
694
+ const versionMetadata = await fetchPypiJson(req.name, version);
695
+ if ('error' in versionMetadata) return versionMetadata;
696
+ const files = versionMetadata.data.urls || releases[version] || [];
697
+ const wheel = selectPureWheel(req.name, version, files);
698
+ if ('error' in wheel) return wheel;
699
+ return {
700
+ package: {
701
+ name: req.name,
702
+ version,
703
+ artifact: wheel.artifact,
704
+ },
705
+ };
706
+ }
707
+
708
+ function resolveSourcePolicy(
709
+ req: PackageRequirement,
710
+ sourcePolicy: PurePythonSourcePackage,
711
+ ): { package: ResolvedPackage } | { error: string } {
712
+ const range = specifierRange(req.specs);
713
+ if (range && !pep440Satisfies(sourcePolicy.version, range)) {
714
+ return { error: `${req.name}${range} needs a Nimbus source artifact; available artifact is ${req.name}==${sourcePolicy.version}` };
715
+ }
716
+ return {
717
+ package: {
718
+ name: req.name,
719
+ version: sourcePolicy.version,
720
+ artifact: { ...sourcePolicy, kind: 'source' },
721
+ },
722
+ };
723
+ }
724
+
725
+ function selectPureWheel(name: string, version: string, files: PypiFile[]): { artifact: RemoteWheelArtifact } | { error: string } {
726
+ const wheels = files.filter((file) => file.packagetype === 'bdist_wheel' || file.filename.endsWith('.whl'));
727
+ const pure = wheels.find((file) => isPurePythonWheel(file.filename) && !file.yanked && file.digests?.sha256);
728
+ if (pure?.digests?.sha256) {
729
+ return {
730
+ artifact: {
731
+ kind: 'remote-wheel',
732
+ canonicalName: canonicalPackageName(name),
733
+ version,
734
+ wheelUrl: pure.url,
735
+ sha256: pure.digests.sha256,
736
+ },
737
+ };
738
+ }
739
+ if (wheels.some((file) => isPyodideExtensionWheel(file.filename))) {
740
+ return { error: pyodideExtensionWheelDiagnostic(name, version, wheels.map((file) => file.filename)) };
741
+ }
742
+ if (wheels.some((file) => isNativePlatformWheel(file.filename))) {
743
+ return { error: `${name}==${version} ships native platform wheels; native Linux wheels cannot run in Nimbus` };
744
+ }
745
+ if (files.some((file) => file.packagetype === 'sdist')) {
746
+ return { error: `${name}==${version} has no compatible pure wheel; source builds need a Nimbus source policy or prebuilt Nimbus ABI artifact` };
747
+ }
748
+ return { error: `${name}==${version} has no compatible Nimbus package artifact` };
749
+ }
750
+
751
+ function findBestVersion(versions: string[], range: string): string | null {
752
+ try {
753
+ return maxSatisfying(versions, range);
754
+ } catch {
755
+ return null;
756
+ }
757
+ }
758
+
759
+ async function fetchPypiJson(name: string, version?: string): Promise<{ data: PypiJson } | { error: string }> {
760
+ const canonical = canonicalPackageName(name);
761
+ const key = version ? `${canonical}@${version}` : canonical;
762
+ let entry = pypiCache.get(key);
763
+ if (!entry) {
764
+ const url = version
765
+ ? `${PYPI_API}/${encodeURIComponent(canonical)}/${encodeURIComponent(version)}/json`
766
+ : `${PYPI_API}/${encodeURIComponent(canonical)}/json`;
767
+ entry = {
768
+ promise: fetch(url).then(async (resp) => {
769
+ if (!resp.ok) throw new Error(`HTTP ${resp.status}`);
770
+ return PypiJsonSchema.parse(await resp.json());
771
+ }),
772
+ };
773
+ pypiCache.set(key, entry);
774
+ }
775
+ try {
776
+ return { data: await entry.promise };
777
+ } catch (e) {
778
+ pypiCache.delete(key);
779
+ return { error: `PyPI metadata fetch failed for ${canonical}: ${e instanceof Error ? e.message : String(e)}` };
780
+ }
781
+ }
782
+
783
+ function projectRequirementToPackageRequirement(requirement: ProjectNameRequirement): PackageRequirement {
784
+ return {
785
+ name: canonicalPackageName(requirement.name),
786
+ specs: versionSpecifiers(requirement.versionSpec || []),
787
+ extras: (requirement.extras || []).map(canonicalPackageName),
788
+ };
789
+ }
790
+
791
+ function versionSpecifiers(specs: VersionSpec[]): string[] {
792
+ return specs.map((spec) => `${spec.operator}${spec.version}`);
793
+ }
794
+
795
+ function specifierRange(specs: string[]): string {
796
+ return specs.join(',');
797
+ }
798
+
799
+ function formatDisplayRequirement(requirement: ProjectNameRequirement): string {
800
+ const extras = requirement.extras?.length ? `[${requirement.extras.join(',')}]` : '';
801
+ return `${canonicalPackageName(requirement.name)}${extras}${specifierRange(versionSpecifiers(requirement.versionSpec || []))}`;
802
+ }
803
+
804
+ function markerApplies(marker: EnvironmentMarker | undefined, extras: string[]): boolean {
805
+ if (!marker) return true;
806
+ const candidates = extras.length > 0 ? extras : [''];
807
+ return candidates.some((extra) => evaluateMarker(marker, extra));
808
+ }
809
+
810
+ function evaluateMarker(marker: EnvironmentMarker, extra: string): boolean {
811
+ if (isMarkerNode(marker)) {
812
+ const left = evaluateMarker(marker.left, extra);
813
+ const right = evaluateMarker(marker.right, extra);
814
+ return marker.operator === 'and' ? left && right : left || right;
815
+ }
816
+ return evaluateMarkerLeaf(marker, extra);
817
+ }
818
+
819
+ function isMarkerNode(marker: EnvironmentMarker): marker is EnvironmentMarkerNode {
820
+ return marker.operator === 'and' || marker.operator === 'or';
821
+ }
822
+
823
+ function evaluateMarkerLeaf(marker: EnvironmentMarkerLeaf, extra: string): boolean {
824
+ const left = markerValue(marker.left, extra);
825
+ const right = markerValue(marker.right, extra);
826
+ if (marker.operator === 'in') return right.includes(left);
827
+ if (marker.operator === 'not in') return !right.includes(left);
828
+ if (marker.operator === '==' || marker.operator === '!=') {
829
+ if (!isVersionMarkerValue(marker.left) && !isVersionMarkerValue(marker.right)) {
830
+ return marker.operator === '==' ? left === right : left !== right;
831
+ }
832
+ }
833
+ const expression = `${marker.operator}${right}`;
834
+ if (validPep440Version(left) && validPep440Range(expression)) {
835
+ return pep440Satisfies(left, expression);
836
+ }
837
+ if (marker.operator === '==') return left === right;
838
+ if (marker.operator === '!=') return left !== right;
839
+ return false;
840
+ }
841
+
842
+ function isVersionMarkerValue(value: EnvironmentMarkerLeaf['left'] | EnvironmentMarkerLeaf['right']): boolean {
843
+ return value === 'python_version'
844
+ || value === 'python_full_version'
845
+ || value === 'implementation_version';
846
+ }
847
+
848
+ function markerValue(value: string, extra: string): string {
849
+ if (value === 'python_version') return '3.13';
850
+ if (value === 'python_full_version') return '3.13.2';
851
+ if (value === 'os_name') return 'posix';
852
+ if (value === 'sys_platform') return 'emscripten';
853
+ if (value === 'platform_release') return 'nimbus';
854
+ if (value === 'platform_system') return 'Emscripten';
855
+ if (value === 'platform_version') return 'nimbus';
856
+ if (value === 'platform_machine') return 'wasm32';
857
+ if (value === 'platform_python_implementation') return 'CPython';
858
+ if (value === 'implementation_name') return 'cpython';
859
+ if (value === 'implementation_version') return '3.13.2';
860
+ if (value === 'extra') return extra;
861
+ return unquotePythonString(value);
862
+ }
863
+
864
+ function unquotePythonString(value: string): string {
865
+ if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
866
+ return value.slice(1, -1);
867
+ }
868
+ return value;
869
+ }
870
+
871
+ function localWheelArtifact(
872
+ rawSpec: string,
873
+ baseDir: string,
874
+ vfs: PythonPipVfs,
875
+ ): { artifact: LocalWheelArtifact | null } | { error: string } {
876
+ const pathSpec = localWheelPathSpec(rawSpec);
877
+ if ('error' in pathSpec) return pathSpec;
878
+ const spec = pathSpec.path;
879
+ const looksLikePath = spec.startsWith('/')
880
+ || spec.startsWith('./')
881
+ || spec.startsWith('../')
882
+ || (!spec.includes(' ') && !spec.includes('\t') && spec.endsWith('.whl'));
883
+ if (!looksLikePath) return { artifact: null };
884
+
885
+ const abs = resolveVfsPath(spec, baseDir);
886
+ const probe = probeVfsPath(vfs, abs);
887
+ if ('error' in probe) return { error: `cannot access local wheel ${rawSpec}: ${probe.error}` };
888
+ if (!probe.exists) return { error: `local wheel not found: ${rawSpec}` };
889
+ const fileName = abs.slice(abs.lastIndexOf('/') + 1);
890
+ if (!fileName.endsWith('.whl')) {
891
+ return { error: `local installs currently require a .whl file: ${rawSpec}` };
892
+ }
893
+ const wheelError = validateWheelFileName(fileName);
894
+ if (wheelError) return { error: wheelError };
895
+ return {
896
+ artifact: {
897
+ path: `/${abs}`,
898
+ displayName: fileName.slice(0, -'.whl'.length),
899
+ },
900
+ };
901
+ }
902
+
903
+ function localWheelPathSpec(rawSpec: string): { path: string } | { error: string } {
904
+ if (!rawSpec.startsWith('file://')) return { path: rawSpec };
905
+ try {
906
+ const url = new URL(rawSpec);
907
+ if (url.protocol !== 'file:') return { path: rawSpec };
908
+ return { path: decodeURIComponent(url.pathname) };
909
+ } catch {
910
+ return { error: `invalid file URL: ${rawSpec}` };
911
+ }
912
+ }
913
+
914
+ function validateWheelFileName(fileName: string): string | null {
915
+ if (isPurePythonWheel(fileName)) return null;
916
+ if (isNativePlatformWheel(fileName)) {
917
+ return `native Linux wheel '${fileName}' cannot run in Nimbus; install a pure wheel or a Nimbus ABI artifact`;
918
+ }
919
+ if (isPyodideExtensionWheel(fileName)) {
920
+ return `Pyodide/Emscripten extension wheel '${fileName}' needs a startup-loaded Nimbus Python package artifact; request-time extension modules cannot run in Workers`;
921
+ }
922
+ return `wheel '${fileName}' targets an unsupported ABI; Nimbus pip supports pure Python wheels and Nimbus ABI artifacts`;
923
+ }
924
+
925
+ function findPyodideCompiledPackage(
926
+ req: PackageRequirement,
927
+ runtimeContext: PythonPipRuntimeContext,
928
+ ): (PyodideLockPackage & { canonicalName: string; abi: typeof PYODIDE_PACKAGE_ABI }) | { error: string } | null {
929
+ const lockfile = parsePyodideLockfile(runtimeContext.pyodideLockfileText);
930
+ if (lockfile && 'error' in lockfile) return lockfile;
931
+ if (!lockfile) return null;
932
+ const canonical = canonicalPackageName(req.name);
933
+ const entry = Object.values(lockfile.data.packages).find((pkg) => canonicalPackageName(pkg.name) === canonical);
934
+ if (!entry || isPurePythonWheel(entry.file_name)) return null;
935
+ const range = specifierRange(req.specs);
936
+ if (range && !pep440Satisfies(entry.version, range)) return null;
937
+ if (!isPyodideExtensionWheel(entry.file_name)) return null;
938
+ return {
939
+ ...entry,
940
+ canonicalName: canonical,
941
+ abi: PYODIDE_PACKAGE_ABI,
942
+ };
943
+ }
944
+
945
+ function parsePyodideLockfile(lockfileText: string | null | undefined): { data: PyodideLockfile } | { error: string } | null {
946
+ if (!lockfileText) return null;
947
+ try {
948
+ return { data: PyodideLockfileSchema.parse(JSON.parse(lockfileText)) };
949
+ } catch (e) {
950
+ return {
951
+ error: `installed Pyodide lockfile is invalid: ${e instanceof Error ? e.message : String(e)}`,
952
+ };
953
+ }
954
+ }
955
+
956
+ function findRuntimePythonPackageArtifact(
957
+ pkg: PyodideLockPackage & { canonicalName: string },
958
+ artifacts: RuntimeArtifactMetadata[],
959
+ ): RuntimePythonPackageArtifactMetadata | null {
960
+ return artifacts.find((artifact): artifact is RuntimePythonPackageArtifactMetadata => {
961
+ if (!isRuntimePythonPackageArtifactMetadata(artifact)) return false;
962
+ return canonicalPackageName(artifact.packageName) === pkg.canonicalName
963
+ && artifact.version === pkg.version
964
+ && artifact.wheelFileName === pkg.file_name
965
+ && artifact.wheelSha256 === pkg.sha256
966
+ && artifact.loadMode === 'startup-module';
967
+ }) ?? null;
968
+ }
969
+
970
+ function pyodideCompiledPackageDiagnostic(
971
+ pkg: PyodideLockPackage & { canonicalName: string; abi: typeof PYODIDE_PACKAGE_ABI },
972
+ ): string {
973
+ return `${pkg.canonicalName}==${pkg.version} is available as a Pyodide/Emscripten wheel (${pkg.file_name}), ` +
974
+ `but Nimbus needs a startup-loaded Python package artifact for compiled modules. ` +
975
+ `Request-time WebAssembly extension loading is not supported in Workers.`;
976
+ }
977
+
978
+ function pyodideExtensionWheelDiagnostic(name: string, version: string, filenames: string[]): string {
979
+ const wheel = filenames.find(isPyodideExtensionWheel) || 'Pyodide/Emscripten wheel';
980
+ return `${canonicalPackageName(name)}==${version} ships a Pyodide/Emscripten extension wheel (${wheel}); ` +
981
+ `Nimbus needs a startup-loaded Python package artifact for compiled modules. ` +
982
+ `Request-time WebAssembly extension loading is not supported in Workers.`;
983
+ }
984
+
985
+ function isPurePythonWheel(fileName: string): boolean {
986
+ const tags = wheelTags(fileName);
987
+ if (!tags) return false;
988
+ return tags.abiTag === 'none'
989
+ && tags.platformTag === 'any'
990
+ && tags.pythonTag.split('.').some((tag) => tag === 'py3' || tag.startsWith('py3') || tag === 'cp313');
991
+ }
992
+
993
+ function isNativePlatformWheel(fileName: string): boolean {
994
+ const tags = wheelTags(fileName);
995
+ if (!tags) return false;
996
+ return /manylinux|musllinux|linux|macosx|win/.test(tags.platformTag);
997
+ }
998
+
999
+ function isPyodideExtensionWheel(fileName: string): boolean {
1000
+ const tags = wheelTags(fileName);
1001
+ if (!tags) return false;
1002
+ return /emscripten|wasm32|pyodide/.test(`${tags.abiTag}-${tags.platformTag}`);
1003
+ }
1004
+
1005
+ function wheelTags(fileName: string): { pythonTag: string; abiTag: string; platformTag: string } | null {
1006
+ const stem = fileName.endsWith('.whl') ? fileName.slice(0, -4) : fileName;
1007
+ const parts = stem.split('-');
1008
+ if (parts.length < 5) return null;
1009
+ return {
1010
+ pythonTag: parts[parts.length - 3].toLowerCase(),
1011
+ abiTag: parts[parts.length - 2].toLowerCase(),
1012
+ platformTag: parts[parts.length - 1].toLowerCase(),
1013
+ };
1014
+ }
1015
+
1016
+ function buildPipInstallCode(plan: PipInstallPlan): string {
1017
+ return [
1018
+ 'import hashlib',
1019
+ 'import io',
1020
+ 'import json',
1021
+ 'import os',
1022
+ 'import shutil',
1023
+ 'import sys',
1024
+ 'import tarfile',
1025
+ 'import zipfile',
1026
+ 'import urllib.error',
1027
+ 'import urllib.request',
1028
+ `remote_wheels = ${JSON.stringify(plan.remoteWheels)}`,
1029
+ `local_wheels = ${JSON.stringify(plan.localWheels)}`,
1030
+ `source_packages = ${JSON.stringify(plan.sourcePackages)}`,
1031
+ `variant_packages = ${JSON.stringify(plan.variantPackages)}`,
1032
+ `pyodide_packages = ${JSON.stringify(plan.pyodidePackages)}`,
1033
+ `display_packages = ${JSON.stringify(plan.displayPackages)}`,
1034
+ `target_site_packages = ${JSON.stringify('/' + PYTHON_SITE_PACKAGES_ROOT)}`,
1035
+ `pyodide_manifest_path = ${JSON.stringify('/' + PYTHON_PYODIDE_PACKAGE_MANIFEST)}`,
1036
+ 'unsupported_extension_suffixes = (".so", ".pyd", ".dll", ".dylib")',
1037
+ 'os.makedirs(target_site_packages, exist_ok=True)',
1038
+ 'if target_site_packages not in sys.path:',
1039
+ ' sys.path.insert(0, target_site_packages)',
1040
+ 'def _nimbus_dist_info_dir(name, version):',
1041
+ ' return os.path.join(target_site_packages, name.replace("-", "_") + "-" + version + ".dist-info")',
1042
+ 'def _nimbus_load_pyodide_manifest():',
1043
+ ' try:',
1044
+ ' with open(pyodide_manifest_path, "r", encoding="utf-8") as f:',
1045
+ ' data = json.load(f)',
1046
+ ' if data.get("version") == 1 and isinstance(data.get("packages"), list):',
1047
+ ' return data',
1048
+ ' except Exception:',
1049
+ ' pass',
1050
+ ' return {"version": 1, "packages": []}',
1051
+ 'def _nimbus_write_pyodide_manifest(data):',
1052
+ ' os.makedirs(os.path.dirname(pyodide_manifest_path), exist_ok=True)',
1053
+ ' tmp = pyodide_manifest_path + ".tmp"',
1054
+ ' with open(tmp, "w", encoding="utf-8") as f:',
1055
+ ' json.dump(data, f, sort_keys=True)',
1056
+ ' os.replace(tmp, pyodide_manifest_path)',
1057
+ 'def _nimbus_record_pyodide_packages(policies):',
1058
+ ' if not policies:',
1059
+ ' return',
1060
+ ' manifest = _nimbus_load_pyodide_manifest()',
1061
+ ' packages = {p.get("id"): p for p in manifest.get("packages", []) if isinstance(p, dict) and p.get("id")}',
1062
+ ' for policy in policies:',
1063
+ ' packages[policy["id"]] = policy',
1064
+ ' manifest["packages"] = sorted(packages.values(), key=lambda p: p["id"])',
1065
+ ' _nimbus_write_pyodide_manifest(manifest)',
1066
+ 'def _nimbus_allowed_extensions(policy):',
1067
+ ' return {module["path"] for module in policy.get("extensionModules", [])}',
1068
+ 'def _nimbus_assert_supported_member(rel, allowed_extensions=None):',
1069
+ ' allowed_extensions = allowed_extensions or set()',
1070
+ ' leaf = rel.rsplit("/", 1)[-1]',
1071
+ ' if leaf.endswith(unsupported_extension_suffixes) and rel not in allowed_extensions:',
1072
+ ' raise RuntimeError("unsupported extension artifact in wheel: " + rel)',
1073
+ 'def _nimbus_safe_target(rel):',
1074
+ ' target = os.path.normpath(os.path.join(target_site_packages, rel))',
1075
+ ' if not target.startswith(target_site_packages + os.sep):',
1076
+ ' raise RuntimeError("unsafe wheel path: " + rel)',
1077
+ ' return target',
1078
+ 'def _nimbus_install_wheel_bytes(data, allowed_extensions=None):',
1079
+ ' allowed_extensions = allowed_extensions or set()',
1080
+ ' with zipfile.ZipFile(io.BytesIO(data)) as wheel:',
1081
+ ' infos = [member for member in wheel.infolist() if not member.is_dir()]',
1082
+ ' for member in infos:',
1083
+ ' _nimbus_assert_supported_member(member.filename, allowed_extensions)',
1084
+ ' for member in infos:',
1085
+ ' target = _nimbus_safe_target(member.filename)',
1086
+ ' os.makedirs(os.path.dirname(target), exist_ok=True)',
1087
+ ' with wheel.open(member) as source, open(target, "wb") as out:',
1088
+ ' shutil.copyfileobj(source, out)',
1089
+ // One fetch, in the standard library. This used to be pyodide.http.pyfetch,
1090
+ // which existed because Pyodide's interpreter has no sockets of its own and
1091
+ // had to borrow the host's fetch. CPython here has real sockets and real
1092
+ // OpenSSL, so urllib does the whole thing - TLS included - inside the guest,
1093
+ // and the download stops being a special case.
1094
+ 'def _nimbus_fetch_bytes(url, what):',
1095
+ ' try:',
1096
+ ' with urllib.request.urlopen(url) as response:',
1097
+ ' if response.status != 200:',
1098
+ ' raise RuntimeError("cannot fetch " + what + ": HTTP " + str(response.status))',
1099
+ ' return response.read()',
1100
+ ' except urllib.error.HTTPError as exc:',
1101
+ ' raise RuntimeError("cannot fetch " + what + ": HTTP " + str(exc.code))',
1102
+ ' except urllib.error.URLError as exc:',
1103
+ ' raise RuntimeError("cannot fetch " + what + ": " + str(exc.reason))',
1104
+ 'def _nimbus_install_remote_wheel(policy):',
1105
+ ' metadata_path = os.path.join(_nimbus_dist_info_dir(policy["canonicalName"], policy["version"]), "METADATA")',
1106
+ ' if os.path.exists(metadata_path):',
1107
+ ' return',
1108
+ ' data = _nimbus_fetch_bytes(policy["wheelUrl"], policy["canonicalName"] + " wheel")',
1109
+ ' digest = hashlib.sha256(data).hexdigest()',
1110
+ ' if digest != policy["sha256"]:',
1111
+ ' raise RuntimeError(policy["canonicalName"] + " wheel hash mismatch")',
1112
+ ' _nimbus_install_wheel_bytes(data)',
1113
+ 'def _nimbus_install_pyodide_package(policy):',
1114
+ ' metadata_path = os.path.join(_nimbus_dist_info_dir(policy["packageName"], policy["version"]), "METADATA")',
1115
+ ' if os.path.exists(metadata_path):',
1116
+ ' return',
1117
+ ' url = "https://cdn.jsdelivr.net/pyodide/v" + policy["pyodideVersion"] + "/full/" + policy["wheelFileName"]',
1118
+ ' data = _nimbus_fetch_bytes(url, policy["packageName"] + " Pyodide wheel")',
1119
+ ' digest = hashlib.sha256(data).hexdigest()',
1120
+ ' if digest != policy["wheelSha256"]:',
1121
+ ' raise RuntimeError(policy["packageName"] + " Pyodide wheel hash mismatch")',
1122
+ ' _nimbus_install_wheel_bytes(data, _nimbus_allowed_extensions(policy))',
1123
+ 'def _nimbus_install_local_wheel(policy):',
1124
+ ' with open(policy["path"], "rb") as f:',
1125
+ ' _nimbus_install_wheel_bytes(f.read())',
1126
+ 'def _nimbus_install_source_package(policy):',
1127
+ ' metadata_path = os.path.join(_nimbus_dist_info_dir(policy["canonicalName"], policy["version"]), "METADATA")',
1128
+ ' if os.path.exists(metadata_path):',
1129
+ ' return',
1130
+ ' data = _nimbus_fetch_bytes(policy["sourceUrl"], policy["canonicalName"] + " source archive")',
1131
+ ' digest = hashlib.sha256(data).hexdigest()',
1132
+ ' if digest != policy["sha256"]:',
1133
+ ' raise RuntimeError(policy["canonicalName"] + " source archive hash mismatch")',
1134
+ ' package_root = os.path.join(target_site_packages, policy["importName"])',
1135
+ ' if os.path.isdir(package_root):',
1136
+ ' shutil.rmtree(package_root)',
1137
+ ' os.makedirs(package_root, exist_ok=True)',
1138
+ ' prefix = policy["sourcePackageDir"].rstrip("/") + "/"',
1139
+ ' with tarfile.open(fileobj=io.BytesIO(data), mode="r:gz") as archive:',
1140
+ ' for member in archive.getmembers():',
1141
+ ' if not member.isfile() or not member.name.startswith(prefix):',
1142
+ ' continue',
1143
+ ' rel = member.name[len(prefix):]',
1144
+ ' if not rel or rel.rsplit("/", 1)[-1].endswith(unsupported_extension_suffixes):',
1145
+ ' raise RuntimeError("unsupported extension artifact in source policy: " + member.name)',
1146
+ ' if not (rel.endswith(".py") or rel.endswith(".pyi") or rel == "py.typed"):',
1147
+ ' continue',
1148
+ ' target = os.path.normpath(os.path.join(package_root, rel))',
1149
+ ' if not target.startswith(package_root + os.sep) and target != package_root:',
1150
+ ' raise RuntimeError("unsafe source archive path: " + member.name)',
1151
+ ' os.makedirs(os.path.dirname(target), exist_ok=True)',
1152
+ ' source = archive.extractfile(member)',
1153
+ ' if source is None:',
1154
+ ' continue',
1155
+ ' with source, open(target, "wb") as out:',
1156
+ ' shutil.copyfileobj(source, out)',
1157
+ ' dist_info = _nimbus_dist_info_dir(policy["canonicalName"], policy["version"])',
1158
+ ' if os.path.isdir(dist_info):',
1159
+ ' shutil.rmtree(dist_info)',
1160
+ ' os.makedirs(dist_info, exist_ok=True)',
1161
+ ' with open(os.path.join(dist_info, "METADATA"), "w", encoding="utf-8") as f:',
1162
+ ' f.write("Metadata-Version: 2.1\\nName: " + policy["canonicalName"] + "\\nVersion: " + policy["version"] + "\\n")',
1163
+ ' with open(os.path.join(dist_info, "WHEEL"), "w", encoding="utf-8") as f:',
1164
+ ' f.write("Wheel-Version: 1.0\\nGenerator: Nimbus pip\\nRoot-Is-Purelib: true\\nTag: py3-none-any\\n")',
1165
+ ' with open(os.path.join(dist_info, "RECORD"), "w", encoding="utf-8") as f:',
1166
+ ' f.write("")',
1167
+ // A variant package's code is already inside the interpreter and on its
1168
+ // sys.path; only the record is missing, and that record is what makes the
1169
+ // next spawn pick the interpreter that has it.
1170
+ 'def _nimbus_install_variant_package(policy):',
1171
+ ' dist_info = _nimbus_dist_info_dir(policy["canonicalName"], policy["version"])',
1172
+ ' if os.path.exists(os.path.join(dist_info, "METADATA")):',
1173
+ ' return',
1174
+ ' os.makedirs(dist_info, exist_ok=True)',
1175
+ ' with open(os.path.join(dist_info, "METADATA"), "w", encoding="utf-8") as f:',
1176
+ ' f.write("Metadata-Version: 2.1\\nName: " + policy["canonicalName"] + "\\nVersion: " + policy["version"] + "\\n")',
1177
+ ' with open(os.path.join(dist_info, "WHEEL"), "w", encoding="utf-8") as f:',
1178
+ ' f.write("Wheel-Version: 1.0\\nGenerator: Nimbus pip\\nRoot-Is-Purelib: true\\nTag: py3-none-any\\n")',
1179
+ ' with open(os.path.join(dist_info, "RECORD"), "w", encoding="utf-8") as f:',
1180
+ ' f.write("")',
1181
+ 'for source_package in source_packages:',
1182
+ ' _nimbus_install_source_package(source_package)',
1183
+ 'for variant_package in variant_packages:',
1184
+ ' _nimbus_install_variant_package(variant_package)',
1185
+ 'for wheel in remote_wheels:',
1186
+ ' _nimbus_install_remote_wheel(wheel)',
1187
+ 'for policy in pyodide_packages:',
1188
+ ' _nimbus_install_pyodide_package(policy)',
1189
+ 'for wheel in local_wheels:',
1190
+ ' _nimbus_install_local_wheel(wheel)',
1191
+ '_nimbus_record_pyodide_packages(pyodide_packages)',
1192
+ 'print("Successfully installed " + " ".join(display_packages))',
1193
+ ].join('\n');
1194
+ }
1195
+
1196
+ function canonicalPackageName(name: string): string {
1197
+ return name.replace(/[_.]+/g, '-').toLowerCase();
1198
+ }
1199
+
1200
+ function uniqueStrings(values: string[]): string[] {
1201
+ return [...new Set(values)];
1202
+ }
1203
+
1204
+ function isIgnoredPipInstallFlag(arg: string): boolean {
1205
+ return IGNORED_PIP_INSTALL_FLAGS.has(arg);
1206
+ }
1207
+
1208
+ function pipFlagTakesValue(arg: string): boolean {
1209
+ if (arg.includes('=')) return false;
1210
+ return PIP_INSTALL_FLAGS_WITH_VALUE.has(arg);
1211
+ }