feat: add multi-engine regex flavour support
This commit is contained in:
806
scripts/rust-engine-pack.mjs
Normal file
806
scripts/rust-engine-pack.mjs
Normal file
@@ -0,0 +1,806 @@
|
||||
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";
|
||||
|
||||
export const RUST_LOCK = Object.freeze({
|
||||
bridgeAbi: 1,
|
||||
engineVersion: "1.13.1",
|
||||
rustcVersion: "1.97.1",
|
||||
rustcIdentity: "rustc 1.97.1 (8bab26f4f 2026-07-14)",
|
||||
rustcCommit: "8bab26f4f68e0e26f0bb7960be334d5b520ea452",
|
||||
cargoIdentity: "cargo 1.97.1 (c980f4866 2026-06-30)",
|
||||
llvmVersion: "22.1.6",
|
||||
wasmBindgenVersion: "0.2.126",
|
||||
wasmBindgenCrateChecksum:
|
||||
"4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4",
|
||||
wasmBindgenCliChecksum:
|
||||
"8e2ebf6eef87c34e662347f1dddfbea541304bb7294711ed2c2acd8746cc5b50",
|
||||
sourceDateEpoch: 1_784_146_753,
|
||||
source: Object.freeze({
|
||||
repository: "https://github.com/rust-lang/regex",
|
||||
tag: "1.13.1",
|
||||
tagObject: "7c28ee4dc4aef53571c96757f4825c51ac0c3d44",
|
||||
commit: "2b527599eb9eea0dcc288c704584f242f26a5c61",
|
||||
tree: "25bea9f6f55a4cf528afefaa00f5335c595c182c",
|
||||
crateChecksum:
|
||||
"f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d",
|
||||
license: "MIT OR Apache-2.0",
|
||||
}),
|
||||
distribution: Object.freeze({
|
||||
manifest: "https://static.rust-lang.org/dist/channel-rust-1.97.1.toml",
|
||||
manifestSha256:
|
||||
"03569b1886ceb5c05276b50c8431ab111de944cd6140fe1fa7d821dd8e0f29cf",
|
||||
rustcArchiveSha256:
|
||||
"1c441e430c1cca49dff54a8d59c41038bf6f79f7b8756596cb2f36511a015eba",
|
||||
wasmStdArchiveSha256:
|
||||
"13902d5573eeea50701d75acc774b6df2dfb4942ec88cdbe40bb07e448c307ea",
|
||||
licenseHashes: Object.freeze({
|
||||
apache:
|
||||
"074e6e32c86a4c0ef8b3ed25b721ca23aca83df277cd88106ef7177c354615ff",
|
||||
mit: "b85dcd3e453d05982552c52b5fc9e0bdd6d23c6f8e844b984a88af32570b0cc0",
|
||||
unicode:
|
||||
"f5062c9a188d81dfe66b56db4182dcf9e4b17c0d9b0d311a8e20b3a1b075c443",
|
||||
stdlibCopyright:
|
||||
"0a65bb747c49c7bb816cbc7188319bd6e4e8d08091c1190b8a3c0971c47968ed",
|
||||
}),
|
||||
}),
|
||||
limits: Object.freeze({
|
||||
maximumPatternBytes: 262_144,
|
||||
maximumSubjectBytes: 16_777_216,
|
||||
maximumReplacementBytes: 262_144,
|
||||
maximumMatches: 10_000,
|
||||
maximumCaptureRows: 100_000,
|
||||
maximumCaptureGroups: 1_000,
|
||||
maximumOutputBytes: 67_108_864,
|
||||
maximumRequestJSONBytes: 103_874_560,
|
||||
compiledSizeLimitBytes: 10_485_760,
|
||||
dfaSizeLimitBytes: 2_097_152,
|
||||
}),
|
||||
});
|
||||
|
||||
const PACK_FILES = Object.freeze([
|
||||
"LICENSE-Apache-2.0.txt",
|
||||
"LICENSE-MIT.txt",
|
||||
"LICENSE-Unicode-3.0.txt",
|
||||
"RUST-STDLIB-COPYRIGHT.html",
|
||||
"SHA256SUMS",
|
||||
"THIRD_PARTY_LICENSES.txt",
|
||||
"engine-metadata.json",
|
||||
"rust-regex.mjs",
|
||||
"rust-regex_bg.wasm",
|
||||
]);
|
||||
const CHECKSUM_FILES = Object.freeze(
|
||||
PACK_FILES.filter((name) => name !== "SHA256SUMS"),
|
||||
);
|
||||
const SOURCE_FILES = Object.freeze([
|
||||
"engines/rust/Cargo.lock",
|
||||
"engines/rust/Cargo.toml",
|
||||
"engines/rust/README.md",
|
||||
"engines/rust/rust-toolchain.toml",
|
||||
"engines/rust/src/lib.rs",
|
||||
]);
|
||||
|
||||
function sha256(value) {
|
||||
return createHash("sha256").update(value).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 file: ${candidate}`);
|
||||
}
|
||||
return realpath(candidate);
|
||||
}
|
||||
|
||||
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 };
|
||||
}
|
||||
|
||||
function parseCargoLock(contents) {
|
||||
const packages = [];
|
||||
for (const block of contents.split("[[package]]").slice(1)) {
|
||||
const field = (name) => {
|
||||
const match = block.match(new RegExp(`^${name} = "([^"]+)"`, "mu"));
|
||||
return match?.[1];
|
||||
};
|
||||
const name = field("name");
|
||||
const version = field("version");
|
||||
if (!name || !version) {
|
||||
throw new Error("Cargo.lock contains a malformed package block.");
|
||||
}
|
||||
const source = field("source");
|
||||
const checksum = field("checksum");
|
||||
packages.push({
|
||||
name,
|
||||
version,
|
||||
...(source ? { source } : {}),
|
||||
...(checksum ? { checksum } : {}),
|
||||
});
|
||||
}
|
||||
return packages;
|
||||
}
|
||||
|
||||
async function findRegistryCrate(cargoHome, package_) {
|
||||
const registrySource = path.join(cargoHome, "registry", "src");
|
||||
const indexes = await readdir(registrySource, { withFileTypes: true });
|
||||
const matches = [];
|
||||
for (const index of indexes) {
|
||||
if (!index.isDirectory() || index.isSymbolicLink()) continue;
|
||||
const candidate = path.join(
|
||||
registrySource,
|
||||
index.name,
|
||||
`${package_.name}-${package_.version}`,
|
||||
);
|
||||
const details = await lstat(candidate).catch(() => null);
|
||||
if (details?.isDirectory() && !details.isSymbolicLink()) {
|
||||
matches.push(await realpath(candidate));
|
||||
}
|
||||
}
|
||||
if (matches.length !== 1) {
|
||||
throw new Error(
|
||||
`Expected one cached source directory for ${package_.name} ${package_.version}.`,
|
||||
);
|
||||
}
|
||||
return matches[0];
|
||||
}
|
||||
|
||||
async function thirdPartyLicenses(cargoHome, packages) {
|
||||
const sections = [
|
||||
"Regex Tools Rust engine third-party source notices",
|
||||
"Generated from the checked-in Cargo.lock; package order is deterministic.",
|
||||
"",
|
||||
];
|
||||
for (const package_ of packages.filter(({ source }) => source)) {
|
||||
const crate = await findRegistryCrate(cargoHome, package_);
|
||||
const entries = (await readdir(crate, { withFileTypes: true }))
|
||||
.filter(
|
||||
(entry) =>
|
||||
entry.isFile() &&
|
||||
!entry.isSymbolicLink() &&
|
||||
/^(?:COPYING|COPYRIGHT|LICENSE|UNLICENSE)/iu.test(entry.name),
|
||||
)
|
||||
.sort((left, right) =>
|
||||
left.name < right.name ? -1 : left.name > right.name ? 1 : 0,
|
||||
);
|
||||
sections.push(
|
||||
`===============================================================================`,
|
||||
`${package_.name} ${package_.version}`,
|
||||
`Cargo source: ${package_.source}`,
|
||||
`Cargo checksum: ${package_.checksum ?? "not recorded"}`,
|
||||
);
|
||||
if (entries.length === 0) {
|
||||
sections.push(
|
||||
"No root licence file was present in the packaged crate source.",
|
||||
"",
|
||||
);
|
||||
continue;
|
||||
}
|
||||
for (const entry of entries) {
|
||||
sections.push(
|
||||
`-------------------------------------------------------------------------------`,
|
||||
entry.name,
|
||||
"",
|
||||
(await readFile(path.join(crate, entry.name), "utf8")).trimEnd(),
|
||||
"",
|
||||
);
|
||||
}
|
||||
}
|
||||
return `${sections.join("\n")}\n`;
|
||||
}
|
||||
|
||||
async function verifyToolchain(toolchainRootDirectory) {
|
||||
const root = await assertRealDirectory(
|
||||
toolchainRootDirectory,
|
||||
"Rust 1.97.1 toolchain",
|
||||
);
|
||||
const cargoHome = await assertRealDirectory(
|
||||
path.join(root, "cargo"),
|
||||
"Rust Cargo home",
|
||||
);
|
||||
const rustupHome = await assertRealDirectory(
|
||||
path.join(root, "rustup"),
|
||||
"Rustup home",
|
||||
);
|
||||
const toolchain = await assertRealDirectory(
|
||||
path.join(rustupHome, "toolchains", "1.97.1-x86_64-unknown-linux-gnu"),
|
||||
"Rust distribution toolchain",
|
||||
);
|
||||
const cargo = await assertRegularFile(
|
||||
path.join(toolchain, "bin", "cargo"),
|
||||
"cargo",
|
||||
);
|
||||
const rustc = await assertRegularFile(
|
||||
path.join(toolchain, "bin", "rustc"),
|
||||
"rustc",
|
||||
);
|
||||
const wasmBindgen = await assertRegularFile(
|
||||
path.join(cargoHome, "bin", "wasm-bindgen"),
|
||||
"wasm-bindgen",
|
||||
);
|
||||
for (const executable of [cargo, rustc, wasmBindgen]) {
|
||||
await access(executable, fsConstants.X_OK);
|
||||
}
|
||||
const baseEnvironment = {
|
||||
...process.env,
|
||||
CARGO_HOME: cargoHome,
|
||||
RUSTUP_HOME: rustupHome,
|
||||
PATH: [
|
||||
path.join(toolchain, "bin"),
|
||||
path.join(cargoHome, "bin"),
|
||||
process.env.PATH ?? "",
|
||||
].join(path.delimiter),
|
||||
};
|
||||
const rustcVersion = await run(rustc, ["--version", "--verbose"], {
|
||||
env: baseEnvironment,
|
||||
});
|
||||
if (
|
||||
!rustcVersion.stdout.includes(`release: ${RUST_LOCK.rustcVersion}`) ||
|
||||
!rustcVersion.stdout.includes(`commit-hash: ${RUST_LOCK.rustcCommit}`) ||
|
||||
!rustcVersion.stdout.includes(`LLVM version: ${RUST_LOCK.llvmVersion}`)
|
||||
) {
|
||||
throw new Error("The Rust engine requires pinned rustc 1.97.1.");
|
||||
}
|
||||
const cargoVersion = await run(cargo, ["--version"], {
|
||||
env: baseEnvironment,
|
||||
});
|
||||
if (cargoVersion.stdout.trim() !== RUST_LOCK.cargoIdentity) {
|
||||
throw new Error("The Rust engine requires pinned Cargo 1.97.1.");
|
||||
}
|
||||
const bindgenVersion = await run(wasmBindgen, ["--version"], {
|
||||
env: baseEnvironment,
|
||||
});
|
||||
if (
|
||||
bindgenVersion.stdout.trim() !==
|
||||
`wasm-bindgen ${RUST_LOCK.wasmBindgenVersion}`
|
||||
) {
|
||||
throw new Error("The Rust engine requires pinned wasm-bindgen 0.2.126.");
|
||||
}
|
||||
|
||||
const docs = path.join(toolchain, "share", "doc", "rust");
|
||||
const files = {
|
||||
apache: await assertRegularFile(
|
||||
path.join(docs, "licenses", "Apache-2.0.txt"),
|
||||
"Rust Apache licence",
|
||||
),
|
||||
mit: await assertRegularFile(
|
||||
path.join(docs, "licenses", "MIT.txt"),
|
||||
"Rust MIT licence",
|
||||
),
|
||||
unicode: await assertRegularFile(
|
||||
path.join(docs, "licenses", "Unicode-3.0.txt"),
|
||||
"Rust Unicode licence",
|
||||
),
|
||||
stdlibCopyright: await assertRegularFile(
|
||||
path.join(docs, "COPYRIGHT-library.html"),
|
||||
"Rust standard-library copyright notice",
|
||||
),
|
||||
};
|
||||
for (const [name, file] of Object.entries(files)) {
|
||||
if (
|
||||
(await sha256File(file)) !== RUST_LOCK.distribution.licenseHashes[name]
|
||||
) {
|
||||
throw new Error(`The pinned Rust ${name} notice does not match 1.97.1.`);
|
||||
}
|
||||
}
|
||||
return {
|
||||
root,
|
||||
cargoHome,
|
||||
rustupHome,
|
||||
cargo,
|
||||
rustc,
|
||||
wasmBindgen,
|
||||
baseEnvironment,
|
||||
files,
|
||||
};
|
||||
}
|
||||
|
||||
function buildEnvironment(toolchain, repositoryRoot, targetDirectory) {
|
||||
const environment = { ...toolchain.baseEnvironment };
|
||||
for (const variable of [
|
||||
"CARGO_BUILD_RUSTC",
|
||||
"CARGO_ENCODED_RUSTFLAGS",
|
||||
"CARGO_PROFILE_RELEASE_DEBUG",
|
||||
"RUSTC",
|
||||
"RUSTDOC",
|
||||
"RUSTDOCFLAGS",
|
||||
"RUSTFLAGS",
|
||||
]) {
|
||||
delete environment[variable];
|
||||
}
|
||||
environment.CARGO_INCREMENTAL = "0";
|
||||
environment.CARGO_NET_OFFLINE = "true";
|
||||
environment.CARGO_TARGET_DIR = targetDirectory;
|
||||
environment.LANG = "C";
|
||||
environment.LC_ALL = "C";
|
||||
environment.REGEX_TOOLS_RUSTC_VERSION = RUST_LOCK.rustcIdentity;
|
||||
environment.RUST_BACKTRACE = "0";
|
||||
environment.RUSTUP_TOOLCHAIN = "1.97.1-x86_64-unknown-linux-gnu";
|
||||
environment.RUSTFLAGS = [
|
||||
`--remap-path-prefix=${repositoryRoot}=.`,
|
||||
`--remap-path-prefix=${toolchain.cargoHome}=./cargo-home`,
|
||||
].join(" ");
|
||||
environment.SOURCE_DATE_EPOCH = String(RUST_LOCK.sourceDateEpoch);
|
||||
environment.TZ = "UTC";
|
||||
return environment;
|
||||
}
|
||||
|
||||
async function sourceMetadata(root) {
|
||||
return Promise.all(
|
||||
SOURCE_FILES.map(async (relativePath) => {
|
||||
const contents = await readFile(path.join(root, relativePath));
|
||||
return {
|
||||
path: relativePath,
|
||||
sha256: sha256(contents),
|
||||
bytes: contents.byteLength,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
async function fileMetadata(pack, name) {
|
||||
const contents = await readFile(path.join(pack, name));
|
||||
return { path: name, sha256: sha256(contents), bytes: contents.byteLength };
|
||||
}
|
||||
|
||||
async function lockedPackages(root) {
|
||||
const contents = await readFile(
|
||||
path.join(root, "engines", "rust", "Cargo.lock"),
|
||||
"utf8",
|
||||
);
|
||||
return parseCargoLock(contents)
|
||||
.filter(({ source }) => source)
|
||||
.map(({ name, version, checksum }) => ({ name, version, checksum }));
|
||||
}
|
||||
|
||||
async function expectedMetadata(root, pack) {
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
engine: "rust",
|
||||
engineName: "Rust regex crate",
|
||||
engineVersion: RUST_LOCK.engineVersion,
|
||||
semanticIdentity:
|
||||
"Rust regex crate 1.13.1 compiled to WebAssembly with rustc 1.97.1",
|
||||
status: "production-native-runtime",
|
||||
bridge: {
|
||||
abiVersion: RUST_LOCK.bridgeAbi,
|
||||
offsetUnit: "utf8-byte",
|
||||
requestEncoding:
|
||||
"Exact UTF-8-bounded JSON values with worst-case escape allowance; fixed exports; base64 UTF-8 replacement output; no source evaluation",
|
||||
supportedApplicationFlags: ["g", "i", "m", "s", "U", "u", "x", "R"],
|
||||
unicode:
|
||||
"Enabled by default, matching the Rust regex crate; u explicitly affirms that default.",
|
||||
replacement:
|
||||
"Bounded streaming expansion with regex::Captures::expand grammar, parity-checked against the native API.",
|
||||
limits: { ...RUST_LOCK.limits },
|
||||
sourceFiles: await sourceMetadata(root),
|
||||
},
|
||||
source: {
|
||||
repository: RUST_LOCK.source.repository,
|
||||
tag: RUST_LOCK.source.tag,
|
||||
tagObjectSha1: RUST_LOCK.source.tagObject,
|
||||
commitSha1: RUST_LOCK.source.commit,
|
||||
treeSha1: RUST_LOCK.source.tree,
|
||||
crateChecksumSha256: RUST_LOCK.source.crateChecksum,
|
||||
license: RUST_LOCK.source.license,
|
||||
},
|
||||
toolchain: {
|
||||
rustcVersion: RUST_LOCK.rustcVersion,
|
||||
rustcIdentity: RUST_LOCK.rustcIdentity,
|
||||
rustcCommitSha1: RUST_LOCK.rustcCommit,
|
||||
llvmVersion: RUST_LOCK.llvmVersion,
|
||||
cargoIdentity: RUST_LOCK.cargoIdentity,
|
||||
target: "wasm32-unknown-unknown",
|
||||
wasmBindgenVersion: RUST_LOCK.wasmBindgenVersion,
|
||||
wasmBindgenCrateChecksumSha256: RUST_LOCK.wasmBindgenCrateChecksum,
|
||||
wasmBindgenCliChecksumSha256: RUST_LOCK.wasmBindgenCliChecksum,
|
||||
distributionManifest: RUST_LOCK.distribution.manifest,
|
||||
distributionManifestSha256: RUST_LOCK.distribution.manifestSha256,
|
||||
rustcArchiveSha256: RUST_LOCK.distribution.rustcArchiveSha256,
|
||||
wasmStdArchiveSha256: RUST_LOCK.distribution.wasmStdArchiveSha256,
|
||||
cargoLockPackages: await lockedPackages(root),
|
||||
},
|
||||
sourceDateEpoch: RUST_LOCK.sourceDateEpoch,
|
||||
files: await Promise.all(
|
||||
CHECKSUM_FILES.filter((name) => name !== "engine-metadata.json").map(
|
||||
(name) => fileMetadata(pack, name),
|
||||
),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
async function checksumText(pack) {
|
||||
const lines = await Promise.all(
|
||||
CHECKSUM_FILES.map(
|
||||
async (name) => `${await sha256File(path.join(pack, name))} ${name}`,
|
||||
),
|
||||
);
|
||||
return `${lines.join("\n")}\n`;
|
||||
}
|
||||
|
||||
function equalJson(left, right) {
|
||||
return JSON.stringify(left) === JSON.stringify(right);
|
||||
}
|
||||
|
||||
async function smokePack(pack) {
|
||||
const moduleUrl = new URL(`file://${path.join(pack, "rust-regex.mjs")}`).href;
|
||||
const wasmPath = path.join(pack, "rust-regex_bg.wasm");
|
||||
const smoke = `
|
||||
import { readFile } from "node:fs/promises";
|
||||
const engine = await import(process.argv[1]);
|
||||
engine.initSync({
|
||||
module: await WebAssembly.compile(await readFile(process.argv[2])),
|
||||
});
|
||||
const identity = JSON.parse(engine.identity());
|
||||
if (
|
||||
identity.abi !== 1 ||
|
||||
identity.ok !== true ||
|
||||
identity.identity.engineVersion !== "1.13.1" ||
|
||||
identity.identity.rustcVersion !==
|
||||
"rustc 1.97.1 (8bab26f4f 2026-07-14)" ||
|
||||
identity.identity.selfTest !== true
|
||||
) throw new Error("Rust identity self-test failed.");
|
||||
const result = JSON.parse(engine.run(JSON.stringify({
|
||||
abi: 1,
|
||||
operation: "replace",
|
||||
pattern: "(?P<word>😀+)",
|
||||
flags: "g",
|
||||
options: {},
|
||||
subject: "x😀😀",
|
||||
scanAll: false,
|
||||
maximumMatches: 10,
|
||||
maximumCaptureRows: 10,
|
||||
replacement: "\${word}-$$",
|
||||
maximumOutputBytes: 100,
|
||||
})));
|
||||
if (
|
||||
result.ok !== true ||
|
||||
result.matches[0].span[0] !== 1 ||
|
||||
result.matches[0].span[1] !== 9 ||
|
||||
Buffer.from(result.outputBase64, "base64").toString("utf8") !== "x😀😀-$"
|
||||
) throw new Error("Rust execution self-test failed.");
|
||||
`;
|
||||
await run(process.execPath, [
|
||||
"--input-type=module",
|
||||
"--eval",
|
||||
smoke,
|
||||
moduleUrl,
|
||||
wasmPath,
|
||||
]);
|
||||
}
|
||||
|
||||
export async function verifyRustPack(packDirectory, root) {
|
||||
const repositoryRoot = await assertRealDirectory(
|
||||
root,
|
||||
"Regex Tools repository",
|
||||
);
|
||||
const pack = await assertRealDirectory(packDirectory, "Rust 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 Rust pack must contain only: ${PACK_FILES.join(", ")}.`,
|
||||
);
|
||||
}
|
||||
const licenseFiles = {
|
||||
"LICENSE-Apache-2.0.txt": RUST_LOCK.distribution.licenseHashes.apache,
|
||||
"LICENSE-MIT.txt": RUST_LOCK.distribution.licenseHashes.mit,
|
||||
"LICENSE-Unicode-3.0.txt": RUST_LOCK.distribution.licenseHashes.unicode,
|
||||
"RUST-STDLIB-COPYRIGHT.html":
|
||||
RUST_LOCK.distribution.licenseHashes.stdlibCopyright,
|
||||
};
|
||||
for (const [name, digest] of Object.entries(licenseFiles)) {
|
||||
if ((await sha256File(path.join(pack, name))) !== digest) {
|
||||
throw new Error(`${name} does not match the Rust 1.97.1 distribution.`);
|
||||
}
|
||||
}
|
||||
const wasm = await readFile(path.join(pack, "rust-regex_bg.wasm"));
|
||||
if (
|
||||
wasm.byteLength < 100_000 ||
|
||||
wasm.subarray(0, 4).toString("hex") !== "0061736d"
|
||||
) {
|
||||
throw new Error("The staged Rust WebAssembly module is invalid.");
|
||||
}
|
||||
const moduleText = await readFile(path.join(pack, "rust-regex.mjs"), "utf8");
|
||||
if (
|
||||
moduleText.includes("sourceMappingURL") ||
|
||||
moduleText.includes("/mnt/") ||
|
||||
moduleText.includes("/home/") ||
|
||||
!moduleText.includes("export function identity") ||
|
||||
!moduleText.includes("export function run") ||
|
||||
!moduleText.includes("export { initSync")
|
||||
) {
|
||||
throw new Error(
|
||||
"rust-regex.mjs leaks a build path, has a source map, or lacks bridge exports.",
|
||||
);
|
||||
}
|
||||
const packages = await lockedPackages(repositoryRoot);
|
||||
const notices = await readFile(
|
||||
path.join(pack, "THIRD_PARTY_LICENSES.txt"),
|
||||
"utf8",
|
||||
);
|
||||
for (const package_ of packages) {
|
||||
if (
|
||||
!notices.includes(`${package_.name} ${package_.version}\n`) ||
|
||||
!notices.includes(`Cargo checksum: ${package_.checksum}`)
|
||||
) {
|
||||
throw new Error(
|
||||
`Third-party notices omit ${package_.name} ${package_.version}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let metadata;
|
||||
try {
|
||||
metadata = JSON.parse(
|
||||
await readFile(path.join(pack, "engine-metadata.json"), "utf8"),
|
||||
);
|
||||
} catch (error) {
|
||||
throw new Error("Rust engine-metadata.json is invalid JSON.", {
|
||||
cause: error,
|
||||
});
|
||||
}
|
||||
if (!equalJson(metadata, await expectedMetadata(repositoryRoot, pack))) {
|
||||
throw new Error(
|
||||
"The Rust metadata does not match the pinned build contract.",
|
||||
);
|
||||
}
|
||||
if (
|
||||
(await readFile(path.join(pack, "SHA256SUMS"), "utf8")) !==
|
||||
(await checksumText(pack))
|
||||
) {
|
||||
throw new Error("The Rust SHA256SUMS file is incorrect.");
|
||||
}
|
||||
await smokePack(pack);
|
||||
return metadata;
|
||||
}
|
||||
|
||||
async function replaceDirectory(stage, target, label) {
|
||||
const details = await lstat(target).catch(() => null);
|
||||
if (details && (!details.isDirectory() || details.isSymbolicLink())) {
|
||||
throw new Error(`${label} must be a real directory.`);
|
||||
}
|
||||
if (!details) {
|
||||
await rename(stage, target);
|
||||
return;
|
||||
}
|
||||
const backup = `${target}.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 });
|
||||
}
|
||||
|
||||
async function bindgen(toolchain, rawWasm, output) {
|
||||
await mkdir(output, { recursive: true });
|
||||
await run(
|
||||
toolchain.wasmBindgen,
|
||||
[
|
||||
"--target",
|
||||
"web",
|
||||
"--no-typescript",
|
||||
"--out-name",
|
||||
"rust-regex",
|
||||
"--out-dir",
|
||||
output,
|
||||
rawWasm,
|
||||
],
|
||||
{ env: toolchain.baseEnvironment },
|
||||
);
|
||||
await rename(
|
||||
path.join(output, "rust-regex.js"),
|
||||
path.join(output, "rust-regex.mjs"),
|
||||
);
|
||||
}
|
||||
|
||||
export async function buildRustPack(
|
||||
root,
|
||||
toolchainRootDirectory,
|
||||
outputDirectory,
|
||||
) {
|
||||
const repositoryRoot = await assertRealDirectory(
|
||||
root,
|
||||
"Regex Tools repository",
|
||||
);
|
||||
const engine = await assertRealDirectory(
|
||||
path.join(repositoryRoot, "engines", "rust"),
|
||||
"Rust engine source",
|
||||
);
|
||||
const toolchain = await verifyToolchain(toolchainRootDirectory);
|
||||
const lockPackages = parseCargoLock(
|
||||
await readFile(path.join(engine, "Cargo.lock"), "utf8"),
|
||||
);
|
||||
const regexPackage = lockPackages.find(
|
||||
({ name, version }) => name === "regex" && version === "1.13.1",
|
||||
);
|
||||
const bindgenPackage = lockPackages.find(
|
||||
({ name, version }) =>
|
||||
name === "wasm-bindgen" && version === RUST_LOCK.wasmBindgenVersion,
|
||||
);
|
||||
if (
|
||||
regexPackage?.checksum !== RUST_LOCK.source.crateChecksum ||
|
||||
bindgenPackage?.checksum !== RUST_LOCK.wasmBindgenCrateChecksum
|
||||
) {
|
||||
throw new Error("Cargo.lock does not contain the pinned engine crates.");
|
||||
}
|
||||
|
||||
const buildParent = path.join(repositoryRoot, ".engine-build");
|
||||
await mkdir(buildParent, { recursive: true });
|
||||
const temporary = await mkdtemp(path.join(buildParent, "rust-build-"));
|
||||
try {
|
||||
const generated = [];
|
||||
for (const label of ["first", "second"]) {
|
||||
const target = path.join(temporary, `${label}-target`);
|
||||
const environment = buildEnvironment(toolchain, repositoryRoot, target);
|
||||
await run(
|
||||
toolchain.cargo,
|
||||
[
|
||||
"build",
|
||||
"--locked",
|
||||
"--offline",
|
||||
"--release",
|
||||
"--target",
|
||||
"wasm32-unknown-unknown",
|
||||
],
|
||||
{ cwd: engine, env: environment },
|
||||
);
|
||||
const rawWasm = await assertRegularFile(
|
||||
path.join(
|
||||
target,
|
||||
"wasm32-unknown-unknown",
|
||||
"release",
|
||||
"regex_tools_rust_engine.wasm",
|
||||
),
|
||||
"compiled Rust WebAssembly",
|
||||
);
|
||||
const output = path.join(temporary, `${label}-bindgen`);
|
||||
await bindgen(toolchain, rawWasm, output);
|
||||
generated.push(output);
|
||||
}
|
||||
for (const name of ["rust-regex.mjs", "rust-regex_bg.wasm"]) {
|
||||
if (
|
||||
(await sha256File(path.join(generated[0], name))) !==
|
||||
(await sha256File(path.join(generated[1], name)))
|
||||
) {
|
||||
throw new Error(
|
||||
`Two pinned Rust builds produced different ${name} hashes.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const target = path.resolve(
|
||||
repositoryRoot,
|
||||
outputDirectory ?? path.join(".engine-build", "rust"),
|
||||
);
|
||||
const stage = await mkdtemp(path.join(buildParent, "rust-pack-"));
|
||||
await copyFile(
|
||||
toolchain.files.apache,
|
||||
path.join(stage, "LICENSE-Apache-2.0.txt"),
|
||||
);
|
||||
await copyFile(toolchain.files.mit, path.join(stage, "LICENSE-MIT.txt"));
|
||||
await copyFile(
|
||||
toolchain.files.unicode,
|
||||
path.join(stage, "LICENSE-Unicode-3.0.txt"),
|
||||
);
|
||||
await copyFile(
|
||||
toolchain.files.stdlibCopyright,
|
||||
path.join(stage, "RUST-STDLIB-COPYRIGHT.html"),
|
||||
);
|
||||
await copyFile(
|
||||
path.join(generated[0], "rust-regex.mjs"),
|
||||
path.join(stage, "rust-regex.mjs"),
|
||||
);
|
||||
await copyFile(
|
||||
path.join(generated[0], "rust-regex_bg.wasm"),
|
||||
path.join(stage, "rust-regex_bg.wasm"),
|
||||
);
|
||||
await writeFile(
|
||||
path.join(stage, "THIRD_PARTY_LICENSES.txt"),
|
||||
await thirdPartyLicenses(toolchain.cargoHome, lockPackages),
|
||||
"utf8",
|
||||
);
|
||||
const metadata = await expectedMetadata(repositoryRoot, stage);
|
||||
await writeFile(
|
||||
path.join(stage, "engine-metadata.json"),
|
||||
`${JSON.stringify(metadata, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
await writeFile(path.join(stage, "SHA256SUMS"), await checksumText(stage));
|
||||
await verifyRustPack(stage, repositoryRoot);
|
||||
await mkdir(path.dirname(target), { recursive: true });
|
||||
await replaceDirectory(stage, target, "Rust staged pack");
|
||||
return { metadata, output: target };
|
||||
} finally {
|
||||
await rm(temporary, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
export async function installRustPack(sourceDirectory, root) {
|
||||
const repositoryRoot = await assertRealDirectory(
|
||||
root,
|
||||
"Regex Tools repository",
|
||||
);
|
||||
const source = await assertRealDirectory(sourceDirectory, "Rust staged pack");
|
||||
const metadata = await verifyRustPack(source, repositoryRoot);
|
||||
const engineParent = path.join(repositoryRoot, "public", "engines");
|
||||
await mkdir(engineParent, { recursive: true });
|
||||
const stage = await mkdtemp(path.join(engineParent, "rust-install-"));
|
||||
for (const name of PACK_FILES) {
|
||||
await copyFile(path.join(source, name), path.join(stage, name));
|
||||
}
|
||||
await verifyRustPack(stage, repositoryRoot);
|
||||
const output = path.join(engineParent, "rust");
|
||||
await replaceDirectory(stage, output, "Installed Rust engine pack");
|
||||
return { metadata, output };
|
||||
}
|
||||
Reference in New Issue
Block a user