Add Windows WSL2 engine runtime
Windows cannot run the bundled Linux TRIM.SP-NL engine natively. Package the same statically linked engine as a private WSL2 rootfs archive and launch it through wsl.exe, mirroring the muSRFlow approach. - packaging/wsl2/build-wsl2.sh builds trimspNL-runtime-rootfs.tar.gz from the static engine (no distro libraries needed; refuses a dynamically linked binary) into a minimal almalinux:8 rootfs at /opt/trimsp-runtime. - wsl-runtime.js: pure main-process module that checks for and imports the distribution on first run, with an injectable spawner and a plain status contract (skipped/missing-rootfs/already-present/imported/import-failed). - main.js imports the runtime in the background at startup, broadcasts wslRuntimeStatus, and exposes wslRuntimeStatus/ensureWslRuntime over IPC. - TrimSPelec.js detects a Windows host with the bundled engine, translates Windows paths to /mnt/<drive>, routes executeFileAsync and the synchronous single-run command through wsl.exe, and gates the first run on the import via window.TrimSPRuntimeGate. - TrimSPlib.js installs a no-op gate default and awaits it in startSequence. - forge.config.js ships the rootfs via packagerConfig.extraResource when built. - tests/wsl-runtime.test.js (11 cases) covers the module and the renderer launch translation, including inertness on Linux.
This commit is contained in:
1 parent
b729c9b047
commit
db390d3969
11 files changed
+903
-4
No files matched your search
@@ -138,3 +138,6 @@ fortran/trimspNL-profile
|
||||
# Generated alternative-RNG validation output trees
|
||||
/tests-pcg32/
|
||||
/tests-xoshiro/
|
||||
|
||||
# WSL2 rootfs build output (built by packaging/wsl2)
|
||||
/packaging/wsl2/out/
|
||||
@@ -2,6 +2,15 @@
|
||||
|
||||
## Unreleased
|
||||
|
||||
### Added
|
||||
|
||||
- Windows builds can now run the bundled TRIM.SP-NL engine through a private
|
||||
WSL2 distribution. The statically linked engine ships as a rootfs archive
|
||||
(`packaging/wsl2/build-wsl2.sh`), is imported into `%LOCALAPPDATA%\TrimSP`
|
||||
on first run, and is launched via `wsl.exe` with Windows paths translated to
|
||||
their `/mnt/<drive>` form. A runtime gate waits for the import before the
|
||||
first run, and progress is reported in the status bar.
|
||||
|
||||
### Fixed
|
||||
|
||||
- The RPM no longer packages ELF build-id links under `/usr/lib/.build-id`,
|
||||
|
||||
+134
-2
@@ -71,10 +71,12 @@ function getFiles(dir, filelist){
|
||||
}
|
||||
|
||||
// Run one TRIM.SP-NL process asynchronously in a given working directory so the
|
||||
// scan loop can keep several local jobs in flight at once.
|
||||
// scan loop can keep several local jobs in flight at once. On Windows the
|
||||
// bundled Linux engine is dispatched through WSL2 (see prepareTrimspLaunch).
|
||||
function executeFileAsync(binary, args, workdir) {
|
||||
return new Promise((resolve, reject) => {
|
||||
execFile(binary, args, {cwd: workdir}, (err) => {
|
||||
let launch = prepareTrimspLaunch(binary, args);
|
||||
execFile(launch.command, launch.args, {cwd: workdir}, (err) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
@@ -190,6 +192,136 @@ function getTrimBinaryPath(configuredPath) {
|
||||
return "trimspNL";
|
||||
}
|
||||
|
||||
// --- Windows / WSL2 execution -------------------------------------------
|
||||
// A packaged Windows build has no native engine. Instead the statically linked
|
||||
// Linux trimspNL shipped with the bundle is imported into a private WSL2
|
||||
// distribution and run through wsl.exe. Keep these constants in sync with
|
||||
// wsl-runtime.js (DEFAULT_DISTRO / DEFAULT_PREFIX) and
|
||||
// packaging/wsl2/build-wsl2.sh (the prefix baked into the rootfs).
|
||||
var TRIMSP_WSL_DISTRO = "trimspNL-runtime";
|
||||
var TRIMSP_WSL_PREFIX = "/opt/trimsp-runtime";
|
||||
|
||||
// Where the bundled engine lives inside the packaged app. On Windows this is
|
||||
// the statically linked ELF marker that triggers WSL routing; on Linux it is the
|
||||
// engine itself.
|
||||
function bundledTrimBinaryPath() {
|
||||
if (!process.resourcesPath) { return ""; }
|
||||
let candidate = path.join(process.resourcesPath, "bin", "trimspNL");
|
||||
return fileExists(candidate) ? candidate : "";
|
||||
}
|
||||
|
||||
function isWslRuntimeHost() {
|
||||
return process.platform === "win32" && bundledTrimBinaryPath() !== "";
|
||||
}
|
||||
|
||||
// Translate an absolute Windows path (C:\dir\file or C:/dir/file) to the path
|
||||
// its WSL mount exposes. Non-path arguments (run names, rng keywords) pass
|
||||
// through unchanged.
|
||||
function windowsPathToWsl(input) {
|
||||
if (typeof input !== "string" || input === "") { return input; }
|
||||
let match = /^([A-Za-z]):[\\/](.*)$/.exec(input);
|
||||
if (!match) { return input; }
|
||||
return "/mnt/" + match[1].toLowerCase() + "/" + match[2].replace(/\\/g, "/");
|
||||
}
|
||||
|
||||
// Wrap the bundled Linux engine as a wsl.exe invocation, translating path
|
||||
// arguments. Commands outside the bundled engine (host tools, or a user-
|
||||
// configured Windows-native engine) are left untouched.
|
||||
function prepareTrimspLaunch(command, args) {
|
||||
args = args || [];
|
||||
if (!isWslRuntimeHost()) {
|
||||
return { command: command, args: args, viaWsl: false };
|
||||
}
|
||||
let bundled = bundledTrimBinaryPath();
|
||||
if (!bundled || command !== bundled) {
|
||||
return { command: command, args: args, viaWsl: false };
|
||||
}
|
||||
let wslArgs = ["-d", TRIMSP_WSL_DISTRO, "--", TRIMSP_WSL_PREFIX + "/bin/trimspNL"]
|
||||
.concat(args.map(windowsPathToWsl));
|
||||
return { command: "wsl.exe", args: wslArgs, viaWsl: true };
|
||||
}
|
||||
|
||||
// Shell-string counterpart used by the synchronous single-run path, which builds
|
||||
// a command such as "cd <dir>; <trimBin> <run> <rng>". Only the bundled engine
|
||||
// and any Windows paths in the string are rewritten.
|
||||
function prepareTrimspCommandString(command) {
|
||||
if (!isWslRuntimeHost() || typeof command !== "string") { return command; }
|
||||
let bundled = bundledTrimBinaryPath();
|
||||
if (!bundled) { return command; }
|
||||
return command.split(bundled).join("wsl.exe -d " + TRIMSP_WSL_DISTRO + " -- "
|
||||
+ TRIMSP_WSL_PREFIX + "/bin/trimspNL");
|
||||
}
|
||||
|
||||
// First-run runtime gate. The main process imports the bundled WSL2 distribution
|
||||
// in the background at startup; until that finishes a Run must wait rather than
|
||||
// launch the engine inside a distribution that does not exist yet.
|
||||
var wslRuntimeReadyPromise = null;
|
||||
var wslRuntimeGateMessage = "";
|
||||
|
||||
function refreshWslRuntimeGate() {
|
||||
if (wslRuntimeReadyPromise) { return wslRuntimeReadyPromise; }
|
||||
wslRuntimeReadyPromise = ipcRenderer.invoke('ensureWslRuntime').then(function (result) {
|
||||
if (!result || result.ok) {
|
||||
wslRuntimeGateMessage = "";
|
||||
return true;
|
||||
}
|
||||
// Let a later action retry (for example after the user installs WSL2).
|
||||
wslRuntimeReadyPromise = null;
|
||||
wslRuntimeGateMessage = result.error || ("runtime setup failed: " + result.status);
|
||||
return false;
|
||||
}).catch(function (error) {
|
||||
wslRuntimeReadyPromise = null;
|
||||
wslRuntimeGateMessage = error && error.message ? error.message : String(error);
|
||||
return false;
|
||||
});
|
||||
return wslRuntimeReadyPromise;
|
||||
}
|
||||
|
||||
function wslRuntimeGateReady() {
|
||||
if (!isWslRuntimeHost()) { return Promise.resolve(true); }
|
||||
let unsubscribe = function () {};
|
||||
unsubscribe = function (event, state) {
|
||||
if (!state) { return; }
|
||||
if (state.status === "checking") {
|
||||
if (typeof updateStatusBar === 'function') {
|
||||
updateStatusBar({ run: "Checking the bundled engine runtime..." });
|
||||
}
|
||||
} else if (state.status === "importing") {
|
||||
if (typeof updateStatusBar === 'function') {
|
||||
updateStatusBar({ run: "Preparing the bundled engine runtime (first run only)..." });
|
||||
}
|
||||
}
|
||||
};
|
||||
ipcRenderer.on("wslRuntimeStatus", unsubscribe);
|
||||
return refreshWslRuntimeGate().then(function (ready) {
|
||||
ipcRenderer.removeListener("wslRuntimeStatus", unsubscribe);
|
||||
return ready;
|
||||
}, function (error) {
|
||||
ipcRenderer.removeListener("wslRuntimeStatus", unsubscribe);
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
// The shared library installs a no-op gate; the Electron shell replaces it with
|
||||
// the WSL-aware one (a no-op on non-Windows hosts).
|
||||
if (window.TrimSPRuntimeGate) {
|
||||
window.TrimSPRuntimeGate.ready = wslRuntimeGateReady;
|
||||
window.TrimSPRuntimeGate.message = function () {
|
||||
return wslRuntimeGateMessage
|
||||
? wslRuntimeGateMessage
|
||||
: "Check that WSL2 is installed, then try again.";
|
||||
};
|
||||
} else {
|
||||
window.TrimSPRuntimeGate = {
|
||||
ready: wslRuntimeGateReady,
|
||||
message: function () {
|
||||
return wslRuntimeGateMessage
|
||||
? wslRuntimeGateMessage
|
||||
: "Check that WSL2 is installed, then try again.";
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
function setWorkFolder(foldername) {
|
||||
const folder = Array.isArray(foldername) ? foldername[0] : foldername;
|
||||
if (folder) {
|
||||
|
||||
@@ -4,6 +4,36 @@ const electronExecSync = (typeof module !== 'undefined' && module.exports)
|
||||
? require('child_process').execSync
|
||||
: null;
|
||||
|
||||
// Before a run touches the external engine the shell may need to wait for its
|
||||
// runtime to become usable (on Windows the bundled WSL2 distribution is imported
|
||||
// on first run). The Electron shell replaces this no-op with a real gate.
|
||||
if (typeof window !== 'undefined' && !window.TrimSPRuntimeGate) {
|
||||
window.TrimSPRuntimeGate = {
|
||||
ready: function () { return Promise.resolve(true); },
|
||||
message: function () { return ''; }
|
||||
};
|
||||
}
|
||||
|
||||
async function ensureRuntimeGateReady(actionLabel) {
|
||||
let gate = (typeof window !== 'undefined') ? window.TrimSPRuntimeGate : null;
|
||||
if (!gate || typeof gate.ready !== 'function') { return true; }
|
||||
let ready = true;
|
||||
try {
|
||||
ready = await gate.ready(actionLabel);
|
||||
} catch (error) {
|
||||
ready = false;
|
||||
}
|
||||
if (ready === false) {
|
||||
updateStatusBar({
|
||||
run: "The " + String(actionLabel || 'run').toLowerCase()
|
||||
+ " runtime is not ready. "
|
||||
+ (typeof gate.message === 'function' ? gate.message() : '')
|
||||
});
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// This is an object that contains all GUI ids and values
|
||||
var All = new Object();
|
||||
|
||||
@@ -4254,6 +4284,9 @@ async function runSimulationSequence() {
|
||||
if (!webOrApp) {
|
||||
cmd = "cd " + runWorkPath;
|
||||
cmd += ";" + trimBin + ' ' + job.filename + ' ' + rngType;
|
||||
if (typeof prepareTrimspCommandString === 'function') {
|
||||
cmd = prepareTrimspCommandString(cmd);
|
||||
}
|
||||
electronExecSync(cmd, {stdio: 'inherit'});
|
||||
}
|
||||
results[job.index] = await collectRunResult(job);
|
||||
@@ -4351,6 +4384,11 @@ async function startSequence() {
|
||||
updateStatusBar({ run: 'A run is already in progress.' });
|
||||
return(0);
|
||||
}
|
||||
// Wait for the shell's external runtime (the bundled WSL2 distribution on
|
||||
// Windows) before launching the engine. No-op everywhere else.
|
||||
if (!await ensureRuntimeGateReady('Run')) {
|
||||
return(0);
|
||||
}
|
||||
runInProgress = true;
|
||||
const runButton = document.getElementById("runSimulationButton");
|
||||
if (runButton) { runButton.disabled = true; }
|
||||
|
||||
+12
-1
@@ -1,4 +1,5 @@
|
||||
const path = require('path');
|
||||
const fs = require('fs');
|
||||
const redhatInstaller = require('electron-installer-redhat');
|
||||
const redhatDependencies = require('electron-installer-redhat/src/dependencies');
|
||||
|
||||
@@ -17,6 +18,16 @@ const linuxDesktopTemplate = path.resolve(__dirname, 'packaging/linux/trimsp.des
|
||||
const linuxRpmSpecTemplate = path.resolve(__dirname, 'packaging/linux/trimsp.spec.ejs');
|
||||
const includeFlatpak = process.env.FORGE_FLATPAK === '1';
|
||||
|
||||
// Windows cannot run the bundled Linux engine natively. The Windows build also
|
||||
// ships the engine as a WSL2 rootfs archive (built by packaging/wsl2) which the
|
||||
// app imports as a private distribution on first run. Only add the archive when
|
||||
// it has actually been built, so Linux/macOS packaging is unaffected.
|
||||
const wsl2Rootfs = path.resolve(__dirname, 'packaging/wsl2/out/trimspNL-runtime-rootfs.tar.gz');
|
||||
const extraResource = ['resources/bin'];
|
||||
if (fs.existsSync(wsl2Rootfs)) {
|
||||
extraResource.push(wsl2Rootfs);
|
||||
}
|
||||
|
||||
// The upstream RPM maker assumes Fedora-style package names. Mageia provides
|
||||
// GTK 3 under lib64gtk+3_0, so allow either name in the generated Requires.
|
||||
redhatDependencies.dependencyMap.gtk3 = '(gtk3 or lib64gtk+3_0)';
|
||||
@@ -101,7 +112,7 @@ if (includeFlatpak) {
|
||||
module.exports = {
|
||||
packagerConfig: {
|
||||
icon: 'appicons/icons/png/1024x1024.png',
|
||||
extraResource: ['resources/bin'],
|
||||
extraResource: extraResource,
|
||||
ignore: [
|
||||
'Readme.md',
|
||||
'ToDo.txt',
|
||||
|
||||
@@ -2,6 +2,7 @@ const path = require('path');
|
||||
const fs = require('fs');
|
||||
const { spawnSync } = require('child_process');
|
||||
const { app, BrowserWindow, Menu, clipboard, dialog, ipcMain, nativeImage } = require('electron');
|
||||
const wslRuntime = require('./wsl-runtime');
|
||||
|
||||
let currentWorkPath = process.cwd();
|
||||
const startupOptions = parseStartupOptions(process.argv);
|
||||
@@ -521,7 +522,58 @@ function createWindow () {
|
||||
// win.openDevTools();
|
||||
}
|
||||
|
||||
app.whenReady().then(createWindow)
|
||||
// Windows runs the bundled TRIM.SP-NL engine inside a private WSL2
|
||||
// distribution: the installer also ships the engine as a rootfs archive, and the
|
||||
// first launch imports it. The check/import runs in the background so the window
|
||||
// appears immediately; status is broadcast to the renderer and mirrored through
|
||||
// IPC so the UI can surface progress or retry a failed import.
|
||||
let wslRuntimeState = { status: process.platform === 'win32' ? 'pending' : 'not-windows' };
|
||||
let wslRuntimeSetup = null;
|
||||
|
||||
function broadcastWslRuntimeState(state) {
|
||||
wslRuntimeState = state;
|
||||
const target = BrowserWindow.getAllWindows()[0];
|
||||
if (target && !target.isDestroyed()) {
|
||||
target.webContents.send('wslRuntimeStatus', state);
|
||||
}
|
||||
}
|
||||
|
||||
function setupWslRuntime() {
|
||||
if (process.platform !== 'win32') {
|
||||
return Promise.resolve(wslRuntimeState);
|
||||
}
|
||||
if (wslRuntimeSetup) return wslRuntimeSetup;
|
||||
broadcastWslRuntimeState({ status: 'checking', distro: wslRuntime.DEFAULT_DISTRO });
|
||||
wslRuntimeSetup = wslRuntime.ensureWSLRuntime({
|
||||
resourcesPath: process.resourcesPath,
|
||||
appDataPath: process.env.LOCALAPPDATA || app.getPath('userData'),
|
||||
onProgress: function (progress) {
|
||||
broadcastWslRuntimeState(Object.assign({ status: progress.phase }, progress));
|
||||
}
|
||||
}).then(function (result) {
|
||||
broadcastWslRuntimeState(result);
|
||||
if (!result.ok) {
|
||||
wslRuntimeSetup = null;
|
||||
console.log('[wsl-runtime] setup failed: ' + result.status
|
||||
+ (result.error ? ' - ' + result.error : ''));
|
||||
} else {
|
||||
console.log('[wsl-runtime] ' + result.status);
|
||||
}
|
||||
return result;
|
||||
}).catch(function (error) {
|
||||
wslRuntimeSetup = null;
|
||||
const result = { ok: false, status: 'error', error: error.message || String(error) };
|
||||
broadcastWslRuntimeState(result);
|
||||
console.log('[wsl-runtime] error: ' + result.error);
|
||||
return result;
|
||||
});
|
||||
return wslRuntimeSetup;
|
||||
}
|
||||
|
||||
app.whenReady().then(() => {
|
||||
createWindow();
|
||||
setupWslRuntime();
|
||||
})
|
||||
global.path = app.getAppPath();
|
||||
//console.log("From main: ",global.path);
|
||||
|
||||
@@ -568,6 +620,17 @@ ipcMain.handle('getStartupOptions', async () => {
|
||||
return startupOptions;
|
||||
});
|
||||
|
||||
// Windows bundled-runtime lifecycle: expose the current status and let the
|
||||
// renderer drive (or retry) the first-run import through the same memoized
|
||||
// promise the background startup path uses.
|
||||
ipcMain.handle('wslRuntimeStatus', async () => {
|
||||
return wslRuntimeState;
|
||||
});
|
||||
|
||||
ipcMain.handle('ensureWslRuntime', async () => {
|
||||
return setupWslRuntime();
|
||||
});
|
||||
|
||||
ipcMain.on('openQuickStartWindow', (event, options) => {
|
||||
const sectionId = options && options.sectionId ? options.sectionId : 'helpLayers';
|
||||
showQuickStartWindow(sectionId);
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
"make": "electron-forge make",
|
||||
"make:dynamic": "npm run build:fortran:dynamic && electron-forge make",
|
||||
"make:flatpak": "npm run build:fortran && FORGE_FLATPAK=1 electron-forge make --targets=@electron-forge/maker-flatpak",
|
||||
"build:wsl2": "bash packaging/wsl2/build-wsl2.sh",
|
||||
"test:wsl-runtime": "node tests/wsl-runtime.test.js",
|
||||
"checksums:md5": "sh -c 'for d in out/make/deb/x64 out/make/rpm/x64 out/make/flatpak/x86_64; do [ -d \"$d\" ] || continue; for f in \"$d\"/*; do [ -f \"$f\" ] || continue; case \"$f\" in *.deb|*.rpm|*.flatpak) base=$(basename \"$f\"); (cd \"$d\" && md5sum \"$base\" > \"$base.md5\");; esac; done; done'",
|
||||
"checksums:sha256": "sh -c 'for d in out/make/deb/x64 out/make/rpm/x64 out/make/flatpak/x86_64; do [ -d \"$d\" ] || continue; for f in \"$d\"/*; do [ -f \"$f\" ] || continue; case \"$f\" in *.deb|*.rpm|*.flatpak) base=$(basename \"$f\"); (cd \"$d\" && sha256sum \"$base\" > \"$base.sha256\");; esac; done; done'",
|
||||
"checksums": "npm run checksums:md5 && npm run checksums:sha256"
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
# Windows (WSL2) engine runtime
|
||||
|
||||
Windows has no native TRIM.SP-NL engine; the Linux engine cannot be run
|
||||
directly. Instead TRIMSP-NL Workbench ships the **same statically linked engine**
|
||||
used by the Linux bundle and runs it inside a bundled WSL2 distribution.
|
||||
|
||||
## How it works
|
||||
|
||||
`TrimSPelec.js` detects the Windows host and the bundled engine
|
||||
(`process.platform === "win32"` and `<resources>/bin/trimspNL` present) and
|
||||
routes the engine through `wsl.exe`:
|
||||
|
||||
```
|
||||
wsl.exe -d trimspNL-runtime -- /opt/trimsp-runtime/bin/trimspNL <run> [rng]
|
||||
```
|
||||
|
||||
Path arguments (`C:\dir\file`) are translated to their WSL mount form
|
||||
(`/mnt/c/dir/file`) so the engine reads and writes the user's Windows files.
|
||||
Run inputs and outputs therefore stay on the Windows side.
|
||||
|
||||
A user-configured **engine binaries** directory with a Windows-native executable
|
||||
is launched directly, and on non-Windows hosts the translation is inert, so the
|
||||
Linux build and development checkouts are unaffected.
|
||||
|
||||
Constants (in `TrimSPelec.js`): the distribution name `trimspNL-runtime` and the
|
||||
in-distro prefix `/opt/trimsp-runtime`.
|
||||
|
||||
## Building the rootfs
|
||||
|
||||
Requires Docker or Podman and the static engine
|
||||
(`npm run build:fortran`, built automatically when missing).
|
||||
|
||||
```sh
|
||||
npm run build:wsl2 # packaging/wsl2/build-wsl2.sh
|
||||
```
|
||||
|
||||
Because `trimspNL-static` is a fully static executable (no libc/libgfortran
|
||||
runtime dependencies) the rootfs is just a minimal base filesystem plus
|
||||
`/opt/trimsp-runtime/bin/trimspNL`. The script writes `/etc/wsl.conf` and exports
|
||||
the container filesystem to
|
||||
`packaging/wsl2/out/trimspNL-runtime-rootfs.tar.gz`.
|
||||
|
||||
The script refuses to run if `resources/bin/trimspNL` is not statically linked.
|
||||
|
||||
## Importing on Windows
|
||||
|
||||
The import is automated. On first run `main.js` (through `wsl-runtime.js`) checks
|
||||
whether the `trimspNL-runtime` distribution is registered and, if not, imports
|
||||
the bundled archive into `%LOCALAPPDATA%\TrimSP\trimspNL-runtime` with
|
||||
`wsl --import ... --version 2`. The work happens in the background so the window
|
||||
opens immediately; progress and failures are broadcast to the renderer on the
|
||||
`wslRuntimeStatus` channel. A failed import resets so a later `ensureWslRuntime`
|
||||
retries. Before running the engine the renderer awaits `window.TrimSPRuntimeGate`,
|
||||
so the first run after startup shows a "Preparing the bundled engine runtime"
|
||||
notice and proceeds once the distribution is ready; the gate is a no-op off
|
||||
Windows.
|
||||
|
||||
The equivalent manual command, useful for debugging:
|
||||
|
||||
```powershell
|
||||
wsl --import trimspNL-runtime "$env:LOCALAPPDATA\TrimSP\trimspNL-runtime" trimspNL-runtime-rootfs.tar.gz --version 2
|
||||
```
|
||||
|
||||
Verify:
|
||||
|
||||
```powershell
|
||||
wsl -d trimspNL-runtime -- /opt/trimsp-runtime/bin/trimspNL
|
||||
```
|
||||
|
||||
## Packaging
|
||||
|
||||
The Windows build ships the rootfs through electron-forge. `forge.config.js`
|
||||
adds the archive to `packagerConfig.extraResource` when it exists, landing it at
|
||||
`resources/wsl-runtime/`, while the engine marker at `resources/bin/trimspNL`
|
||||
lets the renderer detect the bundled runtime and route through WSL.
|
||||
|
||||
## Tests
|
||||
|
||||
```sh
|
||||
npm run test:wsl-runtime # tests/wsl-runtime.test.js
|
||||
```
|
||||
|
||||
Covers the pure main-process module (with an injected `wsl.exe` spawner) and the
|
||||
renderer-side launch translation, including inertness on Linux.
|
||||
|
||||
## Open items
|
||||
|
||||
- The rootfs is built from `almalinux:8`. Adjust `BASE_IMAGE` if the engine is
|
||||
rebuilt against a different baseline.
|
||||
- `wsl.exe` availability is assumed; older Windows builds without WSL2 must
|
||||
install it (`wsl --install`) first.
|
||||
- The macOS runtime is not built.
|
||||
Executable
+108
@@ -0,0 +1,108 @@
|
||||
#!/usr/bin/env bash
|
||||
# Copyright (C) 2026 Zaher Salman
|
||||
# SPDX-License-Identifier: GPL-3.0-or-later
|
||||
#
|
||||
# Build a WSL2 rootfs tarball that carries the TRIM.SP-NL engine for the Windows
|
||||
# edition of TRIMSP-NL Workbench. Windows cannot run the Linux ELF engine
|
||||
# natively, so the same statically linked trimspNL shipped in the Linux bundle
|
||||
# (resources/bin/trimspNL, produced by `make -C fortran trimspNL-static`) is
|
||||
# installed into a private WSL2 distribution. The renderer runs it through
|
||||
# wsl.exe (see TrimSPelec.js::prepareTrimspLaunch).
|
||||
#
|
||||
# Because trimspNL-static is a fully static executable (no libc/libgfortran
|
||||
# runtime dependencies) the rootfs needs no distro-level libraries: it is just a
|
||||
# base filesystem plus /opt/trimsp-runtime/bin/trimspNL.
|
||||
#
|
||||
# Requires Docker or Podman and a built static engine
|
||||
# (npm run build:fortran, invoked automatically when resources/bin/trimspNL is
|
||||
# missing).
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO="$(cd "$HERE/../.." && pwd)"
|
||||
BIN="$REPO/resources/bin/trimspNL"
|
||||
OUT_DIR="$HERE/out"
|
||||
BASE_IMAGE="${BASE_IMAGE:-docker.io/library/almalinux:8}"
|
||||
DISTRO_NAME="${DISTRO_NAME:-trimspNL-runtime}"
|
||||
PREFIX="${PREFIX:-/opt/trimsp-runtime}"
|
||||
OUTPUT="${OUTPUT:-$OUT_DIR/trimspNL-runtime-rootfs.tar.gz}"
|
||||
|
||||
# Honour an explicit ENGINE=docker|podman; otherwise pick the first engine that
|
||||
# is both installed and usable.
|
||||
ENGINE="${ENGINE:-}"
|
||||
if [ -z "$ENGINE" ]; then
|
||||
if command -v docker >/dev/null 2>&1 && docker info >/dev/null 2>&1; then
|
||||
ENGINE=docker
|
||||
elif command -v podman >/dev/null 2>&1 && podman info >/dev/null 2>&1; then
|
||||
ENGINE=podman
|
||||
else
|
||||
echo "ERROR: no usable docker or podman engine (set ENGINE to override)" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
echo "Using container engine: $ENGINE"
|
||||
|
||||
if [ ! -x "$BIN" ]; then
|
||||
echo "resources/bin/trimspNL not built yet; running npm run build:fortran"
|
||||
( cd "$REPO" && npm run build:fortran )
|
||||
fi
|
||||
|
||||
# The Windows runtime ships the engine as a relocatable binary, so a static
|
||||
# build is required. Fail loudly rather than produce a rootfs that cannot run.
|
||||
if command -v file >/dev/null 2>&1; then
|
||||
if ! file "$BIN" | grep -q "statically linked"; then
|
||||
echo "ERROR: $BIN is not statically linked." >&2
|
||||
echo " Build it with: npm run build:fortran (make -C fortran trimspNL-static)" >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
mkdir -p "$OUT_DIR"
|
||||
CONTAINER="trimspNL-wsl2-build"
|
||||
"$ENGINE" rm -f "$CONTAINER" >/dev/null 2>&1 || true
|
||||
trap '"$ENGINE" rm -f "$CONTAINER" >/dev/null 2>&1 || true' EXIT
|
||||
|
||||
echo "Staging WSL2 rootfs in $BASE_IMAGE"
|
||||
"$ENGINE" run --name "$CONTAINER" \
|
||||
-v "$BIN:/tmp/trimspNL:ro" \
|
||||
-e "PREFIX=$PREFIX" \
|
||||
"$BASE_IMAGE" \
|
||||
bash -c '
|
||||
set -euo pipefail
|
||||
|
||||
mkdir -p "$PREFIX/bin"
|
||||
install -m 0755 /tmp/trimspNL "$PREFIX/bin/trimspNL"
|
||||
|
||||
# WSL defaults: no systemd in these tarballs, run as root, and keep the
|
||||
# Windows PATH interop so wsl.exe can be launched with translated paths.
|
||||
printf "[user]\ndefault=root\n[interop]\nappendWindowsPath=true\n" > /etc/wsl.conf
|
||||
printf "TRIMSP-NL bundled TRIM.SP-NL engine\n" > "$PREFIX/README.txt"
|
||||
|
||||
if [ -x "$PREFIX/bin/trimspNL" ]; then
|
||||
echo " ok trimspNL"
|
||||
else
|
||||
echo " MISSING trimspNL" >&2
|
||||
exit 1
|
||||
fi
|
||||
# Smoke test: the engine prints its banner/usage when run without a run
|
||||
# file, so a non-zero exit is still proof that it executed.
|
||||
"$PREFIX/bin/trimspNL" >/dev/null 2>&1 || true
|
||||
echo "runtime self-check OK"
|
||||
'
|
||||
|
||||
echo "Exporting rootfs to $OUTPUT"
|
||||
"$ENGINE" export "$CONTAINER" | gzip -c > "$OUTPUT"
|
||||
|
||||
cat <<EOF
|
||||
|
||||
Done: $OUTPUT
|
||||
|
||||
Import once on Windows (per user):
|
||||
|
||||
wsl --import $DISTRO_NAME %LOCALAPPDATA%\\TrimSP\\$DISTRO_NAME "$(basename "$OUTPUT")" --version 2
|
||||
|
||||
Then TRIMSP-NL Workbench launches the bundled engine as:
|
||||
|
||||
wsl.exe -d $DISTRO_NAME -- $PREFIX/bin/trimspNL <run> [rng]
|
||||
EOF
|
||||
@@ -0,0 +1,231 @@
|
||||
// Tests for the Windows/WSL2 bundled-engine runtime plumbing.
|
||||
//
|
||||
// Two layers are covered:
|
||||
// 1. the pure main-process module wsl-runtime.js, driven with an injected
|
||||
// spawner so no Windows host is required;
|
||||
// 2. the renderer-side launch translation and runtime gate in TrimSPelec.js,
|
||||
// lifted out of the shipped source and run against minimal stubs.
|
||||
//
|
||||
// Run with: node tests/wsl-runtime.test.js
|
||||
|
||||
const assert = require('assert');
|
||||
const fs = require('fs');
|
||||
const os = require('os');
|
||||
const path = require('path');
|
||||
|
||||
const wsl = require('../wsl-runtime');
|
||||
|
||||
// --- helpers ---------------------------------------------------------------
|
||||
|
||||
function makeRun(responses) {
|
||||
const calls = [];
|
||||
const run = function (args, onOutput) {
|
||||
calls.push(args);
|
||||
const next = responses.shift() || { status: 0, stdout: '', stderr: '' };
|
||||
if (typeof onOutput === 'function' && next.stderr) onOutput(next.stderr);
|
||||
return Promise.resolve(next);
|
||||
};
|
||||
run.calls = calls;
|
||||
return run;
|
||||
}
|
||||
|
||||
function makeRootfs() {
|
||||
const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'trimsp-wsl-'));
|
||||
const file = path.join(dir, wsl.ROOTFS_BASENAME);
|
||||
fs.writeFileSync(file, 'rootfs');
|
||||
return { dir, file };
|
||||
}
|
||||
|
||||
// --- 1. pure module --------------------------------------------------------
|
||||
|
||||
function testDecodeWslOutput() {
|
||||
assert.strictEqual(wsl.decodeWslOutput(Buffer.from('abc', 'utf16le')), 'abc');
|
||||
assert.strictEqual(wsl.decodeWslOutput(Buffer.from('abc', 'utf8')), 'abc');
|
||||
assert.strictEqual(wsl.decodeWslOutput('plain'), 'plain');
|
||||
assert.strictEqual(wsl.decodeWslOutput(null), '');
|
||||
assert.strictEqual(wsl.decodeWslOutput(Buffer.alloc(0)), '');
|
||||
}
|
||||
|
||||
function testParseDistroList() {
|
||||
assert.deepStrictEqual(wsl.parseDistroList('a\r\nb\r\n'), ['a', 'b']);
|
||||
assert.deepStrictEqual(wsl.parseDistroList(' a \n\n b '), ['a', 'b']);
|
||||
assert.deepStrictEqual(wsl.parseDistroList('a\u0000\nb\u0000'), ['a', 'b']);
|
||||
assert.deepStrictEqual(wsl.parseDistroList(''), []);
|
||||
assert.deepStrictEqual(wsl.parseDistroList(null), []);
|
||||
}
|
||||
|
||||
function testDistroIsRegistered() {
|
||||
assert.strictEqual(wsl.distroIsRegistered('Ubuntu\ntrimspNL-runtime\n', wsl.DEFAULT_DISTRO), true);
|
||||
assert.strictEqual(wsl.distroIsRegistered('TRIMSPNL-RUNTIME\n', wsl.DEFAULT_DISTRO), true);
|
||||
assert.strictEqual(wsl.distroIsRegistered('Ubuntu\n', wsl.DEFAULT_DISTRO), false);
|
||||
}
|
||||
|
||||
function testArgumentBuilders() {
|
||||
assert.deepStrictEqual(wsl.listArgs(), ['--list', '--quiet']);
|
||||
assert.deepStrictEqual(
|
||||
wsl.importArgs('/tmp/r.tar.gz', 'C:\\dir', 'trimspNL-runtime'),
|
||||
['--import', 'trimspNL-runtime', 'C:\\dir', '/tmp/r.tar.gz', '--version', '2']);
|
||||
assert.strictEqual(
|
||||
wsl.distroInstallDir('C:\\Users\\x\\AppData\\Local', 'trimspNL-runtime'),
|
||||
path.join('C:\\Users\\x\\AppData\\Local', 'TrimSP', 'trimspNL-runtime'));
|
||||
}
|
||||
|
||||
function testFindRootfsTarball() {
|
||||
assert.strictEqual(wsl.findRootfsTarball('/nonexistent'), '');
|
||||
const { dir, file } = makeRootfs();
|
||||
const primary = path.join(dir, 'wsl-runtime');
|
||||
fs.mkdirSync(primary);
|
||||
fs.renameSync(file, path.join(primary, wsl.ROOTFS_BASENAME));
|
||||
assert.strictEqual(wsl.findRootfsTarball(dir), path.join(primary, wsl.ROOTFS_BASENAME));
|
||||
assert.ok(wsl.rootfsCandidates('/x')[0].endsWith(wsl.ROOTFS_BASENAME));
|
||||
}
|
||||
|
||||
async function testNotWindows() {
|
||||
const run = makeRun([]);
|
||||
const result = await wsl.ensureWSLRuntime({ platform: 'linux', run });
|
||||
assert.strictEqual(result.ok, true);
|
||||
assert.strictEqual(result.status, 'skipped');
|
||||
assert.strictEqual(result.reason, 'not-windows');
|
||||
assert.strictEqual(run.calls.length, 0);
|
||||
}
|
||||
|
||||
async function testMissingRootfs() {
|
||||
const run = makeRun([]);
|
||||
const result = await wsl.ensureWSLRuntime({
|
||||
platform: 'win32', resourcesPath: '/nonexistent', run
|
||||
});
|
||||
assert.strictEqual(result.ok, false);
|
||||
assert.strictEqual(result.status, 'missing-rootfs');
|
||||
}
|
||||
|
||||
async function testAlreadyPresentSkipsImport() {
|
||||
const { file } = makeRootfs();
|
||||
const run = makeRun([{ status: 0, stdout: wsl.DEFAULT_DISTRO + '\r\n', stderr: '' }]);
|
||||
const result = await wsl.ensureWSLRuntime({
|
||||
platform: 'win32', rootfsPath: file, appDataPath: '/tmp/appdata', run
|
||||
});
|
||||
assert.strictEqual(result.status, 'already-present');
|
||||
assert.strictEqual(run.calls.length, 1);
|
||||
}
|
||||
|
||||
async function testImportSucceeds() {
|
||||
const { file } = makeRootfs();
|
||||
const phases = [];
|
||||
const run = makeRun([
|
||||
{ status: 0, stdout: 'No installed distributions\r\n', stderr: '' },
|
||||
{ status: 0, stdout: '', stderr: 'importing...' }
|
||||
]);
|
||||
const result = await wsl.ensureWSLRuntime({
|
||||
platform: 'win32', rootfsPath: file, appDataPath: '/tmp/appdata', run,
|
||||
onProgress: function (p) { phases.push(p.phase); }
|
||||
});
|
||||
assert.strictEqual(result.ok, true);
|
||||
assert.strictEqual(result.status, 'imported');
|
||||
assert.strictEqual(result.target, wsl.distroInstallDir('/tmp/appdata', wsl.DEFAULT_DISTRO));
|
||||
assert.deepStrictEqual(run.calls[1], wsl.importArgs(file, result.target, wsl.DEFAULT_DISTRO));
|
||||
assert.strictEqual(phases[0], 'checking');
|
||||
assert.ok(phases.includes('importing'));
|
||||
assert.strictEqual(phases[phases.length - 1], 'ready');
|
||||
}
|
||||
|
||||
async function testImportFailure() {
|
||||
const { file } = makeRootfs();
|
||||
const run = makeRun([
|
||||
{ status: 0, stdout: '', stderr: '' },
|
||||
{ status: 1, stdout: '', stderr: 'boom' }
|
||||
]);
|
||||
const result = await wsl.ensureWSLRuntime({
|
||||
platform: 'win32', rootfsPath: file, appDataPath: '/tmp/appdata', run
|
||||
});
|
||||
assert.strictEqual(result.ok, false);
|
||||
assert.strictEqual(result.status, 'import-failed');
|
||||
assert.ok(result.error.includes('boom'));
|
||||
}
|
||||
|
||||
// --- 2. renderer translation + gate ---------------------------------------
|
||||
|
||||
function testRendererTranslation() {
|
||||
const src = path.join(__dirname, '..', 'TrimSPelec.js');
|
||||
const srcText = fs.readFileSync(src, 'utf8');
|
||||
|
||||
// Extract just the translation helpers by satisfying their free variables.
|
||||
const bundled = 'C:\\app\\resources\\bin\\trimspNL';
|
||||
const fakeProcess = { platform: 'win32', resourcesPath: 'C:\\app\\resources' };
|
||||
const fakePath = path.win32;
|
||||
const fileExists = function (p) { return p === bundled; };
|
||||
|
||||
function grab(name) {
|
||||
const re = new RegExp('function ' + name + '\\s*\\([^)]*\\)\\s*\\{[\\s\\S]*?\\n\\}', 'm');
|
||||
const m = srcText.match(re);
|
||||
if (!m) throw new Error('function not found: ' + name);
|
||||
return m[0];
|
||||
}
|
||||
|
||||
const body = [
|
||||
'var TRIMSP_WSL_DISTRO = ' + JSON.stringify('trimspNL-runtime') + ';',
|
||||
'var TRIMSP_WSL_PREFIX = ' + JSON.stringify('/opt/trimsp-runtime') + ';',
|
||||
grab('bundledTrimBinaryPath'),
|
||||
grab('isWslRuntimeHost'),
|
||||
grab('windowsPathToWsl'),
|
||||
grab('prepareTrimspLaunch'),
|
||||
'return { bundledTrimBinaryPath, isWslRuntimeHost, windowsPathToWsl, prepareTrimspLaunch };'
|
||||
].join('\n');
|
||||
const api = new Function('process', 'path', 'fileExists', body)(
|
||||
fakeProcess, fakePath, fileExists);
|
||||
|
||||
assert.strictEqual(api.isWslRuntimeHost(), true);
|
||||
assert.strictEqual(api.windowsPathToWsl('C:\\work\\job.inp'), '/mnt/c/work/job.inp');
|
||||
assert.strictEqual(api.windowsPathToWsl('D:/data/x.rge'), '/mnt/d/data/x.rge');
|
||||
assert.strictEqual(api.windowsPathToWsl('pcg32'), 'pcg32');
|
||||
|
||||
const launch = api.prepareTrimspLaunch(bundled, ['job', 'pcg32']);
|
||||
assert.strictEqual(launch.command, 'wsl.exe');
|
||||
assert.strictEqual(launch.viaWsl, true);
|
||||
assert.deepStrictEqual(launch.args,
|
||||
['-d', 'trimspNL-runtime', '--', '/opt/trimsp-runtime/bin/trimspNL', 'job', 'pcg32']);
|
||||
|
||||
// A user-configured Windows-native binary must pass through untouched.
|
||||
const native = api.prepareTrimspLaunch('C:\\tools\\trimspNL.exe', ['job']);
|
||||
assert.strictEqual(native.command, 'C:\\tools\\trimspNL.exe');
|
||||
assert.strictEqual(native.viaWsl, false);
|
||||
|
||||
// On Linux the translation is inert.
|
||||
fakeProcess.platform = 'linux';
|
||||
assert.strictEqual(api.isWslRuntimeHost(), false);
|
||||
const inert = api.prepareTrimspLaunch(bundled, ['job']);
|
||||
assert.strictEqual(inert.viaWsl, false);
|
||||
assert.strictEqual(inert.command, bundled);
|
||||
}
|
||||
|
||||
function run() {
|
||||
const tests = [
|
||||
['decodeWslOutput', testDecodeWslOutput],
|
||||
['parseDistroList', testParseDistroList],
|
||||
['distroIsRegistered', testDistroIsRegistered],
|
||||
['argumentBuilders', testArgumentBuilders],
|
||||
['findRootfsTarball', testFindRootfsTarball],
|
||||
['notWindows', testNotWindows],
|
||||
['missingRootfs', testMissingRootfs],
|
||||
['alreadyPresent', testAlreadyPresentSkipsImport],
|
||||
['importSucceeds', testImportSucceeds],
|
||||
['importFailure', testImportFailure],
|
||||
['rendererTranslation', testRendererTranslation]
|
||||
];
|
||||
|
||||
let failed = 0;
|
||||
(async function () {
|
||||
for (const [name, fn] of tests) {
|
||||
try {
|
||||
await fn();
|
||||
console.log('PASS ' + name);
|
||||
} catch (error) {
|
||||
failed++;
|
||||
console.log('FAIL ' + name + ' ' + (error && error.message));
|
||||
}
|
||||
}
|
||||
console.log('\n' + (tests.length - failed) + ' passed, ' + failed + ' failed');
|
||||
process.exit(failed ? 1 : 0);
|
||||
})();
|
||||
}
|
||||
|
||||
run();
|
||||
+210
@@ -0,0 +1,210 @@
|
||||
// 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 <run> [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. The
|
||||
// first is the packaged layout (packagerConfig.extraResource maps
|
||||
// packaging/wsl2/out -> wsl-runtime); the second tolerates running from a
|
||||
// development checkout without repackaging.
|
||||
function rootfsCandidates(resourcesPath) {
|
||||
const base = resourcesPath || ".";
|
||||
return [
|
||||
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 <Distro> <InstallLocation> <FileName> [--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
|
||||
};
|
||||
Reference in new issue
Block a user