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.
This commit is contained in:
salman committed 2026-10-20 09:42:47 +02:00
1 parent 0c8e8f2e03
commit b3e75b2300
6 files changed
+87 -12

No files matched your search

+3
View File
@@ -10,6 +10,9 @@
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.
- 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
+31 -2
View File
@@ -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'),
},
},
{
+2
View File
@@ -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"
+34 -3
View File
@@ -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.
+12 -4
View File
@@ -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));
}
+5 -3
View File
@@ -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)
];