mirror of
https://github.com/onyx-and-iris/nvda-voicemeeter.git
synced 2026-03-12 05:49:15 +00:00
Compare commits
12 Commits
d850581179
...
dfb96777bb
| Author | SHA1 | Date | |
|---|---|---|---|
| dfb96777bb | |||
| 5aa2af2acf | |||
| cc6e187998 | |||
| 1ea1c59f06 | |||
| 054ad040bb | |||
| 94f0b847a7 | |||
| 8d251d1dea | |||
| 5f7d66ceae | |||
| 4ed17a5476 | |||
| b88955a45a | |||
| c3247fa5bf | |||
| 39c14279b2 |
237
.github/workflows/release.yml
vendored
Normal file
237
.github/workflows/release.yml
vendored
Normal file
@ -0,0 +1,237 @@
|
||||
name: Build and Release
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 'v*.*.*'
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: windows-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install PDM
|
||||
uses: pdm-project/setup-pdm@v4
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
- name: Install Task
|
||||
uses: go-task/setup-task@v1
|
||||
with:
|
||||
version: 3.x
|
||||
|
||||
- name: Download NVDA Controller Client
|
||||
shell: pwsh
|
||||
run: |
|
||||
Write-Host "Downloading NVDA Controller Client..."
|
||||
$url = "https://download.nvaccess.org/releases/stable/nvda_2025.3.3_controllerClient.zip"
|
||||
$zipPath = "nvda_controllerClient.zip"
|
||||
|
||||
# Download the zip file
|
||||
Invoke-WebRequest -Uri $url -OutFile $zipPath
|
||||
Write-Host "Downloaded $zipPath"
|
||||
|
||||
# Extract to temp directory
|
||||
$tempDir = "temp_controller"
|
||||
Expand-Archive -Path $zipPath -DestinationPath $tempDir -Force
|
||||
|
||||
# Find and copy DLL files to correct locations
|
||||
Write-Host "Extracting DLL files..."
|
||||
|
||||
# Create directories if they don't exist
|
||||
New-Item -ItemType Directory -Path "controllerClient/x64" -Force | Out-Null
|
||||
New-Item -ItemType Directory -Path "controllerClient/x86" -Force | Out-Null
|
||||
|
||||
# Find and copy the DLL files
|
||||
$dllFiles = Get-ChildItem -Path $tempDir -Recurse -Name "*.dll" | Where-Object { $_ -like "*controllerClient*" }
|
||||
|
||||
foreach ($dll in $dllFiles) {
|
||||
$fullPath = Join-Path $tempDir $dll
|
||||
$dirName = (Get-Item $fullPath).Directory.Name
|
||||
|
||||
if ($dll -match "x64" -or $dirName -match "x64") {
|
||||
Copy-Item $fullPath "controllerClient/x64/nvdaControllerClient.dll"
|
||||
Write-Host "Copied x64 DLL: $dll"
|
||||
} elseif ($dll -match "x86" -or $dirName -match "x86") {
|
||||
Copy-Item $fullPath "controllerClient/x86/nvdaControllerClient.dll"
|
||||
Write-Host "Copied x86 DLL: $dll"
|
||||
} elseif ($dll -match "arm64" -or $dirName -match "arm64") {
|
||||
Write-Host "Skipping ARM64 DLL: $dll (not needed)"
|
||||
} else {
|
||||
Write-Host "Skipping unknown architecture DLL: $dll"
|
||||
}
|
||||
}
|
||||
|
||||
# Clean up
|
||||
Remove-Item $zipPath -Force
|
||||
Remove-Item $tempDir -Recurse -Force
|
||||
|
||||
# Verify files were copied
|
||||
Write-Host "Verifying controller client files..."
|
||||
if (Test-Path "controllerClient/x64/nvdaControllerClient.dll") {
|
||||
Write-Host "[OK] x64 controller client found"
|
||||
} else {
|
||||
Write-Host "[ERROR] x64 controller client missing"
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (Test-Path "controllerClient/x86/nvdaControllerClient.dll") {
|
||||
Write-Host "[OK] x86 controller client found"
|
||||
} else {
|
||||
Write-Host "[ERROR] x86 controller client missing"
|
||||
exit 1
|
||||
}
|
||||
|
||||
- name: Fix dependencies for CI
|
||||
shell: pwsh
|
||||
run: |
|
||||
echo "Fixing local dependencies for CI build..."
|
||||
|
||||
# Remove local path dependency for voicemeeter-api
|
||||
pdm remove -dG dev voicemeeter-api || true
|
||||
|
||||
echo "Updated dependencies for CI build"
|
||||
|
||||
- name: Install dependencies
|
||||
shell: pwsh
|
||||
run: |
|
||||
# Install project dependencies
|
||||
pdm install
|
||||
|
||||
# Verify PyInstaller is available
|
||||
Write-Host "Verifying PyInstaller installation..."
|
||||
pdm list | Select-String pyinstaller
|
||||
|
||||
- name: Get PDM executable path
|
||||
shell: pwsh
|
||||
run: |
|
||||
$pdmPath = Get-Command pdm | Select-Object -ExpandProperty Source
|
||||
Write-Host "PDM path: $pdmPath"
|
||||
echo "PDM_BIN=$pdmPath" >> $env:GITHUB_ENV
|
||||
|
||||
- name: Build artifacts with dynamic taskfile
|
||||
shell: pwsh
|
||||
env:
|
||||
PDM_BIN: ${{ env.PDM_BIN }}
|
||||
run: |
|
||||
Write-Host "Building all executables using dynamic builder..."
|
||||
task --taskfile Taskfile.dynamic.yml build-all
|
||||
|
||||
- name: Compress build artifacts
|
||||
shell: pwsh
|
||||
env:
|
||||
PDM_BIN: ${{ env.PDM_BIN }}
|
||||
run: |
|
||||
Write-Host "Compressing build artifacts..."
|
||||
task --taskfile Taskfile.dynamic.yml compress-all
|
||||
|
||||
- name: Verify build outputs
|
||||
shell: pwsh
|
||||
run: |
|
||||
Write-Host "Verifying build outputs..."
|
||||
|
||||
$expectedFiles = @(
|
||||
"dist/basic.zip",
|
||||
"dist/banana.zip",
|
||||
"dist/potato.zip"
|
||||
)
|
||||
|
||||
$missingFiles = @()
|
||||
$foundFiles = @()
|
||||
|
||||
foreach ($file in $expectedFiles) {
|
||||
if (Test-Path $file) {
|
||||
$size = [math]::Round((Get-Item $file).Length / 1MB, 2)
|
||||
$foundFiles += "$file ($size MB)"
|
||||
} else {
|
||||
$missingFiles += $file
|
||||
}
|
||||
}
|
||||
|
||||
Write-Host "Found files:"
|
||||
$foundFiles | ForEach-Object { Write-Host " $_" }
|
||||
|
||||
if ($missingFiles.Count -gt 0) {
|
||||
Write-Host -ForegroundColor Red "Missing files:"
|
||||
$missingFiles | ForEach-Object { Write-Host " $_" }
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host -ForegroundColor Green "All expected files found!"
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: nvda-voicemeeter-builds
|
||||
path: |
|
||||
dist/basic.zip
|
||||
dist/banana.zip
|
||||
dist/potato.zip
|
||||
|
||||
- name: Build Summary
|
||||
shell: pwsh
|
||||
run: |
|
||||
Write-Host -ForegroundColor Green "Build completed successfully!"
|
||||
Write-Host ""
|
||||
Write-Host "Built artifacts:"
|
||||
Write-Host " - nvda-voicemeeter-basic.zip"
|
||||
Write-Host " - nvda-voicemeeter-banana.zip"
|
||||
Write-Host " - nvda-voicemeeter-potato.zip"
|
||||
|
||||
release:
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
needs: build
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Download all artifacts
|
||||
uses: actions/download-artifact@v4
|
||||
|
||||
- name: Create Release
|
||||
run: |
|
||||
TAG_NAME=${GITHUB_REF#refs/tags/}
|
||||
gh release create $TAG_NAME \
|
||||
--title "NVDA-Voicemeeter $TAG_NAME" \
|
||||
--notes "## NVDA-Voicemeeter Release $TAG_NAME
|
||||
|
||||
### Downloads
|
||||
- **nvda-voicemeeter-basic.zip** - Basic version with dependencies
|
||||
- **nvda-voicemeeter-banana.zip** - Banana version with dependencies
|
||||
- **nvda-voicemeeter-potato.zip** - Potato version with dependencies
|
||||
|
||||
### Requirements
|
||||
- Windows 10/11
|
||||
- Voicemeeter (Basic/Banana/Potato) installed
|
||||
- NVDA screen reader
|
||||
|
||||
### Installation
|
||||
1. Download the appropriate zip for your Voicemeeter version
|
||||
2. Extract and run the executable - no installation required
|
||||
3. The application will integrate with NVDA automatically
|
||||
|
||||
### Notes
|
||||
- Built with dynamic build system using PyInstaller
|
||||
- Includes NVDA Controller Client for screen reader integration"
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Upload release assets
|
||||
run: |
|
||||
TAG_NAME=${GITHUB_REF#refs/tags/}
|
||||
find . -name "*.zip" -exec gh release upload $TAG_NAME {} \;
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
19
.github/workflows/ruff.yml
vendored
Normal file
19
.github/workflows/ruff.yml
vendored
Normal file
@ -0,0 +1,19 @@
|
||||
name: Ruff
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
ruff:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: astral-sh/ruff-action@v3
|
||||
with:
|
||||
args: 'format --check --diff'
|
||||
76
Taskfile.dynamic.yml
Normal file
76
Taskfile.dynamic.yml
Normal file
@ -0,0 +1,76 @@
|
||||
version: '3'
|
||||
|
||||
# Dynamic build system - no spec files needed!
|
||||
# Examples:
|
||||
# - task -t Taskfile.dynamic.yml build KINDS="basic banana"
|
||||
# - task -t Taskfile.dynamic.yml build-all
|
||||
# KINDS can be a space-separated list of kinds to build, or "all" to build everything.
|
||||
#
|
||||
# Compression tasks are also dynamic, allowing you to specify which kind to compress or compress all kinds at once.
|
||||
# Examples:
|
||||
# - task -t Taskfile.dynamic.yml compress KIND=basic
|
||||
# - task -t Taskfile.dynamic.yml compress-all
|
||||
|
||||
vars:
|
||||
KINDS: '{{.KINDS | default "all"}}'
|
||||
SHELL: pwsh
|
||||
|
||||
tasks:
|
||||
build:
|
||||
desc: Build specified kinds dynamically (no spec files needed)
|
||||
preconditions:
|
||||
- sh: |
|
||||
if [ ! -f controllerClient/x64/nvdaControllerClient.dll ] || [ ! -f controllerClient/x86/nvdaControllerClient.dll ]; then
|
||||
exit 1
|
||||
fi
|
||||
msg: 'nvdaControllerClient.dll is missing. See https://github.com/nvaccess/nvda/blob/master/extras/controllerClient/readme.md for instructions on how to obtain it.'
|
||||
cmds:
|
||||
- ${PDM_BIN:-pdm} run python tools/dynamic_builder.py {{.KINDS}}
|
||||
|
||||
build-all:
|
||||
desc: Build all kinds
|
||||
preconditions:
|
||||
- sh: |
|
||||
if [ ! -f controllerClient/x64/nvdaControllerClient.dll ] || [ ! -f controllerClient/x86/nvdaControllerClient.dll ]; then
|
||||
exit 1
|
||||
fi
|
||||
msg: 'nvdaControllerClient.dll is missing. See https://github.com/nvaccess/nvda/blob/master/extras/controllerClient/readme.md for instructions on how to obtain it.'
|
||||
cmds:
|
||||
- ${PDM_BIN:-pdm} run python tools/dynamic_builder.py all
|
||||
|
||||
compress:
|
||||
desc: Compress artifacts for specified kind
|
||||
cmds:
|
||||
- task: compress-{{.KIND}}
|
||||
|
||||
compress-all:
|
||||
desc: Compress artifacts for all kinds
|
||||
cmds:
|
||||
- for:
|
||||
matrix:
|
||||
KIND: [basic, banana, potato]
|
||||
task: compress-{{.ITEM.KIND}}
|
||||
|
||||
compress-basic:
|
||||
desc: Compress basic build artifacts
|
||||
cmd: '{{.SHELL}} -Command "Compress-Archive -Path dist/basic -DestinationPath dist/basic.zip -Force"'
|
||||
generates:
|
||||
- dist/basic.zip
|
||||
|
||||
compress-banana:
|
||||
desc: Compress banana build artifacts
|
||||
cmd: '{{.SHELL}} -Command "Compress-Archive -Path dist/banana -DestinationPath dist/banana.zip -Force"'
|
||||
generates:
|
||||
- dist/banana.zip
|
||||
|
||||
compress-potato:
|
||||
desc: Compress potato build artifacts
|
||||
cmd: '{{.SHELL}} -Command "Compress-Archive -Path dist/potato -DestinationPath dist/potato.zip -Force"'
|
||||
generates:
|
||||
- dist/potato.zip
|
||||
|
||||
clean:
|
||||
desc: Clean all build artifacts
|
||||
cmds:
|
||||
- |
|
||||
{{.SHELL}} -Command "Remove-Item -Path build/*,dist/* -Recurse -Force -ErrorAction SilentlyContinue"
|
||||
@ -1,6 +1,6 @@
|
||||
[project]
|
||||
name = "nvda-voicemeeter"
|
||||
version = "1.0.0"
|
||||
version = "1.1.0"
|
||||
description = "A Voicemeeter app compatible with NVDA"
|
||||
authors = [{ name = "Onyx and Iris", email = "code@onyxandiris.online" }]
|
||||
dependencies = [
|
||||
|
||||
@ -438,13 +438,20 @@ class Builder:
|
||||
def make_tab3_button_row(self, i) -> psg.Frame:
|
||||
"""tab3 row represents bus composite toggle"""
|
||||
|
||||
def add_strip_outputs(layout):
|
||||
params = ['MONO', 'EQ', 'MUTE']
|
||||
def add_bus_buttons(layout):
|
||||
busmono = util.get_bus_mono()
|
||||
params = ['EQ', 'MUTE']
|
||||
if self.kind.name == 'basic':
|
||||
params.remove('EQ')
|
||||
busmodes = [util._bus_mode_map[mode] for mode in util.get_bus_modes(self.vm)]
|
||||
layout.append(
|
||||
[
|
||||
psg.ButtonMenu(
|
||||
'Mono',
|
||||
size=(6, 2),
|
||||
menu_def=['', busmono],
|
||||
key=f'BUS {i}||MONO',
|
||||
),
|
||||
*[
|
||||
psg.Button(
|
||||
param.capitalize(),
|
||||
@ -454,7 +461,7 @@ class Builder:
|
||||
for param in params
|
||||
],
|
||||
psg.ButtonMenu(
|
||||
'BUSMODE',
|
||||
'Bus Mode',
|
||||
size=(12, 2),
|
||||
menu_def=['', busmodes],
|
||||
key=f'BUS {i}||MODE',
|
||||
@ -463,7 +470,7 @@ class Builder:
|
||||
)
|
||||
|
||||
outputs = []
|
||||
[step(outputs) for step in (add_strip_outputs,)]
|
||||
[step(outputs) for step in (add_bus_buttons,)]
|
||||
return psg.Frame(
|
||||
self.window.cache['labels'][f'BUS {i}||LABEL'],
|
||||
outputs,
|
||||
|
||||
@ -155,6 +155,10 @@ def get_bus_modes(vm) -> list:
|
||||
]
|
||||
|
||||
|
||||
def get_bus_mono() -> list:
|
||||
return ['off', 'on', 'stereo reverse']
|
||||
|
||||
|
||||
def check_bounds(val, bounds: tuple) -> int | float:
|
||||
lower, upper = bounds
|
||||
if val > upper:
|
||||
|
||||
@ -58,6 +58,7 @@ class NVDAVMWindow(psg.Window):
|
||||
self[f'STRIP {i}||SLIDER LIMIT'].Widget.config(**slider_opts)
|
||||
for i in range(self.kind.num_bus):
|
||||
self[f'BUS {i}||SLIDER GAIN'].Widget.config(**slider_opts)
|
||||
self[f'BUS {i}||MONO'].Widget.config(**buttonmenu_opts)
|
||||
self[f'BUS {i}||MODE'].Widget.config(**buttonmenu_opts)
|
||||
|
||||
self.register_events()
|
||||
@ -251,13 +252,16 @@ class NVDAVMWindow(psg.Window):
|
||||
self[f'STRIP {i}||SLIDER {param}'].bind('<Control-Shift-KeyPress-R>', '||KEY CTRL SHIFT R')
|
||||
|
||||
# Bus Params
|
||||
params = ['MONO', 'EQ', 'MUTE']
|
||||
params = ['EQ', 'MUTE']
|
||||
if self.kind.name == 'basic':
|
||||
params.remove('EQ')
|
||||
for i in range(self.kind.num_bus):
|
||||
for param in params:
|
||||
self[f'BUS {i}||{param}'].bind('<FocusIn>', '||FOCUS IN')
|
||||
self[f'BUS {i}||{param}'].bind('<Return>', '||KEY ENTER')
|
||||
self[f'BUS {i}||MONO'].bind('<FocusIn>', '||FOCUS IN')
|
||||
self[f'BUS {i}||MONO'].bind('<space>', '||KEY SPACE', propagate=False)
|
||||
self[f'BUS {i}||MONO'].bind('<Return>', '||KEY ENTER')
|
||||
self[f'BUS {i}||MODE'].bind('<FocusIn>', '||FOCUS IN')
|
||||
self[f'BUS {i}||MODE'].bind('<space>', '||KEY SPACE', propagate=False)
|
||||
self[f'BUS {i}||MODE'].bind('<Return>', '||KEY ENTER', propagate=False)
|
||||
@ -306,7 +310,7 @@ class NVDAVMWindow(psg.Window):
|
||||
mode = None
|
||||
continue
|
||||
|
||||
match parsed_cmd := self.parser.match.parseString(event):
|
||||
match parsed_cmd := self.parser.match.parse_string(event):
|
||||
# Slider mode
|
||||
case [['ALT', 'LEFT' | 'RIGHT' | 'UP' | 'DOWN' as direction], ['PRESS' | 'RELEASE' as e]]:
|
||||
if mode:
|
||||
@ -972,7 +976,7 @@ class NVDAVMWindow(psg.Window):
|
||||
self.nvda.speak,
|
||||
'on' if val else 'off',
|
||||
)
|
||||
case 'MONO' | 'MUTE':
|
||||
case 'MUTE':
|
||||
val = not val
|
||||
setattr(self.vm.bus[int(index)], param.lower(), val)
|
||||
self.cache['bus'][event] = val
|
||||
@ -981,6 +985,15 @@ class NVDAVMWindow(psg.Window):
|
||||
self.nvda.speak,
|
||||
'on' if val else 'off',
|
||||
)
|
||||
case 'MONO':
|
||||
chosen = values[event]
|
||||
self.vm.bus[int(index)].mono = util.get_bus_mono().index(chosen)
|
||||
self.cache['bus'][event] = chosen
|
||||
self.TKroot.after(
|
||||
200,
|
||||
self.nvda.speak,
|
||||
f'mono {chosen}',
|
||||
)
|
||||
case 'MODE':
|
||||
chosen = util._bus_mode_map_reversed[values[event]]
|
||||
setattr(self.vm.bus[int(index)].mode, chosen, True)
|
||||
@ -996,11 +1009,19 @@ class NVDAVMWindow(psg.Window):
|
||||
val = self.cache['bus'][f'BUS {index}||{param}']
|
||||
if param == 'MODE':
|
||||
self.nvda.speak(f'{label} bus {param} {util._bus_mode_map[val]}')
|
||||
elif param == 'MONO':
|
||||
busmode = util.get_bus_mono()[val]
|
||||
if busmode in ('on', 'off'):
|
||||
self.nvda.speak(f'{label} {param} {busmode}')
|
||||
else:
|
||||
self.nvda.speak(f'{label} {busmode}')
|
||||
else:
|
||||
self.nvda.speak(f'{label} {param} {"on" if val else "off"}')
|
||||
case [['BUS', index], [param], ['KEY', 'SPACE' | 'ENTER']]:
|
||||
if param == 'MODE':
|
||||
util.open_context_menu_for_buttonmenu(self, f'BUS {index}||MODE')
|
||||
elif param == 'MONO':
|
||||
util.open_context_menu_for_buttonmenu(self, f'BUS {index}||MONO')
|
||||
else:
|
||||
self.find_element_with_focus().click()
|
||||
|
||||
|
||||
302
tools/dynamic_builder.py
Normal file
302
tools/dynamic_builder.py
Normal file
@ -0,0 +1,302 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
Dynamic build system for nvda-voicemeeter.
|
||||
|
||||
This script generates PyInstaller spec files on-the-fly and builds executables
|
||||
without storing intermediate files in the repository.
|
||||
|
||||
Usage:
|
||||
python tools/dynamic_builder.py # Build all kinds
|
||||
python tools/dynamic_builder.py basic # Build basic only
|
||||
python tools/dynamic_builder.py basic banana # Build specific kinds
|
||||
python tools/dynamic_builder.py all # Build all kinds (explicit)
|
||||
|
||||
Requirements:
|
||||
- PDM environment with PyInstaller installed
|
||||
- controllerClient DLL files in controllerClient/{x64,x86}/
|
||||
- nvda_voicemeeter package installed in environment
|
||||
|
||||
Environment Variables:
|
||||
- PDM_BIN: Path to PDM executable (default: 'pdm')
|
||||
|
||||
Exit Codes:
|
||||
- 0: All builds successful
|
||||
- 1: One or more builds failed
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import os
|
||||
import shutil
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Dict
|
||||
|
||||
# Build configuration
|
||||
KINDS = ['basic', 'banana', 'potato']
|
||||
|
||||
# Templates
|
||||
PYTHON_TEMPLATE = """import voicemeeterlib
|
||||
|
||||
import nvda_voicemeeter
|
||||
|
||||
KIND_ID = '{kind}'
|
||||
|
||||
with voicemeeterlib.api(KIND_ID) as vm:
|
||||
with nvda_voicemeeter.draw(KIND_ID, vm) as window:
|
||||
window.run()
|
||||
"""
|
||||
|
||||
SPEC_TEMPLATE = """# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
block_cipher = None
|
||||
|
||||
added_files = [
|
||||
( '{controller_x64_path}', 'controllerClient/x64' ),
|
||||
( '{controller_x86_path}', 'controllerClient/x86' ),
|
||||
]
|
||||
|
||||
a = Analysis(
|
||||
['{script_path}'],
|
||||
pathex=[],
|
||||
binaries=[],
|
||||
datas=added_files,
|
||||
hiddenimports=[],
|
||||
hookspath=[],
|
||||
hooksconfig={{}},
|
||||
runtime_hooks=[],
|
||||
excludes=[],
|
||||
win_no_prefer_redirects=False,
|
||||
win_private_assemblies=False,
|
||||
cipher=block_cipher,
|
||||
noarchive=False,
|
||||
)
|
||||
pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher)
|
||||
|
||||
exe = EXE(
|
||||
pyz,
|
||||
a.scripts,
|
||||
[],
|
||||
exclude_binaries=True,
|
||||
name='{kind}',
|
||||
debug=False,
|
||||
bootloader_ignore_signals=False,
|
||||
strip=False,
|
||||
upx=True,
|
||||
console=False,
|
||||
disable_windowed_traceback=False,
|
||||
argv_emulation=False,
|
||||
target_arch=None,
|
||||
codesign_identity=None,
|
||||
entitlements_file=None,
|
||||
)
|
||||
coll = COLLECT(
|
||||
exe,
|
||||
a.binaries,
|
||||
a.zipfiles,
|
||||
a.datas,
|
||||
strip=False,
|
||||
upx=True,
|
||||
upx_exclude=[],
|
||||
name='{kind}',
|
||||
)
|
||||
"""
|
||||
|
||||
|
||||
class DynamicBuilder:
|
||||
def __init__(self, base_dir: Path, dist_dir: Path):
|
||||
self.base_dir = base_dir
|
||||
self.dist_dir = dist_dir
|
||||
self.temp_dir = None
|
||||
|
||||
def validate_environment(self) -> bool:
|
||||
"""Validate that all required files and dependencies are present."""
|
||||
|
||||
# Check for controller client DLLs
|
||||
x64_dll = self.base_dir / 'controllerClient' / 'x64' / 'nvdaControllerClient.dll'
|
||||
x86_dll = self.base_dir / 'controllerClient' / 'x86' / 'nvdaControllerClient.dll'
|
||||
|
||||
if not x64_dll.exists():
|
||||
print(f'[ERROR] Missing x64 controller client: {x64_dll}')
|
||||
return False
|
||||
|
||||
if not x86_dll.exists():
|
||||
print(f'[ERROR] Missing x86 controller client: {x86_dll}')
|
||||
return False
|
||||
|
||||
print('[OK] Controller client DLLs found')
|
||||
|
||||
# Check PyInstaller availability
|
||||
try:
|
||||
result = subprocess.run(['pdm', 'list'], capture_output=True, text=True)
|
||||
if 'pyinstaller' not in result.stdout.lower():
|
||||
print('[ERROR] PyInstaller not found in PDM environment')
|
||||
return False
|
||||
print('[OK] PyInstaller available')
|
||||
except subprocess.CalledProcessError:
|
||||
print('[ERROR] Failed to check PDM environment')
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
def __enter__(self):
|
||||
# Validate environment first
|
||||
if not self.validate_environment():
|
||||
print('[ERROR] Environment validation failed!')
|
||||
sys.exit(1)
|
||||
|
||||
self.temp_dir = Path(tempfile.mkdtemp(prefix='nvda_voicemeeter_build_'))
|
||||
print(f'Using temp directory: {self.temp_dir}')
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_val, exc_tb):
|
||||
if self.temp_dir and self.temp_dir.exists():
|
||||
shutil.rmtree(self.temp_dir)
|
||||
print(f'Cleaned up temp directory: {self.temp_dir}')
|
||||
|
||||
def create_python_file(self, kind: str) -> Path:
|
||||
"""Create a temporary Python launcher file."""
|
||||
content = PYTHON_TEMPLATE.format(kind=kind)
|
||||
|
||||
py_file = self.temp_dir / f'{kind}.py'
|
||||
with open(py_file, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
return py_file
|
||||
|
||||
def create_spec_file(self, kind: str, py_file: Path) -> Path:
|
||||
"""Create a temporary PyInstaller spec file."""
|
||||
controller_x64_path = (self.base_dir / 'controllerClient' / 'x64').as_posix()
|
||||
controller_x86_path = (self.base_dir / 'controllerClient' / 'x86').as_posix()
|
||||
|
||||
content = SPEC_TEMPLATE.format(
|
||||
script_path=py_file.as_posix(),
|
||||
controller_x64_path=controller_x64_path,
|
||||
controller_x86_path=controller_x86_path,
|
||||
kind=kind,
|
||||
)
|
||||
|
||||
spec_file = self.temp_dir / f'{kind}.spec'
|
||||
with open(spec_file, 'w') as f:
|
||||
f.write(content)
|
||||
|
||||
return spec_file
|
||||
|
||||
def build_variant(self, kind: str) -> bool:
|
||||
"""Build a single kind variant."""
|
||||
print(f'Building {kind}...')
|
||||
|
||||
# Validate kind
|
||||
if kind not in KINDS:
|
||||
print(f'[ERROR] Unknown kind: {kind}. Valid kinds: {", ".join(KINDS)}')
|
||||
return False
|
||||
|
||||
# Create temporary files
|
||||
py_file = self.create_python_file(kind)
|
||||
spec_file = self.create_spec_file(kind, py_file)
|
||||
|
||||
# Build with PyInstaller
|
||||
dist_path = self.dist_dir / kind
|
||||
pdm_bin = os.getenv('PDM_BIN', 'pdm')
|
||||
cmd = [
|
||||
pdm_bin,
|
||||
'run',
|
||||
'pyinstaller',
|
||||
'--noconfirm',
|
||||
'--distpath',
|
||||
str(dist_path.parent),
|
||||
str(spec_file),
|
||||
]
|
||||
|
||||
try:
|
||||
result = subprocess.run(cmd, cwd=self.base_dir, capture_output=True, text=True)
|
||||
if result.returncode == 0:
|
||||
print(f'[OK] Built {kind}')
|
||||
return True
|
||||
else:
|
||||
print(f'[FAIL] Failed to build {kind}')
|
||||
print(f'Error: {result.stderr}')
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f'[ERROR] Exception building {kind}: {e}')
|
||||
return False
|
||||
|
||||
def build_all_kinds(self) -> Dict[str, bool]:
|
||||
"""Build all kind variants."""
|
||||
results = {}
|
||||
|
||||
for kind in KINDS:
|
||||
success = self.build_variant(kind)
|
||||
results[kind] = success
|
||||
|
||||
return results
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description='Dynamic build system for nvda-voicemeeter')
|
||||
parser.add_argument(
|
||||
'kinds',
|
||||
nargs='*',
|
||||
choices=KINDS + ['all'],
|
||||
help='Kinds to build (default: all)',
|
||||
)
|
||||
parser.add_argument(
|
||||
'--dist-dir',
|
||||
type=Path,
|
||||
default=Path('dist'),
|
||||
help='Distribution output directory',
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if not args.kinds or 'all' in args.kinds:
|
||||
kinds_to_build = KINDS
|
||||
else:
|
||||
kinds_to_build = args.kinds
|
||||
|
||||
base_dir = Path.cwd()
|
||||
args.dist_dir.mkdir(exist_ok=True)
|
||||
|
||||
print(f'Building kinds: {", ".join(kinds_to_build)}')
|
||||
|
||||
all_results = {}
|
||||
|
||||
with DynamicBuilder(base_dir, args.dist_dir) as builder:
|
||||
if 'all' in kinds_to_build or len(kinds_to_build) == len(KINDS):
|
||||
# Build all kinds
|
||||
results = builder.build_all_kinds()
|
||||
all_results.update(results)
|
||||
else:
|
||||
# Build specific kinds
|
||||
for kind in kinds_to_build:
|
||||
success = builder.build_variant(kind)
|
||||
all_results[kind] = success
|
||||
|
||||
# Report results
|
||||
print('\n' + '=' * 50)
|
||||
print('BUILD SUMMARY')
|
||||
print('=' * 50)
|
||||
|
||||
success_count = 0
|
||||
total_count = 0
|
||||
|
||||
for build_name, success in all_results.items():
|
||||
status = '[OK]' if success else '[FAIL]'
|
||||
print(f'{status} {build_name}')
|
||||
if success:
|
||||
success_count += 1
|
||||
total_count += 1
|
||||
|
||||
print(f'\nSuccess: {success_count}/{total_count}')
|
||||
|
||||
if success_count == total_count:
|
||||
print('All builds completed successfully!')
|
||||
sys.exit(0)
|
||||
else:
|
||||
print('Some builds failed!')
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
Loading…
x
Reference in New Issue
Block a user