import { spawn } from "node:child_process"; import { createHash } from "node:crypto"; import { access, constants as fsConstants, copyFile, lstat, mkdir, mkdtemp, readFile, readdir, realpath, rename, rm, stat, writeFile, } from "node:fs/promises"; import path from "node:path"; import { pathToFileURL } from "node:url"; export const PCRE2_LOCK = Object.freeze({ bridgeAbi: 3, maximumPatternBytes: 1_048_576, maximumSubjectBytes: 16_777_216, maximumReplacementBytes: 262_144, maximumMatches: 10_000, maximumCaptureRows: 100_000, maximumCaptureGroups: 1_000, maximumTraceEvents: 50_000, maximumTraceBytes: 10_485_760, maximumTraceMarkBytes: 1_024, sourceDateEpoch: 1_760_997_684, source: Object.freeze({ repository: "https://github.com/PCRE2Project/pcre2.git", tag: "pcre2-10.47", tagObject: "cd007b4466798f66d479d1442a407099e7c40050", commit: "f454e231fe5006dd7ff8f4693fd2b8eb94333429", tree: "81a83a3552bd68d0ea7b7004f8bb6e7892f583ba", version: "10.47 2025-10-21", license: "BSD-3-Clause WITH PCRE2-exception", criticalFiles: Object.freeze({ "CMakeLists.txt": "74f65935e2b1120e7d7aeb4be81755e0f7a875732467ae64a9cc6ee7174fa5ba", "LICENCE.md": "197d8a73ffee0d6b09adba2f9c677b5f5aede24edf89258a68e48248d010d811", "src/pcre2.h.generic": "058462dc030e845ee627e19ac36347f561f5cc44d89eaf1c0010f2b363ab68d5", }), }), emscripten: Object.freeze({ version: "6.0.4", compilerRevision: "fe5be6afdff43ad58860d821fcc8572a23f92d19", repositoryCommit: "224ec5f9f2f72f09f9ce0e26d66bae7dbd8b692f", emccSha256: "498eba9b6aacffca1c358ba340fc7b37015d7867cb211c7f2d45784ee9310f03", emcmakeSha256: "18c8397cf60835533e0db88d1172e04fa0b753dea68e978149d923a9e6378af3", }), cmakeVersion: "4.3.4", ninjaVersion: "1.13.2", }); const PACK_FILES = Object.freeze([ "LICENSE.txt", "SHA256SUMS", "engine-metadata.json", "pcre2.mjs", "pcre2.wasm", ]); const CHECKSUM_FILES = Object.freeze([ "LICENSE.txt", "engine-metadata.json", "pcre2.mjs", "pcre2.wasm", ]); const BRIDGE_SOURCE_FILES = Object.freeze([ "engines/pcre2/CMakeLists.txt", "engines/pcre2/pcre2_bridge.c", "engines/pcre2/pcre2_bridge.h", ]); const EXPORTED_FUNCTIONS = Object.freeze([ "_free", "_malloc", "_regex_pcre2_bridge_abi_version", "_regex_pcre2_compile_probe", "_regex_pcre2_config_flags", "_regex_pcre2_error_message", "_regex_pcre2_execute", "_regex_pcre2_self_test", "_regex_pcre2_substitute", "_regex_pcre2_trace", "_regex_pcre2_version", ]); function sha256(data) { return createHash("sha256").update(data).digest("hex"); } async function sha256File(file) { return sha256(await readFile(file)); } function commandLabel(command, arguments_) { return [command, ...arguments_] .map((part) => /^[A-Za-z0-9_./:=,+-]+$/u.test(part) ? part : JSON.stringify(part), ) .join(" "); } async function run(command, arguments_, options = {}) { const child = spawn(command, arguments_, { cwd: options.cwd, env: options.env, stdio: ["ignore", "pipe", "pipe"], }); const stdout = []; const stderr = []; child.stdout.on("data", (chunk) => stdout.push(chunk)); child.stderr.on("data", (chunk) => stderr.push(chunk)); const result = await new Promise((resolve, reject) => { child.once("error", reject); child.once("close", (code, signal) => resolve({ code, signal })); }); const output = Buffer.concat(stdout).toString("utf8"); const errorOutput = Buffer.concat(stderr).toString("utf8"); if (result.code !== 0) { const detail = [output, errorOutput].filter(Boolean).join("\n").trim(); throw new Error( `${commandLabel(command, arguments_)} failed with ${ result.signal ? `signal ${result.signal}` : `exit code ${result.code}` }${detail ? `:\n${detail}` : "."}`, ); } return { stdout: output, stderr: errorOutput }; } 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}.`, ); } } async function resolveExecutable(name, environment = process.env) { const searchPath = environment.PATH ?? ""; for (const directory of searchPath.split(path.delimiter)) { if (!directory) continue; const candidate = path.join(directory, name); try { await access(candidate, fsConstants.X_OK); const details = await stat(candidate); if (details.isFile()) return realpath(candidate); } catch { // Continue through PATH. } } throw new Error(`${name} is required on PATH for the explicit PCRE2 build.`); } function singleVersionLine(output) { return output.trim().split(/\r?\n/u)[0] ?? ""; } async function verifyPcre2Source(sourceDirectory) { const source = await assertRealDirectory(sourceDirectory, "PCRE2 source"); const git = await resolveExecutable("git"); const status = await run( git, ["status", "--porcelain=v1", "--untracked-files=all"], { cwd: source }, ); if (status.stdout !== "") { throw new Error("The PCRE2 source checkout must be completely clean."); } const checks = [ [["rev-parse", "HEAD"], PCRE2_LOCK.source.commit, "PCRE2 commit"], [["rev-parse", "HEAD^{tree}"], PCRE2_LOCK.source.tree, "PCRE2 source tree"], [ ["rev-parse", `refs/tags/${PCRE2_LOCK.source.tag}^{tag}`], PCRE2_LOCK.source.tagObject, "PCRE2 signed tag object", ], [ ["rev-parse", `refs/tags/${PCRE2_LOCK.source.tag}^{commit}`], PCRE2_LOCK.source.commit, "PCRE2 tag target", ], ]; for (const [arguments_, expected, label] of checks) { const { stdout } = await run(git, arguments_, { cwd: source }); const actual = stdout.trim(); if (actual !== expected) { throw new Error( `${label} mismatch: expected ${expected}, got ${actual}.`, ); } } for (const [relativePath, expected] of Object.entries( PCRE2_LOCK.source.criticalFiles, )) { await assertHash( path.join(source, relativePath), expected, `PCRE2 ${relativePath}`, ); } return source; } async function verifyEmscripten(emccFile) { const emcc = await assertRegularFile(emccFile, "emcc"); const emcmake = await assertRegularFile( path.join(path.dirname(emcc), "emcmake"), "emcmake", ); await assertHash(emcc, PCRE2_LOCK.emscripten.emccSha256, "emcc"); await assertHash(emcmake, PCRE2_LOCK.emscripten.emcmakeSha256, "emcmake"); const version = await run(emcc, ["--version"]); const expectedVersion = `emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) ` + `${PCRE2_LOCK.emscripten.version} (${PCRE2_LOCK.emscripten.compilerRevision})`; if (singleVersionLine(version.stdout) !== expectedVersion) { throw new Error( `Emscripten identity mismatch: expected ${expectedVersion}, got ${singleVersionLine( version.stdout, )}.`, ); } const git = await resolveExecutable("git"); const repository = ( await run(git, ["rev-parse", "--show-toplevel"], { cwd: path.dirname(emcc), }) ).stdout.trim(); const commit = ( await run(git, ["rev-parse", "HEAD"], { cwd: repository }) ).stdout.trim(); if (commit !== PCRE2_LOCK.emscripten.repositoryCommit) { throw new Error( `Emscripten repository mismatch: expected ${PCRE2_LOCK.emscripten.repositoryCommit}, got ${commit}.`, ); } const tagTarget = ( await run( git, ["rev-parse", `refs/tags/${PCRE2_LOCK.emscripten.version}^{commit}`], { cwd: repository, }, ) ).stdout.trim(); if (tagTarget !== PCRE2_LOCK.emscripten.repositoryCommit) { throw new Error( "The Emscripten 6.0.4 tag does not resolve to the pinned commit.", ); } const status = await run( git, ["status", "--porcelain=v1", "--untracked-files=all"], { cwd: repository }, ); if (status.stdout !== "") { throw new Error("The Emscripten checkout must be completely clean."); } return { emcc, emcmake }; } async function verifyNativeBuildTools() { const cmake = await resolveExecutable("cmake"); const ninja = await resolveExecutable("ninja"); const cmakeOutput = await run(cmake, ["--version"]); const ninjaOutput = await run(ninja, ["--version"]); if ( singleVersionLine(cmakeOutput.stdout) !== `cmake version ${PCRE2_LOCK.cmakeVersion}` ) { throw new Error( `The PCRE2 pack requires CMake ${PCRE2_LOCK.cmakeVersion}.`, ); } if (ninjaOutput.stdout.trim() !== PCRE2_LOCK.ninjaVersion) { throw new Error( `The PCRE2 pack requires Ninja ${PCRE2_LOCK.ninjaVersion}.`, ); } return { cmake, ninja }; } function parseValue(argv, index, option) { const value = argv[index + 1]; if (!value || value.startsWith("--")) { throw new Error(`${option} requires a path.`); } return value; } export function parsePcre2BuildArguments(argv) { if (argv.length === 0) return { mode: "ecmascript" }; if (argv.length === 1 && argv[0] === "--help") return { mode: "help" }; let pcre2 = false; let sourceDirectory; let emccFile; let force = false; for (let index = 0; index < argv.length; index += 1) { const argument = argv[index]; if (argument === "--pcre2") { if (pcre2) throw new Error("--pcre2 may be supplied only once."); pcre2 = true; } else if (argument === "--source-dir") { if (sourceDirectory) throw new Error("--source-dir may be supplied only once."); sourceDirectory = parseValue(argv, index, argument); index += 1; } else if (argument === "--emcc") { if (emccFile) throw new Error("--emcc may be supplied only once."); emccFile = parseValue(argv, index, argument); index += 1; } else if (argument === "--force") { if (force) throw new Error("--force may be supplied only once."); force = true; } else { throw new Error(`Unknown engine-build argument: ${argument}`); } } if (!pcre2) { throw new Error( "External engine options require the explicit --pcre2 gate.", ); } if (!sourceDirectory || !emccFile) { throw new Error( "The offline PCRE2 build requires both --source-dir and --emcc; it never downloads them.", ); } return { mode: "pcre2", sourceDirectory, emccFile, force }; } async function bridgeSourceFiles(root) { return Promise.all( BRIDGE_SOURCE_FILES.map(async (relativePath) => { const contents = await readFile(path.join(root, relativePath)); return { path: relativePath, sha256: sha256(contents), bytes: contents.byteLength, }; }), ); } async function describedFile(directory, name) { const contents = await readFile(path.join(directory, name)); return { path: name, sha256: sha256(contents), bytes: contents.byteLength }; } async function expectedMetadata(root, packDirectory) { const files = await Promise.all( ["LICENSE.txt", "pcre2.mjs", "pcre2.wasm"].map((name) => describedFile(packDirectory, name), ), ); return { schemaVersion: 1, engine: "pcre2", engineVersion: PCRE2_LOCK.source.version, status: "production-runtime", bridge: { abiVersion: PCRE2_LOCK.bridgeAbi, maximumPatternBytes: PCRE2_LOCK.maximumPatternBytes, maximumSubjectBytes: PCRE2_LOCK.maximumSubjectBytes, maximumReplacementBytes: PCRE2_LOCK.maximumReplacementBytes, maximumMatches: PCRE2_LOCK.maximumMatches, maximumCaptureRows: PCRE2_LOCK.maximumCaptureRows, maximumCaptureGroups: PCRE2_LOCK.maximumCaptureGroups, maximumTraceEvents: PCRE2_LOCK.maximumTraceEvents, maximumTraceBytes: PCRE2_LOCK.maximumTraceBytes, maximumTraceMarkBytes: PCRE2_LOCK.maximumTraceMarkBytes, sourceFiles: await bridgeSourceFiles(root), exports: [...EXPORTED_FUNCTIONS], }, source: { repository: PCRE2_LOCK.source.repository, tag: PCRE2_LOCK.source.tag, tagObjectSha1: PCRE2_LOCK.source.tagObject, commitSha1: PCRE2_LOCK.source.commit, treeSha1: PCRE2_LOCK.source.tree, license: PCRE2_LOCK.source.license, }, toolchain: { emscriptenVersion: PCRE2_LOCK.emscripten.version, emscriptenCompilerRevision: PCRE2_LOCK.emscripten.compilerRevision, emsdkCommitSha1: PCRE2_LOCK.emscripten.repositoryCommit, cmakeVersion: PCRE2_LOCK.cmakeVersion, ninjaVersion: PCRE2_LOCK.ninjaVersion, }, configuration: { codeUnitWidth: 8, unicode: true, jit: false, threads: false, filesystem: false, initialMemoryBytes: 16_777_216, maximumMemoryBytes: 268_435_456, }, sourceDateEpoch: PCRE2_LOCK.sourceDateEpoch, files, }; } function deterministicJson(value) { return `${JSON.stringify(value, null, 2)}\n`; } async function expectedChecksums(packDirectory) { const entries = await Promise.all( CHECKSUM_FILES.map( async (name) => `${await sha256File(path.join(packDirectory, name))} ${name}`, ), ); return `${entries.join("\n")}\n`; } function equalJson(left, right) { return JSON.stringify(left) === JSON.stringify(right); } async function smokePcre2Pack(packDirectory) { const wasm = await readFile(path.join(packDirectory, "pcre2.wasm")); if ( wasm.byteLength < 8 || !wasm.subarray(0, 4).equals(Buffer.from([0x00, 0x61, 0x73, 0x6d])) || !WebAssembly.validate(wasm) ) { throw new Error("pcre2.wasm is not a valid WebAssembly module."); } const moduleUrl = `${pathToFileURL(path.join(packDirectory, "pcre2.mjs")).href}?sha256=${sha256( await readFile(path.join(packDirectory, "pcre2.mjs")), )}`; const imported = await import(moduleUrl); if (typeof imported.default !== "function") { throw new Error("pcre2.mjs does not export its Emscripten factory."); } const diagnostics = []; const module = await imported.default({ locateFile(name) { return path.join(packDirectory, name); }, print() {}, printErr(line) { diagnostics.push(String(line)); }, }); if (module._regex_pcre2_bridge_abi_version() !== PCRE2_LOCK.bridgeAbi) { throw new Error("The WebAssembly bridge ABI version is incorrect."); } if (module._regex_pcre2_config_flags() !== 1) { throw new Error( "The WebAssembly pack must have Unicode enabled and JIT disabled.", ); } if ( module.UTF8ToString(module._regex_pcre2_version()) !== PCRE2_LOCK.source.version ) { throw new Error("The WebAssembly PCRE2 version is incorrect."); } if (module._regex_pcre2_self_test() !== 0) { throw new Error( `The WebAssembly bridge self-test failed: ${diagnostics.join("\n")}`, ); } const pattern = new TextEncoder().encode("(?\\p{L}+)-(?\\d+)"); const patternPointer = module._malloc(pattern.byteLength); const resultPointer = module._malloc(20); if (!patternPointer || !resultPointer) { if (patternPointer) module._free(patternPointer); if (resultPointer) module._free(resultPointer); throw new Error("The WebAssembly smoke test could not allocate memory."); } try { module.HEAPU8.set(pattern, patternPointer); module.HEAPU8.fill(0, resultPointer, resultPointer + 20); const status = module._regex_pcre2_compile_probe( patternPointer, pattern.byteLength, 0x20 | 0x40, resultPointer, ); const result = new DataView(module.HEAPU8.buffer, resultPointer, 20); if ( status !== 0 || result.getInt32(0, true) !== 0 || result.getUint32(8, true) !== 2 || result.getUint32(12, true) !== 2 ) { throw new Error("The WebAssembly compile-probe result is incorrect."); } module.HEAPU8.fill(0, resultPointer, resultPointer + 20); const unsupportedStatus = module._regex_pcre2_compile_probe( patternPointer, pattern.byteLength, 0x80000000, resultPointer, ); if (unsupportedStatus !== -10002 || result.getInt32(0, true) !== -10002) { throw new Error("The WebAssembly bridge accepted an unknown option bit."); } module.HEAPU8[patternPointer] = 0x28; module.HEAPU8.fill(0, resultPointer, resultPointer + 20); const invalidStatus = module._regex_pcre2_compile_probe( patternPointer, 1, 0, resultPointer, ); if ( invalidStatus <= 0 || result.getInt32(0, true) !== invalidStatus || result.getUint32(4, true) > 1 ) { throw new Error( "The WebAssembly bridge did not preserve the compile error.", ); } if ( module._regex_pcre2_compile_probe(0, 1, 0, resultPointer) !== -10001 || module._regex_pcre2_compile_probe( patternPointer, PCRE2_LOCK.maximumPatternBytes + 1, 0, resultPointer, ) !== -10003 ) { throw new Error("The WebAssembly bridge input limits are not enforced."); } } finally { module._free(resultPointer); module._free(patternPointer); } const runPattern = new TextEncoder().encode("(?😀)|(?\\p{L}+)"); const runSubject = new TextEncoder().encode("x😀 Grüße"); const runPointers = []; const allocate = (bytes) => { const pointer = module._malloc(Math.max(1, bytes)); if (!pointer) throw new Error("The PCRE2 runtime smoke allocation failed."); runPointers.push(pointer); return pointer; }; try { const runPatternPointer = allocate(runPattern.byteLength); const runSubjectPointer = allocate(runSubject.byteLength); const limitsPointer = allocate(20); const recordsPointer = allocate(20 * 16); const namesPointer = allocate(10 * 12); const nameBytesPointer = allocate(256); const runResultPointer = allocate(60); module.HEAPU8.set(runPattern, runPatternPointer); module.HEAPU8.set(runSubject, runSubjectPointer); let view = new DataView(module.HEAPU8.buffer); view.setUint32(limitsPointer, 10, true); view.setUint32(limitsPointer + 4, 100, true); view.setUint32(limitsPointer + 8, 1_000_000, true); view.setUint32(limitsPointer + 12, 1_000, true); view.setUint32(limitsPointer + 16, 32_768, true); module.HEAPU8.fill(0, runResultPointer, runResultPointer + 60); const runStatus = module._regex_pcre2_execute( runPatternPointer, runPattern.byteLength, runSubjectPointer, runSubject.byteLength, 0x20 | 0x40 | 0x100, limitsPointer, recordsPointer, 20, namesPointer, 10, nameBytesPointer, 256, runResultPointer, ); view = new DataView(module.HEAPU8.buffer); if ( runStatus !== 0 || view.getInt32(runResultPointer, true) !== 0 || view.getUint32(runResultPointer + 12, true) !== 2 || view.getUint32(runResultPointer + 20, true) !== 3 || view.getUint32(runResultPointer + 24, true) !== 9 || view.getUint32(recordsPointer + 3 * 16 + 8, true) !== 1 || view.getUint32(recordsPointer + 3 * 16 + 12, true) !== 5 ) { throw new Error("The WebAssembly bounded-match smoke result is wrong."); } const replacementPattern = new TextEncoder().encode("(?\\p{L}+)"); const replacementSubject = new TextEncoder().encode("Grüße 42"); const replacement = new TextEncoder().encode("${word}!"); const replacementPatternPointer = allocate(replacementPattern.byteLength); const replacementSubjectPointer = allocate(replacementSubject.byteLength); const replacementPointer = allocate(replacement.byteLength); const outputPointer = allocate(65); module.HEAPU8.set(replacementPattern, replacementPatternPointer); module.HEAPU8.set(replacementSubject, replacementSubjectPointer); module.HEAPU8.set(replacement, replacementPointer); module.HEAPU8.fill(0, runResultPointer, runResultPointer + 60); const substituteStatus = module._regex_pcre2_substitute( replacementPatternPointer, replacementPattern.byteLength, replacementSubjectPointer, replacementSubject.byteLength, replacementPointer, replacement.byteLength, 0x20 | 0x40 | 0x100, limitsPointer, recordsPointer, 20, namesPointer, 10, nameBytesPointer, 256, outputPointer, 64, runResultPointer, ); view = new DataView(module.HEAPU8.buffer); const outputLength = view.getUint32(runResultPointer + 48, true); const output = new TextDecoder().decode( module.HEAPU8.slice(outputPointer, outputPointer + outputLength), ); if ( substituteStatus !== 0 || output !== "Grüße! 42" || view.getUint32(runResultPointer + 52, true) !== 1 || view.getUint32(runResultPointer + 56, true) !== 0 ) { throw new Error( "The WebAssembly bounded-substitution smoke result is wrong.", ); } module.HEAPU8.fill(0, runResultPointer, runResultPointer + 60); const missingOutputStatus = module._regex_pcre2_substitute( replacementPatternPointer, replacementPattern.byteLength, replacementSubjectPointer, replacementSubject.byteLength, replacementPointer, replacement.byteLength, 0x20 | 0x40, limitsPointer, recordsPointer, 20, namesPointer, 10, nameBytesPointer, 256, 0, 0, runResultPointer, ); if ( missingOutputStatus !== -10001 || view.getInt32(runResultPointer, true) !== -10001 ) { throw new Error( "The WebAssembly substitution bridge accepted a missing NUL byte.", ); } const traceEventsPointer = allocate(64 * 32); const traceMarksPointer = allocate(256); const traceResultPointer = allocate(52); view.setUint32(limitsPointer, 64, true); view.setUint32(limitsPointer + 4, 4_096, true); module.HEAPU8.fill(0, traceResultPointer, traceResultPointer + 52); const traceStatus = module._regex_pcre2_trace( runPatternPointer, runPattern.byteLength, runSubjectPointer, runSubject.byteLength, 0x20 | 0x40, limitsPointer, traceEventsPointer, 64, traceMarksPointer, 256, traceResultPointer, ); view = new DataView(module.HEAPU8.buffer); const traceEventCount = view.getUint32(traceResultPointer + 20, true); if ( traceStatus !== 0 || view.getInt32(traceResultPointer, true) !== 0 || view.getInt32(traceResultPointer + 12, true) <= 0 || traceEventCount === 0 || view.getUint32(traceResultPointer + 24, true) !== traceEventCount || view.getUint32(traceResultPointer + 36, true) !== 0 || view.getUint32(traceResultPointer + 44, true) !== traceEventCount - 1 || view.getUint32(traceResultPointer + 48, true) !== 1 ) { throw new Error("The WebAssembly automatic-callout trace is wrong."); } view.setUint32(limitsPointer, 1, true); module.HEAPU8.fill(0, traceResultPointer, traceResultPointer + 52); const boundedTraceStatus = module._regex_pcre2_trace( runPatternPointer, runPattern.byteLength, runSubjectPointer, runSubject.byteLength, 0x20 | 0x40, limitsPointer, traceEventsPointer, 64, traceMarksPointer, 256, traceResultPointer, ); if ( boundedTraceStatus !== 0 || view.getInt32(traceResultPointer, true) !== 0 || view.getInt32(traceResultPointer + 12, true) !== -37 || view.getUint32(traceResultPointer + 20, true) !== 1 || view.getUint32(traceResultPointer + 24, true) !== 2 || view.getUint32(traceResultPointer + 36, true) !== 1 || view.getUint32(traceResultPointer + 44, true) !== 0 ) { throw new Error("The WebAssembly trace cap is not authoritative."); } } finally { for (const pointer of runPointers.reverse()) module._free(pointer); } } export async function verifyPcre2Pack(packDirectory, root) { const repositoryRoot = await assertRealDirectory( root, "Regex Tools repository", ); const pack = await assertRealDirectory(packDirectory, "PCRE2 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 PCRE2 pack must contain only: ${PACK_FILES.join(", ")}.`, ); } await assertHash( path.join(pack, "LICENSE.txt"), PCRE2_LOCK.source.criticalFiles["LICENCE.md"], "staged PCRE2 licence", ); const moduleText = await readFile(path.join(pack, "pcre2.mjs"), "utf8"); if ( moduleText.includes("sourceMappingURL") || moduleText.includes("/mnt/") || moduleText.includes("/home/") ) { throw new Error("pcre2.mjs leaks a build path or source-map reference."); } 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, }); } const expected = await expectedMetadata(repositoryRoot, pack); if (!equalJson(metadata, expected)) { throw new Error( "The staged PCRE2 metadata does not match the pinned build contract.", ); } const checksums = await readFile(path.join(pack, "SHA256SUMS"), "utf8"); if (checksums !== (await expectedChecksums(pack))) { throw new Error("The staged PCRE2 SHA256SUMS file is incorrect."); } await smokePcre2Pack(pack); return metadata; } export async function installPcre2Pack(packDirectory, root) { const repositoryRoot = await assertRealDirectory( root, "Regex Tools repository", ); const sourcePack = await assertRealDirectory( packDirectory, "verified PCRE2 pack", ); const metadata = await verifyPcre2Pack(sourcePack, repositoryRoot); const engineRoot = await assertRealDirectory( path.join(repositoryRoot, "public", "engines"), "public engine directory", ); const target = path.join(engineRoot, "pcre2"); const stage = await mkdtemp(path.join(engineRoot, ".pcre2-install-")); const targetDetails = await lstat(target).catch(() => null); if ( targetDetails && (!targetDetails.isDirectory() || targetDetails.isSymbolicLink()) ) { await rm(stage, { recursive: true, force: true }); throw new Error("public/engines/pcre2 must be a real directory."); } try { for (const name of PACK_FILES) { await copyFile( path.join(sourcePack, name), path.join(stage, name), fsConstants.COPYFILE_EXCL, ); } await verifyPcre2Pack(stage, repositoryRoot); if (targetDetails) { const backup = path.join(engineRoot, `.pcre2-replaced-${process.pid}`); await rename(target, backup); try { await rename(stage, target); } catch (error) { await rename(backup, target); throw error; } await rm(backup, { recursive: true }); } else { await rename(stage, target); } return { output: target, metadata }; } finally { await rm(stage, { recursive: true, force: true }); } } function cleanBuildEnvironment() { const environment = { ...process.env }; for (const variable of [ "CFLAGS", "CPPFLAGS", "CXXFLAGS", "DESTDIR", "EMCC_CFLAGS", "EMMAKEN_CFLAGS", "LDFLAGS", ]) { delete environment[variable]; } environment.LANG = "C"; environment.LC_ALL = "C"; environment.SOURCE_DATE_EPOCH = String(PCRE2_LOCK.sourceDateEpoch); environment.TZ = "UTC"; environment.ZERO_AR_DATE = "1"; return environment; } async function ensurePrivateBuildRoot(root) { const buildRoot = path.join(root, ".engine-build"); const existing = await lstat(buildRoot).catch(() => null); if (existing && (!existing.isDirectory() || existing.isSymbolicLink())) { throw new Error(".engine-build must be a real directory."); } await mkdir(buildRoot, { recursive: true }); return realpath(buildRoot); } function assertGeneratedChild(buildRoot, candidate) { const relative = path.relative(buildRoot, candidate); if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) { throw new Error( `Refusing to remove a path outside .engine-build: ${candidate}`, ); } } export async function buildPcre2Pack(options, root) { const repositoryRoot = await assertRealDirectory( root, "Regex Tools repository", ); const source = await verifyPcre2Source(options.sourceDirectory); const { emcmake } = await verifyEmscripten(options.emccFile); const { cmake, ninja } = await verifyNativeBuildTools(); const buildRoot = await ensurePrivateBuildRoot(repositoryRoot); const output = path.join(buildRoot, "pcre2"); const outputDetails = await lstat(output).catch(() => null); if (outputDetails && !options.force) { throw new Error( `${output} already exists; pass --force to replace that exact staged pack.`, ); } const work = await mkdtemp(path.join(buildRoot, ".pcre2-work-")); const stage = await mkdtemp(path.join(buildRoot, ".pcre2-stage-")); assertGeneratedChild(buildRoot, work); assertGeneratedChild(buildRoot, stage); const environment = cleanBuildEnvironment(); try { await run( emcmake, [ cmake, "-S", path.join(repositoryRoot, "engines", "pcre2"), "-B", work, "-G", "Ninja", "-DCMAKE_BUILD_TYPE=Release", `-DCMAKE_MAKE_PROGRAM=${ninja}`, `-DPCRE2_SOURCE_DIR=${source}`, `-DREGEX_PCRE2_OUTPUT_DIR=${stage}`, ], { cwd: repositoryRoot, env: environment }, ); await run( cmake, ["--build", work, "--target", "regex-pcre2-pack", "--parallel", "1"], { cwd: repositoryRoot, env: environment, }, ); const generatedEntries = (await readdir(stage)).sort(); if ( generatedEntries.length !== 2 || generatedEntries[0] !== "pcre2.mjs" || generatedEntries[1] !== "pcre2.wasm" ) { throw new Error( `Emscripten produced unexpected files: ${generatedEntries.join(", ") || "(none)"}.`, ); } await copyFile( path.join(source, "LICENCE.md"), path.join(stage, "LICENSE.txt"), fsConstants.COPYFILE_EXCL, ); const metadata = await expectedMetadata(repositoryRoot, stage); await writeFile( path.join(stage, "engine-metadata.json"), deterministicJson(metadata), { encoding: "utf8", flag: "wx" }, ); await writeFile( path.join(stage, "SHA256SUMS"), await expectedChecksums(stage), { encoding: "utf8", flag: "wx" }, ); await verifyPcre2Pack(stage, repositoryRoot); if (outputDetails) { const backup = path.join(buildRoot, `.pcre2-replaced-${process.pid}`); assertGeneratedChild(buildRoot, backup); await rename(output, backup); try { await rename(stage, output); } catch (error) { await rename(backup, output); throw error; } await rm(backup, { recursive: true }); } else { await rename(stage, output); } return { output, metadata }; } finally { await rm(work, { recursive: true, force: true }); await rm(stage, { recursive: true, force: true }); } }