Skip to content

Commit 427d2b1

Browse files
authored
Add lockfileContents option to loadPyodide() (pyodide#5764)
And also `packageBaseUrl` option: 1. If `lockfileContents` is provided and not `packageBaseUrl`, then in the browser attempting to load a lock entry with a relative path fails. 2. In Node, `packageBaseUrl` is used in place of jsdelivr as the cdn url if passed. See discussion in pyodide#5736.
1 parent ab45a20 commit 427d2b1

File tree

11 files changed

+363
-89
lines changed

11 files changed

+363
-89
lines changed

docs/expected_js_docs.txt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,21 @@ js:function
9393
js:attribute
9494
pyodide.ERRNO_CODES
9595
pyodide.FS
96+
pyodide.Lockfile.info
97+
pyodide.Lockfile.packages
98+
pyodide.LockfileInfo.abi_version
99+
pyodide.LockfileInfo.arch
100+
pyodide.LockfileInfo.platform
101+
pyodide.LockfileInfo.python
102+
pyodide.LockfileInfo.version
103+
pyodide.LockfilePackage.depends
104+
pyodide.LockfilePackage.file_name
105+
pyodide.LockfilePackage.imports
106+
pyodide.LockfilePackage.install_dir
107+
pyodide.LockfilePackage.name
108+
pyodide.LockfilePackage.package_type
109+
pyodide.LockfilePackage.sha256
110+
pyodide.LockfilePackage.version
96111
pyodide.PATH
97112
pyodide.PackageData.fileName
98113
pyodide.PackageData.name
@@ -119,6 +134,9 @@ js:attribute
119134
pyodide.pyodide_py
120135
pyodide.version
121136
js:interface
137+
pyodide.Lockfile
138+
pyodide.LockfileInfo
139+
pyodide.LockfilePackage
122140
pyodide.PackageData
123141
js:typealias
124142
pyodide.TypedArray

docs/project/changelog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,12 @@ myst:
2424
- {{ Fix }} Fixed a bug in Node.js which providing a relative path to `lockFileURL` parameter of `loadPyodide()` did not work.
2525
{pr}`5750`
2626

27+
- {{ Enhancement }} Added `lockfileContents` and `packageBaseURL` options to
28+
`loadPyodide`. This allows providing a lock file as a `Promise` for the
29+
contents rather than a URL. If `lockfileContents` is provided, then
30+
`packageBaseURL` must also be provided in order to resolve relative paths in
31+
the lockfile.
32+
2733
## Version 0.28.0
2834

2935
_July 4, 2025_

docs/sphinx_pyodide/sphinx_pyodide/jsdoc.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
from collections.abc import Iterator
22

33
from sphinx_js import ir
4-
from sphinx_js.ir import Class
4+
from sphinx_js.ir import Class, TypeXRefInternal
55
from sphinx_js.typedoc import Analyzer as TsAnalyzer
66

77
__all__ = ["ts_xref_formatter", "patch_sphinx_js"]
@@ -16,6 +16,10 @@ def ts_xref_formatter(_config, xref):
1616
from sphinx_pyodide.mdn_xrefs import JSDATA
1717

1818
name = xref.name
19+
if name == "Lockfile":
20+
name = "~pyodide.Lockfile"
21+
if name == "TypedArray":
22+
name = "~pyodide.TypedArray"
1923
if name == "PyodideAPI":
2024
return ":ref:`PyodideAPI <js-api-pyodide>`"
2125
if name in JSDATA:
@@ -24,6 +28,8 @@ def ts_xref_formatter(_config, xref):
2428
return f":js:class:`~pyodide.ffi.{name}`"
2529
if name in ["ConcatArray", "IterableIterator", "unknown", "U"]:
2630
return f"``{name}``"
31+
if isinstance(xref, TypeXRefInternal):
32+
return f":js:{xref.kind}:`{name}`"
2733
return f":js:class:`{name}`"
2834

2935

@@ -118,6 +124,9 @@ def get_obj_mod(doclet: ir.TopLevel) -> str:
118124
filename = key[0]
119125
doclet.name = doclet.name.rpartition(".")[2]
120126

127+
if kind := doclet.block_tags.get("docgroup"):
128+
return kind[0][0].text
129+
121130
if filename == "pyodide.":
122131
return "globalThis"
123132

src/js/api.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import { loadBinaryFile, nodeFSMod } from "./compat";
77
import { version } from "./version";
88
import { setStdin, setStdout, setStderr } from "./streams";
99
import { scheduleCallback } from "./scheduler";
10-
import { TypedArray, PackageData, FSType } from "./types";
10+
import { TypedArray, PackageData, FSType, Lockfile } from "./types";
1111
import { IN_NODE, detectEnvironment } from "./environments";
1212
// @ts-ignore
1313
import LiteralMap from "./common/literal-map";
@@ -691,16 +691,16 @@ export class PyodideAPI {
691691
* The format of the lockfile is defined in the `pyodide/pyodide-lock
692692
* <https://github.com/pyodide/pyodide-lock>`_ repository.
693693
*/
694-
static get lockfile() {
694+
static get lockfile(): Lockfile {
695695
return API.lockfile;
696696
}
697697

698698
/**
699-
* Returns the base URL of the lockfile, which is used to locate the packages
700-
* distributed with the lockfile.
699+
* Returns the URL or path with respect to which relative paths in the lock
700+
* file are resolved, or undefined.
701701
*/
702-
static get lockfileBaseUrl() {
703-
return API.lockfileBaseUrl;
702+
static get lockfileBaseUrl(): string | undefined {
703+
return API.config.packageCacheDir ?? API.config.packageBaseUrl;
704704
}
705705
}
706706

src/js/compat.ts

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ declare var globalThis: {
2525
importScripts: (url: string) => void;
2626
document?: typeof document;
2727
fetch?: typeof fetch;
28+
location?: URL;
2829
};
2930

3031
/**
@@ -74,6 +75,10 @@ export async function initNodeModules() {
7475
};
7576
}
7677

78+
export function isAbsolute(path: string): boolean {
79+
return path.includes("://") || path.startsWith("/");
80+
}
81+
7782
function node_resolvePath(path: string, base?: string): string {
7883
return nodePath.resolve(base || ".", path);
7984
}
@@ -322,10 +327,13 @@ export async function calculateDirname(): Promise<string> {
322327
* Ensure that the directory exists before trying to download files into it (Node.js only).
323328
* @param dir The directory to ensure exists
324329
*/
325-
export async function ensureDirNode(dir: string) {
330+
export async function ensureDirNode(dir?: string) {
326331
if (!IN_NODE) {
327332
return;
328333
}
334+
if (!dir) {
335+
return;
336+
}
329337

330338
try {
331339
// Check if the `installBaseUrl` directory exists
@@ -337,3 +345,22 @@ export async function ensureDirNode(dir: string) {
337345
});
338346
}
339347
}
348+
349+
/**
350+
* Calculates the install base url for the package manager.
351+
* exported for testing
352+
* @param lockFileURL
353+
* @returns the install base url
354+
* @private
355+
*/
356+
export function calculateInstallBaseUrl(lockFileURL: string) {
357+
// 1. If the lockfile URL includes a path with slash (file url in Node.js or http url in browser), use the directory of the lockfile URL
358+
// 2. Otherwise, fallback to the current location
359+
// 2.1. In the browser, use `location` to get the current location
360+
// 2.2. In Node.js just use the pwd
361+
return (
362+
lockFileURL.substring(0, lockFileURL.lastIndexOf("/") + 1) ||
363+
globalThis.location?.toString() ||
364+
"."
365+
);
366+
}

src/js/load-package.ts

Lines changed: 30 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import "./constants";
22
import {
33
Lockfile,
44
PackageData,
5-
InternalPackageData,
5+
LockfilePackage,
66
PackageLoadMetadata,
77
PackageManagerAPI,
88
PackageManagerModule,
@@ -23,6 +23,7 @@ import {
2323
resolvePath,
2424
initNodeModules,
2525
ensureDirNode,
26+
isAbsolute,
2627
} from "./compat";
2728
import { Installer } from "./installer";
2829
import { createContextWrapper } from "./common/contextManager";
@@ -35,10 +36,12 @@ import { createContextWrapper } from "./common/contextManager";
3536
* @private
3637
*/
3738
export async function initializePackageIndex(
38-
lockFilePromise: Promise<Lockfile>,
39+
lockFilePromise: Promise<Lockfile | string>,
3940
) {
4041
await initNodeModules();
41-
const lockfile = await lockFilePromise;
42+
const lockfile_ = await lockFilePromise;
43+
const lockfile: Lockfile =
44+
typeof lockfile_ === "string" ? JSON.parse(lockfile_) : lockfile_;
4245
if (!lockfile.packages) {
4346
throw new Error(
4447
"Loaded pyodide lock file does not contain the expected key 'packages'.",
@@ -123,7 +126,7 @@ export class PackageManager {
123126

124127
private _lock = createLock();
125128

126-
public installBaseUrl: string;
129+
public installBaseUrl?: string;
127130

128131
/**
129132
* The function to use for stdout and stderr, defaults to console.log and console.error
@@ -147,11 +150,15 @@ export class PackageManager {
147150
this.#module = pyodideModule;
148151
this.#installer = new Installer(api, pyodideModule);
149152

150-
const lockfileBase = calculateInstallBaseUrl(this.#api.config.lockFileURL);
151153
if (IN_NODE) {
152-
this.installBaseUrl = this.#api.config.packageCacheDir ?? lockfileBase;
154+
// In node, we'll try first to load from the packageCacheDir and then fall
155+
// back to cdnURL
156+
this.installBaseUrl =
157+
this.#api.config.packageCacheDir ?? API.config.packageBaseUrl;
158+
this.cdnURL = this.#api.config.cdnUrl;
153159
} else {
154-
this.installBaseUrl = lockfileBase;
160+
// use packageBaseUrl as the base URL for the packages
161+
this.installBaseUrl = this.#api.config.packageBaseUrl;
155162
}
156163

157164
this.stdout = (msg: string) => {
@@ -248,7 +255,7 @@ export class PackageManager {
248255
checkIntegrity: true,
249256
},
250257
): Promise<Array<PackageData>> {
251-
const loadedPackageData = new Set<InternalPackageData>();
258+
const loadedPackageData = new Set<LockfilePackage>();
252259
const pkgNames = toStringArray(names);
253260

254261
const toLoad = this.recursiveDependencies(pkgNames);
@@ -450,6 +457,13 @@ export class PackageManager {
450457
}
451458
const lockfilePackage = this.#api.lockfile_packages[pkg.normalizedName];
452459
fileName = lockfilePackage.file_name;
460+
// TODO: Node caching logic assumes relative here...
461+
if (!isAbsolute(fileName) && !this.installBaseUrl) {
462+
throw new Error(
463+
`Lock file file_name for package "${pkg.name}" is relative path "${fileName}" but no packageBaseUrl provided to loadPyodide.`,
464+
);
465+
}
466+
453467
uri = resolvePath(fileName, this.installBaseUrl);
454468
fileSubResourceHash = "sha256-" + base16ToBase64(lockfilePackage.sha256);
455469
} else {
@@ -464,7 +478,12 @@ export class PackageManager {
464478
DEBUG && console.debug(`Downloading package ${pkg.name} from ${uri}`);
465479
return await loadBinaryFile(uri, fileSubResourceHash);
466480
} catch (e) {
467-
if (!IN_NODE || pkg.channel !== this.defaultChannel) {
481+
if (
482+
!IN_NODE ||
483+
pkg.channel !== this.defaultChannel ||
484+
!fileName ||
485+
fileName.startsWith("/")
486+
) {
468487
throw e;
469488
}
470489
}
@@ -538,7 +557,7 @@ export class PackageManager {
538557
private async downloadAndInstall(
539558
pkg: PackageLoadMetadata,
540559
toLoad: Map<string, PackageLoadMetadata>,
541-
loaded: Set<InternalPackageData>,
560+
loaded: Set<LockfilePackage>,
542561
failed: Map<string, Error>,
543562
checkIntegrity: boolean = true,
544563
) {
@@ -572,10 +591,6 @@ export class PackageManager {
572591
}
573592
}
574593

575-
public setCdnUrl(url: string) {
576-
this.cdnURL = url;
577-
}
578-
579594
/**
580595
* Flushes the stdout and stderr buffers, that were collected before the
581596
* stdout and stderr functions were set.
@@ -641,7 +656,7 @@ function filterPackageData({
641656
version,
642657
file_name,
643658
package_type,
644-
}: InternalPackageData): PackageData {
659+
}: LockfilePackage): PackageData {
645660
return { name, version, fileName: file_name, packageType: package_type };
646661
}
647662

@@ -666,25 +681,6 @@ export function toStringArray(str: string | PyProxy | string[]): string[] {
666681
return str;
667682
}
668683

669-
/**
670-
* Calculates the install base url for the package manager.
671-
* exported for testing
672-
* @param lockFileURL
673-
* @returns the install base url
674-
* @private
675-
*/
676-
export function calculateInstallBaseUrl(lockFileURL: string) {
677-
// 1. If the lockfile URL includes a path with slash (file url in Node.js or http url in browser), use the directory of the lockfile URL
678-
// 2. Otherwise, fallback to the current location
679-
// 2.1. In the browser, use `location` to get the current location
680-
// 2.2. In Node.js just use the pwd
681-
return (
682-
lockFileURL.substring(0, lockFileURL.lastIndexOf("/") + 1) ||
683-
globalThis.location?.toString() ||
684-
"."
685-
);
686-
}
687-
688684
export let loadPackage: typeof PackageManager.prototype.loadPackage;
689685
/**
690686
* An object whose keys are the names of the loaded packages and whose values
@@ -710,13 +706,6 @@ if (typeof API !== "undefined" && typeof Module !== "undefined") {
710706
*/
711707
loadedPackages = singletonPackageManager.loadedPackages;
712708

713-
// TODO: Find a better way to register these functions
714-
API.setCdnUrl = singletonPackageManager.setCdnUrl.bind(
715-
singletonPackageManager,
716-
);
717-
718-
API.lockfileBaseUrl = singletonPackageManager.installBaseUrl;
719-
720709
API.flushPackageManagerBuffers = singletonPackageManager.flushBuffers.bind(
721710
singletonPackageManager,
722711
);

0 commit comments

Comments
 (0)