summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--.gitignore4
-rw-r--r--packages/meshbay-client/build/installer-light.nsh67
-rw-r--r--packages/meshbay-client/package.json3
-rw-r--r--packages/meshbay-node/tests/test_packaging_win.py249
-rw-r--r--packaging/win/build-win-common.ps164
-rw-r--r--packaging/win/build-win-light.ps185
-rw-r--r--packaging/win/build-win.ps143
-rw-r--r--packaging/win/electron-builder.light.yml54
8 files changed, 538 insertions, 31 deletions
diff --git a/.gitignore b/.gitignore
index 90d2536..e1cbdf2 100644
--- a/.gitignore
+++ b/.gitignore
@@ -63,6 +63,10 @@ packages/meshbay-client/node_modules/
# packaging/win/build-node-runtime.ps1 and carried into the installer as an
# electron-builder extraResource. Build artifact, never committed.
packages/meshbay-client/node-runtime/
+# build-win-light.ps1's own output dir (electron-builder.light.yml
+# directories.output) -- the bare `dist/` rule above does not match this
+# different name, so it needs its own line.
+packages/meshbay-client/dist-light/
# electron-builder's buildResources dir — a real source (app icon), not a
# build artifact, despite living under a path the generic `build/` rule above
diff --git a/packages/meshbay-client/build/installer-light.nsh b/packages/meshbay-client/build/installer-light.nsh
new file mode 100644
index 0000000..86e41ba
--- /dev/null
+++ b/packages/meshbay-client/build/installer-light.nsh
@@ -0,0 +1,67 @@
+; electron-builder NSIS customisation for the "Light" target
+; (packaging/win/electron-builder.light.yml's nsis.include).
+;
+; Deliberately much smaller than build/installer.nsh (Full's): there is no
+; bundled node, so there is nothing to autostart, no PATH entry to add, no
+; process to stop before overwriting a file, and no boot-time Scheduled Task.
+; The only thing this installer does, all conditional on interactive setup
+; (never ${Silent}): set up the Windows Firewall rules Light still needs --
+; the client's own WebRTC ICE rule and the two LAN-casting rules (see
+; packaging/win/firewall.ps1's own header for why the *client*, not just a
+; node, needs an inbound allow). One elevation, no radio page -- there is
+; nothing to choose, so unlike Full's installer.nsh there is no mode
+; question at all.
+;
+; Per-user only, same reasoning as Full: the keystore and the DPAPI-protected
+; hub device key are bound to the signed-in account (MESHBAY_DESIGN.md
+; 11.2 / 7.5), and build.win's nsis config forbids elevation at install time.
+;
+; MUST NOT touch anything a co-installed Full client owns. Full and Light can
+; be installed side by side (distinct appId/productName/install dir), and it
+; is entirely plausible for a machine to have both -- Light's installer must
+; be a no-op with respect to a Full install's node process, its per-user
+; Startup entry, its Scheduled Task, and the "MeshBay Node" firewall rule.
+; Concretely: this file never stops a process by image name, never touches
+; the per-user PATH registry value, never references either of Full's two
+; service scripts, and never removes a startup shortcut of its own --
+; test_packaging_win.py pins that absence.
+
+!include "WinMessages.nsh"
+!include "LogicLib.nsh"
+
+!define MB_PWSH "$SYSDIR\WindowsPowerShell\v1.0\powershell.exe"
+
+; ── force per-user, skip the all-users / current-user page ──────────────────
+; Same as Full's installer.nsh -- see that file's own comment for why.
+!macro customInstallMode
+ StrCpy $isForceCurrentInstall "1"
+!macroend
+
+!macro customInstall
+ ${IfNot} ${Silent}
+ ; Unelevated first: Get-NetFirewallRule needs no admin, only New/Remove
+ ; do, so a repeat/repair install that already has the rules in place
+ ; raises no UAC prompt.
+ nsExec::Exec '"${MB_PWSH}" -NoProfile -ExecutionPolicy Bypass -File "$INSTDIR\resources\firewall.ps1" check'
+ Pop $R0 ; 0 = every rule already present
+ ${If} $R0 != 0
+ ExecShellWait "runas" "${MB_PWSH}" \
+ '-NoProfile -ExecutionPolicy Bypass -File "$INSTDIR\resources\firewall.ps1" add' \
+ SW_HIDE
+ ${EndIf}
+ ${EndIf}
+!macroend
+
+!macro customUnInstall
+ ; Opt-in, default No -- a stale allow-rule is inert, so this should not
+ ; nag. A silent uninstall skips it entirely (no UAC prompt of its own).
+ ${IfNot} ${Silent}
+ MessageBox MB_YESNO|MB_ICONQUESTION \
+ "Remove MeshBay Light's Windows Firewall rules? This needs one administrator confirmation. They are harmless if left." \
+ /SD IDNO IDNO mb_keep_firewall
+ ExecShellWait "runas" "${MB_PWSH}" \
+ '-NoProfile -ExecutionPolicy Bypass -File "$INSTDIR\resources\firewall.ps1" remove' \
+ SW_HIDE
+ mb_keep_firewall:
+ ${EndIf}
+!macroend
diff --git a/packages/meshbay-client/package.json b/packages/meshbay-client/package.json
index e88b2f0..614e6d6 100644
--- a/packages/meshbay-client/package.json
+++ b/packages/meshbay-client/package.json
@@ -11,7 +11,8 @@
"start": "electron .",
"sync-ui": "node scripts/sync-ui.js",
"dist": "bash ../../packaging/build/build-client.sh",
- "dist:win": "powershell -NoProfile -ExecutionPolicy Bypass -File ../../packaging/win/build-win.ps1"
+ "dist:win": "powershell -NoProfile -ExecutionPolicy Bypass -File ../../packaging/win/build-win.ps1",
+ "dist:win:light": "powershell -NoProfile -ExecutionPolicy Bypass -File ../../packaging/win/build-win-light.ps1"
},
"devDependencies": {
"electron": "^44.2.0",
diff --git a/packages/meshbay-node/tests/test_packaging_win.py b/packages/meshbay-node/tests/test_packaging_win.py
index 7421a8c..b223156 100644
--- a/packages/meshbay-node/tests/test_packaging_win.py
+++ b/packages/meshbay-node/tests/test_packaging_win.py
@@ -21,6 +21,20 @@ CLIENT = ROOT / "packages" / "meshbay-client"
PKG_JSON = CLIENT / "package.json"
WIN = ROOT / "packaging" / "win"
NSH = CLIENT / "build" / "installer.nsh"
+MAIN_JS = CLIENT / "src" / "main.js"
+PRELOAD_JS = CLIENT / "src" / "preload.js"
+
+# The "Light" target: Electron client + UI, no bundled node. See
+# C:\Users\admin\devel\light-client.md for the evaluation this implements.
+LIGHT_NSH = CLIENT / "build" / "installer-light.nsh"
+LIGHT_YML = WIN / "electron-builder.light.yml"
+BUILD_WIN_LIGHT = WIN / "build-win-light.ps1"
+BUILD_WIN_COMMON = WIN / "build-win-common.ps1"
+
+# sync-ui.js copies this into CLIENT/ui/ verbatim -- read the source of
+# truth, same as every other cross-package check in this file already does
+# for meshbay-client's own src/.
+HUB_STATIC = ROOT / "packages" / "meshbay-hub" / "src" / "meshbay_hub" / "static"
pytestmark = pytest.mark.skipif(
not PKG_JSON.exists() or not WIN.exists(),
@@ -659,3 +673,238 @@ def test_ffmpeg_bundling_is_the_default_not_opt_in():
win_src = (WIN / "build-win.ps1").read_text(encoding="utf-8")
assert "SkipFfmpeg" in win_src
assert "FfmpegDir" not in win_src
+
+
+# ------------------------------------------------------------------------
+# The "Light" target: Electron client + UI, no bundled node. See
+# C:\Users\admin\devel\light-client.md for the evaluation. Weak, text-
+# reading evidence throughout, same reasoning as the rest of this file:
+# there is no electron-builder/PowerShell/NSIS runner here, and it is the
+# right kind of evidence for what these guard against -- a config drifting
+# back to carrying the node-runtime it must not, or the two orchestrators'
+# shared steps diverging silently.
+# ------------------------------------------------------------------------
+
+def test_light_config_is_standalone_and_ships_no_node():
+ """
+ electron-builder.light.yml is passed via --config, which (per
+ app-builder-lib/out/util/config/load.js's getConfig) makes electron-
+ builder read ONLY that file -- package.json's own build field, and
+ therefore its node-runtime/service*.ps1 extraResources, is never even
+ loaded. Pins the config's own content regardless: it must name neither
+ the node runtime nor the two service scripts, and must still carry
+ firewall.ps1 (the one thing Light still needs -- packaging/win/
+ firewall.ps1's header explains why the client needs an inbound rule too,
+ not only a node).
+ """
+ assert LIGHT_YML.exists(), f"{LIGHT_YML} is missing"
+ yml = LIGHT_YML.read_text(encoding="utf-8")
+
+ assert "appId: org.meshbay.client.light" in yml
+ assert "productName: MeshBay Light" in yml
+ assert "output: dist-light" in yml
+
+ assert "node-runtime" not in yml
+ assert "service.ps1" not in yml
+ assert "service-mode.ps1" not in yml
+ assert "firewall.ps1" in yml
+
+ assert "include: installer-light.nsh" in yml, (
+ "nsis.include must point at the Light NSIS customisation, not the "
+ "default build/installer.nsh (Full's)")
+
+
+def test_light_config_keeps_the_same_no_admin_nsis_policy():
+ """Per-user, no elevation at install time -- identical policy to Full's
+ package.json build.nsis, for the same reason (MESHBAY_DESIGN.md
+ 11.2/7.5: the DPAPI-protected hub device key is account-bound)."""
+ yml = LIGHT_YML.read_text(encoding="utf-8")
+ assert "oneClick: false" in yml
+ assert "perMachine: false" in yml
+ assert "allowElevation: false" in yml
+
+
+def test_dist_win_light_delegates_to_the_light_orchestrator():
+ pkg = _pkg()
+ scripts = pkg["scripts"]
+ assert "dist:win:light" in scripts
+ assert "build-win-light.ps1" in scripts["dist:win:light"]
+ assert "dist:win" in scripts
+ assert "build-win.ps1" in scripts["dist:win"]
+ assert "build-win-light.ps1" not in scripts["dist:win"]
+
+
+def test_build_win_light_skips_the_node_runtime_step():
+ """The entire point of Light: no PyInstaller freeze, no ffmpeg fetch."""
+ assert BUILD_WIN_LIGHT.exists(), f"{BUILD_WIN_LIGHT} is missing"
+ src = BUILD_WIN_LIGHT.read_text(encoding="utf-8")
+ assert "build-node-runtime.ps1" not in src
+ assert "electron-builder.light.yml" in src
+ assert "--config" in src
+
+
+def test_build_orchestrators_share_the_common_steps_not_a_copy():
+ """
+ Node check / npm ci / Electron bump / sync-ui must live in exactly one
+ place (build-win-common.ps1), dot-sourced by both -- a copy would let
+ the two drift the way the installer flow itself once did (the W3
+ one-shot dialog bug). Both orchestrators must call the shared
+ functions, neither may inline its own npm ci / Electron-bump logic.
+ """
+ assert BUILD_WIN_COMMON.exists(), f"{BUILD_WIN_COMMON} is missing"
+ common = BUILD_WIN_COMMON.read_text(encoding="utf-8")
+ for fn in ("Assert-NodeVersion", "Invoke-NpmCi", "Invoke-ElectronBump", "Invoke-SyncUi"):
+ assert f"function {fn}" in common, f"{fn} is not defined in build-win-common.ps1"
+
+ full = (WIN / "build-win.ps1").read_text(encoding="utf-8")
+ light = BUILD_WIN_LIGHT.read_text(encoding="utf-8")
+ for src, name in ((full, "build-win.ps1"), (light, "build-win-light.ps1")):
+ assert '. (Join-Path $WinDir "build-win-common.ps1")' in src, (
+ f"{name} does not dot-source build-win-common.ps1")
+ for fn in ("Assert-NodeVersion", "Invoke-NpmCi", "Invoke-ElectronBump", "Invoke-SyncUi"):
+ assert fn in src, f"{name} does not call {fn}"
+ assert "npm ci --ignore-scripts" not in src, (
+ f"{name} must not re-implement npm ci inline")
+
+
+def test_firewall_ps1_skips_the_node_rule_when_the_exe_is_missing():
+ """
+ The one behaviour Light structurally depends on: firewall.ps1 is
+ shipped unforked (test_light_config_is_standalone_and_ships_no_node
+ above), relying on `add` already skipping any rule whose target .exe
+ does not exist -- true for "MeshBay Node" in a Light install, where
+ node-runtime\\meshbay-node.exe is never there. If this guard were ever
+ removed, New-NetFirewallRule would be pointed at a path that does not
+ exist and Light's one firewall elevation would start failing.
+ """
+ src = (WIN / "firewall.ps1").read_text(encoding="utf-8")
+ add_block = src.split('if ($Action -eq "add")', 1)[1]
+ assert "if (-not (Test-Path $r.Path))" in add_block
+ skip_stanza = add_block.split("if (-not (Test-Path $r.Path))", 1)[1].split("}", 1)[0]
+ assert "continue" in skip_stanza
+
+
+def test_light_installer_forces_per_user_and_elevates_firewall_once():
+ assert LIGHT_NSH.exists(), f"{LIGHT_NSH} is missing"
+ nsh = LIGHT_NSH.read_text(encoding="utf-8")
+ install = _macro_body(nsh, "customInstall")
+ mode = _macro_body(nsh, "customInstallMode")
+
+ assert 'StrCpy $isForceCurrentInstall "1"' in mode
+
+ assert "${IfNot} ${Silent}" in install
+ i_check = install.index('firewall.ps1" check')
+ i_runas = install.index('ExecShellWait "runas"')
+ assert i_check < i_runas, "the unelevated check must run before any elevation"
+ assert 'firewall.ps1" add' in install
+
+ assert "MB_AutoMode" not in nsh, "there is no autostart choice for Light"
+ assert "Page custom" not in nsh
+ assert "MessageBox MB_YESNO" not in install, (
+ "customInstall itself must ask nothing -- only customUnInstall's "
+ "opt-in firewall-removal prompt uses MessageBox")
+
+
+def test_light_installer_never_touches_a_co_installed_full_clients_node():
+ """
+ Full and Light can be installed side by side (distinct appId/
+ productName/install dir -- test_light_config_is_standalone_and_ships_
+ no_node above). Light's installer/uninstaller must be a complete no-op
+ with respect to anything a co-installed Full client owns: its node
+ process, its Startup .vbs, its Scheduled Task, its own firewall rule.
+ The easy mistake here is copy-pasting installer.nsh and trimming it,
+ leaving one of these in by accident.
+ """
+ nsh = LIGHT_NSH.read_text(encoding="utf-8")
+ for forbidden in ("taskkill", 'HKCU "Environment"', "service.ps1",
+ "service-mode.ps1", ".vbs", "MeshBay Node.vbs"):
+ assert forbidden not in nsh, (
+ f"installer-light.nsh must not reference {forbidden!r} -- that "
+ "belongs to a co-installed Full client, not to Light")
+
+
+def test_light_uninstaller_offers_to_remove_the_firewall_rules_default_no():
+ nsh = LIGHT_NSH.read_text(encoding="utf-8")
+ uninstall = _macro_body(nsh, "customUnInstall")
+ assert "${IfNot} ${Silent}" in uninstall
+ assert "/SD IDNO" in uninstall
+ assert 'firewall.ps1" remove' in uninstall
+ assert 'ExecShellWait "runas"' in uninstall
+
+
+# ── the app-side gaps a bundle-less build would otherwise hit ──────────────
+
+def test_has_bundled_node_is_windows_only_and_packaged_only():
+ """
+ Off win32, or unpackaged (dev), this must stay true unconditionally --
+ only a packaged Windows build can even be Light, so nothing here may
+ change today's behaviour for Linux, macOS or `npm start`.
+ """
+ src = MAIN_JS.read_text(encoding="utf-8")
+ assert "function hasBundledNode()" in src
+ body = src.split("function hasBundledNode()", 1)[1].split("\n }", 1)[0]
+ assert "process.platform === 'win32'" in body
+ assert "app.isPackaged" in body
+ assert "return true" in body, (
+ "must fall back to true (today's behaviour) off win32 / unpackaged")
+
+
+def test_node_bundled_ipc_channel_exists_end_to_end():
+ """main.js handles it, preload.js exposes it, platform.js wraps it --
+ the same three-layer shape every other node.* capability already has."""
+ main = MAIN_JS.read_text(encoding="utf-8")
+ assert "ipcMain.handle('node:bundled'" in main
+ assert "hasBundledNode()" in main
+
+ preload = PRELOAD_JS.read_text(encoding="utf-8")
+ assert "ipcRenderer.invoke('node:bundled')" in preload
+
+ platform_js = (HUB_STATIC / "platform.js").read_text(encoding="utf-8")
+ assert "bridge.node.bundled()" in platform_js
+
+
+def test_can_elevate_checks_service_mode_ps1_actually_exists():
+ """
+ app.isPackaged alone used to gate canElevate -- true for Light too,
+ where service-mode.ps1 is never shipped, so the Node page would offer
+ "background service" and only fail when clicked. Both nodeServiceStatus
+ branches (service installed, and the per-user Startup branch) must go
+ through the same helper rather than reimplementing the check.
+ """
+ src = MAIN_JS.read_text(encoding="utf-8")
+ assert "function winCanElevateServiceMode()" in src
+ helper = src.split("function winCanElevateServiceMode()", 1)[1].split("\n }", 1)[0]
+ assert "app.isPackaged" in helper
+ assert "'service-mode.ps1'" in helper
+ assert src.count("canElevate: winCanElevateServiceMode()") == 2, (
+ "both nodeServiceStatus branches must use the helper, not a bare "
+ "app.isPackaged")
+ assert "canElevate: app.isPackaged," not in src
+
+
+def test_startup_mode_is_null_not_a_lie_when_no_node_is_found():
+ """
+ The per-user Startup branch used to report mode: 'startup' unconditionally,
+ so a Light install with no node anywhere (bundled or on PATH) showed a
+ working-looking autostart dropdown for a node that did not exist --
+ node-page.js's showStartupRow is gated on typeof info.mode === 'string'.
+ """
+ src = MAIN_JS.read_text(encoding="utf-8")
+ assert "mode: bin ? 'startup' : null," in src
+ assert "mode: 'startup'," not in src
+
+
+def test_create_group_page_falls_back_when_no_node_is_bundled():
+ """
+ The wizard's node-linking step is exactly the known "Detecting local
+ node..." hang when nothing ever answers. Gating it on `bundled` (not
+ just `available`, which is true for Light too -- both are Electron)
+ routes a Light install to CreateGroupFormSimple: not a lesser feature,
+ but the same node-free form a plain browser member already uses.
+ """
+ src = (HUB_STATIC / "create-group-page.js").read_text(encoding="utf-8")
+ body = src.split("export function CreateGroupPage(props) {", 1)[1] \
+ .split("\nfunction CreateGroupFormSimple", 1)[0]
+ assert "platform.node.bundled()" in body
+ assert "platform.node.available && bundled" in body
+ assert "CreateGroupWizard" in body and "CreateGroupFormSimple" in body
diff --git a/packaging/win/build-win-common.ps1 b/packaging/win/build-win-common.ps1
new file mode 100644
index 0000000..e60abc5
--- /dev/null
+++ b/packaging/win/build-win-common.ps1
@@ -0,0 +1,64 @@
+<#
+.SYNOPSIS
+ Steps shared by build-win.ps1 (Full) and build-win-light.ps1 (Light).
+
+.DESCRIPTION
+ Dot-sourced, not run directly -- it defines functions, it does not call
+ them. The two orchestrators differ only in whether they freeze a node
+ runtime and which electron-builder config they hand to the final step;
+ everything before that (Node version check, npm ci, the Electron-bump
+ policy, sync-ui) is identical, and living in one place means it cannot
+ drift between the two the way a copy-paste would.
+
+ Every function assumes it runs from packages/meshbay-client (both
+ orchestrators Push-Location there first).
+#>
+
+function Step($msg) { Write-Host "==> $msg" -ForegroundColor Cyan }
+
+function Assert-NodeVersion {
+ if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
+ throw "Node.js not found. Install Node 22 or newer from nodejs.org."
+ }
+ $nodeMajor = [int](& node -e "process.stdout.write(String(process.versions.node.split('.')[0]))")
+ if ($nodeMajor -lt 22) { throw "Node $nodeMajor is too old -- need 22 or newer for Electron" }
+ Step "Node $(& node --version)"
+}
+
+function Invoke-NpmCi {
+ Step "npm ci"
+ & npm ci --ignore-scripts
+ if ($LASTEXITCODE -ne 0) { throw "npm ci failed" }
+}
+
+# Chromium-CVE policy: build against the latest Electron release unless
+# -NoElectronBump was passed. Writes package.json + package-lock.json when it
+# actually bumps something (that is by design -- commit the new pin); the
+# Chromium download itself always runs, bump or not, same as before this was
+# factored out.
+function Invoke-ElectronBump {
+ param(
+ [switch]$NoElectronBump,
+ [Parameter(Mandatory = $true)][string]$WinDir
+ )
+ if (-not $NoElectronBump) {
+ Step "checking for a newer Electron"
+ $bumped = & node (Join-Path $WinDir "bump-electron.mjs")
+ if ($LASTEXITCODE -ne 0) { throw "electron bump failed" }
+ if ($bumped) {
+ Write-Host " Electron -> $bumped (package.json + lock updated, commit them)" -ForegroundColor Yellow
+ } else {
+ Write-Host " Electron is already current"
+ }
+ }
+
+ Step "downloading Electron's Chromium"
+ & npm approve-scripts electron 2>$null
+ & node node_modules/electron/install.js
+}
+
+function Invoke-SyncUi {
+ Step "npm run sync-ui"
+ & npm run sync-ui
+ if ($LASTEXITCODE -ne 0) { throw "sync-ui failed" }
+}
diff --git a/packaging/win/build-win-light.ps1 b/packaging/win/build-win-light.ps1
new file mode 100644
index 0000000..5a8a06e
--- /dev/null
+++ b/packaging/win/build-win-light.ps1
@@ -0,0 +1,85 @@
+<#
+.SYNOPSIS
+ Build the Windows "Light" installer: Electron client + UI only, no
+ bundled node.
+
+.DESCRIPTION
+ The counterpart of build-win.ps1 (Full) for anyone who only wants to
+ *use* MeshBay -- join groups, chat, browse, download, stream, cast --
+ without ever hosting content from this machine. See
+ C:\Users\admin\devel\light-client.md for the evaluation this
+ implements: a member never needs a local node to begin with (identity
+ keys are per node -- the *host's* node, not the joiner's), so Light is
+ the existing browser-only usage pattern wrapped in the Electron shell,
+ minus the frozen meshbay-node.exe / ffmpeg / autostart bundle.
+
+ Steps 1-4 are identical to build-win.ps1 (build-win-common.ps1). Step 5,
+ PyInstaller freezing a node runtime, does not happen at all -- that is
+ the entire point of this target. Step 6 hands electron-builder a
+ standalone config (electron-builder.light.yml) instead of package.json's
+ `build` field: passing --config makes electron-builder read ONLY that
+ file (see app-builder-lib/out/util/config/load.js's getConfig -- when a
+ config path is given, package.json's own `build` field is never loaded,
+ so there is no array-merge ambiguity to worry about between the two
+ targets' very different extraResources).
+
+ Output: packages/meshbay-client/dist-light/MeshBay Light-Setup-<version>.exe
+ (a different output directory from Full's dist/, so the two builds never
+ race on the same files).
+
+.PARAMETER NoElectronBump
+ Keep the pinned Electron instead of upgrading to the latest release.
+#>
+[CmdletBinding()]
+param(
+ [switch]$NoElectronBump
+)
+
+$ErrorActionPreference = "Stop"
+Set-StrictMode -Version Latest
+
+$WinDir = $PSScriptRoot
+$Repo = (Resolve-Path (Join-Path $WinDir "..\..")).Path
+$Client = Join-Path $Repo "packages\meshbay-client"
+$Config = Join-Path $WinDir "electron-builder.light.yml"
+
+. (Join-Path $WinDir "build-win-common.ps1")
+
+if (-not (Test-Path $Config)) { throw "missing $Config" }
+
+# --- 1. Node -------------------------------------------------------------
+Assert-NodeVersion
+
+Push-Location $Client
+try {
+ # --- 2. deps ------------------------------------------------------
+ Invoke-NpmCi
+
+ # --- 3. Electron: build against the latest release --------------
+ Invoke-ElectronBump -NoElectronBump:$NoElectronBump -WinDir $WinDir
+
+ # --- 4. UI -----------------------------------------------------
+ Invoke-SyncUi
+
+ # --- 5. (no node runtime -- that is what makes this "Light") -----
+
+ # --- 6. installer ---------------------------------------
+ Step "electron-builder --win nsis --config electron-builder.light.yml"
+ & npx electron-builder --win nsis --config $Config
+ if ($LASTEXITCODE -ne 0) { throw "electron-builder failed" }
+}
+finally {
+ Pop-Location
+}
+
+$setup = Get-ChildItem (Join-Path $Client "dist-light") -Filter "*Setup*.exe" -ErrorAction SilentlyContinue |
+ Sort-Object LastWriteTime | Select-Object -Last 1
+Write-Host ""
+if ($setup) {
+ Write-Host "OK installer: $($setup.FullName)" -ForegroundColor Green
+ Write-Host (" ({0:N0} MB)" -f ($setup.Length / 1MB))
+}
+else {
+ Write-Host "!! no *Setup*.exe found in $Client\dist-light" -ForegroundColor Red
+ exit 1
+}
diff --git a/packaging/win/build-win.ps1 b/packaging/win/build-win.ps1
index 0a30ad5..ea54c6f 100644
--- a/packaging/win/build-win.ps1
+++ b/packaging/win/build-win.ps1
@@ -9,13 +9,18 @@
meshbay-node daemon. No hub -- a desktop machine installs client + node
(+ common, which is inside the node runtime).
+ This is the "Full" target. See build-win-light.ps1 for "Light" (client +
+ UI only, no bundled node) -- steps 1-4 below live in build-win-common.ps1,
+ shared by both, so they cannot drift apart.
+
Steps:
1. Node >= 22 check
2. npm ci (+ approve Electron's install script, download Chromium)
3. bump Electron to the latest release (Chromium CVE policy -- see
build-client.sh; skip with -NoElectronBump)
4. npm run sync-ui (copy the interface from the hub package)
- 5. build-node-runtime.ps1 (PyInstaller freeze of the daemon)
+ 5. build-node-runtime.ps1 (PyInstaller freeze of the daemon) -- Light
+ skips this step entirely, which is the whole difference
6. electron-builder --win nsis
.PARAMETER SkipFfmpeg
@@ -44,45 +49,23 @@ $WinDir = $PSScriptRoot
$Repo = (Resolve-Path (Join-Path $WinDir "..\..")).Path
$Client = Join-Path $Repo "packages\meshbay-client"
-function Step($msg) { Write-Host "==> $msg" -ForegroundColor Cyan }
+. (Join-Path $WinDir "build-win-common.ps1")
# --- 1. Node -------------------------------------------------------------
-if (-not (Get-Command node -ErrorAction SilentlyContinue)) {
- throw "Node.js not found. Install Node 22 or newer from nodejs.org."
-}
-$nodeMajor = [int](& node -e "process.stdout.write(String(process.versions.node.split('.')[0]))")
-if ($nodeMajor -lt 22) { throw "Node $nodeMajor is too old -- need 22 or newer for Electron" }
-Step "Node $(& node --version)"
+Assert-NodeVersion
Push-Location $Client
try {
# --- 2. deps ------------------------------------------------------
- Step "npm ci"
- & npm ci --ignore-scripts
- if ($LASTEXITCODE -ne 0) { throw "npm ci failed" }
+ Invoke-NpmCi
# --- 3. Electron: build against the latest release --------------
- # Writes package.json + package-lock.json, so the build leaves the repo
- # dirty on purpose -- commit the new pin.
- if (-not $NoElectronBump) {
- Step "checking for a newer Electron"
- $bumped = & node (Join-Path $WinDir "bump-electron.mjs")
- if ($LASTEXITCODE -ne 0) { throw "electron bump failed" }
- if ($bumped) {
- Write-Host " Electron -> $bumped (package.json + lock updated, commit them)" -ForegroundColor Yellow
- } else {
- Write-Host " Electron is already current"
- }
- }
-
- Step "downloading Electron's Chromium"
- & npm approve-scripts electron 2>$null
- & node node_modules/electron/install.js
+ # Writes package.json + package-lock.json when it bumps something, so the
+ # build leaves the repo dirty on purpose -- commit the new pin.
+ Invoke-ElectronBump -NoElectronBump:$NoElectronBump -WinDir $WinDir
# --- 4. UI -----------------------------------------------------
- Step "npm run sync-ui"
- & npm run sync-ui
- if ($LASTEXITCODE -ne 0) { throw "sync-ui failed" }
+ Invoke-SyncUi
# --- 5. node runtime ---------------------------------------
$rtExe = Join-Path $Client "node-runtime\meshbay-node.exe"
diff --git a/packaging/win/electron-builder.light.yml b/packaging/win/electron-builder.light.yml
new file mode 100644
index 0000000..155ffa6
--- /dev/null
+++ b/packaging/win/electron-builder.light.yml
@@ -0,0 +1,54 @@
+# Standalone electron-builder config for the "Light" Windows target: Electron
+# client + UI, no bundled node. Deliberately NOT layered onto package.json's
+# `build` field -- passed via `electron-builder --config <this file>`, which
+# reads ONLY this file (app-builder-lib/out/util/config/load.js's getConfig:
+# a given configPath replaces package.json's own "build", it does not merge
+# with it). That is what keeps Light's extraResources from ever accidentally
+# inheriting Full's frozen daemon and its two service scripts through some
+# array-merge surprise -- there is nothing to merge.
+#
+# Invoked from packages/meshbay-client (see build-win-light.ps1), so every
+# relative path below resolves the same way package.json's `build` field's
+# already do.
+#
+# See C:\Users\admin\devel\light-client.md for the evaluation this
+# implements.
+
+appId: org.meshbay.client.light
+productName: MeshBay Light
+
+directories:
+ # Full's own build lives in dist/ -- a separate output dir means the two
+ # targets never race on, or clobber, each other's files.
+ output: dist-light
+
+files:
+ - src/**
+ - ui/**
+
+win:
+ target: nsis
+ icon: build/icon.ico
+ artifactName: "MeshBay-Light-Setup-${version}.${ext}"
+ extraResources:
+ # firewall.ps1 needs no Light-specific fork: `add` already does
+ # `if (-not (Test-Path $r.Path)) { skip }` per rule, and the "MeshBay
+ # Node" rule's target -- the frozen daemon, inside the folder this
+ # config never ships -- simply is not here. One script, one source of
+ # truth; it just silently adds the 3 rules that apply (client WebRTC
+ # ICE + the 2 LAN-casting rules) and logs the node rule as skipped.
+ - from: ../../packaging/win/firewall.ps1
+ to: firewall.ps1
+
+nsis:
+ oneClick: false
+ perMachine: false
+ allowElevation: false
+ allowToChangeInstallationDirectory: true
+ createDesktopShortcut: true
+ createStartMenuShortcut: true
+ deleteAppDataOnUninstall: false
+ runAfterFinish: true
+ # Resolved against buildResourcesDir (build/), same as Full's implicit
+ # default of build/installer.nsh -- see platformPackager.js's getResource.
+ include: installer-light.nsh