Build the Windows installer with electron-builder (NSIS)

The Squirrel path (electron-forge maker-squirrel -> electron-winstaller)
required Wine and Mono, plus a 64-bit Wine prefix, to cross-build on Linux.
electron-builder's NSIS target uses its own native tooling, so the same
cross-build now needs no Wine and no Mono, matching how muSRFlow is packaged.

- package.json: add an electron-builder 'build' block (win/nsis, extraResources
  for the engine and the flat rootfs, files allow-list, nsis options) and point
  package:win/make:win at 'electron-builder --win nsis'; drop maker-squirrel.
- packaging/wsl2/require-rootfs.js: fail the Windows build when the rootfs is
  missing (electron-builder has no config-time abort hook).
- forge.config.js: drop the Squirrel maker and the Windows rootfs guard; forge
  now builds Linux/macOS only.
- README/CHANGELOG: document the NSIS build and that no Wine/Mono is needed.

Verified locally: TRIMSP-NL-Workbench-Setup.exe (NSIS, 186 MB) builds with no
Wine/Mono; resources/bin/trimspNL and resources/trimspNL-runtime-rootfs.tar.gz
land where wsl-runtime.js looks; guard fails on a missing rootfs.
This commit is contained in:
salman committed 2026-10-20 13:35:17 +02:00
1 parent 752c8363bf
commit 5110ea4827
6 files changed
+1655 -125

No files matched your search

+4 -3
View File
@@ -10,9 +10,10 @@
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.
- An NSIS Windows installer (`npm run make:win`,
`TRIMSP-NL-Workbench-Setup.exe`) built with electron-builder that bundles the
WSL2 runtime; the build refuses to proceed without it. Linux/macOS packaging
continues to use electron-forge.
### Fixed
+1 -44
View File
@@ -1,5 +1,4 @@
const path = require('path');
const fs = require('fs');
const redhatInstaller = require('electron-installer-redhat');
const redhatDependencies = require('electron-installer-redhat/src/dependencies');
@@ -18,42 +17,8 @@ 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.
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();
// The packaged Linux engine.
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
// GTK 3 under lib64gtk+3_0, so allow either name in the generated Requires.
@@ -67,14 +32,6 @@ redhatInstaller.Installer.prototype.createSpec = function () {
};
const makers = [
{
name: '@electron-forge/maker-squirrel',
config: {
name: 'TrimSP',
setupExe: 'TRIMSP-NL-Workbench-Setup.exe',
setupIcon: path.resolve(__dirname, 'icon.ico'),
},
},
{
name: '@electron-forge/maker-zip',
platforms: ['darwin'],
+1556 -39
View File
File diff suppressed because it is too large. Load diff
+51 -4
View File
@@ -16,8 +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",
"package:win": "npm run build:fortran && npm run build:wsl2 && node packaging/wsl2/require-rootfs.js && electron-builder --win nsis --x64 --dir",
"make:win": "npm run build:fortran && npm run build:wsl2 && node packaging/wsl2/require-rootfs.js && electron-builder --win nsis --x64",
"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"
@@ -37,9 +37,9 @@
"@electron-forge/maker-deb": "7.11.2",
"@electron-forge/maker-flatpak": "7.11.2",
"@electron-forge/maker-rpm": "7.11.2",
"@electron-forge/maker-squirrel": "7.11.2",
"@electron-forge/maker-zip": "7.11.2",
"electron": "44.9.0"
"electron": "44.9.0",
"electron-builder": "26.15.3"
},
"dependencies": {
"plotly.js-dist": "^1.58.4"
@@ -49,5 +49,52 @@
"repository": {
"type": "git",
"url": "https://gitea.psi.ch/LMU/TRIMSP.git"
},
"build": {
"appId": "ch.psi.lem.trimsp",
"productName": "TRIMSP-NL Workbench",
"artifactName": "TRIMSP-NL-Workbench-Setup.${ext}",
"asar": true,
"directories": {
"output": "dist"
},
"files": [
"main.js",
"wsl-runtime.js",
"TrimSP.html",
"quick-start.html",
"ZGUI.css",
"plotly.css",
"TrimSPweb.js",
"TrimSPelec.js",
"TrimSPlib.js",
"myplots.js",
"appicons/**/*",
"node_modules/**/*"
],
"win": {
"target": [
"nsis"
],
"icon": "icon.ico",
"extraResources": [
{
"from": "resources/bin",
"to": "bin"
},
{
"from": "packaging/wsl2/out",
"to": ".",
"filter": [
"trimspNL-runtime-rootfs.tar.gz"
]
}
]
},
"nsis": {
"oneClick": false,
"perMachine": false,
"allowToChangeInstallationDirectory": true
}
}
}
+18 -35
View File
@@ -69,12 +69,13 @@ 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 (copied flat
into `resources/`), while the engine marker at `resources/bin/trimspNL` lets the
renderer detect the bundled runtime and route through WSL.
The Windows build ships the rootfs through **electron-builder** (NSIS). The
`build.win.extraResources` entries in `package.json` copy the rootfs archive flat
into `resources/` and the engine to `resources/bin/`, matching what
`wsl-runtime.js` looks for. Linux/macOS packaging still uses electron-forge
(`forge.config.js`); the two tools coexist.
Build the Squirrel `Setup.exe`, which contains the app **and** the WSL2 rootfs:
Build the NSIS `Setup.exe`, which contains the app **and** the WSL2 rootfs:
```sh
npm run make:win
@@ -82,41 +83,24 @@ 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).
rootfs — `packaging/wsl2/require-rootfs.js` fails if `packaging/wsl2/out/` is
empty, so the `.exe` can never silently ship without its engine. Build from
Linux/WSL or Windows after building the rootfs 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`.
The installer is written to `dist/` 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.
It also needs a **64-bit (WoW64) Wine prefix**. Squirrel is a 32-bit .NET
assembly but zips the package by `ShellExecuteEx`-ing the 64-bit
`node_modules/electron-winstaller/vendor/7z.exe`; a 32-bit-only prefix
(`#arch=win32`) cannot launch a 64-bit PE and the build fails with
`ShellExecuteEx failed: Bad EXE format`. Build against a WoW64 prefix without
disturbing an existing one:
```sh
export WINEARCH=win64 WINEPREFIX="$HOME/.wine-trimsp64"
wineboot -u
npm run make:win
```
(Wine ≥ 9 supports running 32-bit apps from a `win64` prefix, so a single
prefix is enough.) On Windows none of this applies.
- Building the `.exe` **on Linux** (cross-build): nothing extra either.
electron-builder's NSIS target uses its own native tooling, so **no Wine and no
Mono** are required (unlike the former Squirrel path).
### 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`.
The app inside the install loads the engine only through WSL, so the same manual
remote-session check below applies after installing the `.exe`.
## Tests
@@ -160,6 +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, Mono and a
64-bit (WoW64) Wine prefix (see above); building on Windows needs none of
these.
- Cross-building the `.exe` from Linux needs no extra tooling (electron-builder
NSIS); Windows builds need none either.
+25
View File
@@ -0,0 +1,25 @@
#!/usr/bin/env node
// Refuse to build the Windows installer without the WSL2 engine runtime.
//
// Windows cannot run the bundled Linux engine natively, so the installer must
// ship the rootfs archive (packaging/wsl2/out/trimspNL-runtime-rootfs.tar.gz)
// that the app imports as a private WSL2 distribution on first run. Building
// without it would silently produce an installer whose engine cannot start.
//
// electron-builder has no config-time hook that can abort the build, so the
// `dist:win` script runs this first.
const fs = require('fs');
const path = require('path');
const rootfs = path.resolve(
__dirname, 'out', 'trimspNL-runtime-rootfs.tar.gz');
if (!fs.existsSync(rootfs)) {
console.error(
'Windows build requires the WSL2 engine runtime, but\n ' + rootfs
+ '\nis missing. Build it first from Linux/WSL with:\n npm run build:wsl2');
process.exit(1);
}
console.log('WSL2 rootfs found: ' + rootfs);