Skip to content

Commit b097cd5

Browse files
committed
tools: add Windows PyInstaller build script for data_forwarder_host
Jira: NCSDK-39917 Signed-off-by: Bartosz Meus <bartosz.meus@nordicsemi.no>
1 parent cf58ded commit b097cd5

4 files changed

Lines changed: 125 additions & 25 deletions

File tree

tools/data_forwarder_host/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,3 +15,4 @@ __pycache__/
1515
# matches this build SCRIPT by name. It is a tracked source deliverable, so
1616
# re-include it explicitly.
1717
!scripts/build_linux_binary.sh
18+
!scripts/build_windows_binary.ps1

tools/data_forwarder_host/README.md

Lines changed: 17 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -74,30 +74,23 @@ Set `DFH_SINGLE_PROCESS=1` to fall back to the legacy in-GUI acquisition path.
7474
## Standalone single-file binary (no install on the target)
7575

7676
To run the application on a machine that has **no Python and no dependencies
77-
installed**, build a single self-contained executable with PyInstaller:
78-
79-
```bash
80-
./scripts/build_linux_binary.sh
81-
```
82-
83-
This produces one file, `dist/data-forwarder-host`, that bundles the Python
84-
interpreter, Qt6 and every dependency. Copy that single file to any reasonably
85-
recent x86_64 Ubuntu machine and run it directly — nothing has to be installed
86-
there:
87-
88-
```bash
89-
./data-forwarder-host
90-
```
91-
92-
Notes:
93-
94-
- Build on the **oldest** Ubuntu you intend to support: glibc is forward- but
95-
not backward-compatible, so a binary built on 22.04 runs on 22.04+, while one
96-
built on 24.04 may not run on 22.04.
97-
- The build is driven by `data_forwarder_host.spec`; `scripts/build_linux_binary.sh`
98-
just provisions an isolated build virtualenv (kept out of git) and invokes
99-
PyInstaller against that spec.
100-
- The binary is large (~100 MB) because the whole Qt6 stack is embedded.
77+
installed**, build a single self-contained executable with PyInstaller. Each
78+
platform script creates an isolated `.build-venv` (gitignored) from a standard
79+
system CPython — not nrfutil's toolchain Python — and runs PyInstaller from
80+
that environment against `data_forwarder_host.spec`. The result bundles the
81+
Python interpreter, Qt6 and every dependency into one file (~100 MB).
82+
83+
Build on the **oldest** OS version you intend to support. On Linux, glibc is
84+
forward-compatible only — a binary built on Ubuntu 22.04 runs on 22.04+, but one
85+
built on 24.04 may not run on 22.04.
86+
87+
| Platform | Build command | Output |
88+
|----------|---------------|--------|
89+
| Linux | `./scripts/build_linux_binary.sh` | `dist/data-forwarder-host` |
90+
| Windows | `powershell -ExecutionPolicy Bypass -File .\scripts\build_windows_binary.ps1` | `dist\data-forwarder-host.exe` |
91+
92+
Copy the output file to a target machine and run it directly — nothing has to be
93+
installed there.
10194

10295
## First-time use
10396

tools/data_forwarder_host/scripts/build_linux_binary.sh

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -65,9 +65,12 @@ python -m pip install --upgrade pip wheel >/dev/null
6565
python -m pip install "." "pyinstaller>=6.6"
6666

6767
# 3. Clean previous artefacts and build from the spec.
68+
# -I (isolated): the package root contains a `platform/` subpackage; without
69+
# isolation, CWD on sys.path shadows stdlib `platform` and PyInstaller dies
70+
# with "module 'platform' has no attribute 'system'" / missing `win32_ver`.
6871
echo ">> Building single-file binary"
6972
rm -rf build dist
70-
pyinstaller --noconfirm --clean data_forwarder_host.spec
73+
python -I -m PyInstaller --noconfirm --clean data_forwarder_host.spec
7174

7275
BIN="$PKG_DIR/dist/data-forwarder-host"
7376
if [ -x "$BIN" ]; then
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Copyright (c) 2026 Nordic Semiconductor ASA
2+
# SPDX-License-Identifier: LicenseRef-Nordic-5-Clause
3+
#
4+
# Build a single self-contained Windows executable for the Data Forwarder Host.
5+
#
6+
# The result is ONE file, dist\data-forwarder-host.exe, that bundles the Python
7+
# interpreter and every dependency (PySide6/Qt6, numpy, bleak, ...). Copy it to
8+
# another Windows machine and run it directly - nothing has to be installed there.
9+
#
10+
# Usage (from the package directory or anywhere):
11+
# powershell -ExecutionPolicy Bypass -File .\scripts\build_windows_binary.ps1
12+
#
13+
# Notes:
14+
# * Build on the OLDEST Windows version you intend to support (e.g. Windows 10
15+
# or 11). Test on a clean VM before distributing broadly.
16+
# * Requires a standard Windows CPython 3.12+ on PATH (python.org or winget).
17+
# Do not use the nrfutil/NCS toolchain Python - use $env:PYTHON_BIN to point
18+
# at a normal install, e.g. py -3.12
19+
# * Internet access for the one-time dependency download.
20+
# * Unsigned executables may trigger SmartScreen on first run; code-sign for
21+
# external distribution.
22+
# * BLE on Windows is implemented but not yet validated in project docs - test
23+
# UART and BLE after building.
24+
25+
#Requires -Version 5.1
26+
$ErrorActionPreference = "Stop"
27+
28+
$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Path
29+
$PkgDir = Split-Path -Parent $ScriptDir
30+
Set-Location $PkgDir
31+
32+
$PythonBin = if ($env:PYTHON_BIN) { $env:PYTHON_BIN } else { "python" }
33+
$BuildVenv = if ($env:BUILD_VENV) { $env:BUILD_VENV } else { Join-Path $PkgDir ".build-venv" }
34+
$VenvPython = Join-Path $BuildVenv "Scripts\python.exe"
35+
36+
Write-Host ">> Using interpreter: $(& $PythonBin --version)"
37+
$InterpreterPath = & $PythonBin -c "import sys; print(sys.executable)"
38+
Write-Host ">> Interpreter path: $InterpreterPath"
39+
$Platform = & $PythonBin -c "import sys; print(sys.platform)"
40+
if ($Platform -ne "win32") {
41+
Write-Error "!! Expected Windows CPython (sys.platform=win32), got '$Platform'. Use a standard python.org install."
42+
}
43+
44+
function Test-VenvUsable {
45+
if (-not (Test-Path -LiteralPath $VenvPython)) {
46+
return $false
47+
}
48+
& $VenvPython -m pip --version *> $null
49+
return $LASTEXITCODE -eq 0
50+
}
51+
52+
# 1. Isolated build environment (kept out of git via .gitignore).
53+
# Reuse only if python -m pip works; stale/copied venvs get recreated.
54+
if (-not (Test-VenvUsable)) {
55+
if (Test-Path -LiteralPath $BuildVenv) {
56+
Write-Host ">> Existing build venv is unusable (stale/foreign) - recreating"
57+
Remove-Item -LiteralPath $BuildVenv -Recurse -Force
58+
}
59+
Write-Host ">> Creating build venv at $BuildVenv"
60+
& $PythonBin -m venv $BuildVenv
61+
if ($LASTEXITCODE -ne 0) {
62+
Write-Error "!! Failed to create venv. Install Python 3.12+ with the venv module."
63+
}
64+
}
65+
66+
# 2. Install the app's runtime deps (from pyproject.toml) + PyInstaller.
67+
Write-Host ">> Installing runtime dependencies and PyInstaller"
68+
& $VenvPython -m pip install --upgrade pip wheel *> $null
69+
if ($LASTEXITCODE -ne 0) {
70+
Write-Error "!! pip upgrade failed."
71+
}
72+
# pywin32-ctypes is required by PyInstaller on Windows (imported from compat.py).
73+
& $VenvPython -m pip install "." "pyinstaller>=6.6" "pywin32-ctypes>=0.2"
74+
if ($LASTEXITCODE -ne 0) {
75+
Write-Error "!! Dependency install failed."
76+
}
77+
78+
# 3. Clean previous artefacts and build from the spec.
79+
# -I (isolated): the package root contains a `platform/` subpackage; without
80+
# isolation, CWD on sys.path shadows stdlib `platform` and PyInstaller dies
81+
# with "module 'platform' has no attribute 'win32_ver'".
82+
Write-Host ">> Building single-file binary"
83+
foreach ($dir in @("build", "dist")) {
84+
if (Test-Path -LiteralPath $dir) {
85+
Remove-Item -LiteralPath $dir -Recurse -Force
86+
}
87+
}
88+
& $VenvPython -I -m PyInstaller --noconfirm --clean data_forwarder_host.spec
89+
if ($LASTEXITCODE -ne 0) {
90+
Write-Error "!! PyInstaller build failed."
91+
}
92+
93+
$Bin = Join-Path $PkgDir "dist\data-forwarder-host.exe"
94+
if (Test-Path -LiteralPath $Bin) {
95+
$sizeMb = [math]::Round((Get-Item -LiteralPath $Bin).Length / 1MB, 1)
96+
Write-Host ""
97+
Write-Host ">> Done. Single-file binary:"
98+
Write-Host " $Bin"
99+
Write-Host " size: ${sizeMb} MB"
100+
Write-Host ">> Run it with: $Bin"
101+
} else {
102+
Write-Error "!! Build finished but $Bin was not produced."
103+
}

0 commit comments

Comments
 (0)