// Copyright (C) 2026 Zaher Salman // SPDX-License-Identifier: GPL-3.0-or-later // Windows support for the bundled TRIM.SP-NL engine. The Linux engine is a // statically linked ELF binary; it cannot run natively on Windows, so the // Windows installer also ships the whole engine as a WSL2 root filesystem and // the main process imports it as a private distribution on first run. From then // on the renderer launches the engine as // wsl.exe -d trimspNL-runtime -- /opt/trimsp-runtime/bin/trimspNL [rng] // (see prepareTrimspLaunch in TrimSPelec.js). // // All heavy lifting is expressed as small pure helpers so the import flow can be // unit-tested without Windows: the only Windows-specific piece is the default // spawner, which is injectable. const fs = require("fs"); const path = require("path"); const childProcess = require("child_process"); // Keep these in sync with TrimSPelec.js (TRIMSP_WSL_DISTRO / TRIMSP_WSL_PREFIX) // and packaging/wsl2/build-wsl2.sh (the prefix baked into the rootfs). const DEFAULT_DISTRO = "trimspNL-runtime"; const DEFAULT_PREFIX = "/opt/trimsp-runtime"; const ROOTFS_BASENAME = "trimspNL-runtime-rootfs.tar.gz"; // Candidate locations for the shipped rootfs tarball, in priority order. A // forge/electron-packager `extraResource` entry is copied *flat* into the // resources directory (the first candidate); the `wsl-runtime/` subdirectory is // kept for an explicit directory layout; the last tolerates running from a // development checkout without repackaging. function rootfsCandidates(resourcesPath) { const base = resourcesPath || "."; return [ path.join(base, ROOTFS_BASENAME), path.join(base, "wsl-runtime", ROOTFS_BASENAME), path.join(base, "packaging", "wsl2", "out", ROOTFS_BASENAME) ]; } function findRootfsTarball(resourcesPath) { const candidates = rootfsCandidates(resourcesPath); for (const candidate of candidates) { try { if (fs.statSync(candidate).isFile()) return candidate; } catch (error) { // Missing candidate: keep looking. } } return ""; } // Where the imported distribution's virtual disk is stored on the Windows host. function distroInstallDir(appDataPath, distro) { const name = distro || DEFAULT_DISTRO; return path.join(appDataPath || ".", "TrimSP", name); } // wsl.exe writes UTF-16LE for --list output but UTF-8 for progress text. Detect // UTF-16LE by the presence of NUL bytes rather than trusting the locale. function decodeWslOutput(chunk) { if (chunk == null) return ""; if (!Buffer.isBuffer(chunk)) return String(chunk); if (chunk.length === 0) return ""; return chunk.includes(0) ? chunk.toString("utf16le") : chunk.toString("utf8"); } // Turn `wsl.exe --list --quiet` output into a clean list of distribution names. function parseDistroList(text) { return String(text || "") .replace(/\u0000/g, "") .split(/\r?\n/) .map((line) => line.replace(/[\u0000\s]+$/g, "").replace(/^\u0000+/, "").trim()) .filter((line) => line.length > 0); } function distroIsRegistered(text, distro) { const wanted = String(distro || DEFAULT_DISTRO).toLowerCase(); return parseDistroList(text).some((name) => name.toLowerCase() === wanted); } // `wsl --import [--version <1|2>]`. function importArgs(rootfsPath, targetDir, distro) { return [ "--import", distro || DEFAULT_DISTRO, targetDir, rootfsPath, "--version", "2" ]; } function listArgs() { return ["--list", "--quiet"]; } // Default spawner: runs wsl.exe and resolves with { status, stdout, stderr }. // Output is streamed to onOutput as it arrives so long imports can report // progress without blocking the Electron main process. function defaultRunWsl(args, onOutput) { return new Promise((resolve) => { let child; try { child = childProcess.spawn("wsl.exe", args, { windowsHide: true }); } catch (error) { resolve({ status: -1, stdout: "", stderr: "", error: error.message || String(error) }); return; } let stdout = ""; let stderr = ""; const forward = (stream, sink) => { stream.on("data", (chunk) => { const text = decodeWslOutput(chunk); if (sink === "stdout") stdout += text; else stderr += text; if (typeof onOutput === "function") onOutput(text); }); }; forward(child.stdout, "stdout"); forward(child.stderr, "stderr"); child.on("error", (error) => { resolve({ status: -1, stdout, stderr, error: error.message || String(error) }); }); child.on("close", (code) => { resolve({ status: code == null ? -1 : code, stdout, stderr, error: null }); }); }); } // Ensure the private WSL2 distribution exists, importing it from the bundled // rootfs on first run. Resolves with a plain status object; never throws. // // options: // platform - override process.platform (tests) // resourcesPath - Electron process.resourcesPath (where the rootfs ships) // appDataPath - %LOCALAPPDATA% (where the distribution is imported) // distro - distribution name override // rootfsPath - explicit rootfs path (tests) // run - spawner override, signature run(args, onOutput) -> Promise // onProgress - callback receiving { phase, ... } as work proceeds async function ensureWSLRuntime(options) { const opts = options || {}; const platform = opts.platform || process.platform; const distro = opts.distro || DEFAULT_DISTRO; const run = opts.run || defaultRunWsl; const onProgress = typeof opts.onProgress === "function" ? opts.onProgress : function () {}; if (platform !== "win32") { return { ok: true, status: "skipped", reason: "not-windows", distro: distro }; } const rootfs = opts.rootfsPath || findRootfsTarball(opts.resourcesPath || ""); if (!rootfs) { return { ok: false, status: "missing-rootfs", distro: distro, error: "Bundled WSL runtime archive not found under " + (opts.resourcesPath || "resources") }; } onProgress({ phase: "checking", distro: distro }); const listed = await run(listArgs()); if (listed && listed.status === 0 && distroIsRegistered(listed.stdout, distro)) { onProgress({ phase: "ready", distro: distro }); return { ok: true, status: "already-present", distro: distro, rootfs: rootfs }; } const target = distroInstallDir(opts.appDataPath || "", distro); try { fs.mkdirSync(path.dirname(target), { recursive: true }); } catch (error) { return { ok: false, status: "error", distro: distro, error: "Cannot create install directory: " + (error.message || String(error)) }; } onProgress({ phase: "importing", distro: distro, rootfs: rootfs, target: target }); const imported = await run(importArgs(rootfs, target, distro), (text) => { onProgress({ phase: "importing", distro: distro, output: text }); }); if (!imported || imported.status !== 0) { return { ok: false, status: "import-failed", distro: distro, target: target, error: (imported && (imported.error || imported.stderr || imported.stdout)) || "wsl --import failed" }; } onProgress({ phase: "ready", distro: distro, target: target }); return { ok: true, status: "imported", distro: distro, target: target, rootfs: rootfs }; } module.exports = { DEFAULT_DISTRO, DEFAULT_PREFIX, ROOTFS_BASENAME, rootfsCandidates, findRootfsTarball, distroInstallDir, decodeWslOutput, parseDistroList, distroIsRegistered, importArgs, listArgs, defaultRunWsl, ensureWSLRuntime };