import { createHash } from "node:crypto"; import { constants as fsConstants, copyFile, lstat, mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, writeFile, } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath, pathToFileURL } from "node:url"; import { createPythonSourceManifest, PYTHON_LEGAL_RESOURCES, transformPythonLegalResource, } from "./python-engine-provenance.mjs"; export const PYTHON_ENGINE_LOCK = Object.freeze({ bridgeAbi: 1, pythonVersion: "3.14.2", pyodideVersion: "314.0.3", maximumPatternUtf16: 65_536, maximumSubjectBytes: 16_777_216, maximumReplacementUtf16: 65_536, maximumOutputBytes: 67_108_864, maximumMatches: 10_000, maximumCaptureRows: 100_000, maximumCaptureGroups: 1_000, maximumNativeExpansionCodePoints: 16_777_216, npm: Object.freeze({ package: "pyodide@314.0.3", resolved: "https://registry.npmjs.org/pyodide/-/pyodide-314.0.3.tgz", integrity: "sha512-sK40My6m8tmBUYtYH9au9rXUeh9x0wfahtHdOlGmJxZDsKBGKtP6KznyFB2+u/klbQTdDionR0uaVd176zVQzQ==", packageJsonSha256: "0393eb01a0707cddc251f1a66090998773fe1aa3cfde66eedc96e83e43861845", }), pyodide: Object.freeze({ repository: "https://github.com/pyodide/pyodide.git", tag: "314.0.3", commit: "ac57031be7564f864d061cb37c5c152e59f83ad4", license: "MPL-2.0", licenseUrl: "https://raw.githubusercontent.com/pyodide/pyodide/314.0.3/LICENSE", licenseSha256: "1f256ecad192880510e84ad60474eab7589218784b9a50bc7ceee34c2b91f1d5", }), cpython: Object.freeze({ repository: "https://github.com/python/cpython.git", tag: "v3.14.2", tagObject: "a1d0069daf8e85b25a0c3f96abc43182be6d429e", commit: "df793163d5821791d4e7caf88885a2c11a107986", license: "Python-2.0", licenseUrl: "https://raw.githubusercontent.com/python/cpython/v3.14.2/LICENSE", licenseSha256: "b0e25a78cffb43f4d92de8b61ccfa1f1f98ecbc22330b54b5251e7b6ba010231", }), emscriptenVersion: "5.0.3", abiVersion: "2026_0", lockPythonVersion: "3.14.0", assets: Object.freeze({ "pyodide-lock.json": "c963d22858f6bcb8f41586a2142f03905ab370c88ea22a86a2736e95fac2a8f3", "pyodide.asm.mjs": "1a9775427ef6e8abaa7db88ece0515422d1886915ae5c9093776410c865dfd8d", "pyodide.asm.wasm": "e7f8fac36f8bf11085309cbc5c829b3ec3057c18bf1d73b05a6741612d63cdbf", "pyodide.mjs": "5cfc46f5dcbaf2a16f26e2363f441873eb424762609cc03db00d6a2ace4d00e5", "python_stdlib.zip": "444c770dfd75a32097fc0a7d5c1413fd3140601f49c3a1f2e9af0376fcd124b4", }), strippedLoaderSha256: "640ac750bf7cf42ee58b524faa09f803732d2237753cd630d6878edda8a107bf", }); const PACK_FILES = Object.freeze( [ ...PYTHON_LEGAL_RESOURCES.map(({ output }) => output), "SHA256SUMS", "SOURCE-MANIFEST.json", "engine-metadata.json", ...Object.keys(PYTHON_ENGINE_LOCK.assets), ].sort(), ); const CHECKSUM_FILES = Object.freeze( PACK_FILES.filter((name) => name !== "SHA256SUMS"), ); function sha256(data) { return createHash("sha256").update(data).digest("hex"); } async function sha256File(file) { return sha256(await readFile(file)); } async function assertRealDirectory(candidate, label) { const details = await lstat(candidate).catch(() => null); if (!details?.isDirectory() || details.isSymbolicLink()) { throw new Error(`${label} must be a real directory: ${candidate}`); } return realpath(candidate); } async function assertRegularFile(candidate, label) { const details = await lstat(candidate).catch(() => null); if (!details?.isFile() || details.isSymbolicLink()) { throw new Error( `${label} must be a regular, non-symlink file: ${candidate}`, ); } return realpath(candidate); } async function assertHash(file, expected, label) { const actual = await sha256File(file); if (actual !== expected) { throw new Error( `${label} SHA-256 mismatch: expected ${expected}, got ${actual}.`, ); } } function withoutSourceMapReference(source) { const result = source.replace( /\n\/\/# sourceMappingURL=pyodide\.mjs\.map\s*$/u, "\n", ); if (result === source) { throw new Error( "The pinned Pyodide loader no longer has the expected source-map trailer.", ); } return result; } async function verifyPyodideSource(sourceDirectory) { const source = await assertRealDirectory( sourceDirectory, "Pyodide npm package", ); for (const [name, expected] of Object.entries(PYTHON_ENGINE_LOCK.assets)) { await assertRegularFile(path.join(source, name), `Pyodide ${name}`); await assertHash(path.join(source, name), expected, `Pyodide ${name}`); } const packageFile = path.join(source, "package.json"); await assertHash( packageFile, PYTHON_ENGINE_LOCK.npm.packageJsonSha256, "Pyodide package.json", ); const packageJson = JSON.parse(await readFile(packageFile, "utf8")); if ( packageJson.name !== "pyodide" || packageJson.version !== PYTHON_ENGINE_LOCK.pyodideVersion || packageJson.license !== PYTHON_ENGINE_LOCK.pyodide.license || packageJson.repository?.url !== "git+https://github.com/pyodide/pyodide.git" ) { throw new Error("The Pyodide npm package identity is not pinned."); } const lock = JSON.parse( await readFile(path.join(source, "pyodide-lock.json"), "utf8"), ); if ( lock.info?.abi_version !== PYTHON_ENGINE_LOCK.abiVersion || lock.info?.arch !== "wasm32" || lock.info?.python !== PYTHON_ENGINE_LOCK.lockPythonVersion || lock.info?.platform !== `emscripten_${PYTHON_ENGINE_LOCK.emscriptenVersion.replaceAll(".", "_")}` ) { throw new Error("The Pyodide lock-file platform identity is not pinned."); } return source; } async function fetchPinned(url, expectedHash, label) { const response = await fetch(url, { redirect: "follow", signal: AbortSignal.timeout(30_000), }); if (!response.ok) { throw new Error(`${label} download failed with HTTP ${response.status}.`); } const value = Buffer.from(await response.arrayBuffer()); const actual = sha256(value); if (actual !== expectedHash) { throw new Error( `${label} SHA-256 mismatch: expected ${expectedHash}, got ${actual}.`, ); } return value; } async function fileRecord(directory, name) { const file = path.join(directory, name); const value = await readFile(file); return { path: name, sha256: sha256(value), bytes: value.byteLength, }; } async function expectedSourceManifest(pack) { const legalFiles = []; for (const { output } of PYTHON_LEGAL_RESOURCES) { legalFiles.push(await fileRecord(pack, output)); } return createPythonSourceManifest(legalFiles); } async function expectedMetadata(root, pack) { const bridgePath = path.join(root, "engines", "python", "bridge.py"); const bridge = await readFile(bridgePath); const sourceManifest = await fileRecord(pack, "SOURCE-MANIFEST.json"); const files = []; for (const name of CHECKSUM_FILES) { if (name !== "engine-metadata.json") { files.push(await fileRecord(pack, name)); } } return { schemaVersion: 1, engine: "python", engineVersion: `CPython ${PYTHON_ENGINE_LOCK.pythonVersion} re`, runtime: `Pyodide ${PYTHON_ENGINE_LOCK.pyodideVersion}`, status: "production-runtime", bridge: { abiVersion: PYTHON_ENGINE_LOCK.bridgeAbi, sourceFile: { path: "engines/python/bridge.py", sha256: sha256(bridge), bytes: bridge.byteLength, }, userValuesAreFunctionArguments: true, regexModule: "re", supportModules: { serialization: "json", replacementOutputEncoding: "canonical RFC 4648 base64", runtimeIdentity: "sys", }, maximumPatternUtf16: PYTHON_ENGINE_LOCK.maximumPatternUtf16, maximumSubjectBytes: PYTHON_ENGINE_LOCK.maximumSubjectBytes, maximumReplacementUtf16: PYTHON_ENGINE_LOCK.maximumReplacementUtf16, maximumOutputBytes: PYTHON_ENGINE_LOCK.maximumOutputBytes, maximumMatches: PYTHON_ENGINE_LOCK.maximumMatches, maximumCaptureRows: PYTHON_ENGINE_LOCK.maximumCaptureRows, maximumCaptureGroups: PYTHON_ENGINE_LOCK.maximumCaptureGroups, maximumNativeExpansionCodePoints: PYTHON_ENGINE_LOCK.maximumNativeExpansionCodePoints, }, source: { manifest: sourceManifest, pyodide: { repository: PYTHON_ENGINE_LOCK.pyodide.repository, tag: PYTHON_ENGINE_LOCK.pyodide.tag, commitSha1: PYTHON_ENGINE_LOCK.pyodide.commit, license: PYTHON_ENGINE_LOCK.pyodide.license, npmPackage: PYTHON_ENGINE_LOCK.npm.package, npmResolved: PYTHON_ENGINE_LOCK.npm.resolved, npmIntegrity: PYTHON_ENGINE_LOCK.npm.integrity, }, cpython: { repository: PYTHON_ENGINE_LOCK.cpython.repository, tag: PYTHON_ENGINE_LOCK.cpython.tag, tagObjectSha1: PYTHON_ENGINE_LOCK.cpython.tagObject, commitSha1: PYTHON_ENGINE_LOCK.cpython.commit, license: PYTHON_ENGINE_LOCK.cpython.license, }, }, toolchain: { abiVersion: PYTHON_ENGINE_LOCK.abiVersion, architecture: "wasm32", emscriptenVersion: PYTHON_ENGINE_LOCK.emscriptenVersion, lockPythonVersion: PYTHON_ENGINE_LOCK.lockPythonVersion, executablePythonVersion: PYTHON_ENGINE_LOCK.pythonVersion, threads: false, }, packaging: { loaderTransformation: "Removed only the upstream sourceMappingURL trailer; executable JavaScript is unchanged.", upstreamLoaderSha256: PYTHON_ENGINE_LOCK.assets["pyodide.mjs"], packedLoaderSha256: PYTHON_ENGINE_LOCK.strippedLoaderSha256, externalPackagesIncluded: false, legalClosure: "SOURCE-MANIFEST.json inventories every loader and native base-runtime component, exact preferred-form input and patch, plus independently anchored legal text.", lockPythonVersionNote: "The upstream lock's 3.14.0 field identifies its CPython 3.14 ABI baseline; the executable runtime self-identifies as 3.14.2.", }, files, }; } function equalJson(left, right) { return JSON.stringify(left) === JSON.stringify(right); } async function expectedChecksums(pack) { const lines = []; for (const name of CHECKSUM_FILES) { lines.push(`${await sha256File(path.join(pack, name))} ${name}`); } return `${lines.join("\n")}\n`; } async function smokePythonPack(pack) { await WebAssembly.compile( await readFile(path.join(pack, "pyodide.asm.wasm")), ); const moduleUrl = pathToFileURL(path.join(pack, "pyodide.mjs")); moduleUrl.searchParams.set("regex-tools-verification", String(Date.now())); const imported = await import(moduleUrl.href); if ( imported.version !== PYTHON_ENGINE_LOCK.pyodideVersion || typeof imported.loadPyodide !== "function" ) { throw new Error("The staged Pyodide module identity is invalid."); } const runtime = await imported.loadPyodide({ indexURL: `${pack}${path.sep}`, stdout: () => undefined, stderr: () => undefined, }); const result = JSON.parse( runtime.runPython(` import _zstd, decimal, json, pyexpat, re, sqlite3, sys, zlib expression = re.compile(r"(?P😀)|(?P[a-z]+)", re.ASCII) matches = [ [match.span(0), match.lastgroup] for match in expression.finditer("😀 abc") ] replacement_match = re.compile(r"(?P[a-z]+)").search("abc") json.dumps({ "python": ".".join(str(part) for part in sys.version_info[:3]), "implementation": sys.implementation.name, "libmpdec": decimal.__libmpdec_version__, "expat": pyexpat.EXPAT_VERSION, "sqlite": sqlite3.sqlite_version, "zlib": zlib.ZLIB_RUNTIME_VERSION, "zstd": _zstd.zstd_version, "matches": matches, "replacement": replacement_match.expand(r"[\\g]"), }) `), ); if ( result.python !== PYTHON_ENGINE_LOCK.pythonVersion || result.implementation !== "cpython" || result.libmpdec !== "2.5.1" || result.expat !== "expat_2.7.3" || result.sqlite !== "3.39.0" || result.zlib !== "1.3.1" || result.zstd !== "1.5.7" || JSON.stringify(result.matches) !== JSON.stringify([ [[0, 1], "emoji"], [[2, 5], "word"], ]) || result.replacement !== "[abc]" ) { throw new Error("The staged CPython re runtime smoke test failed."); } } export async function verifyPythonPack(packDirectory, root) { const repositoryRoot = await assertRealDirectory( root, "Regex Tools repository", ); const pack = await assertRealDirectory(packDirectory, "Python engine pack"); const entries = (await readdir(pack, { withFileTypes: true })).sort( (left, right) => left.name < right.name ? -1 : left.name > right.name ? 1 : 0, ); if ( entries.length !== PACK_FILES.length || entries.some( (entry, index) => entry.name !== PACK_FILES[index] || !entry.isFile() || entry.isSymbolicLink(), ) ) { throw new Error( `The staged Python pack must contain only: ${PACK_FILES.join(", ")}.`, ); } for (const resource of PYTHON_LEGAL_RESOURCES) { await assertHash( path.join(pack, resource.output), resource.outputSha256, `staged ${resource.component} legal material`, ); } await assertHash( path.join(pack, "pyodide.mjs"), PYTHON_ENGINE_LOCK.strippedLoaderSha256, "staged Pyodide loader", ); for (const [name, expected] of Object.entries(PYTHON_ENGINE_LOCK.assets)) { if (name !== "pyodide.mjs") { await assertHash(path.join(pack, name), expected, `staged ${name}`); } } const loader = await readFile(path.join(pack, "pyodide.mjs"), "utf8"); if ( loader.includes("sourceMappingURL") || loader.includes("/mnt/DATA/") || loader.includes("/home/zemion/") ) { throw new Error( "The staged Pyodide loader leaks a source-map or local build path.", ); } let metadata; try { metadata = JSON.parse( await readFile(path.join(pack, "engine-metadata.json"), "utf8"), ); } catch (error) { throw new Error("engine-metadata.json is not valid JSON.", { cause: error, }); } let sourceManifest; try { sourceManifest = JSON.parse( await readFile(path.join(pack, "SOURCE-MANIFEST.json"), "utf8"), ); } catch (error) { throw new Error("SOURCE-MANIFEST.json is not valid JSON.", { cause: error, }); } const expectedManifest = await expectedSourceManifest(pack); if (!equalJson(sourceManifest, expectedManifest)) { throw new Error( "The staged Python source manifest does not match the pinned preferred-form and legal inventory.", ); } const expected = await expectedMetadata(repositoryRoot, pack); if (!equalJson(metadata, expected)) { throw new Error( "The staged Python metadata does not match the pinned pack contract.", ); } if ( (await readFile(path.join(pack, "SHA256SUMS"), "utf8")) !== (await expectedChecksums(pack)) ) { throw new Error("The staged Python SHA256SUMS file is incorrect."); } await smokePythonPack(pack); return metadata; } async function replaceDirectory(stage, target, label) { const targetDetails = await lstat(target).catch(() => null); if ( targetDetails && (!targetDetails.isDirectory() || targetDetails.isSymbolicLink()) ) { throw new Error(`${label} must be a real directory.`); } if (!targetDetails) { await rename(stage, target); return; } const backup = `${target}.replaced-${process.pid}-${Date.now()}`; await rename(target, backup); try { await rename(stage, target); } catch (error) { await rename(backup, target); throw error; } await rm(backup, { recursive: true }); } export async function installPythonPack(packDirectory, root) { const repositoryRoot = await assertRealDirectory( root, "Regex Tools repository", ); const sourcePack = await assertRealDirectory( packDirectory, "verified Python engine pack", ); const metadata = await verifyPythonPack(sourcePack, repositoryRoot); const engineRoot = await assertRealDirectory( path.join(repositoryRoot, "public", "engines"), "public engine directory", ); const target = path.join(engineRoot, "python"); const stage = await mkdtemp(path.join(engineRoot, ".python-install-")); try { for (const name of PACK_FILES) { await copyFile( path.join(sourcePack, name), path.join(stage, name), fsConstants.COPYFILE_EXCL, ); } await verifyPythonPack(stage, repositoryRoot); await replaceDirectory(stage, target, "public/engines/python"); return { output: target, metadata }; } finally { await rm(stage, { recursive: true, force: true }); } } export async function buildPythonPack(options, root) { const repositoryRoot = await assertRealDirectory( root, "Regex Tools repository", ); const source = await verifyPyodideSource(options.sourceDirectory); const output = path.resolve(options.outputDirectory); await mkdir(path.dirname(output), { recursive: true }); const stage = await mkdtemp( path.join(path.dirname(output), ".python-pack-build-"), ); try { const legalFiles = await Promise.all( PYTHON_LEGAL_RESOURCES.map(async (resource) => { const sourceValue = await fetchPinned( resource.url, resource.sourceSha256, `${resource.component} legal source`, ); return [resource, transformPythonLegalResource(resource, sourceValue)]; }), ); for (const [resource, value] of legalFiles) { await writeFile(path.join(stage, resource.output), value, { flag: "wx" }); } for (const name of Object.keys(PYTHON_ENGINE_LOCK.assets)) { if (name === "pyodide.mjs") { const upstream = await readFile(path.join(source, name), "utf8"); const packed = withoutSourceMapReference(upstream); if (sha256(packed) !== PYTHON_ENGINE_LOCK.strippedLoaderSha256) { throw new Error("The deterministic Pyodide loader output changed."); } await writeFile(path.join(stage, name), packed, { flag: "wx" }); } else { await copyFile( path.join(source, name), path.join(stage, name), fsConstants.COPYFILE_EXCL, ); } } await writeFile( path.join(stage, "SOURCE-MANIFEST.json"), `${JSON.stringify(await expectedSourceManifest(stage), null, 2)}\n`, { flag: "wx" }, ); const metadata = await expectedMetadata(repositoryRoot, stage); await writeFile( path.join(stage, "engine-metadata.json"), `${JSON.stringify(metadata, null, 2)}\n`, { flag: "wx" }, ); await writeFile( path.join(stage, "SHA256SUMS"), await expectedChecksums(stage), { flag: "wx" }, ); await verifyPythonPack(stage, repositoryRoot); await replaceDirectory(stage, output, "Python pack output"); return { output, metadata }; } finally { await rm(stage, { recursive: true, force: true }); } } const invokedFile = process.argv[1] ? pathToFileURL(path.resolve(process.argv[1])).href : ""; if (import.meta.url === invokedFile) { const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const arguments_ = process.argv.slice(2); if (arguments_.length > 2) { throw new Error( "Usage: node scripts/python-engine-pack.mjs [node_modules/pyodide] [.engine-build/python]", ); } const result = await buildPythonPack( { sourceDirectory: path.resolve( root, arguments_[0] ?? "node_modules/pyodide", ), outputDirectory: path.resolve( root, arguments_[1] ?? ".engine-build/python", ), }, root, ); console.log( `Built and verified ${result.metadata.runtime} / ${result.metadata.engineVersion} at ${result.output}.`, ); }