<# .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 bundled by DEFAULT (fetch-ffmpeg.ps1) -- a real, silent Windows install cannot ask an end user to separately run `winget install ffmpeg`, and video streaming needs a genuine H.264 encoder (libx264, GPL; no LGPL-only build has one), so there is no smaller "it'll resolve from PATH" fallback worth defaulting to. Pass -SkipFfmpeg for a smaller, streaming-less build for local iteration. .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 SkipFfmpeg Skip bundling ffmpeg. The node then resolves it from PATH at startup (meshbay_node.platform.check_media_tools), and streaming needs ffmpeg installed separately -- fine for a quick local iteration, not for a build anyone else will install. .PARAMETER KeepBuildVenv Do not delete the throwaway build venv on success (faster re-runs). #> [CmdletBinding()] param( [string]$Python = "", [switch]$SkipFfmpeg, [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. ffmpeg (bundled by default) ----------------------------------- if ($SkipFfmpeg -or $env:MESHBAY_SKIP_FFMPEG -eq "1") { Write-Host " !! ffmpeg not bundled (-SkipFfmpeg) -- the node will look for it on PATH, and streaming needs it installed separately" -ForegroundColor Yellow } else { Step "fetching ffmpeg (verified against a pinned checksum; cached after the first build)" & (Join-Path $WinDir "fetch-ffmpeg.ps1") -OutDir $frozen } # --- 5. default.env (shared TMDB token) ------------------------------ # Beside the exe, where platform.packaged_default_env() looks for it, and the # same placement ffmpeg gets above. `meshbay-node init` copies it to # %LOCALAPPDATA%\meshbay\node.env, and the daemon loads that file itself: # Windows autostart is a Startup-folder .vbs, with no systemd EnvironmentFile. function Get-TmdbToken([string]$File) { if (-not $File -or -not (Test-Path -LiteralPath $File)) { return "" } $lines = Get-Content -LiteralPath $File foreach ($line in $lines) { if ($line -match '^\s*MESHBAY_TMDB_DEFAULT_TOKEN\s*=\s*(.+)$') { return $Matches[1].Trim().Trim('"').Trim("'") } } # QE\tmdb.txt is free-form prose: match the v4 read token by shape. The # 32-char v3 API key in the same file is NOT what tmdb.py sends (Bearer). foreach ($line in $lines) { if ($line -match '^(eyJ[A-Za-z0-9._-]{40,})\s*$') { return $Matches[1] } } return "" } $tmdb = $env:MESHBAY_TMDB_TOKEN if (-not $tmdb) { $tmdb = Get-TmdbToken $env:MESHBAY_TMDB_TOKEN_FILE } if (-not $tmdb) { $tmdb = Get-TmdbToken (Join-Path $Repo "QE\node.env") } if (-not $tmdb) { $tmdb = Get-TmdbToken (Join-Path $Repo "QE\tmdb.txt") } $envFile = Join-Path $frozen "default.env" $noBom = New-Object System.Text.UTF8Encoding $false # a BOM would break parsing if ($tmdb) { $body = @( "# Default environment for meshbay-node.", "# Copied to \node.env by 'meshbay-node init' if it does not exist.", "", "# TMDB API token for the Videos app (read-only, shared across installations)", "MESHBAY_TMDB_DEFAULT_TOKEN=$tmdb" ) -join "`n" [System.IO.File]::WriteAllText($envFile, $body + "`n", $noBom) Step ("TMDB token baked into default.env ({0} chars)" -f $tmdb.Length) } elseif ($env:MESHBAY_ALLOW_NO_TMDB -eq "1") { [System.IO.File]::WriteAllText($envFile, "", $noBom) Write-Host " !! no TMDB token; default.env left empty (MESHBAY_ALLOW_NO_TMDB=1)" -ForegroundColor Yellow } else { throw ("TMDB token not found (MESHBAY_TMDB_TOKEN, MESHBAY_TMDB_TOKEN_FILE, " + "QE\node.env, QE\tmdb.txt). Set MESHBAY_ALLOW_NO_TMDB=1 to build without it.") } # --- 6. 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 } # --- 7. smoke test ----------------------------------------------- # Capture, do NOT pipe to Select-Object -First: that stops the native process # mid-write and reports a spurious non-zero exit. `& exe ... 2>&1` returns an # ARRAY of lines once the output wraps past one line (which --help's now does, # with autostart/service added) -- `$array -notmatch X` is a FILTER, not a # boolean test: it returns the *non-matching* lines, and any non-empty array # is truthy in `if()` regardless of what is in it. Almost every help line # lacks the literal string "meshbay-node", so this threw unconditionally the # moment --help grew past one line, having never actually been exercised # against multi-line output before. Join to one string first, so this is a # real substring test again. One retry after a short pause too: right after # extracting ~180 MB of freshly-written DLLs (ffmpeg) an antivirus real-time # scan can transiently slow or interfere with the very next process launch -- # print the actual captured output on a genuine failure instead of a bare # assertion, so it is diagnosable from the log rather than needing a re-run. Step "smoke test: meshbay-node --help" $exe = Join-Path $OutDir "meshbay-node.exe" $help = (& $exe --help 2>&1) -join "`n" if ($LASTEXITCODE -ne 0 -or $help -notmatch "meshbay-node") { Start-Sleep -Seconds 3 $help = (& $exe --help 2>&1) -join "`n" } if ($LASTEXITCODE -ne 0) { throw "frozen meshbay-node --help exited $LASTEXITCODE`n$help" } if ($help -notmatch "meshbay-node") { throw "frozen --help output looks wrong:`n$help" } $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