1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
|
<#
.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 NOT bundled by default. Pass -FfmpegDir (or set
MESHBAY_FFMPEG_DIR) to copy them in beside the daemon; otherwise the node
resolves them from PATH at startup (meshbay_node.platform.check_media_tools)
and streaming needs ffmpeg installed separately.
.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 FfmpegDir
Directory containing ffmpeg.exe and ffprobe.exe to bundle. Optional.
.PARAMETER KeepBuildVenv
Do not delete the throwaway build venv on success (faster re-runs).
#>
[CmdletBinding()]
param(
[string]$Python = "",
[string]$FfmpegDir = $env:MESHBAY_FFMPEG_DIR,
[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. optional ffmpeg ----------------------------------------------
if ($FfmpegDir) {
foreach ($tool in @("ffmpeg.exe", "ffprobe.exe")) {
$src = Join-Path $FfmpegDir $tool
if (-not (Test-Path $src)) { throw "$tool not found in $FfmpegDir" }
Copy-Item $src (Join-Path $frozen $tool)
Step "bundled $tool"
}
}
else {
Write-Host " ffmpeg not bundled -- the node will look for it on PATH" -ForegroundColor Yellow
}
# --- 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 <config>\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.
Step "smoke test: meshbay-node --help"
$exe = Join-Path $OutDir "meshbay-node.exe"
$help = & $exe --help 2>&1
if ($LASTEXITCODE -ne 0) { throw "frozen meshbay-node --help exited $LASTEXITCODE" }
if ($help -notmatch "meshbay-node") { throw "frozen --help output looks wrong" }
$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
|