diff options
Diffstat (limited to 'packaging/win')
| -rw-r--r-- | packaging/win/README.md | 20 | ||||
| -rw-r--r-- | packaging/win/build-node-runtime.ps1 | 38 | ||||
| -rw-r--r-- | packaging/win/cli-entry.py | 14 | ||||
| -rw-r--r-- | packaging/win/firewall.ps1 | 10 | ||||
| -rw-r--r-- | packaging/win/meshbay-node.spec | 127 |
5 files changed, 178 insertions, 31 deletions
diff --git a/packaging/win/README.md b/packaging/win/README.md index 4ce1459..89185fe 100644 --- a/packaging/win/README.md +++ b/packaging/win/README.md @@ -11,13 +11,15 @@ Linux). `meshbay-common` rides along inside the node runtime. ├─ MeshBay.exe Electron client ├─ resources\ │ ├─ app.asar src/ + ui/ (the interface ships in the package) -│ ├─ firewall.ps1 adds/removes the four inbound rules (see below) +│ ├─ firewall.ps1 adds/removes the five inbound rules (see below) │ ├─ service.ps1 install/remove/status/run/end the boot-time task │ ├─ service-mode.ps1 elevated helper: service.ps1 + firewall.ps1 in one UAC prompt │ └─ node-runtime\ │ ├─ meshbay-node.exe frozen daemon and CLI (PyInstaller onedir) │ ├─ meshbay-nodew.exe the same daemon without a console (the Store │ │ package's startup task runs it) +│ ├─ meshbay-cli.exe the command-line client, a member's files from +│ │ a terminal (Full and Store, not Light) │ ├─ _internal\ … its Python + deps (aiortc, av, aioquic, …) │ ├─ ffmpeg.exe, ffprobe.exe bundled by default, see Video (ffmpeg) below │ ├─ av*.dll, swscale/swresample*.dll what those two link against @@ -33,8 +35,9 @@ as this same signed-in user (S4U, see below), so it is the same profile either way — not LocalSystem/NetworkService, which would have none of this. `build/installer.nsh` also adds `…\resources\node-runtime` to the **per-user** -`Path` (`HKCU\Environment`) so `meshbay-node` works in a terminal, and takes it -back out on uninstall. New shells only — a `WM_SETTINGCHANGE` broadcast nudges +`Path` (`HKCU\Environment`) so `meshbay-node` and `meshbay-cli` work in a +terminal, and takes it back out on uninstall. The Store package declares an +execution alias for each instead (`build/appx-extensions.xml`). New shells only — a `WM_SETTINGCHANGE` broadcast nudges open ones. It uses stock `WordFunc.nsh` (the `EnVar` plugin is not in electron-builder's NSIS bundle). @@ -151,7 +154,7 @@ That runs [`build-win.ps1`](build-win.ps1): | 2 | `npm ci` + download Electron's Chromium | | 3 | bump Electron to the latest release (Chromium CVE policy; `-NoElectronBump` to skip) | | 4 | `npm run sync-ui` — copy the interface from `meshbay-hub/.../static` | -| 5 | [`build-node-runtime.ps1`](build-node-runtime.ps1) — PyInstaller freeze → `packages/meshbay-client/node-runtime/`, then [`smoke-node-runtime.ps1`](smoke-node-runtime.ps1) starts the frozen daemon in a throwaway profile and checks it answers with the right version and writes its log, and [`smoke-connection-process.py`](smoke-connection-process.py) starts both executables as connection processes (`connection_processes` above 1) and carries a connection through each | +| 5 | [`build-node-runtime.ps1`](build-node-runtime.ps1) — PyInstaller freeze → `packages/meshbay-client/node-runtime/`, then [`smoke-node-runtime.ps1`](smoke-node-runtime.ps1) starts the frozen daemon in a throwaway profile and checks it answers with the right version and writes its log, and [`smoke-connection-process.py`](smoke-connection-process.py) starts both executables as connection processes (`connection_processes` above 1) and carries a connection through each; `meshbay-cli.exe` is run too — `--version`, `groups --help`, and its self-test (`MESHBAY_CLI_SELFTEST=1 meshbay-cli`), which loads what a frozen build can miss without failing until run: the interactive mode drawn off-screen, keyring's backends, the WebRTC stack | | 6 | `electron-builder --win nsis` → `packages/meshbay-client/dist/MeshBay-Setup-<version>.exe` | ### Video (ffmpeg) @@ -215,7 +218,7 @@ publish their host candidate as an unresolvable `<uuid>.local` mDNS name that `aioice` discards — the browser always dials the node, never the reverse. So the node has to accept unsolicited inbound UDP from its peers. -**Windows Defender Firewall.** The setup wizard offers to add four inbound +**Windows Defender Firewall.** The setup wizard offers to add five inbound rules in one step — it needs one admin confirmation (`build/installer.nsh` runs `firewall.ps1` via NSIS `ExecShellWait "runas"`; the per-user install itself never elevates): @@ -224,6 +227,7 @@ itself never elevates): |---|---|---|---| | `MeshBay` | `MeshBay.exe` | UDP, any port | WebRTC (ICE checks touch the client too) | | `MeshBay Node` | `meshbay-node.exe` | UDP, any port | WebRTC (the node — unsolicited inbound, see above) | +| `MeshBay CLI` | `meshbay-cli.exe` | UDP, any port | WebRTC (the command-line client during a transfer; ICE checks touch it as they do the app) | | `MeshBay Cast` | `MeshBay.exe` | TCP 19550–19553 | LAN cast HTTP relay (`src/cast-relay.js`) | | `MeshBay Cast Discovery` | `MeshBay.exe` | UDP 5353 | Chromecast/Smart TV mDNS discovery (`bonjour-service`) | @@ -284,6 +288,12 @@ FFmpeg DLLs), `aioquic`, `pydantic_core`, `uvicorn`, `watchdog`, `guessit`, add the package to `COLLECT_ALL` / `HIDDEN` / `COPY_META` in the spec — that is the expected way the list grows. +The command-line client has its own analysis in the same spec +(`CLI_COLLECT_ALL`, `CLI_COPY_META`): Textual loads its widgets lazily, Rich its +Unicode tables by version, keyring its backends through entry points, which +is why keyring's metadata is copied. The build's self-test run catches what +those lists miss. + ## Not done - **Authenticode signing** — Phase 13.9. Unsigned installer triggers SmartScreen. diff --git a/packaging/win/build-node-runtime.ps1 b/packaging/win/build-node-runtime.ps1 index c6a8af1..28b96e8 100644 --- a/packaging/win/build-node-runtime.ps1 +++ b/packaging/win/build-node-runtime.ps1 @@ -1,10 +1,12 @@ <# .SYNOPSIS - Freeze the MeshBay node daemon into a relocatable Windows binary. + Freeze the MeshBay node daemon, and the command-line client beside it, + into relocatable Windows binaries. .DESCRIPTION - Produces packages/meshbay-client/node-runtime/meshbay-node.exe (+ its - onedir payload) with PyInstaller. electron-builder then carries that tree + Produces packages/meshbay-client/node-runtime/meshbay-node.exe, + meshbay-nodew.exe and meshbay-cli.exe (+ their shared 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 @@ -76,11 +78,12 @@ if ($verMajor -lt 3 -or ($verMajor -eq 3 -and $verMinor -lt 12)) { Step "python $verMajor.$verMinor ($Python)" # --- 2. dependencies ----------------------------------------------------- -Step "installing meshbay-common, meshbay-node and PyInstaller" +Step "installing meshbay-common, meshbay-node, meshbay-cli 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") ` + (Join-Path $Repo "packages\meshbay-cli") ` "pyinstaller>=6.10" ` "tzdata" # tzdata: Windows ships no IANA zone database, so zoneinfo (pulled in @@ -105,7 +108,7 @@ finally { } $frozen = Join-Path $pyiDist "meshbay-node" -foreach ($name in "meshbay-node.exe", "meshbay-nodew.exe") { +foreach ($name in "meshbay-node.exe", "meshbay-nodew.exe", "meshbay-cli.exe") { if (-not (Test-Path (Join-Path $frozen $name))) { throw "PyInstaller did not produce $name at $frozen" } @@ -124,7 +127,7 @@ Copy-Item (Join-Path $Repo "packages\meshbay-common\COPYING.LESSER") (Join-Path Copy-Item (Join-Path $Repo "packages\meshbay-common\COPYING") (Join-Path $frozen "LICENSE-GPL-3.0.txt") & $Python (Join-Path $Repo "packaging\third_party_notices.py") ` -o (Join-Path $frozen "THIRD-PARTY-NOTICES.txt") ` - meshbay-node tzdata --extra pyinstaller --with-python + meshbay-node meshbay-cli tzdata --extra pyinstaller --with-python if ($LASTEXITCODE -ne 0) { throw "third_party_notices.py failed" } # --- 4. ffmpeg (bundled by default) ----------------------------------- @@ -198,6 +201,29 @@ Step "smoke test: the frozen node as a connection process" & $Python (Join-Path $WinDir "smoke-connection-process.py") $exe (Join-Path $OutDir "meshbay-nodew.exe") if ($LASTEXITCODE -ne 0) { throw "a frozen connection process did not carry a connection" } +# The client is frozen from its own analysis, so what the daemon's checks +# proved says nothing about it: a module PyInstaller missed fails at run time +# only. --version and `groups --help` build the parser, which imports every +# action and the client library; the self-test loads what the parser does not +# -- the interactive mode drawn off-screen (Textual, Rich), keyring's backends +# (Windows Credential Manager), an aiortc offer. The CLI has the node's version +# (all packages share one). +Step "smoke test: meshbay-cli" +$cli = Join-Path $OutDir "meshbay-cli.exe" +$out = (& $cli --version 2>&1) -join "`n" +if ($LASTEXITCODE -ne 0 -or $out -notmatch "meshbay-cli $expect") { + throw "frozen meshbay-cli --version exited $LASTEXITCODE`n$out" +} +$out = (& $cli groups --help 2>&1) -join "`n" +if ($LASTEXITCODE -ne 0 -or $out -notmatch "List the groups") { + throw "frozen meshbay-cli groups --help exited $LASTEXITCODE`n$out" +} +$env:MESHBAY_CLI_SELFTEST = "1" +try { $out = (& $cli 2>&1) -join "`n" } finally { Remove-Item Env:MESHBAY_CLI_SELFTEST } +if ($LASTEXITCODE -ne 0 -or $out -notmatch "self-test ok") { + throw "frozen meshbay-cli self-test exited $LASTEXITCODE`n$out" +} + if ($createdVenv -and -not $KeepBuildVenv) { Remove-Item -Recurse -Force $BuildVenv -ErrorAction SilentlyContinue } diff --git a/packaging/win/cli-entry.py b/packaging/win/cli-entry.py new file mode 100644 index 0000000..908d4c7 --- /dev/null +++ b/packaging/win/cli-entry.py @@ -0,0 +1,14 @@ +"""PyInstaller entry point for the command-line client, `meshbay-cli.exe`. + +`meshbay_cli.__main__:main` is a module function; PyInstaller freezes a script. +Built beside meshbay-node.exe in the same runtime directory (meshbay-node.spec), +which the Full installer puts on the per-user PATH and the Store package names +in an execution alias. +""" + +import sys + +from meshbay_cli.__main__ import main + +if __name__ == "__main__": + sys.exit(main()) diff --git a/packaging/win/firewall.ps1 b/packaging/win/firewall.ps1 index 939a58e..827daf5 100644 --- a/packaging/win/firewall.ps1 +++ b/packaging/win/firewall.ps1 @@ -6,7 +6,9 @@ Two independent things need an inbound allow, and Windows prompts "Allow access" for each the first time it happens if there is no rule: - 1. WebRTC (MeshBay.exe and meshbay-node.exe). It binds an ephemeral UDP + 1. WebRTC (MeshBay.exe, meshbay-node.exe and meshbay-cli.exe, the + command-line client, which carries files over WebRTC as the app does). + It binds an ephemeral UDP port per connection, and the browser always dials the node -- aioice cannot resolve the peer's mDNS `.local` candidate to call back -- so the node (and, during ICE connectivity checks, the client) must accept @@ -23,12 +25,12 @@ are fixed and known. Matches packaging/firewall/*/meshbay-cast.xml. The installer runs this once, elevated, so the user answers one UAC prompt - instead of up to four dialogs spread across first use of chat, downloads + instead of up to five dialogs spread across first use of chat, downloads and casting. Declining the installer's offer is fine -- the dialogs are the fallback, one per program/port combination as each is first used. Shipped as an extraResource at <install>\resources\firewall.ps1, so it - locates the two executables from its own path and takes no arguments + locates the executables from its own path and takes no arguments beyond the action. add/remove run elevated and windowless, leaving a trace at %TEMP%\meshbay-firewall.log; check is a plain read (no admin needed -- Get-NetFirewallRule does not require it, only New/Remove do) @@ -59,12 +61,14 @@ $GROUP = "MeshBay" $client = Join-Path $install "MeshBay.exe" $node = Join-Path $resources "node-runtime\meshbay-node.exe" +$cli = Join-Path $resources "node-runtime\meshbay-cli.exe" # Program-scoped, any port: the ephemeral-UDP-port problem WebRTC always has. # Port-scoped as well: the two fixed, known ports LAN casting actually uses. $rules = @( @{ Name = "MeshBay"; Path = $client; Protocol = "UDP"; LocalPort = "Any" } @{ Name = "MeshBay Node"; Path = $node; Protocol = "UDP"; LocalPort = "Any" } + @{ Name = "MeshBay CLI"; Path = $cli; Protocol = "UDP"; LocalPort = "Any" } @{ Name = "MeshBay Cast"; Path = $client; Protocol = "TCP"; LocalPort = "19550-19553" } @{ Name = "MeshBay Cast Discovery"; Path = $client; Protocol = "UDP"; LocalPort = "5353" } ) diff --git a/packaging/win/meshbay-node.spec b/packaging/win/meshbay-node.spec index a1173a2..8a6e384 100644 --- a/packaging/win/meshbay-node.spec +++ b/packaging/win/meshbay-node.spec @@ -1,6 +1,8 @@ -# PyInstaller spec for the bundled Windows node daemon. +# PyInstaller spec for the bundled Windows node daemon, and the command-line +# client beside it. # -# Output: dist/meshbay-node/meshbay-node.exe (--onedir, relocatable) +# Output: dist/meshbay-node/meshbay-node.exe, meshbay-nodew.exe and +# meshbay-cli.exe (--onedir, relocatable, one _internal\ for the three) # Consumed by: packaging/win/build-node-runtime.ps1, which moves the tree to # packages/meshbay-client/node-runtime/ for electron-builder to carry as an # extraResource. @@ -35,21 +37,27 @@ _vtuple = tuple( int(p) for p in (_ver.split("+")[0].split(".") + ["0", "0", "0", "0"])[:4] ) -_version_info = VSVersionInfo( - ffi=FixedFileInfo(filevers=_vtuple, prodvers=_vtuple), - kids=[ - StringFileInfo([StringTable("040904B0", [ - StringStruct("CompanyName", "MeshBay"), - StringStruct("FileDescription", "MeshBay Node"), - StringStruct("FileVersion", _ver), - StringStruct("InternalName", "meshbay-node"), - StringStruct("OriginalFilename", "meshbay-node.exe"), - StringStruct("ProductName", "MeshBay Node"), - StringStruct("ProductVersion", _ver), - ])]), - VarFileInfo([VarStruct("Translation", [0x0409, 1200])]), - ], -) + + +def _version_resource(name: str, description: str) -> VSVersionInfo: + return VSVersionInfo( + ffi=FixedFileInfo(filevers=_vtuple, prodvers=_vtuple), + kids=[ + StringFileInfo([StringTable("040904B0", [ + StringStruct("CompanyName", "MeshBay"), + StringStruct("FileDescription", description), + StringStruct("FileVersion", _ver), + StringStruct("InternalName", name), + StringStruct("OriginalFilename", f"{name}.exe"), + StringStruct("ProductName", description), + StringStruct("ProductVersion", _ver), + ])]), + VarFileInfo([VarStruct("Translation", [0x0409, 1200])]), + ], + ) + + +_version_info = _version_resource("meshbay-node", "MeshBay Node") # Packages taken in full: data files + shared libs + every submodule. COLLECT_ALL = [ @@ -177,11 +185,96 @@ exe_windowless = EXE( icon="../../packages/meshbay-client/build/icon.ico", version=_version_info, ) + +# --- The command-line client --------------------------------------------------- +# Its own analysis: another entry point, and packages the daemon does not use +# (Textual loads its widgets lazily, Rich its Unicode tables by version, +# Pygments its lexers by name, keyring its backends through entry points — +# hence keyring's metadata). What both import (aiortc, av, cryptography, ...) +# lands once in _internal\: COLLECT keeps one copy of a file the two share. +CLI_COLLECT_ALL = [ + "meshbay_cli", + "meshbay_common", + "aiortc", + "aioice", + "av", + "cryptography", + "pylibsrtp", + "pyee", + "google_crc32c", + "httpx", + "httpcore", + "h11", + "anyio", + "dns", + "argon2", + "_argon2_cffi_bindings", + "keyring", + "win32ctypes", + "textual", + "rich", + "pygments", + "markdown_it", + "mdit_py_plugins", + "linkify_it", + "platformdirs", + "blake3", + "msgpack", + "jwt", + "tzdata", +] +CLI_COPY_META = ["meshbay-cli", "meshbay-common", "keyring", "textual"] + +cli_datas, cli_binaries, cli_hidden = [], [], [] +for pkg in CLI_COLLECT_ALL: + d, b, h = collect_all(pkg) + cli_datas += d + cli_binaries += b + cli_hidden += h +for dist in CLI_COPY_META: + cli_datas += copy_metadata(dist) + +cli_a = Analysis( + ["cli-entry.py"], + pathex=[], + binaries=cli_binaries, + datas=cli_datas, + hiddenimports=cli_hidden, + hookspath=[], + hooksconfig={}, + runtime_hooks=[], + excludes=["tkinter", "pytest", "ruff", "IPython", "meshbay_node", "meshbay_hub"], + noarchive=False, +) +cli_pyz = PYZ(cli_a.pure) +exe_cli = EXE( + cli_pyz, + cli_a.scripts, + [], + exclude_binaries=True, + name="meshbay-cli", + debug=False, + bootloader_ignore_signals=False, + strip=False, + upx=False, + console=True, + disable_windowed_traceback=False, + argv_emulation=False, + target_arch=None, + codesign_identity=None, + entitlements_file=None, + icon="../../packages/meshbay-client/build/icon.ico", + version=_version_resource("meshbay-cli", "MeshBay command-line client"), +) + coll = COLLECT( exe, exe_windowless, + exe_cli, a.binaries, a.datas, + cli_a.binaries, + cli_a.datas, strip=False, upx=False, upx_exclude=[], |