617 lines
19 KiB
JavaScript
617 lines
19 KiB
JavaScript
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 { homedir } from "node:os";
|
|
import path from "node:path";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
|
export const JAVA_LOCK = Object.freeze({
|
|
bridgeAbi: 1,
|
|
engineIdentity: "TeaVM java.util.regex 0.15.0",
|
|
engineVersion: "0.15.0",
|
|
sourceDateEpoch: 1_780_759_630,
|
|
maximumPatternUtf16: 65_536,
|
|
maximumSubjectBytes: 16_777_216,
|
|
maximumReplacementUtf16: 65_536,
|
|
maximumMatches: 10_000,
|
|
maximumCaptureRows: 100_000,
|
|
maximumCaptureGroups: 1_000,
|
|
maximumOutputBytes: 67_108_864,
|
|
source: Object.freeze({
|
|
repository: "https://github.com/konsoletyper/teavm.git",
|
|
tag: "0.15.0",
|
|
commit: "ee91b03e616c4b45401cd11fb0cd7eb0daf6649b",
|
|
tree: "1cdc5c332d5809828c8952f96d73f9e7504a0cec",
|
|
license: "Apache-2.0",
|
|
licenseSha256:
|
|
"cfc7749b96f63bd31c3c42b5c471bf756814053e847c10f3eb003417bc523d30",
|
|
noticeSha256:
|
|
"6c2b3372a6beee7fbaad482248ac0f5ad2b099d92ff2a528c04261cc9c0636f4",
|
|
packagedNoticeSha256:
|
|
"71bbe79ff78d0e5e64096acdd418589abfe886b8f827048dc655df438a57e477",
|
|
}),
|
|
toolchain: Object.freeze({
|
|
mavenVersion: "3.9.15",
|
|
javaVersion: "25.0.3",
|
|
artifacts: Object.freeze([
|
|
Object.freeze({
|
|
coordinate: "org.teavm:teavm-classlib:0.15.0",
|
|
relativePath:
|
|
"org/teavm/teavm-classlib/0.15.0/teavm-classlib-0.15.0.jar",
|
|
sha256:
|
|
"7d805be4670a892a34c89d9d750a8afd65e2f8201b7b3a10343634ea19d1c410",
|
|
}),
|
|
Object.freeze({
|
|
coordinate: "org.teavm:teavm-jso:0.15.0",
|
|
relativePath: "org/teavm/teavm-jso/0.15.0/teavm-jso-0.15.0.jar",
|
|
sha256:
|
|
"58b337b93924106bf819c45890c7c231a0c740145afe77795f1e0f099f621d71",
|
|
}),
|
|
Object.freeze({
|
|
coordinate: "org.teavm:teavm-maven-plugin:0.15.0",
|
|
relativePath:
|
|
"org/teavm/teavm-maven-plugin/0.15.0/teavm-maven-plugin-0.15.0.jar",
|
|
sha256:
|
|
"1133e16e45f1fd3cb93129fd96d142e1f5ef3608891d83b2273442d5446a3879",
|
|
}),
|
|
]),
|
|
}),
|
|
});
|
|
|
|
const PACK_FILES = Object.freeze([
|
|
"LICENSE.txt",
|
|
"NOTICE.txt",
|
|
"SHA256SUMS",
|
|
"engine-metadata.json",
|
|
"java-regex.mjs",
|
|
]);
|
|
const CHECKSUM_FILES = Object.freeze([
|
|
"LICENSE.txt",
|
|
"NOTICE.txt",
|
|
"engine-metadata.json",
|
|
"java-regex.mjs",
|
|
]);
|
|
const BRIDGE_SOURCE_FILES = Object.freeze([
|
|
"engines/java/pom.xml",
|
|
"engines/java/README.md",
|
|
"engines/java/LICENSE-TeaVM.txt",
|
|
"engines/java/NOTICE-TeaVM.txt",
|
|
"engines/java/src/main/java/de/addideas/regextools/javaengine/RegexBridge.java",
|
|
]);
|
|
const EXPORTED_FUNCTIONS = Object.freeze([
|
|
"bridgeAbiVersion",
|
|
"engineIdentity",
|
|
"execute",
|
|
"replace",
|
|
"selfTest",
|
|
]);
|
|
|
|
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 resolveExecutable(name, environment = process.env) {
|
|
for (const directory of (environment.PATH ?? "").split(path.delimiter)) {
|
|
if (!directory) continue;
|
|
const candidate = path.join(directory, name);
|
|
try {
|
|
await access(candidate, fsConstants.X_OK);
|
|
if ((await stat(candidate)).isFile()) return realpath(candidate);
|
|
} catch {
|
|
// Continue through PATH.
|
|
}
|
|
}
|
|
throw new Error(`${name} is required on PATH for the TeaVM Java build.`);
|
|
}
|
|
|
|
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 verifyToolchain() {
|
|
const maven = await resolveExecutable("mvn");
|
|
const java = await resolveExecutable("java");
|
|
const mavenVersion = await run(maven, ["--version"]);
|
|
const javaVersion = await run(java, ["-version"]);
|
|
if (
|
|
!mavenVersion.stdout.startsWith(
|
|
`Apache Maven ${JAVA_LOCK.toolchain.mavenVersion} `,
|
|
)
|
|
) {
|
|
throw new Error(
|
|
`The Java pack requires Maven ${JAVA_LOCK.toolchain.mavenVersion}.`,
|
|
);
|
|
}
|
|
const javaText = `${javaVersion.stdout}\n${javaVersion.stderr}`;
|
|
if (
|
|
!javaText.includes(`openjdk version "${JAVA_LOCK.toolchain.javaVersion}`)
|
|
) {
|
|
throw new Error(
|
|
`The Java pack requires OpenJDK ${JAVA_LOCK.toolchain.javaVersion}.`,
|
|
);
|
|
}
|
|
|
|
const repository = path.join(homedir(), ".m2", "repository");
|
|
for (const artifact of JAVA_LOCK.toolchain.artifacts) {
|
|
const file = await assertRegularFile(
|
|
path.join(repository, artifact.relativePath),
|
|
artifact.coordinate,
|
|
);
|
|
const actual = await sha256File(file);
|
|
if (actual !== artifact.sha256) {
|
|
throw new Error(
|
|
`${artifact.coordinate} SHA-256 mismatch: expected ${artifact.sha256}, got ${actual}.`,
|
|
);
|
|
}
|
|
}
|
|
return { maven };
|
|
}
|
|
|
|
function cleanBuildEnvironment() {
|
|
const environment = { ...process.env };
|
|
for (const variable of [
|
|
"CLASSPATH",
|
|
"JAVA_TOOL_OPTIONS",
|
|
"MAVEN_ARGS",
|
|
"MAVEN_OPTS",
|
|
"_JAVA_OPTIONS",
|
|
]) {
|
|
delete environment[variable];
|
|
}
|
|
environment.LANG = "C";
|
|
environment.LC_ALL = "C";
|
|
environment.SOURCE_DATE_EPOCH = String(JAVA_LOCK.sourceDateEpoch);
|
|
environment.TZ = "UTC";
|
|
return environment;
|
|
}
|
|
|
|
async function sourceFiles(root) {
|
|
return Promise.all(
|
|
BRIDGE_SOURCE_FILES.map(async (relativePath) => {
|
|
const file = await assertRegularFile(
|
|
path.join(root, relativePath),
|
|
`Java bridge source ${relativePath}`,
|
|
);
|
|
const contents = await readFile(file);
|
|
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 expectedMetadata(root, pack) {
|
|
return {
|
|
schemaVersion: 1,
|
|
engine: "java",
|
|
engineName: "TeaVM java.util.regex",
|
|
engineVersion: JAVA_LOCK.engineVersion,
|
|
semanticIdentity:
|
|
"TeaVM 0.15.0 classlib java.util.regex; Apache Harmony-derived; not OpenJDK",
|
|
status: "production-compatibility-runtime",
|
|
bridge: {
|
|
abiVersion: JAVA_LOCK.bridgeAbi,
|
|
maximumPatternUtf16: JAVA_LOCK.maximumPatternUtf16,
|
|
maximumSubjectBytes: JAVA_LOCK.maximumSubjectBytes,
|
|
maximumReplacementUtf16: JAVA_LOCK.maximumReplacementUtf16,
|
|
maximumMatches: JAVA_LOCK.maximumMatches,
|
|
maximumCaptureRows: JAVA_LOCK.maximumCaptureRows,
|
|
maximumCaptureGroups: JAVA_LOCK.maximumCaptureGroups,
|
|
maximumOutputBytes: JAVA_LOCK.maximumOutputBytes,
|
|
offsetUnit: "utf16",
|
|
exports: [...EXPORTED_FUNCTIONS],
|
|
sourceFiles: await sourceFiles(root),
|
|
},
|
|
source: {
|
|
repository: JAVA_LOCK.source.repository,
|
|
tag: JAVA_LOCK.source.tag,
|
|
commitSha1: JAVA_LOCK.source.commit,
|
|
treeSha1: JAVA_LOCK.source.tree,
|
|
license: JAVA_LOCK.source.license,
|
|
upstreamLicenseSha256: JAVA_LOCK.source.licenseSha256,
|
|
upstreamNoticeSha256: JAVA_LOCK.source.noticeSha256,
|
|
packagedNoticeSha256: JAVA_LOCK.source.packagedNoticeSha256,
|
|
noticeNormalization:
|
|
"The packaged notice removes one redundant blank line and adds a final LF; its attribution text is unchanged.",
|
|
},
|
|
toolchain: {
|
|
mavenVersion: JAVA_LOCK.toolchain.mavenVersion,
|
|
javaVersion: JAVA_LOCK.toolchain.javaVersion,
|
|
teaVmArtifacts: JAVA_LOCK.toolchain.artifacts.map(
|
|
({ coordinate, sha256: digest }) => ({
|
|
coordinate,
|
|
sha256: digest,
|
|
}),
|
|
),
|
|
},
|
|
configuration: {
|
|
target: "JavaScript ES2015 module",
|
|
minified: true,
|
|
optimizationLevel: "ADVANCED",
|
|
sourceMaps: false,
|
|
filesystem: false,
|
|
supportedApplicationFlags: ["g", "i", "d", "m", "s", "u", "x", "U"],
|
|
unixLines: "d maps to TeaVM Pattern.UNIX_LINES.",
|
|
unicodeCharacterClassCompatibility:
|
|
"TeaVM 0.15.0 lacks OpenJDK UNICODE_CHARACTER_CLASS; U implies Unicode case folding only.",
|
|
replacement:
|
|
"TeaVM 0.15.0 Matcher appendReplacement/appendTail semantics: only single-digit $n references; ${name} is rejected; not OpenJDK Matcher parity.",
|
|
},
|
|
sourceDateEpoch: JAVA_LOCK.sourceDateEpoch,
|
|
files: await Promise.all(
|
|
["LICENSE.txt", "NOTICE.txt", "java-regex.mjs"].map((name) =>
|
|
fileMetadata(pack, name),
|
|
),
|
|
),
|
|
};
|
|
}
|
|
|
|
async function expectedChecksums(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);
|
|
}
|
|
|
|
function readField(payload, state) {
|
|
const colon = payload.indexOf(":", state.cursor);
|
|
if (colon < 0) throw new Error("Malformed Java smoke-test payload.");
|
|
const length = Number.parseInt(payload.slice(state.cursor, colon), 10);
|
|
if (!Number.isSafeInteger(length) || length < 0) {
|
|
throw new Error("Malformed Java smoke-test field length.");
|
|
}
|
|
const start = colon + 1;
|
|
const end = start + length;
|
|
if (end > payload.length) {
|
|
throw new Error("Truncated Java smoke-test payload.");
|
|
}
|
|
state.cursor = end;
|
|
return payload.slice(start, end);
|
|
}
|
|
|
|
async function smokeJavaPack(pack) {
|
|
const moduleFile = path.join(pack, "java-regex.mjs");
|
|
const imported = await import(
|
|
`${pathToFileURL(moduleFile).href}?verify=${await sha256File(moduleFile)}`
|
|
);
|
|
for (const name of EXPORTED_FUNCTIONS) {
|
|
if (typeof imported[name] !== "function") {
|
|
throw new Error(`The Java module is missing export ${name}.`);
|
|
}
|
|
}
|
|
if (
|
|
imported.bridgeAbiVersion() !== JAVA_LOCK.bridgeAbi ||
|
|
imported.engineIdentity() !== JAVA_LOCK.engineIdentity ||
|
|
imported.selfTest() !== 0
|
|
) {
|
|
throw new Error("The TeaVM Java bridge identity or self-test failed.");
|
|
}
|
|
|
|
const execution = imported.execute("(?<=a)(b)\\1", "abb", 0, true, 10, 10);
|
|
const executionState = { cursor: 0 };
|
|
const executionFields = Array.from({ length: 9 }, () =>
|
|
readField(execution, executionState),
|
|
);
|
|
if (
|
|
executionState.cursor !== execution.length ||
|
|
JSON.stringify(executionFields) !==
|
|
JSON.stringify(["1", "ok", "1", "0", "1", "1", "3", "1", "2"])
|
|
) {
|
|
throw new Error("The TeaVM Java matching smoke test failed.");
|
|
}
|
|
|
|
const replacement = imported.replace("(a)", "a a", "<$1>", 0, true, 10, 10);
|
|
const replacementState = { cursor: 0 };
|
|
const replacementFields = [];
|
|
while (replacementState.cursor < replacement.length) {
|
|
replacementFields.push(readField(replacement, replacementState));
|
|
}
|
|
if (
|
|
replacementFields.at(-2) !== "0" ||
|
|
replacementFields.at(-1) !== "<a> <a>"
|
|
) {
|
|
throw new Error("The TeaVM Java replacement smoke test failed.");
|
|
}
|
|
}
|
|
|
|
export async function verifyJavaPack(packDirectory, root) {
|
|
const repositoryRoot = await assertRealDirectory(
|
|
root,
|
|
"Regex Tools repository",
|
|
);
|
|
const pack = await assertRealDirectory(packDirectory, "Java 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 Java pack must contain only: ${PACK_FILES.join(", ")}.`,
|
|
);
|
|
}
|
|
if (
|
|
(await sha256File(path.join(pack, "LICENSE.txt"))) !==
|
|
JAVA_LOCK.source.licenseSha256
|
|
) {
|
|
throw new Error("The staged TeaVM licence does not match tag 0.15.0.");
|
|
}
|
|
if (
|
|
(await sha256File(path.join(pack, "NOTICE.txt"))) !==
|
|
JAVA_LOCK.source.packagedNoticeSha256
|
|
) {
|
|
throw new Error(
|
|
"The staged TeaVM notice does not match the documented normalized notice.",
|
|
);
|
|
}
|
|
const moduleText = await readFile(path.join(pack, "java-regex.mjs"), "utf8");
|
|
if (
|
|
moduleText.includes("sourceMappingURL") ||
|
|
moduleText.includes("/mnt/") ||
|
|
moduleText.includes("/home/") ||
|
|
!moduleText.includes("as bridgeAbiVersion") ||
|
|
!moduleText.includes("as engineIdentity") ||
|
|
!moduleText.includes("as execute") ||
|
|
!moduleText.includes("as replace") ||
|
|
!moduleText.includes("as selfTest")
|
|
) {
|
|
throw new Error(
|
|
"java-regex.mjs leaks a build path, has a source map, or lacks bridge exports.",
|
|
);
|
|
}
|
|
|
|
let metadata;
|
|
try {
|
|
metadata = JSON.parse(
|
|
await readFile(path.join(pack, "engine-metadata.json"), "utf8"),
|
|
);
|
|
} catch (error) {
|
|
throw new Error("Java engine-metadata.json is not valid JSON.", {
|
|
cause: error,
|
|
});
|
|
}
|
|
const expected = await expectedMetadata(repositoryRoot, pack);
|
|
if (!equalJson(metadata, expected)) {
|
|
throw new Error(
|
|
"The staged Java metadata does not match the pinned build contract.",
|
|
);
|
|
}
|
|
if (
|
|
(await readFile(path.join(pack, "SHA256SUMS"), "utf8")) !==
|
|
(await expectedChecksums(pack))
|
|
) {
|
|
throw new Error("The staged Java SHA256SUMS file is incorrect.");
|
|
}
|
|
await smokeJavaPack(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}`;
|
|
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 buildJavaPack(root, outputDirectory) {
|
|
const repositoryRoot = await assertRealDirectory(
|
|
root,
|
|
"Regex Tools repository",
|
|
);
|
|
const engine = await assertRealDirectory(
|
|
path.join(repositoryRoot, "engines", "java"),
|
|
"Java engine source",
|
|
);
|
|
const { maven } = await verifyToolchain();
|
|
const environment = cleanBuildEnvironment();
|
|
const buildArguments = [
|
|
"--batch-mode",
|
|
"--no-transfer-progress",
|
|
"--offline",
|
|
"clean",
|
|
"package",
|
|
];
|
|
await run(maven, buildArguments, { cwd: engine, env: environment });
|
|
const generated = await assertRegularFile(
|
|
path.join(engine, "target", "generated", "java-regex.mjs"),
|
|
"generated TeaVM module",
|
|
);
|
|
const firstBuild = await readFile(generated);
|
|
await run(maven, buildArguments, { cwd: engine, env: environment });
|
|
const secondBuild = await readFile(generated);
|
|
if (sha256(firstBuild) !== sha256(secondBuild)) {
|
|
throw new Error(
|
|
"Two clean TeaVM builds produced different Java module hashes.",
|
|
);
|
|
}
|
|
|
|
const output = path.resolve(
|
|
repositoryRoot,
|
|
outputDirectory ?? path.join(".engine-build", "java"),
|
|
);
|
|
await mkdir(path.dirname(output), { recursive: true });
|
|
const stage = await mkdtemp(path.join(path.dirname(output), ".java-build-"));
|
|
try {
|
|
await copyFile(
|
|
path.join(engine, "LICENSE-TeaVM.txt"),
|
|
path.join(stage, "LICENSE.txt"),
|
|
fsConstants.COPYFILE_EXCL,
|
|
);
|
|
await copyFile(
|
|
path.join(engine, "NOTICE-TeaVM.txt"),
|
|
path.join(stage, "NOTICE.txt"),
|
|
fsConstants.COPYFILE_EXCL,
|
|
);
|
|
await writeFile(path.join(stage, "java-regex.mjs"), secondBuild, {
|
|
flag: "wx",
|
|
});
|
|
const metadata = await expectedMetadata(repositoryRoot, stage);
|
|
await writeFile(
|
|
path.join(stage, "engine-metadata.json"),
|
|
`${JSON.stringify(metadata, null, 2)}\n`,
|
|
{ encoding: "utf8", flag: "wx" },
|
|
);
|
|
await writeFile(
|
|
path.join(stage, "SHA256SUMS"),
|
|
await expectedChecksums(stage),
|
|
{ encoding: "utf8", flag: "wx" },
|
|
);
|
|
await verifyJavaPack(stage, repositoryRoot);
|
|
await replaceDirectory(stage, output, "Java build output");
|
|
return {
|
|
output,
|
|
metadata,
|
|
moduleSha256: sha256(secondBuild),
|
|
moduleBytes: secondBuild.byteLength,
|
|
};
|
|
} finally {
|
|
await rm(stage, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
export async function installJavaPack(packDirectory, root) {
|
|
const repositoryRoot = await assertRealDirectory(
|
|
root,
|
|
"Regex Tools repository",
|
|
);
|
|
const source = await assertRealDirectory(packDirectory, "verified Java pack");
|
|
const metadata = await verifyJavaPack(source, repositoryRoot);
|
|
const engineRoot = await assertRealDirectory(
|
|
path.join(repositoryRoot, "public", "engines"),
|
|
"public engine directory",
|
|
);
|
|
const target = path.join(engineRoot, "java");
|
|
const stage = await mkdtemp(path.join(engineRoot, ".java-install-"));
|
|
try {
|
|
for (const name of PACK_FILES) {
|
|
await copyFile(
|
|
path.join(source, name),
|
|
path.join(stage, name),
|
|
fsConstants.COPYFILE_EXCL,
|
|
);
|
|
}
|
|
await verifyJavaPack(stage, repositoryRoot);
|
|
await replaceDirectory(stage, target, "public/engines/java");
|
|
return { output: target, metadata };
|
|
} finally {
|
|
await rm(stage, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
const invokedFile = process.argv[1] ? path.resolve(process.argv[1]) : "";
|
|
if (invokedFile === fileURLToPath(import.meta.url)) {
|
|
if (process.argv.length > 3) {
|
|
throw new Error(
|
|
"Usage: node scripts/java-engine-pack.mjs [.engine-build/java]",
|
|
);
|
|
}
|
|
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
const result = await buildJavaPack(root, process.argv[2]);
|
|
console.log(
|
|
`Built deterministic ${result.metadata.semanticIdentity} module: ${result.moduleSha256} (${result.moduleBytes.toLocaleString()} bytes).`,
|
|
);
|
|
}
|