mirror of
https://github.com/onyx-and-iris/nvda-voicemeeter.git
synced 2026-03-12 05:49:15 +00:00
add spec_generator.py as well as generate-specs task
This commit is contained in:
parent
415f2e2ba3
commit
47d38e4468
@ -15,8 +15,13 @@ tasks:
|
||||
- task: build
|
||||
- task: compress
|
||||
|
||||
generate-specs:
|
||||
desc: Generate all spec files from templates
|
||||
cmd: pdm run python tools/spec_generator.py
|
||||
|
||||
build:
|
||||
desc: Build the project
|
||||
deps: [generate-specs]
|
||||
cmds:
|
||||
- for:
|
||||
matrix:
|
||||
@ -35,6 +40,4 @@ tasks:
|
||||
desc: Clean the project
|
||||
cmds:
|
||||
- |
|
||||
{{.SHELL}} -Command "
|
||||
Remove-Item -Recurse -Force build/basic,build/banana,build/potato
|
||||
Remove-Item -Recurse -Force dist/*"
|
||||
{{.SHELL}} -Command "Remove-Item -Path build/*,dist/* -Recurse -Force -ErrorAction SilentlyContinue"
|
||||
|
||||
280
tools/spec_generator.py
Normal file
280
tools/spec_generator.py
Normal file
@ -0,0 +1,280 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PyInstaller spec file generator for nvda-voicemeeter project.
|
||||
|
||||
This script generates .spec files for different Voicemeeter kinds (basic, banana, potato).
|
||||
Based on the existing spec file patterns in the spec/ directory.
|
||||
|
||||
Usage:
|
||||
python tools/spec_generator.py [kind]
|
||||
|
||||
Arguments:
|
||||
kind: The Voicemeeter kind to generate spec for (basic, banana, potato).
|
||||
If not provided, generates all spec files.
|
||||
|
||||
Examples:
|
||||
python tools/spec_generator.py basic
|
||||
python tools/spec_generator.py
|
||||
"""
|
||||
|
||||
import argparse
|
||||
from pathlib import Path
|
||||
from typing import List
|
||||
|
||||
|
||||
class SpecGenerator:
|
||||
"""Generate PyInstaller spec files for nvda-voicemeeter project."""
|
||||
|
||||
# Supported Voicemeeter kinds
|
||||
KINDS = ['basic', 'banana', 'potato']
|
||||
|
||||
def __init__(self, project_root: Path = None):
|
||||
"""Initialize the spec generator.
|
||||
|
||||
Args:
|
||||
project_root: Path to the project root. If None, uses current directory.
|
||||
"""
|
||||
if project_root is None:
|
||||
# Assume we're running from tools/ directory
|
||||
project_root = Path(__file__).parent.parent
|
||||
|
||||
self.project_root = Path(project_root)
|
||||
self.spec_dir = self.project_root / 'spec'
|
||||
|
||||
# Ensure spec directory exists
|
||||
self.spec_dir.mkdir(exist_ok=True)
|
||||
|
||||
def generate_spec_template(self, kind: str) -> str:
|
||||
"""Generate a PyInstaller spec file template for the given kind.
|
||||
|
||||
Args:
|
||||
kind: The Voicemeeter kind (basic, banana, potato).
|
||||
|
||||
Returns:
|
||||
The generated spec file content as a string.
|
||||
"""
|
||||
if kind not in self.KINDS:
|
||||
raise ValueError(f'Unsupported kind: {kind}. Supported kinds: {self.KINDS}')
|
||||
|
||||
template = f"""# -*- mode: python ; coding: utf-8 -*-
|
||||
|
||||
|
||||
block_cipher = None
|
||||
|
||||
added_files = [
|
||||
( '../controllerClient/x64', 'controllerClient/x64' ),
|
||||
( '../controllerClient/x86', 'controllerClient/x86' ),
|
||||
]
|
||||
|
||||
a = Analysis(
|
||||
['{kind}.py'],
|
||||
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}',
|
||||
)
|
||||
"""
|
||||
return template
|
||||
|
||||
def generate_entry_point(self, kind: str) -> str:
|
||||
"""Generate a Python entry point script for the given kind.
|
||||
|
||||
Args:
|
||||
kind: The Voicemeeter kind (basic, banana, potato).
|
||||
|
||||
Returns:
|
||||
The generated Python script content as a string.
|
||||
"""
|
||||
if kind not in self.KINDS:
|
||||
raise ValueError(f'Unsupported kind: {kind}. Supported kinds: {self.KINDS}')
|
||||
|
||||
entry_point = f"""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()
|
||||
"""
|
||||
return entry_point
|
||||
|
||||
def write_spec_file(self, kind: str, overwrite: bool = False) -> Path:
|
||||
"""Write a spec file for the given kind.
|
||||
|
||||
Args:
|
||||
kind: The Voicemeeter kind (basic, banana, potato).
|
||||
overwrite: Whether to overwrite existing files.
|
||||
|
||||
Returns:
|
||||
Path to the created spec file.
|
||||
|
||||
Raises:
|
||||
FileExistsError: If file exists and overwrite is False.
|
||||
"""
|
||||
spec_file = self.spec_dir / f'{kind}.spec'
|
||||
entry_file = self.spec_dir / f'{kind}.py'
|
||||
|
||||
if spec_file.exists() and not overwrite:
|
||||
raise FileExistsError(f'Spec file already exists: {spec_file}')
|
||||
|
||||
if entry_file.exists() and not overwrite:
|
||||
raise FileExistsError(f'Entry point file already exists: {entry_file}')
|
||||
|
||||
# Write spec file
|
||||
spec_content = self.generate_spec_template(kind)
|
||||
spec_file.write_text(spec_content, encoding='utf-8')
|
||||
print(f'Generated spec file: {spec_file}')
|
||||
|
||||
# Write entry point script
|
||||
entry_content = self.generate_entry_point(kind)
|
||||
entry_file.write_text(entry_content, encoding='utf-8')
|
||||
print(f'Generated entry point: {entry_file}')
|
||||
|
||||
return spec_file
|
||||
|
||||
def generate_all_specs(self, overwrite: bool = False) -> List[Path]:
|
||||
"""Generate spec files for all supported kinds.
|
||||
|
||||
Args:
|
||||
overwrite: Whether to overwrite existing files.
|
||||
|
||||
Returns:
|
||||
List of paths to the created spec files.
|
||||
"""
|
||||
spec_files = []
|
||||
for kind in self.KINDS:
|
||||
try:
|
||||
spec_file = self.write_spec_file(kind, overwrite=overwrite)
|
||||
spec_files.append(spec_file)
|
||||
except FileExistsError as e:
|
||||
print(f'Skipped {kind}: {e}')
|
||||
continue
|
||||
|
||||
return spec_files
|
||||
|
||||
def list_existing_specs(self) -> List[str]:
|
||||
"""List existing spec files in the spec directory.
|
||||
|
||||
Returns:
|
||||
List of existing spec file kinds.
|
||||
"""
|
||||
existing = []
|
||||
for kind in self.KINDS:
|
||||
spec_file = self.spec_dir / f'{kind}.spec'
|
||||
if spec_file.exists():
|
||||
existing.append(kind)
|
||||
|
||||
return existing
|
||||
|
||||
|
||||
def main():
|
||||
"""Main entry point for the spec generator."""
|
||||
parser = argparse.ArgumentParser(
|
||||
description='Generate PyInstaller spec files for nvda-voicemeeter project',
|
||||
formatter_class=argparse.RawDescriptionHelpFormatter,
|
||||
epilog="""
|
||||
Examples:
|
||||
python tools/spec_generator.py basic # Generate basic.spec
|
||||
python tools/spec_generator.py # Generate all spec files
|
||||
python tools/spec_generator.py --list # List existing spec files
|
||||
python tools/spec_generator.py --force # Overwrite existing files
|
||||
""",
|
||||
)
|
||||
|
||||
parser.add_argument(
|
||||
'kind',
|
||||
nargs='?',
|
||||
choices=SpecGenerator.KINDS,
|
||||
help=f'Voicemeeter kind to generate spec for ({", ".join(SpecGenerator.KINDS)})',
|
||||
)
|
||||
|
||||
parser.add_argument('--force', '-f', action='store_true', help='Overwrite existing spec files')
|
||||
|
||||
parser.add_argument('--list', '-l', action='store_true', help='List existing spec files')
|
||||
|
||||
parser.add_argument(
|
||||
'--project-root', type=Path, help='Path to project root (default: detect from script location)'
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
# Initialize generator
|
||||
generator = SpecGenerator(project_root=args.project_root)
|
||||
|
||||
# List existing files if requested
|
||||
if args.list:
|
||||
existing = generator.list_existing_specs()
|
||||
if existing:
|
||||
print('Existing spec files:')
|
||||
for kind in existing:
|
||||
spec_file = generator.spec_dir / f'{kind}.spec'
|
||||
print(f' {kind}: {spec_file}')
|
||||
else:
|
||||
print('No existing spec files found.')
|
||||
return
|
||||
|
||||
try:
|
||||
if args.kind:
|
||||
# Generate specific kind
|
||||
generator.write_spec_file(args.kind, overwrite=args.force)
|
||||
print(f'Successfully generated spec for: {args.kind}')
|
||||
else:
|
||||
# Generate all kinds
|
||||
spec_files = generator.generate_all_specs(overwrite=args.force)
|
||||
if spec_files:
|
||||
print(f'Successfully generated {len(spec_files)} spec files.')
|
||||
else:
|
||||
print('No spec files were generated (use --force to overwrite existing files).')
|
||||
|
||||
except ValueError as e:
|
||||
print(f'Error: {e}')
|
||||
return 1
|
||||
except FileExistsError as e:
|
||||
print(f'Error: {e}')
|
||||
print('Use --force to overwrite existing files.')
|
||||
return 1
|
||||
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
exit(main())
|
||||
Loading…
x
Reference in New Issue
Block a user