From b3e75b2300e72bd524863dc2d6f681e07db3db03 Mon Sep 17 00:00:00 2001 From: salman Date: Tue, 20 Oct 2026 09:42:47 +0200 Subject: [PATCH] Ship the WSL2 runtime in a Squirrel Windows .exe The Windows installer now bundles the WSL2 engine runtime so the installed app works out of the box. - forge.config.js: configure the Squirrel maker (setupExe name, setup icon) and require the rootfs for any Windows build, throwing a clear error if packaging/wsl2/out/ is empty so the .exe can never ship without its engine. - wsl-runtime.js: rootfsCandidates now checks the flat resources/ path first, which is where forge's extraResource copies the archive (the wsl-runtime/ subdirectory and the dev-checkout path remain as fallbacks). - package.json: add package:win / make:win scripts that build the static engine, the rootfs, then package/make for win32. - tests: cover the flat-first rootfs lookup. - README/CHANGELOG: document the .exe build, its Wine+Mono cross-build prerequisite on Linux, and the required-rootfs guard. --- CHANGELOG.md | 3 +++ forge.config.js | 33 +++++++++++++++++++++++++++++++-- package.json | 2 ++ packaging/wsl2/README.md | 37 ++++++++++++++++++++++++++++++++++--- tests/wsl-runtime.test.js | 16 ++++++++++++---- wsl-runtime.js | 8 +++++--- 6 files changed, 87 insertions(+), 12 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 75ef0a5..2e39ece 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,9 @@ on first run, and is launched via `wsl.exe` with Windows paths translated to their `/mnt/` form. A runtime gate waits for the import before the first run, and progress is reported in the status bar. +- A Squirrel Windows installer (`npm run make:win`, + `TRIMSP-NL-Workbench-Setup.exe`) that bundles the WSL2 runtime; a Windows + build refuses to proceed without it. ### Fixed diff --git a/forge.config.js b/forge.config.js index a51e296..749c239 100644 --- a/forge.config.js +++ b/forge.config.js @@ -20,12 +20,39 @@ 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. +// app imports as a private distribution on first run. const wsl2Rootfs = path.resolve(__dirname, 'packaging/wsl2/out/trimspNL-runtime-rootfs.tar.gz'); + +// Detect a Windows build so we can (a) include the rootfs and (b) refuse to +// produce a Windows installer that would be missing its engine. Forge is asked +// for a target platform either as `--platform win32` / `-p win32` (make/package) +// or implicitly when running on Windows itself. +function wantsWindowsTarget() { + if (process.platform === 'win32') return true; + const argv = process.argv.slice(2); + for (let i = 0; i < argv.length; i++) { + const arg = argv[i]; + if (arg === '--platform' || arg === '-p') { + if ((argv[i + 1] || '').toLowerCase() === 'win32') return true; + } + for (const prefix of ['--platform=', '-p=']) { + if (arg.toLowerCase().startsWith(prefix) + && arg.slice(prefix.length).toLowerCase() === 'win32') { + return true; + } + } + } + return false; +} + +const windowsBuild = wantsWindowsTarget(); const extraResource = ['resources/bin']; if (fs.existsSync(wsl2Rootfs)) { extraResource.push(wsl2Rootfs); +} else if (windowsBuild) { + throw new Error( + 'Windows build requires the WSL2 engine runtime, but\n ' + wsl2Rootfs + + '\nis missing. Build it first from Linux/WSL with:\n npm run build:wsl2'); } // The upstream RPM maker assumes Fedora-style package names. Mageia provides @@ -44,6 +71,8 @@ const makers = [ name: '@electron-forge/maker-squirrel', config: { name: 'TrimSP', + setupExe: 'TRIMSP-NL-Workbench-Setup.exe', + setupIcon: path.resolve(__dirname, 'icon.ico'), }, }, { diff --git a/package.json b/package.json index ea15731..5493bec 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ "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 && node tests/runtime-gate.test.js", + "package:win": "npm run build:fortran && npm run build:wsl2 && electron-forge package --platform=win32", + "make:win": "npm run build:fortran && npm run build:wsl2 && electron-forge make --platform=win32 --targets=@electron-forge/maker-squirrel", "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" diff --git a/packaging/wsl2/README.md b/packaging/wsl2/README.md index 71175d1..479ab73 100644 --- a/packaging/wsl2/README.md +++ b/packaging/wsl2/README.md @@ -70,9 +70,38 @@ 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. +adds the archive to `packagerConfig.extraResource` when it exists (copied flat +into `resources/`), while the engine marker at `resources/bin/trimspNL` lets the +renderer detect the bundled runtime and route through WSL. + +Build the Squirrel `Setup.exe`, which contains the app **and** the WSL2 rootfs: + +```sh +npm run make:win +``` + +This runs the whole chain: it compiles the static engine, builds the rootfs, then +packages and makes the installer for `win32`. A Windows build **requires** the +rootfs — `forge.config.js` throws if `packaging/wsl2/out/` is empty, so the +`.exe` can never silently ship without its engine. Build from Linux/WSL first +(Windows cannot compile the Fortran engine or run the rootfs builder). + +The installer is written to `out/make/squirrel.windows/x64/` as +`TRIMSP-NL-Workbench-Setup.exe`. + +### Build-host prerequisites + +- Building the `.exe` **on Windows**: nothing extra. +- Building the `.exe` **on Linux** (cross-build): `electron-winstaller` needs + **both Wine and Mono** installed, otherwise it fails with + `You must install both Mono and Wine on non-Windows`. Install e.g. + `wine` and `mono-complete` (or `mono-core`) first. + +### Verifying the shipped .exe + +The `TrimSP.exe` inside the install loads the engine only through WSL, so the +same manual remote-session check below applies after installing the `.exe`. + ## Tests @@ -115,3 +144,5 @@ workbench is often driven remotely. After the first-run import, verify: - `wsl.exe` availability is assumed; older Windows builds without WSL2 must install it (`wsl --install`) first. - The macOS runtime is not built. +- Cross-building the `.exe` from Linux additionally needs Wine and Mono (see + above); building on Windows needs neither. diff --git a/tests/wsl-runtime.test.js b/tests/wsl-runtime.test.js index 0e12050..c0f2b19 100644 --- a/tests/wsl-runtime.test.js +++ b/tests/wsl-runtime.test.js @@ -72,11 +72,19 @@ function testArgumentBuilders() { function testFindRootfsTarball() { assert.strictEqual(wsl.findRootfsTarball('/nonexistent'), ''); + // forge copies extraResource entries flat into resources/, so the flat path + // is the primary candidate. + const flat = fs.mkdtempSync(path.join(os.tmpdir(), 'trimsp-flat-')); + fs.writeFileSync(path.join(flat, wsl.ROOTFS_BASENAME), 'rootfs'); + assert.strictEqual(wsl.findRootfsTarball(flat), path.join(flat, wsl.ROOTFS_BASENAME)); + + // A wsl-runtime/ subdirectory is also honoured. 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)); + const sub = path.join(dir, 'wsl-runtime'); + fs.mkdirSync(sub); + fs.renameSync(file, path.join(sub, wsl.ROOTFS_BASENAME)); + assert.strictEqual(wsl.findRootfsTarball(dir), path.join(sub, wsl.ROOTFS_BASENAME)); + assert.ok(wsl.rootfsCandidates('/x')[0].endsWith(wsl.ROOTFS_BASENAME)); } diff --git a/wsl-runtime.js b/wsl-runtime.js index 46db37c..ae7d5ca 100644 --- a/wsl-runtime.js +++ b/wsl-runtime.js @@ -23,13 +23,15 @@ 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 +// 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) ];