From 3ce52774760b222d94d78bc0118e9da2662a809f Mon Sep 17 00:00:00 2001 From: Christophe Besson Date: Fri, 4 Sep 2026 09:28:14 +0200 Subject: feat: Windows installer (W4) — one per-user NSIS package, client + node MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `npm run dist:win` produces MeshBay-Setup-.exe: the Electron client and, beside it under resources/node-runtime/, the frozen meshbay-node daemon (meshbay-common inside it). No hub. Per-user, no elevation — matches the W3 constraint that a logon-triggered scheduled task needs admin. electron-builder / package.json build.win nsis, build/icon.ico, extraResources -> node-runtime/ build.nsis oneClick:false perMachine:false allowElevation:false allowToChangeInstallationDirectory:true dist:win -> packaging/win/build-win.ps1 (mirrors dist -> build-client.sh) packaging/win/ meshbay-node.spec + node-entry.py PyInstaller freeze of meshbay_node.daemon:main. The awkward deps (aiortc, av, aioquic, pydantic_core, uvicorn, watchdog, guessit, blake3, tzdata) are pulled in whole with collect_all — that list is expected to grow when a frozen run raises ModuleNotFoundError. build-node-runtime.ps1 throwaway venv -> pip install -> PyInstaller -> packages/meshbay-client/node-runtime/ (gitignored) build-win.ps1 Node>=22 check, npm ci, Electron bump, sync-ui, node runtime, electron-builder --win nsis bump-electron.mjs the Chromium-CVE "build against latest Electron" policy, out of the PS script (5.1 here-string terminator rules) README.md PyInstaller, not the python-embed zip: the frozen meshbay-node.exe is a genuine relocatable single binary, which is what src/main.js:findNodeBinary spawns (process.resourcesPath/node-runtime/meshbay-node.exe when packaged) and what the W3 autostart launcher points at. The embeddable zip needs pip to make that wrapper and the wrapper bakes in an absolute interpreter path. build/installer.nsh: on uninstall, taskkill meshbay-node.exe and delete the W3 Startup .vbs (it would point wscript at a deleted binary every sign-in). %LOCALAPPDATA%\meshbay\ — node.toml, keystore.enc — is never touched. ffmpeg is not bundled by default (node finds it on PATH); build-win.ps1 -FfmpegDir copies ffmpeg.exe/ffprobe.exe in for a self-contained installer. Verified on the Windows guest: PyInstaller freeze builds first try (node-runtime 147 MB), frozen `meshbay-node status` talks to the live daemon's loopback API; electron-builder --win nsis produces MeshBay-Setup-0.1.0.exe (155 MB), oneClick/perMachine flags applied, node-runtime bundled at the path findNodeBinary expects. test_packaging_win.py (14) pins the config invariants and the NSIS <-> platform.py autostart seam. Node suite 798 pass / 34 skip. Open: Authenticode signing (13.9 — unsigned => SmartScreen), Windows CI (18.3), electron-updater. First clean-machine install + DPAPI + autostart round-trip is a manual check. Co-Authored-By: Claude Sonnet 5 --- packaging/win/build-node-runtime.ps1 | 138 +++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 packaging/win/build-node-runtime.ps1 (limited to 'packaging/win/build-node-runtime.ps1') diff --git a/packaging/win/build-node-runtime.ps1 b/packaging/win/build-node-runtime.ps1 new file mode 100644 index 0000000..c432e01 --- /dev/null +++ b/packaging/win/build-node-runtime.ps1 @@ -0,0 +1,138 @@ +<# +.SYNOPSIS + Freeze the MeshBay node daemon into a relocatable Windows binary. + +.DESCRIPTION + Produces packages/meshbay-client/node-runtime/meshbay-node.exe (+ its + onedir payload) with PyInstaller. electron-builder then carries that tree + into the NSIS installer as an extraResource (see package.json build.win). + + The frozen binary is what the desktop client spawns and what the W3 + autostart launcher points at, so it must be a genuine single .exe -- which + is why this uses PyInstaller and not the python-embed zip (whose pip + console-script wrapper bakes in an absolute interpreter path and does not + survive being installed somewhere else). + + ffmpeg/ffprobe are NOT bundled by default. Pass -FfmpegDir (or set + MESHBAY_FFMPEG_DIR) to copy them in beside the daemon; otherwise the node + resolves them from PATH at startup (meshbay_node.platform.check_media_tools) + and streaming needs ffmpeg installed separately. + +.PARAMETER Python + Interpreter used to build. Must be 3.12+ and able to install the packages. + Default: a throwaway venv this script creates under build/_node-build-venv. + +.PARAMETER FfmpegDir + Directory containing ffmpeg.exe and ffprobe.exe to bundle. Optional. + +.PARAMETER KeepBuildVenv + Do not delete the throwaway build venv on success (faster re-runs). +#> +[CmdletBinding()] +param( + [string]$Python = "", + [string]$FfmpegDir = $env:MESHBAY_FFMPEG_DIR, + [switch]$KeepBuildVenv +) + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version Latest + +$WinDir = $PSScriptRoot +$Repo = (Resolve-Path (Join-Path $WinDir "..\..")).Path +$Client = Join-Path $Repo "packages\meshbay-client" +$OutDir = Join-Path $Client "node-runtime" +$BuildVenv = Join-Path $Client "build\_node-build-venv" +$SpecFile = Join-Path $WinDir "meshbay-node.spec" + +function Step($msg) { Write-Host "==> $msg" -ForegroundColor Cyan } + +# --- 1. interpreter -------------------------------------------------------- +$createdVenv = $false +if (-not $Python) { + Step "creating build venv ($BuildVenv)" + if (Get-Command py -ErrorAction SilentlyContinue) { + & py -3.12 -m venv --clear $BuildVenv + } else { + & python -m venv --clear $BuildVenv + } + if ($LASTEXITCODE -ne 0) { throw "venv creation failed" } + $Python = Join-Path $BuildVenv "Scripts\python.exe" + $createdVenv = $true +} +if (-not (Test-Path $Python)) { throw "Python not found: $Python" } + +$verOut = (& $Python -c "import sys; print(sys.version_info[0]); print(sys.version_info[1])") +$verMajor = [int]$verOut[0] +$verMinor = [int]$verOut[1] +if ($verMajor -lt 3 -or ($verMajor -eq 3 -and $verMinor -lt 12)) { + throw "need Python 3.12 or newer, got $verMajor.$verMinor" +} +Step "python $verMajor.$verMinor ($Python)" + +# --- 2. dependencies ----------------------------------------------------- +Step "installing meshbay-common, meshbay-node and PyInstaller" +& $Python -m pip install --upgrade pip --quiet +& $Python -m pip install --quiet ` + (Join-Path $Repo "packages\meshbay-common") ` + (Join-Path $Repo "packages\meshbay-node") ` + "pyinstaller>=6.10" ` + "tzdata" +# tzdata: Windows ships no IANA zone database, so zoneinfo (pulled in +# transitively) has nothing to read without it. PyInstaller warns +# 'Hidden import "tzdata" not found' when it is absent. +if ($LASTEXITCODE -ne 0) { throw "pip install failed" } + +# --- 3. freeze -------------------------------------------------------- +if (Test-Path $OutDir) { Remove-Item -Recurse -Force $OutDir } +$pyiWork = Join-Path $Client "build\_pyinstaller" +$pyiDist = Join-Path $Client "build\_pyinstaller-dist" +Step "running PyInstaller (this takes a few minutes)" +Push-Location $WinDir +try { + & $Python -m PyInstaller --noconfirm --clean ` + --workpath $pyiWork --distpath $pyiDist ` + $SpecFile + if ($LASTEXITCODE -ne 0) { throw "PyInstaller failed" } +} +finally { + Pop-Location +} + +$frozen = Join-Path $pyiDist "meshbay-node" +if (-not (Test-Path (Join-Path $frozen "meshbay-node.exe"))) { + throw "PyInstaller did not produce meshbay-node.exe at $frozen" +} + +# --- 4. optional ffmpeg ---------------------------------------------- +if ($FfmpegDir) { + foreach ($tool in @("ffmpeg.exe", "ffprobe.exe")) { + $src = Join-Path $FfmpegDir $tool + if (-not (Test-Path $src)) { throw "$tool not found in $FfmpegDir" } + Copy-Item $src (Join-Path $frozen $tool) + Step "bundled $tool" + } +} +else { + Write-Host " ffmpeg not bundled -- the node will look for it on PATH" -ForegroundColor Yellow +} + +# --- 5. publish ---------------------------------------------------- +Move-Item $frozen $OutDir +Remove-Item -Recurse -Force $pyiWork, $pyiDist -ErrorAction SilentlyContinue +if ($createdVenv -and -not $KeepBuildVenv) { + Remove-Item -Recurse -Force $BuildVenv -ErrorAction SilentlyContinue +} + +# --- 6. smoke test ----------------------------------------------- +# Capture, do NOT pipe to Select-Object -First: that stops the native process +# mid-write and reports a spurious non-zero exit. +Step "smoke test: meshbay-node --help" +$exe = Join-Path $OutDir "meshbay-node.exe" +$help = & $exe --help 2>&1 +if ($LASTEXITCODE -ne 0) { throw "frozen meshbay-node --help exited $LASTEXITCODE" } +if ($help -notmatch "meshbay-node") { throw "frozen --help output looks wrong" } + +$mb = (Get-ChildItem $OutDir -Recurse | Measure-Object Length -Sum).Sum / 1MB +Write-Host "" +Write-Host ("OK node-runtime ready at {0} ({1:N0} MB)" -f $OutDir, $mb) -ForegroundColor Green -- cgit v1.2.3