diff options
Diffstat (limited to 'packages')
| -rw-r--r-- | packages/meshbay-client/build/icon.ico | bin | 0 -> 117250 bytes | |||
| -rw-r--r-- | packages/meshbay-client/build/installer.nsh | 24 | ||||
| -rw-r--r-- | packages/meshbay-client/package.json | 27 | ||||
| -rw-r--r-- | packages/meshbay-client/src/main.js | 10 | ||||
| -rw-r--r-- | packages/meshbay-node/tests/test_packaging_win.py | 153 |
5 files changed, 211 insertions, 3 deletions
diff --git a/packages/meshbay-client/build/icon.ico b/packages/meshbay-client/build/icon.ico Binary files differnew file mode 100644 index 0000000..2766f24 --- /dev/null +++ b/packages/meshbay-client/build/icon.ico diff --git a/packages/meshbay-client/build/installer.nsh b/packages/meshbay-client/build/installer.nsh new file mode 100644 index 0000000..cf8e5ff --- /dev/null +++ b/packages/meshbay-client/build/installer.nsh @@ -0,0 +1,24 @@ +; electron-builder NSIS customisation (auto-included: build/installer.nsh). +; +; Per-user install, no elevation (package.json build.nsis). These macros only +; deal with the one piece of state that lives outside the install directory: +; the W3 "run at sign-in" launcher the node's own CLI can create. +; +; Deliberately NOT touched here: +; - %LOCALAPPDATA%\meshbay\ (node.toml, keystore.enc, unlock.key, data/) -- +; the keystore must survive an uninstall/reinstall; installers place files, +; never remove secrets. + +!macro customInstall + ; resources\node-runtime\meshbay-node.exe is overwritten by this install; a + ; daemon still running from a previous version holds the file open. + nsExec::Exec 'taskkill /IM meshbay-node.exe /F' +!macroend + +!macro customUnInstall + nsExec::Exec 'taskkill /IM meshbay-node.exe /F' + ; meshbay_node.platform._startup_vbs() -- if the user ran "meshbay-node + ; autostart install" (or toggled it in the client), this points wscript at + ; the binary we are about to delete, and would error at every sign-in. + Delete "$APPDATA\Microsoft\Windows\Start Menu\Programs\Startup\MeshBay Node.vbs" +!macroend diff --git a/packages/meshbay-client/package.json b/packages/meshbay-client/package.json index 42af9c5..57acff8 100644 --- a/packages/meshbay-client/package.json +++ b/packages/meshbay-client/package.json @@ -10,7 +10,8 @@ "scripts": { "start": "electron .", "sync-ui": "node scripts/sync-ui.js", - "dist": "bash ../../packaging/build/build-client.sh" + "dist": "bash ../../packaging/build/build-client.sh", + "dist:win": "powershell -NoProfile -ExecutionPolicy Bypass -File ../../packaging/win/build-win.ps1" }, "devDependencies": { "electron": "^44.1.1", @@ -22,7 +23,29 @@ "files": [ "src/**", "ui/**" - ] + ], + "win": { + "target": "nsis", + "icon": "build/icon.ico", + "artifactName": "${productName}-Setup-${version}.${ext}", + "extraResources": [ + { + "from": "node-runtime", + "to": "node-runtime", + "filter": ["**/*"] + } + ] + }, + "nsis": { + "oneClick": false, + "perMachine": false, + "allowElevation": false, + "allowToChangeInstallationDirectory": true, + "createDesktopShortcut": true, + "createStartMenuShortcut": true, + "deleteAppDataOnUninstall": false, + "runAfterFinish": true + } }, "dependencies": { "bonjour-service": "^1.4.4", diff --git a/packages/meshbay-client/src/main.js b/packages/meshbay-client/src/main.js index d341619..bd82342 100644 --- a/packages/meshbay-client/src/main.js +++ b/packages/meshbay-client/src/main.js @@ -792,7 +792,15 @@ function registerBridge() { }); function findNodeBinary() { - if (process.platform !== 'win32') { + if (process.platform === 'win32') { + // A packaged Windows build carries the frozen daemon as an + // extraResource (package.json build.win, packaging/win/). Prefer it — + // it is the version that shipped with this client. + if (app.isPackaged) { + const bundled = path.join(process.resourcesPath, 'node-runtime', 'meshbay-node.exe'); + if (fs.existsSync(bundled)) return bundled; + } + } else { const local = path.join(os.homedir(), '.local', 'bin', 'meshbay-node'); if (fs.existsSync(local)) return local; } diff --git a/packages/meshbay-node/tests/test_packaging_win.py b/packages/meshbay-node/tests/test_packaging_win.py new file mode 100644 index 0000000..1b7521a --- /dev/null +++ b/packages/meshbay-node/tests/test_packaging_win.py @@ -0,0 +1,153 @@ +""" +The Windows installer (W4): a single per-user NSIS package carrying the Electron +client and the frozen node daemon. + +Like test_packaging_units.py this reads the config rather than building anything +— there is no electron-builder or PyInstaller run here. Weak evidence, and the +right kind for the defects it guards against: a per-machine flag that would make +the installer demand admin, a build step wired to the wrong file, the 150 MB +node-runtime artifact slipping into git, the autostart seam between the NSIS +uninstaller and meshbay_node.platform drifting apart. +""" + +import json +import re +from pathlib import Path + +import pytest + +ROOT = Path(__file__).resolve().parents[3] +CLIENT = ROOT / "packages" / "meshbay-client" +PKG_JSON = CLIENT / "package.json" +WIN = ROOT / "packaging" / "win" +NSH = CLIENT / "build" / "installer.nsh" + +pytestmark = pytest.mark.skipif( + not PKG_JSON.exists() or not WIN.exists(), + reason="Windows packaging not present") + + +def _pkg() -> dict: + return json.loads(PKG_JSON.read_text(encoding="utf-8")) + + +# ── electron-builder: Windows target ──────────────────────────────────────── + +def test_the_windows_target_is_nsis_with_the_committed_icon(): + win = _pkg()["build"]["win"] + assert win["target"] == "nsis" + icon = CLIENT / win["icon"] + assert icon.suffix == ".ico" and icon.exists(), f"{icon} is missing" + + +def test_the_installer_is_per_user_and_never_asks_for_admin(): + """ + A logon-triggered scheduled task needs elevation (that is why W3 uses the + Startup folder), and the whole desktop design is no-admin. perMachine or + allowElevation here would undo that at install time. + """ + nsis = _pkg()["build"]["nsis"] + assert nsis["oneClick"] is False + assert nsis["perMachine"] is False + assert nsis["allowElevation"] is False + assert nsis["allowToChangeInstallationDirectory"] is True + + +def test_the_node_runtime_is_carried_as_an_extraresource(): + """ + PyInstaller output lands in packages/meshbay-client/node-runtime/ and rides + into the package under resources/node-runtime/. src/main.js:findNodeBinary + resolves exactly that path (process.resourcesPath / node-runtime / + meshbay-node.exe), so the two names must agree. + """ + extra = _pkg()["build"]["win"]["extraResources"] + entry = next((e for e in extra if e.get("to") == "node-runtime"), None) + assert entry, "no extraResources entry mapping to node-runtime" + assert entry["from"] == "node-runtime" + + main_js = (CLIENT / "src" / "main.js").read_text(encoding="utf-8") + assert "'node-runtime', 'meshbay-node.exe'" in main_js, ( + "findNodeBinary no longer looks for the bundled daemon where " + "extraResources puts it") + + +def test_dist_win_delegates_to_the_build_script(): + """`dist` (Linux) delegates to build-client.sh; `dist:win` is its + counterpart and must not be a second inline electron-builder invocation.""" + scripts = _pkg()["scripts"] + assert "dist:win" in scripts + assert "build-win.ps1" in scripts["dist:win"] + # `dist` stays Linux-only and unchanged (test_desktop_shell.py guards it too). + assert "win" not in scripts["dist"].lower() + + +# ── the build scripts exist and point at real files ──────────────────────── + +@pytest.mark.parametrize("name", [ + "build-win.ps1", + "build-node-runtime.ps1", + "meshbay-node.spec", + "node-entry.py", + "README.md", +]) +def test_packaging_win_ships_its_scripts(name): + assert (WIN / name).exists(), f"packaging/win/{name} is missing" + + +def test_the_pyinstaller_entry_point_is_the_daemon_main(): + src = (WIN / "node-entry.py").read_text(encoding="utf-8") + assert "from meshbay_node.daemon import main" in src + assert "main()" in src + + +def test_the_spec_pulls_in_the_awkward_dependencies_whole(): + """ + The C/Rust-extension and dynamic-import packages are the ones PyInstaller's + static pass drops. If someone trims collect_all to shrink the build, the + frozen daemon fails at runtime, not at build time. + """ + spec = (WIN / "meshbay-node.spec").read_text(encoding="utf-8") + for pkg in ("aiortc", "av", "aioquic", "pydantic_core", "uvicorn", + "watchdog", "guessit", "blake3", "meshbay_node", "meshbay_common"): + assert re.search(rf'["\']{re.escape(pkg)}["\']', spec), ( + f"{pkg} dropped from the PyInstaller spec's collect list") + + +# ── the artifact never gets committed ────────────────────────────────────── + +def test_the_node_runtime_output_is_gitignored(): + """It is ~150 MB of frozen Python. The `ui/` fork guard in + test_desktop_shell.py exists for the same reason.""" + gitignore = (ROOT / ".gitignore").read_text(encoding="utf-8") + assert "packages/meshbay-client/node-runtime/" in gitignore + + +# ── the NSIS ↔ platform.py autostart seam ───────────────────────────────── + +def test_the_uninstaller_clears_the_autostart_launcher(): + """ + W3's `meshbay-node autostart install` drops a .vbs in the Startup folder + (meshbay_node.platform._startup_vbs). After an uninstall it would point + wscript at a deleted binary every sign-in, so customUnInstall must delete + it — and at the path platform.py actually uses. + """ + from meshbay_node import platform as plat + + nsh = NSH.read_text(encoding="utf-8") + assert "!macro customUnInstall" in nsh + assert "taskkill /IM meshbay-node.exe /F" in nsh + + # The tail platform.py builds, made NSIS-relative ($APPDATA == %APPDATA%). + tail = plat._startup_vbs() + parts = tail.parts + i = parts.index("Microsoft") + rel = "\\".join(parts[i:]) # Microsoft\...\Startup\MeshBay Node.vbs + assert rel in nsh, ( + f"customUnInstall does not delete {rel!r} — the W3 autostart path " + "changed and installer.nsh was not updated") + + +def test_customInstall_stops_a_running_daemon_before_overwriting_it(): + nsh = NSH.read_text(encoding="utf-8") + body = nsh.split("!macro customInstall", 1)[1].split("!macroend", 1)[0] + assert "taskkill /IM meshbay-node.exe /F" in body |