talkis/scripts/verify-windows-release.mjs
2026-07-21 00:09:30 +03:00

171 lines
4.9 KiB
JavaScript

import { execFileSync } from "node:child_process";
import {
closeSync,
existsSync,
openSync,
readSync,
readdirSync,
} from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const rootDir = dirname(dirname(fileURLToPath(import.meta.url)));
const tauriDir = join(rootDir, "src-tauri");
const expectedTargetTriple =
process.env.TALKIS_WINDOWS_TARGET_TRIPLE || "x86_64-pc-windows-msvc";
const IMAGE_FILE_MACHINE_I386 = 0x014c;
const IMAGE_FILE_MACHINE_AMD64 = 0x8664;
function parsePeMachine(header, label) {
if (header.length < 64 || header.toString("ascii", 0, 2) !== "MZ") {
throw new Error(
`${label} is not a Windows PE executable (missing MZ header)`,
);
}
const peOffset = header.readUInt32LE(0x3c);
if (peOffset + 6 > header.length) {
throw new Error(`${label} PE header is outside the inspected bytes`);
}
if (header.toString("ascii", peOffset, peOffset + 4) !== "PE\u0000\u0000") {
throw new Error(
`${label} is not a Windows PE executable (missing PE signature)`,
);
}
return header.readUInt16LE(peOffset + 4);
}
function readPeMachine(path) {
const fd = openSync(path, "r");
try {
const dosHeader = Buffer.alloc(64);
const dosBytes = readSync(fd, dosHeader, 0, dosHeader.length, 0);
if (dosBytes < dosHeader.length) {
throw new Error(`${path} is too small to contain a PE header`);
}
const peOffset = dosHeader.readUInt32LE(0x3c);
const header = Buffer.alloc(peOffset + 6);
const bytes = readSync(fd, header, 0, header.length, 0);
if (bytes < header.length) {
throw new Error(`${path} is truncated before its PE header`);
}
return parsePeMachine(header, path);
} finally {
closeSync(fd);
}
}
function machineLabel(machine) {
if (machine === IMAGE_FILE_MACHINE_AMD64) return "x86-64";
if (machine === IMAGE_FILE_MACHINE_I386) return "x86";
return `unknown (0x${machine.toString(16).padStart(4, "0")})`;
}
function requireMachine(path, expectedMachine) {
if (!existsSync(path)) {
throw new Error(`Required Windows release executable is missing: ${path}`);
}
const machine = readPeMachine(path);
if (machine !== expectedMachine) {
throw new Error(
`${path} has ${machineLabel(machine)} architecture; expected ${machineLabel(expectedMachine)}`,
);
}
console.log(`Verified ${machineLabel(machine)} PE: ${path}`);
}
function runSelfTest() {
const fixture = Buffer.alloc(256);
fixture.write("MZ", 0, "ascii");
fixture.writeUInt32LE(0x80, 0x3c);
fixture.write("PE\u0000\u0000", 0x80, "ascii");
fixture.writeUInt16LE(IMAGE_FILE_MACHINE_AMD64, 0x84);
const machine = parsePeMachine(fixture, "synthetic PE");
if (machine !== IMAGE_FILE_MACHINE_AMD64) {
throw new Error("PE parser self-test returned an unexpected architecture");
}
console.log("Windows PE verifier self-test passed");
}
if (process.argv.includes("--self-test")) {
runSelfTest();
process.exit(0);
}
if (process.platform !== "win32") {
throw new Error(
"Windows release verification must run on a native Windows runner",
);
}
const rustVersion = execFileSync("rustc", ["-vV"], { encoding: "utf8" });
const rustHost = rustVersion
.split("\n")
.find((line) => line.startsWith("host:"))
?.slice("host:".length)
.trim();
if (rustHost !== expectedTargetTriple) {
throw new Error(
`Windows release host is ${rustHost || "(unknown)"}; expected ${expectedTargetTriple}`,
);
}
const releaseDir = join(tauriDir, "target", "release");
const binariesDir = join(tauriDir, "binaries");
const mainExecutableCandidates = [
join(releaseDir, "Talkis.exe"),
join(releaseDir, "talkis.exe"),
];
const mainExecutable = mainExecutableCandidates.find(existsSync);
if (!mainExecutable) {
throw new Error(
`Talkis Windows executable is missing: ${mainExecutableCandidates.join(" or ")}`,
);
}
requireMachine(mainExecutable, IMAGE_FILE_MACHINE_AMD64);
for (const sidecar of [
"talkis-ffmpeg",
"talkis-stt",
"talkis-diarize",
"talkis-llm",
]) {
requireMachine(
join(binariesDir, `${sidecar}-${expectedTargetTriple}.exe`),
IMAGE_FILE_MACHINE_AMD64,
);
}
const nsisDir = join(releaseDir, "bundle", "nsis");
const installers = existsSync(nsisDir)
? readdirSync(nsisDir)
.filter((name) => name.toLowerCase().endsWith(".exe"))
.map((name) => join(nsisDir, name))
: [];
if (installers.length === 0) {
throw new Error(`Windows NSIS installer is missing from ${nsisDir}`);
}
for (const installer of installers) {
const machine = readPeMachine(installer);
if (
machine !== IMAGE_FILE_MACHINE_I386 &&
machine !== IMAGE_FILE_MACHINE_AMD64
) {
throw new Error(
`${installer} uses unsupported installer architecture ${machineLabel(machine)}`,
);
}
console.log(
`Verified Windows-compatible NSIS stub (${machineLabel(machine)}): ${installer}`,
);
}
console.log(
`Windows release architecture verification passed for ${expectedTargetTriple}`,
);