Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 4 additions & 24 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,6 @@ jobs:
build:
runs-on: windows-latest

env:
# Nuitka stores the downloaded MinGW toolchain and its ccache results
# here; caching this dir is the single biggest CI speedup. Forward
# slashes are fine on Windows for Nuitka.
NUITKA_CACHE_DIR: ${{ github.workspace }}/nuitka-cache

steps:
- uses: actions/checkout@v4

Expand All @@ -33,11 +27,10 @@ jobs:
run: uv python install 3.12

- name: Install dependencies
# Installs the dev group too (PyInstaller), which build.py imports.
run: uv sync --all-extras

# Caches the native CMake build trees so unchanged C isn't recompiled.
# Marginal next to Nuitka; drop this step if it ever causes a stale
# "compiler changed" CMake error (just bump/clear the key).
- name: Cache native build trees
uses: actions/cache@v4
with:
Expand All @@ -57,33 +50,20 @@ jobs:
MSBUILDDISABLENODEREUSE: "1"
run: ./build_native.ps1

# ccache + the downloaded MinGW toolchain live under NUITKA_CACHE_DIR.
# The SHA in the key makes every run save a fresh cache; restore-keys
# fall back to the most recent one so ccache stays warm even when
# dependencies change.
- name: Cache Nuitka build cache
uses: actions/cache@v4
with:
path: ${{ env.NUITKA_CACHE_DIR }}
key: nuitka-${{ runner.os }}-${{ hashFiles('uv.lock') }}-${{ github.sha }}
restore-keys: |
nuitka-${{ runner.os }}-${{ hashFiles('uv.lock') }}-
nuitka-${{ runner.os }}-

- name: Build with Nuitka
- name: Build with PyInstaller
run: uv run python build.py

- name: Upload build artifact
uses: actions/upload-artifact@v4
with:
name: PubStreamer-windows
path: dist/main.dist/
path: dist/PubStreamer/
if-no-files-found: error

# Create a GitHub Release and attach the build when a version tag is pushed.
- name: Zip release folder
if: startsWith(github.ref, 'refs/tags/')
run: Compress-Archive -Path dist/main.dist/* -DestinationPath dist/PubStreamer-${{ github.ref_name }}-windows.zip
run: Compress-Archive -Path dist/PubStreamer/* -DestinationPath dist/PubStreamer-${{ github.ref_name }}-windows.zip

- name: Create release
if: startsWith(github.ref, 'refs/tags/')
Expand Down
1 change: 0 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,6 @@ native/**/Makefile
/test_*.py
/*.wav
C*Temptest*.txt
nuitka-crash-report.xml
hook_audio.obj
*.obj

Expand Down
10 changes: 7 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,15 +25,19 @@ Releases are built on GitHub Actions. Push a `v*` tag to trigger a build and cre
git tag v1.0.0 && git push origin v1.0.0
```

The workflow uses Nuitka with MinGW on `windows-latest` and produces a standalone `PubStreamer.dist/` folder. MSVC is not used because certain generated C files (pyasn1, requests) exceed its internal heap limit on constrained machines.
The workflow uses PyInstaller on `windows-latest` and produces a one-dir standalone under `dist/PubStreamer/`, with the launcher at `dist/PubStreamer/PubStreamer.exe`.

To build locally, install MinGW and run:
To build locally:

```
uv run python build.py
```

Set `GITHUB_ACTIONS=true` in the environment or edit `build.py` to pass `--mingw64` explicitly.
Build the native components first (see below) so the audio hook DLLs and `injector32.exe` get bundled. For a build with a console attached (so tracebacks are visible while debugging), pass `--console`:

```
uv run python build.py --console
```

## Audio sources

Expand Down
123 changes: 51 additions & 72 deletions build.py
Original file line number Diff line number Diff line change
@@ -1,90 +1,69 @@
"""Build Pub-Streamer into a standalone Windows executable using Nuitka.
"""Build Pub-Streamer into a standalone Windows app using PyInstaller.

Usage (from project root):
uv run python build.py
uv run python build.py # windowed release build (no console)
uv run python build.py --console # debug build with a console for tracebacks

On CI the GITHUB_ACTIONS env var is set automatically, which switches the
compiler to MinGW (GCC) — it handles the large generated C files that cause
MSVC to run out of heap on constrained machines.
Produces a one-dir standalone under dist/PubStreamer/ with the launcher at
dist/PubStreamer/PubStreamer.exe.

Note: build the native components first (see build_native.ps1) so the audio
hook DLLs and injector32.exe get bundled — LegacyCapture needs them.
"""

import os
import subprocess
import sys
import pathlib
import sys

import PyInstaller.__main__

ROOT = pathlib.Path(__file__).parent
DIST = ROOT / "dist"
LOCALE = ROOT / "locale"
SOUNDS = ROOT / "sounds"
WORK = ROOT / "build"

ON_CI = os.environ.get("GITHUB_ACTIONS") == "true"

# Data directories to bundle as-is
data_dirs = []
if LOCALE.exists():
data_dirs.append(f"--include-data-dir={LOCALE}=locale")
if SOUNDS.exists():
data_dirs.append(f"--include-data-dir={SOUNDS}=sounds")
native_dir = ROOT / "native"
if native_dir.exists():
data_dirs.append(f"--include-data-dir={native_dir}=native")
plugins_dir = ROOT / "plugins"
if plugins_dir.exists():
data_dirs.append(f"--include-data-dir={plugins_dir}=plugins")
CONSOLE = "--console" in sys.argv

icon = ROOT / "pubstreamer" / "ui" / "icon.ico"

cmd = [
sys.executable, "-m", "nuitka",
"--standalone",
"--windows-console-mode=disable",
"--assume-yes-for-downloads",

# MinGW on CI — avoids MSVC heap exhaustion on large generated C files.
# Remove this line to force MSVC locally if you have enough RAM.
"--mingw64" if ON_CI else "",

f"--windows-icon-from-ico={icon}" if icon.exists() else "",

# Compile everything — no frozen-bytecode shortcuts.
"--follow-import-to=pubstreamer",
"--include-package=wx",
"--include-package=numpy",
"--include-package=pedalboard",
"--include-package=win32com",
"--include-package=win32api",
"--include-package=win32con",
"--include-package=pywintypes",
"--include-package=comtypes",
"--include-package=httpx",
"--include-package=httpcore",
"--include-package=certifi",
"--include-package=pyaudiowpatch",
"--include-package=edge_tts",
"--include-package=gtts",
"--include-package=google",
"--include-package=grpc",
"--include-package=pyasn1",
"--include-package=pyasn1_modules",
"--include-package=piper",
"--include-package=onnxruntime",
"--include-package=cffi",
"--include-package=aiohttp",

f"--output-dir={DIST}",
"--output-filename=PubStreamer",

*data_dirs,

args = [
"main.py",
"--name=PubStreamer",
"--noconfirm",
"--onedir",
"--console" if CONSOLE else "--windowed",
f"--distpath={DIST}",
f"--workpath={WORK}",
f"--specpath={WORK}",
]

cmd = [c for c in cmd if c]

print("Running Nuitka...")
print(" ".join(cmd))
if icon.exists():
args.append(f"--icon={icon}")

# Data directories bundled as-is (src<pathsep>dest).
for d in ("locale", "sounds", "plugins"):
p = ROOT / d
if p.exists():
args.append(f"--add-data={p}{os.pathsep}{d}")

# Native binaries — land at native/dist/ where capture_legacy.py looks for them.
# PyInstaller's --add-data copies these .dll/.exe files as-is.
native_dist = ROOT / "native" / "dist"
for binary in ("audio_hook32.dll", "audio_hook64.dll", "injector32.exe"):
src = native_dist / binary
if src.exists():
args.append(f"--add-data={src}{os.pathsep}native/dist")

# Packages that ship data/binaries/submodules PyInstaller's hooks won't
# fully discover on their own.
for pkg in ("onnxruntime", "grpc", "piper", "pedalboard", "edge_tts", "gtts",
"google", "comtypes", "pyaudiowpatch", "aiohttp", "certifi",
"pyasn1", "pyasn1_modules", "httpx", "httpcore"):
args.append(f"--collect-all={pkg}")

for m in ("win32com", "win32api", "win32con", "pywintypes"):
args.append(f"--hidden-import={m}")

print("Running PyInstaller...")
print("\n".join(args))
print()

result = subprocess.run(cmd, cwd=ROOT)
sys.exit(result.returncode)
PyInstaller.__main__.run(args)
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,5 +23,5 @@ package = false

[dependency-groups]
dev = [
"nuitka>=4.1.1",
"pyinstaller>=6.10",
]
Loading