Files
Multi-Debrid-Downloader/scripts/build-extractor-jvm.mjs
T
Sucukdeluxe 808c2a432c release: harden archive recovery and lifecycle for v2.0.63
Corroborate CRC failures across extraction backends, retry only implicated multipart volumes, and distinguish corruption, missing volumes, I/O failures, and wrong passwords across native, Zip4j, and JBinding paths.

Make disk retries generation-safe, preserve selective run scopes and cooldowns, protect shared output files during cleanup, validate manual extraction batches atomically, and restore interrupted integrity work safely.

Prioritize active package operations in the UI, strengthen extraction IPC validation, compile the JVM sidecar before release builds, and verify shipped JVM resources byte-for-byte in every Windows artifact.
2026-08-23 03:56:24 +02:00

214 lines
7.2 KiB
JavaScript

import crypto from "node:crypto";
import fs from "node:fs";
import path from "node:path";
import { spawnSync } from "node:child_process";
import { fileURLToPath } from "node:url";
const scriptDir = path.dirname(fileURLToPath(import.meta.url));
const defaultRuntimeRoot = path.resolve(scriptDir, "..", "resources", "extractor-jvm");
const requiredLibNames = [
"sevenzipjbinding.jar",
"sevenzipjbinding-all-platforms.jar",
"zip4j.jar"
];
const stampName = ".source.sha256";
function parseArguments(args) {
let runtimeRoot = defaultRuntimeRoot;
let checkOnly = false;
for (let index = 0; index < args.length; index += 1) {
const value = args[index];
if (value === "--check") {
checkOnly = true;
continue;
}
if (value === "--runtime-root") {
const next = args[index + 1];
if (!next || next.startsWith("--")) {
throw new Error("--runtime-root benötigt einen Pfad");
}
runtimeRoot = path.resolve(next);
index += 1;
continue;
}
throw new Error(`Unbekanntes Argument: ${value}`);
}
return { runtimeRoot, checkOnly };
}
function requireFile(filePath, label) {
let stat;
try {
stat = fs.lstatSync(filePath);
} catch (error) {
if (error?.code === "ENOENT") {
throw new Error(`${label} fehlt: ${filePath}`);
}
throw error;
}
if (!stat.isFile() || stat.size === 0 || stat.isSymbolicLink()) {
throw new Error(`${label} ist keine reguläre, nicht leere Datei: ${filePath}`);
}
}
function listJavaSources(sourceRoot) {
const sources = [];
const pending = [sourceRoot];
while (pending.length > 0) {
const current = pending.pop();
for (const entry of fs.readdirSync(current, { withFileTypes: true })) {
const entryPath = path.join(current, entry.name);
if (entry.isDirectory()) {
pending.push(entryPath);
} else if (entry.isFile() && entry.name.endsWith(".java")) {
sources.push(entryPath);
}
}
}
sources.sort((left, right) => left.localeCompare(right, "en"));
if (sources.length === 0) {
throw new Error(`Keine Java-Quellen gefunden: ${sourceRoot}`);
}
return sources;
}
function updateDigest(hash, root, filePath) {
const relativePath = path.relative(root, filePath).replace(/\\/g, "/");
const content = fs.readFileSync(filePath);
hash.update(relativePath, "utf8");
hash.update("\0", "utf8");
hash.update(String(content.length), "utf8");
hash.update("\0", "utf8");
hash.update(content);
}
function sourceDigest(runtimeRoot, sources, libs) {
const hash = crypto.createHash("sha256");
hash.update("javac:-source=8:-target=8:-encoding=UTF-8:-g:none\0", "utf8");
for (const filePath of [...sources, ...libs]) {
updateDigest(hash, runtimeRoot, filePath);
}
return hash.digest("hex");
}
function verifyClassesDirectory(classesDir, expectedDigest) {
const mainClass = path.join(classesDir, "com", "sucukdeluxe", "extractor", "JBindExtractorMain.class");
const stampPath = path.join(classesDir, stampName);
requireFile(mainClass, "JVM-Extractor-Hauptklasse");
requireFile(stampPath, "JVM-Extractor-Quellhash");
const mainClassBytes = fs.readFileSync(mainClass);
if (mainClassBytes.length < 8 || mainClassBytes.readUInt16BE(6) !== 52) {
throw new Error("JVM-Extractor-Hauptklasse muss Java-8-Bytecode verwenden");
}
const actualDigest = fs.readFileSync(stampPath, "utf8").trim();
if (actualDigest !== expectedDigest) {
throw new Error("JVM-Extractor-Klassen sind veraltet; Java-Quelle oder Bibliotheken wurden seit dem letzten Build geändert");
}
}
function verifyClasses(runtimeRoot, expectedDigest) {
verifyClassesDirectory(path.join(runtimeRoot, "classes"), expectedDigest);
}
function resolveJavac() {
const javaHome = String(process.env.JAVA_HOME || "").trim();
const javaHomeCompiler = javaHome
? path.join(javaHome, "bin", process.platform === "win32" ? "javac.exe" : "javac")
: "";
if (javaHomeCompiler && fs.existsSync(javaHomeCompiler)) {
return javaHomeCompiler;
}
return "javac";
}
function requireJavac() {
const command = resolveJavac();
const probe = spawnSync(command, ["-version"], { encoding: "utf8", windowsHide: true });
if (probe.error?.code === "ENOENT" || probe.status === null) {
throw new Error("JDK fehlt: javac wurde nicht gefunden; installiere ein JDK und setze JAVA_HOME oder PATH");
}
if (probe.status !== 0) {
throw new Error(`JDK-Compiler javac ist nicht verwendbar: ${String(probe.stderr || probe.stdout || `Exit ${probe.status}`).trim()}`);
}
return command;
}
function replaceClasses(runtimeRoot, temporaryClasses) {
const classesDir = path.join(runtimeRoot, "classes");
const backupDir = path.join(runtimeRoot, `.classes-previous-${process.pid}-${Date.now()}`);
let previousMoved = false;
try {
if (fs.existsSync(classesDir)) {
fs.renameSync(classesDir, backupDir);
previousMoved = true;
}
fs.renameSync(temporaryClasses, classesDir);
if (previousMoved) {
fs.rmSync(backupDir, { recursive: true, force: true });
}
} catch (error) {
if (!fs.existsSync(classesDir) && previousMoved && fs.existsSync(backupDir)) {
fs.renameSync(backupDir, classesDir);
}
throw error;
}
}
function compileClasses(runtimeRoot, sources, libs, digest) {
const javac = requireJavac();
const temporaryClasses = fs.mkdtempSync(path.join(runtimeRoot, ".classes-build-"));
try {
const result = spawnSync(javac, [
"-source", "8",
"-target", "8",
"-encoding", "UTF-8",
"-g:none",
"-classpath", libs.join(path.delimiter),
"-d", temporaryClasses,
...sources
], { encoding: "utf8", windowsHide: true });
if (result.error?.code === "ENOENT") {
throw new Error("JDK fehlt: javac wurde nicht gefunden; installiere ein JDK und setze JAVA_HOME oder PATH");
}
if (result.status !== 0) {
throw new Error(`javac fehlgeschlagen: ${String(result.stderr || result.stdout || `Exit ${result.status}`).trim()}`);
}
fs.writeFileSync(path.join(temporaryClasses, stampName), `${digest}\n`, "utf8");
verifyClassesDirectory(temporaryClasses, digest);
replaceClasses(runtimeRoot, temporaryClasses);
} finally {
if (fs.existsSync(temporaryClasses)) {
fs.rmSync(temporaryClasses, { recursive: true, force: true });
}
}
}
function main() {
const { runtimeRoot, checkOnly } = parseArguments(process.argv.slice(2));
const sourceRoot = path.join(runtimeRoot, "src");
const sources = listJavaSources(sourceRoot);
const libs = requiredLibNames.map((name) => path.join(runtimeRoot, "lib", name));
for (const source of sources) {
requireFile(source, "Java-Quelle");
}
for (const lib of libs) {
requireFile(lib, "JVM-Extractor-Bibliothek");
}
const digest = sourceDigest(runtimeRoot, sources, libs);
if (checkOnly) {
verifyClasses(runtimeRoot, digest);
process.stdout.write(`JVM-Extractor-Klassen aktuell: ${digest}\n`);
return;
}
compileClasses(runtimeRoot, sources, libs, digest);
verifyClasses(runtimeRoot, digest);
process.stdout.write(`JVM-Extractor-Klassen gebaut: ${digest}\n`);
}
try {
main();
} catch (error) {
process.stderr.write(`JVM-Extractor-Build fehlgeschlagen: ${String(error?.message || error)}\n`);
process.exitCode = 1;
}