aboutsummaryrefslogtreecommitdiffstats
path: root/packaging/win/build-node-runtime.ps1
blob: 42d1faa5c3b260c31c81660392b3ad54d3e600ae (plain) (blame)
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
187
188
189
190
191
192
193
194
195
<#
.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"
}

# --- 3b. licences ----------------------------------------------------
# The frozen tree is a distribution of every package in it, so each one's
# licence goes with it: MeshBay's own (AGPL for the node, LGPL for the common
# library it embeds) and THIRD-PARTY-NOTICES.txt, generated from the build
# venv's own metadata -- PyAV's wheel, for one, grafts in a GPL FFmpeg build
# (libx264, libx265) that its BSD licence does not mention. PyInstaller's
# bootloader and the interpreter ship too, without being a requirement.
Step "writing licence notices"
Copy-Item (Join-Path $Repo "LICENSE") (Join-Path $frozen "LICENSE.txt")
Copy-Item (Join-Path $Repo "packages\meshbay-common\COPYING.LESSER") (Join-Path $frozen "LICENSE-meshbay-common.txt")
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
if ($LASTEXITCODE -ne 0) { throw "third_party_notices.py failed" }

# --- 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) ------------------------------
# Copied as is from QE\default.env (never versioned), one line:
# MESHBAY_TMDB_DEFAULT_TOKEN=eyJ... Beside the exe, where
# platform.packaged_default_env() looks for it. The daemon reads it beneath
# %LOCALAPPDATA%\meshbay\node.env and the operator's own token, so it is only
# the fallback. No token, no build: an empty one goes unnoticed. The check is
# on the raw bytes, so a BOM (which would break the daemon's parsing) fails too.
$defaultEnv = Join-Path $Repo "QE\default.env"
$text = if (Test-Path -LiteralPath $defaultEnv) {
    [System.Text.Encoding]::ASCII.GetString([System.IO.File]::ReadAllBytes($defaultEnv))
} else { "" }
if (-not $text.StartsWith("MESHBAY_TMDB_DEFAULT_TOKEN=eyJ")) {
    throw "$defaultEnv missing, or not starting with MESHBAY_TMDB_DEFAULT_TOKEN=eyJ... (no BOM)"
}
Copy-Item -LiteralPath $defaultEnv (Join-Path $frozen "default.env")
Step "default.env copied from QE\"

# --- 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" }

# --help imports the parser and nothing else; start the daemon itself.
Step "smoke test: the frozen daemon starts and answers"
$initPy = Join-Path $Repo "packages\meshbay-node\src\meshbay_node\__init__.py"
$expect = [regex]::Match((Get-Content -LiteralPath $initPy -Raw), '__version__\s*=\s*"([^"]+)"').Groups[1].Value
if (-not $expect) { throw "cannot read __version__ from $initPy" }
& (Join-Path $WinDir "smoke-node-runtime.ps1") -Exe $exe -ExpectVersion $expect

$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