feat: add multi-engine regex flavour support
This commit is contained in:
537
scripts/ruby-engine-pack.mjs
Normal file
537
scripts/ruby-engine-pack.mjs
Normal file
@@ -0,0 +1,537 @@
|
||||
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 { File, PreopenDirectory } from "@bjorn3/browser_wasi_shim";
|
||||
import { DefaultRubyVM } from "@ruby/wasm-wasi/dist/browser";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
||||
|
||||
export const RUBY_ENGINE_LOCK = Object.freeze({
|
||||
bridgeAbi: 2,
|
||||
rubyVersion: "4.0.0",
|
||||
platform: "wasm32-wasi",
|
||||
packageVersion: "2.9.3-2.9.4",
|
||||
maximumPatternUtf16: 65_536,
|
||||
maximumSubjectBytes: 16_777_216,
|
||||
maximumReplacementUtf16: 65_536,
|
||||
maximumOutputBytes: 67_108_864,
|
||||
maximumMatches: 10_000,
|
||||
maximumCaptureRows: 100_000,
|
||||
maximumCaptureGroups: 1_000,
|
||||
npm: Object.freeze({
|
||||
runtime: Object.freeze({
|
||||
name: "@ruby/4.0-wasm-wasi",
|
||||
version: "2.9.3-2.9.4",
|
||||
resolved:
|
||||
"https://registry.npmjs.org/@ruby/4.0-wasm-wasi/-/4.0-wasm-wasi-2.9.3-2.9.4.tgz",
|
||||
integrity:
|
||||
"sha512-USLEWmCjM/DIota/9c7Mwzzl3v8mvmg4x24m+zMq8+vLx5+AdOS6dyXvsL/B8+XjQn5UgYhh3c8ekaUdWFkUMA==",
|
||||
packageJsonSha256:
|
||||
"a03a9475d54494a351c9c37ee9aca2db05f2652098907938628522d8a0f43807",
|
||||
}),
|
||||
host: Object.freeze({
|
||||
name: "@ruby/wasm-wasi",
|
||||
version: "2.9.3-2.9.4",
|
||||
resolved:
|
||||
"https://registry.npmjs.org/@ruby/wasm-wasi/-/wasm-wasi-2.9.3-2.9.4.tgz",
|
||||
integrity:
|
||||
"sha512-WxW9wON/TIf+8Ktng8qDJeV/6iH8kw+YwxOsOyXdAdLJgfYDPAXOqpIVd/96y2C9V8VJ2yqZm/IRv6nLeV6EKg==",
|
||||
packageJsonSha256:
|
||||
"4a6eacec227c5cf81f580435d0105933a456e9685cb3f1821db967307431aa20",
|
||||
}),
|
||||
wasiShim: Object.freeze({
|
||||
name: "@bjorn3/browser_wasi_shim",
|
||||
version: "0.4.2",
|
||||
integrity:
|
||||
"sha512-/iHkCVUG3VbcbmEHn5iIUpIrh7a7WPiwZ3sHy4HZKZzBdSadwdddYDZAII2zBvQYV0Lfi8naZngPCN7WPHI/hA==",
|
||||
packageJsonSha256:
|
||||
"4025e157abfadd44037158899001a4ea0407651c2ee9c8a9f51d5c9d0ecf613c",
|
||||
}),
|
||||
tslib: Object.freeze({
|
||||
name: "tslib",
|
||||
version: "2.8.1",
|
||||
integrity:
|
||||
"sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
packageJsonSha256:
|
||||
"f02b4851df316f29bdfcd404aa1eb41e4e216b5859015f81fcd3e91caf8be931",
|
||||
}),
|
||||
}),
|
||||
sourceAssets: Object.freeze({
|
||||
"ruby.wasm":
|
||||
"fa1d5af1b4d601191739e6c4980d060d0d202181a7c83abf4930b3ca35b90f94",
|
||||
"LICENSE.ruby-wasm.txt":
|
||||
"90357d3794c968704914d42a52354a83f2d8b10cb43df3b63ef1ca0e5bbc0bf2",
|
||||
"NOTICE.txt":
|
||||
"343c246a6e1f1234e29e51707a54799ea82b50d3a2a41c5221fa12058b2395b2",
|
||||
"LICENSE.browser-wasi-shim-MIT.txt":
|
||||
"23f18e03dc49df91622fe2a76176497404e46ced8a715d9d2b67a7446571cca3",
|
||||
"LICENSE.browser-wasi-shim-Apache-2.0.txt":
|
||||
"c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4",
|
||||
"LICENSE.tslib.txt":
|
||||
"210b19e543130388c68654b7497e967119ce17145f66ab7d85688fbd70f08751",
|
||||
}),
|
||||
});
|
||||
|
||||
const SOURCE_FILES = Object.freeze({
|
||||
"ruby.wasm": ["@ruby", "4.0-wasm-wasi", "dist", "ruby.wasm"],
|
||||
"LICENSE.ruby-wasm.txt": ["@ruby", "4.0-wasm-wasi", "dist", "LICENSE"],
|
||||
"NOTICE.txt": ["@ruby", "4.0-wasm-wasi", "dist", "NOTICE"],
|
||||
"LICENSE.browser-wasi-shim-MIT.txt": [
|
||||
"@bjorn3",
|
||||
"browser_wasi_shim",
|
||||
"LICENSE-MIT",
|
||||
],
|
||||
"LICENSE.browser-wasi-shim-Apache-2.0.txt": [
|
||||
"@bjorn3",
|
||||
"browser_wasi_shim",
|
||||
"LICENSE-APACHE",
|
||||
],
|
||||
"LICENSE.tslib.txt": ["tslib", "LICENSE.txt"],
|
||||
});
|
||||
|
||||
const PACK_FILES = Object.freeze(
|
||||
[...Object.keys(SOURCE_FILES), "SHA256SUMS", "engine-metadata.json"].sort(),
|
||||
);
|
||||
const CHECKSUM_FILES = Object.freeze(
|
||||
PACK_FILES.filter((name) => name !== "SHA256SUMS"),
|
||||
);
|
||||
|
||||
function compareText(left, right) {
|
||||
return left < right ? -1 : left > right ? 1 : 0;
|
||||
}
|
||||
|
||||
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, 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 verifyPackage(projectRoot, descriptor) {
|
||||
const directory = path.join(
|
||||
projectRoot,
|
||||
"node_modules",
|
||||
...descriptor.name.split("/"),
|
||||
);
|
||||
await assertRealDirectory(directory, `${descriptor.name} npm package`);
|
||||
const packageFile = path.join(directory, "package.json");
|
||||
await assertRegularFile(packageFile, `${descriptor.name} package.json`);
|
||||
await assertHash(
|
||||
packageFile,
|
||||
descriptor.packageJsonSha256,
|
||||
`${descriptor.name} package.json`,
|
||||
);
|
||||
const packageJson = JSON.parse(await readFile(packageFile, "utf8"));
|
||||
if (
|
||||
packageJson.name !== descriptor.name ||
|
||||
packageJson.version !== descriptor.version
|
||||
) {
|
||||
throw new Error(`${descriptor.name} npm package identity drifted.`);
|
||||
}
|
||||
return directory;
|
||||
}
|
||||
|
||||
async function verifyNpmLock(projectRoot) {
|
||||
const lock = JSON.parse(
|
||||
await readFile(path.join(projectRoot, "package-lock.json"), "utf8"),
|
||||
);
|
||||
for (const descriptor of Object.values(RUBY_ENGINE_LOCK.npm)) {
|
||||
const entry = lock.packages?.[`node_modules/${descriptor.name}`];
|
||||
if (
|
||||
entry?.version !== descriptor.version ||
|
||||
entry?.integrity !== descriptor.integrity ||
|
||||
(descriptor.resolved !== undefined &&
|
||||
entry?.resolved !== descriptor.resolved)
|
||||
) {
|
||||
throw new Error(
|
||||
`${descriptor.name}@${descriptor.version} is not exactly pinned in package-lock.json.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function verifySources(projectRoot) {
|
||||
await verifyNpmLock(projectRoot);
|
||||
const runtime = await verifyPackage(
|
||||
projectRoot,
|
||||
RUBY_ENGINE_LOCK.npm.runtime,
|
||||
);
|
||||
await verifyPackage(projectRoot, RUBY_ENGINE_LOCK.npm.host);
|
||||
await verifyPackage(projectRoot, RUBY_ENGINE_LOCK.npm.wasiShim);
|
||||
await verifyPackage(projectRoot, RUBY_ENGINE_LOCK.npm.tslib);
|
||||
|
||||
const nodeModules = path.join(projectRoot, "node_modules");
|
||||
for (const [output, components] of Object.entries(SOURCE_FILES)) {
|
||||
const source = path.join(nodeModules, ...components);
|
||||
await assertRegularFile(source, `Ruby source ${output}`);
|
||||
await assertHash(
|
||||
source,
|
||||
RUBY_ENGINE_LOCK.sourceAssets[output],
|
||||
`Ruby source ${output}`,
|
||||
);
|
||||
}
|
||||
return { nodeModules, runtime };
|
||||
}
|
||||
|
||||
async function fileRecord(directory, name) {
|
||||
const value = await readFile(path.join(directory, name));
|
||||
return { path: name, sha256: sha256(value), bytes: value.byteLength };
|
||||
}
|
||||
|
||||
async function expectedMetadata(projectRoot, pack) {
|
||||
const bridgePath = path.join(projectRoot, "engines", "ruby", "bridge.rb");
|
||||
const bridge = await readFile(bridgePath);
|
||||
const files = [];
|
||||
for (const name of CHECKSUM_FILES) {
|
||||
if (name !== "engine-metadata.json") {
|
||||
files.push(await fileRecord(pack, name));
|
||||
}
|
||||
}
|
||||
return {
|
||||
schemaVersion: 1,
|
||||
engine: "ruby",
|
||||
engineVersion: `CRuby ${RUBY_ENGINE_LOCK.rubyVersion} Regexp`,
|
||||
runtime: `ruby.wasm ${RUBY_ENGINE_LOCK.packageVersion}`,
|
||||
platform: RUBY_ENGINE_LOCK.platform,
|
||||
status: "production-runtime",
|
||||
bridge: {
|
||||
abiVersion: RUBY_ENGINE_LOCK.bridgeAbi,
|
||||
sourceFile: {
|
||||
path: "engines/ruby/bridge.rb",
|
||||
sha256: sha256(bridge),
|
||||
bytes: bridge.byteLength,
|
||||
},
|
||||
requestEncoding:
|
||||
"bounded length-prefixed UTF-8 fields in a worker-local WASI memory file",
|
||||
responseEncoding:
|
||||
"bounded inert JSON metadata plus a worker-local bounded UTF-8 output file",
|
||||
userSourceEvaluation: false,
|
||||
replacement:
|
||||
"native Ruby String#gsub/String#sub match iteration with a bounded CRuby 4.0.0 rb_reg_regsub-compatible expander",
|
||||
nativeOffsetUnit: "Unicode code points",
|
||||
maximumPatternUtf16: RUBY_ENGINE_LOCK.maximumPatternUtf16,
|
||||
maximumSubjectBytes: RUBY_ENGINE_LOCK.maximumSubjectBytes,
|
||||
maximumReplacementUtf16: RUBY_ENGINE_LOCK.maximumReplacementUtf16,
|
||||
maximumOutputBytes: RUBY_ENGINE_LOCK.maximumOutputBytes,
|
||||
maximumMatches: RUBY_ENGINE_LOCK.maximumMatches,
|
||||
maximumCaptureRows: RUBY_ENGINE_LOCK.maximumCaptureRows,
|
||||
maximumCaptureGroups: RUBY_ENGINE_LOCK.maximumCaptureGroups,
|
||||
},
|
||||
source: {
|
||||
repository: "https://github.com/ruby/ruby.wasm",
|
||||
packages: Object.values(RUBY_ENGINE_LOCK.npm).map(
|
||||
({ name, version, integrity }) => ({ name, version, integrity }),
|
||||
),
|
||||
runtimeVariant: "minimal ruby.wasm (without bundled stdlib/debug)",
|
||||
cruby: {
|
||||
engine: "ruby",
|
||||
version: RUBY_ENGINE_LOCK.rubyVersion,
|
||||
platform: RUBY_ENGINE_LOCK.platform,
|
||||
},
|
||||
},
|
||||
files,
|
||||
};
|
||||
}
|
||||
|
||||
async function smokeRuby(pack, projectRoot) {
|
||||
const moduleBytes = await readFile(path.join(pack, "ruby.wasm"));
|
||||
if (!WebAssembly.validate(moduleBytes)) {
|
||||
throw new Error("Ruby runtime is not a valid WebAssembly module.");
|
||||
}
|
||||
const module = await WebAssembly.compile(moduleBytes);
|
||||
const { vm, wasi } = await DefaultRubyVM(module, { consolePrint: false });
|
||||
const requestFile = new File([]);
|
||||
const outputFile = new File([]);
|
||||
const rootDirectory = wasi.fds[3];
|
||||
if (!(rootDirectory instanceof PreopenDirectory)) {
|
||||
throw new Error("Ruby smoke runtime has no writable root filesystem.");
|
||||
}
|
||||
rootDirectory.dir.contents.set("regex-tools-request.bin", requestFile);
|
||||
rootDirectory.dir.contents.set("regex-tools-output.bin", outputFile);
|
||||
const bridgeSource = await readFile(
|
||||
path.join(projectRoot, "engines", "ruby", "bridge.rb"),
|
||||
"utf8",
|
||||
);
|
||||
const bridge = vm.eval(bridgeSource);
|
||||
const encoder = new TextEncoder();
|
||||
const call = (...values) => {
|
||||
const fields = values.map((value) => encoder.encode(String(value)));
|
||||
const payload = new Uint8Array(
|
||||
8 + fields.reduce((total, field) => total + 4 + field.byteLength, 0),
|
||||
);
|
||||
payload.set(encoder.encode("RGXRUBY1"));
|
||||
const view = new DataView(payload.buffer);
|
||||
let offset = 8;
|
||||
for (const field of fields) {
|
||||
view.setUint32(offset, field.byteLength, false);
|
||||
offset += 4;
|
||||
payload.set(field, offset);
|
||||
offset += field.byteLength;
|
||||
}
|
||||
requestFile.data = payload;
|
||||
const response = JSON.parse(bridge.call("run").toString());
|
||||
if (String(values[0]) === "replace" && response?.ok === true) {
|
||||
response.output = new TextDecoder("utf-8", { fatal: true }).decode(
|
||||
outputFile.data,
|
||||
);
|
||||
}
|
||||
return response;
|
||||
};
|
||||
const identity = call("identity", "", "", "", 1, 1, "", 1);
|
||||
if (
|
||||
identity?.ok !== true ||
|
||||
identity.identity?.implementation !== "ruby" ||
|
||||
identity.identity?.rubyVersion !== RUBY_ENGINE_LOCK.rubyVersion ||
|
||||
identity.identity?.platform !== RUBY_ENGINE_LOCK.platform
|
||||
) {
|
||||
throw new Error("Ruby runtime failed its identity smoke test.");
|
||||
}
|
||||
const execution = call(
|
||||
"execute",
|
||||
"(?<word>é+)",
|
||||
"g",
|
||||
"😀éé x",
|
||||
10,
|
||||
10,
|
||||
"",
|
||||
1,
|
||||
);
|
||||
if (
|
||||
execution?.ok !== true ||
|
||||
execution.groupCount !== 1 ||
|
||||
JSON.stringify(execution.groupNames) !== JSON.stringify([[1, "word"]]) ||
|
||||
JSON.stringify(execution.matches?.[0]?.span) !== JSON.stringify([1, 3])
|
||||
) {
|
||||
throw new Error("Ruby runtime failed its native match smoke test.");
|
||||
}
|
||||
const replacement = call(
|
||||
"replace",
|
||||
"(?<word>é+)",
|
||||
"g",
|
||||
"😀éé x",
|
||||
10,
|
||||
10,
|
||||
"[\\k<word>]",
|
||||
1_024,
|
||||
);
|
||||
if (
|
||||
replacement?.ok !== true ||
|
||||
replacement.output !== "😀[éé] x" ||
|
||||
replacement.outputTruncated !== false
|
||||
) {
|
||||
throw new Error("Ruby runtime failed its native replacement smoke test.");
|
||||
}
|
||||
}
|
||||
|
||||
export async function verifyRubyPack(directory, projectRoot = root) {
|
||||
const pack = await assertRealDirectory(directory, "Ruby engine pack");
|
||||
const names = (await readdir(pack, { withFileTypes: true }))
|
||||
.map((entry) => {
|
||||
if (!entry.isFile() || entry.isSymbolicLink()) {
|
||||
throw new Error(
|
||||
`Ruby pack contains a non-regular entry: ${entry.name}`,
|
||||
);
|
||||
}
|
||||
return entry.name;
|
||||
})
|
||||
.sort(compareText);
|
||||
if (JSON.stringify(names) !== JSON.stringify(PACK_FILES)) {
|
||||
throw new Error(`Ruby pack must contain only: ${PACK_FILES.join(", ")}.`);
|
||||
}
|
||||
|
||||
const checksumLines = (await readFile(path.join(pack, "SHA256SUMS"), "utf8"))
|
||||
.trimEnd()
|
||||
.split("\n");
|
||||
const expectedLines = [];
|
||||
for (const name of CHECKSUM_FILES) {
|
||||
expectedLines.push(`${await sha256File(path.join(pack, name))} ${name}`);
|
||||
}
|
||||
if (JSON.stringify(checksumLines) !== JSON.stringify(expectedLines)) {
|
||||
throw new Error("Ruby pack checksum manifest is stale or malformed.");
|
||||
}
|
||||
|
||||
for (const [name, expected] of Object.entries(
|
||||
RUBY_ENGINE_LOCK.sourceAssets,
|
||||
)) {
|
||||
await assertHash(path.join(pack, name), expected, `Packed Ruby ${name}`);
|
||||
}
|
||||
const metadata = JSON.parse(
|
||||
await readFile(path.join(pack, "engine-metadata.json"), "utf8"),
|
||||
);
|
||||
const expected = await expectedMetadata(projectRoot, pack);
|
||||
if (JSON.stringify(metadata) !== JSON.stringify(expected)) {
|
||||
throw new Error("Ruby engine metadata does not match the pinned pack.");
|
||||
}
|
||||
await smokeRuby(pack, projectRoot);
|
||||
return metadata;
|
||||
}
|
||||
|
||||
export async function buildRubyPack({
|
||||
projectRoot = root,
|
||||
output = path.join(root, ".engine-build", "ruby"),
|
||||
force = false,
|
||||
} = {}) {
|
||||
const sources = await verifySources(projectRoot);
|
||||
const target = path.resolve(output);
|
||||
const existing = await lstat(target).catch(() => null);
|
||||
if (existing && !force) {
|
||||
throw new Error(`${target} exists; pass --force to replace it.`);
|
||||
}
|
||||
if (existing && (existing.isSymbolicLink() || !existing.isDirectory())) {
|
||||
throw new Error("Ruby pack output must be a real directory when replaced.");
|
||||
}
|
||||
|
||||
await mkdir(path.dirname(target), { recursive: true });
|
||||
const stage = await mkdtemp(path.join(path.dirname(target), ".ruby-pack-"));
|
||||
try {
|
||||
for (const [name, components] of Object.entries(SOURCE_FILES)) {
|
||||
await copyFile(
|
||||
path.join(sources.nodeModules, ...components),
|
||||
path.join(stage, name),
|
||||
fsConstants.COPYFILE_EXCL,
|
||||
);
|
||||
}
|
||||
const metadata = await expectedMetadata(projectRoot, stage);
|
||||
await writeFile(
|
||||
path.join(stage, "engine-metadata.json"),
|
||||
`${JSON.stringify(metadata, null, 2)}\n`,
|
||||
{ flag: "wx", mode: 0o644 },
|
||||
);
|
||||
const checksumLines = [];
|
||||
for (const name of CHECKSUM_FILES) {
|
||||
checksumLines.push(
|
||||
`${await sha256File(path.join(stage, name))} ${name}`,
|
||||
);
|
||||
}
|
||||
await writeFile(
|
||||
path.join(stage, "SHA256SUMS"),
|
||||
`${checksumLines.join("\n")}\n`,
|
||||
{ flag: "wx", mode: 0o644 },
|
||||
);
|
||||
await verifyRubyPack(stage, projectRoot);
|
||||
if (existing) await rm(target, { recursive: true });
|
||||
await rename(stage, target);
|
||||
} catch (error) {
|
||||
await rm(stage, { recursive: true, force: true });
|
||||
throw error;
|
||||
}
|
||||
return {
|
||||
output: target,
|
||||
metadata: await verifyRubyPack(target, projectRoot),
|
||||
};
|
||||
}
|
||||
|
||||
export async function installRubyPack(source, projectRoot = root) {
|
||||
const sourcePath = await assertRealDirectory(source, "Ruby staged pack");
|
||||
const target = path.join(projectRoot, "public", "engines", "ruby");
|
||||
if (sourcePath === (await realpath(target).catch(() => ""))) {
|
||||
throw new Error("Ruby staged pack and installation target must differ.");
|
||||
}
|
||||
const metadata = await verifyRubyPack(sourcePath, projectRoot);
|
||||
await mkdir(path.dirname(target), { recursive: true });
|
||||
const stage = await mkdtemp(
|
||||
path.join(path.dirname(target), ".ruby-install-"),
|
||||
);
|
||||
let backup;
|
||||
try {
|
||||
for (const name of PACK_FILES) {
|
||||
await copyFile(
|
||||
path.join(sourcePath, name),
|
||||
path.join(stage, name),
|
||||
fsConstants.COPYFILE_EXCL,
|
||||
);
|
||||
}
|
||||
await verifyRubyPack(stage, projectRoot);
|
||||
const existing = await lstat(target).catch(() => null);
|
||||
if (existing) {
|
||||
if (existing.isSymbolicLink() || !existing.isDirectory()) {
|
||||
throw new Error("Installed Ruby pack target must be a real directory.");
|
||||
}
|
||||
backup = `${target}.backup-${process.pid}-${Date.now()}`;
|
||||
await rename(target, backup);
|
||||
}
|
||||
await rename(stage, target);
|
||||
if (backup) await rm(backup, { recursive: true });
|
||||
} catch (error) {
|
||||
await rm(stage, { recursive: true, force: true });
|
||||
if (backup && !(await lstat(target).catch(() => null))) {
|
||||
await rename(backup, target);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
return { output: target, metadata };
|
||||
}
|
||||
|
||||
function parseArguments(arguments_) {
|
||||
let output = path.join(root, ".engine-build", "ruby");
|
||||
let force = false;
|
||||
for (let index = 0; index < arguments_.length; index += 1) {
|
||||
const argument = arguments_[index];
|
||||
if (argument === "--force") {
|
||||
if (force) throw new Error("--force was provided more than once.");
|
||||
force = true;
|
||||
} else if (argument === "--output") {
|
||||
const value = arguments_[index + 1];
|
||||
if (!value || value.startsWith("--")) {
|
||||
throw new Error("--output requires a directory.");
|
||||
}
|
||||
output = path.resolve(root, value);
|
||||
index += 1;
|
||||
} else {
|
||||
throw new Error(`Unknown Ruby engine-pack argument: ${argument}`);
|
||||
}
|
||||
}
|
||||
return { output, force };
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const result = await buildRubyPack(parseArguments(process.argv.slice(2)));
|
||||
console.log(
|
||||
`Built verified ${result.metadata.engineVersion} pack at ${result.output}.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (import.meta.url === pathToFileURL(process.argv[1] ?? "").href) {
|
||||
await main();
|
||||
}
|
||||
Reference in New Issue
Block a user