diff options
| -rw-r--r-- | .gitignore | 4 | ||||
| -rw-r--r-- | packages/meshbay-client/build/installer-light.nsh | 67 | ||||
| -rw-r--r-- | packages/meshbay-client/package.json | 3 | ||||
| -rw-r--r-- | packages/meshbay-node/tests/test_packaging_win.py | 249 | ||||
| -rw-r--r-- | packaging/win/build-win-common.ps1 | 64 | ||||
| -rw-r--r-- | packaging/win/build-win-light.ps1 | 85 | ||||
| -rw-r--r-- | packaging/win/build-win.ps1 | 43 | ||||
| -rw-r--r-- | packaging/win/electron-builder.light.yml | 54 |
8 files changed, 538 insertions, 31 deletions
@@ -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 |