import { createHash } from "node:crypto"; import { readFile, writeFile } from "node:fs/promises"; import path from "node:path"; const [distributionDir, packageBaseUrl] = process.argv.slice(2); if (!distributionDir || !packageBaseUrl) { throw new Error("Usage: vendor_pyodide_packages.mjs "); } const normalize = (name) => name.toLowerCase().replace(/[-_.]+/g, "-"); const lock = JSON.parse(await readFile(path.join(distributionDir, "pyodide-lock.json"), "utf8")); const records = lock.packages || {}; const names = new Map(Object.keys(records).map((name) => [normalize(name), name])); const pygameKey = names.get("pygame-ce"); if (!pygameKey) throw new Error("Pinned Pyodide lock does not contain pygame-ce"); const pending = [pygameKey]; const required = new Set(); while (pending.length) { const name = pending.pop(); if (required.has(name)) continue; required.add(name); for (const dependency of records[name].depends || []) { const dependencyKey = names.get(normalize(dependency)); if (!dependencyKey) throw new Error(`Unknown Pyodide dependency ${dependency} required by ${name}`); pending.push(dependencyKey); } } const base = packageBaseUrl.endsWith("/") ? packageBaseUrl : `${packageBaseUrl}/`; for (const name of required) { const record = records[name]; if (!record.file_name) continue; const response = await fetch(new URL(encodeURIComponent(record.file_name), base)); if (!response.ok) throw new Error(`Could not fetch pinned Pyodide package ${name}: HTTP ${response.status}`); const bytes = Buffer.from(await response.arrayBuffer()); const digest = createHash("sha256").update(bytes).digest("hex"); if (!record.sha256 || digest !== record.sha256) { throw new Error(`Pyodide package integrity check failed for ${name}`); } await writeFile(path.join(distributionDir, record.file_name), bytes, { mode: 0o644 }); console.log(`Vendored ${name} ${record.version} (${bytes.length} bytes)`); }