Skip to content
File

Blob: src/pyodide/internal/loadPackage.ts

typescript87 lines
1// Copyright (c) 2026 Cloudflare, Inc.
2// Licensed under the Apache 2.0 license found in the LICENSE file or at:
3// https://opensource.org/licenses/Apache-2.0
4 
5/**
6 * This file contains code that roughly replaces pyodide.loadPackage, with workerd-specific
7 * optimizations:
8 * - Wheels are decompressed with a DecompressionStream instead of in Python
9 * - Wheels are overlaid onto the site-packages dir instead of actually being copied
10 * - Wheels are fetched from a disk cache if available.
11 *
12 * Note that loadPackages is only used in local dev for now, internally we use the full big bundle
13 * that contains all the packages ready to go.
14 */
15 
16import {
17 LOCKFILE,
18 PACKAGES_VERSION,
19 USING_OLDEST_PACKAGES_VERSION,
20} from 'pyodide-internal:metadata';
21import {
22 VIRTUALIZED_DIR,
23 STDLIB_PACKAGES,
24} from 'pyodide-internal:setupPackages';
25import { parseTarInfo } from 'pyodide-internal:tar';
26import { createTarFS } from 'pyodide-internal:tarfs';
27import { default as ArtifactBundler } from 'pyodide-internal:artifacts';
28import {
29 PythonUserError,
30 PythonWorkersInternalError,
31} from 'pyodide-internal:util';
32 
33function getPackageMetadata(requirement: string): PackageDeclaration {
34 const obj = LOCKFILE['packages'][requirement];
35 if (!obj) {
36 throw new PythonUserError(
37 'Requirement ' + requirement + ' not found in lockfile'
38 );
39 }
40 
41 return obj;
42}
43 
44function loadBundleFromArtifactBundler(requirement: string): Reader {
45 const filename = getPackageMetadata(requirement).file_name;
46 const fullPath = `python-package-bucket/${PACKAGES_VERSION}/${filename}`;
47 const reader = ArtifactBundler.getPackage(fullPath);
48 if (!reader) {
49 throw new PythonWorkersInternalError(
50 'Failed to get package ' + fullPath + ' from ArtifactBundler'
51 );
52 }
53 return reader;
54}
55 
56/**
57 * Downloads the requirements specified and loads them into Pyodide. Note that this does not
58 * do any dependency resolution, it just installs the requirements that are specified. See
59 * `getTransitiveRequirements` for the code that deals with this.
60 */
61export function loadPackages(Module: Module, requirements: Set<string>): void {
62 let pkgsToLoad = requirements;
63 // TODO: Package snapshot created with '20240829.4' needs the stdlib packages to be added here.
64 // We should remove this check once the next Python and packages versions are rolled
65 // out.
66 if (USING_OLDEST_PACKAGES_VERSION) {
67 pkgsToLoad = pkgsToLoad.union(new Set(STDLIB_PACKAGES));
68 }
69 
70 for (const req of pkgsToLoad) {
71 if (req === 'test') {
72 continue; // Skip the test package, it is only useful for internal Python regression testing.
73 }
74 if (VIRTUALIZED_DIR.hasRequirementLoaded(req)) {
75 continue;
76 }
77 
78 const reader = loadBundleFromArtifactBundler(req);
79 const [tarInfo, soFiles] = parseTarInfo(reader);
80 const pkg = getPackageMetadata(req);
81 VIRTUALIZED_DIR.addSmallBundle(tarInfo, soFiles, req, pkg.install_dir);
82 }
83 
84 const tarFS = createTarFS(Module);
85 VIRTUALIZED_DIR.mount(Module, tarFS);
86}