diff --git a/Taskfile.yml b/Taskfile.yml index 081b3c8..c52378c 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -30,8 +30,14 @@ tasks: - task: compress - echo "Release complete" + generate-specs: + desc: Generate all spec files from templates + cmds: + - poetry run python tools/spec_generator.py --clean + build: desc: Build all artifacts + deps: [generate-specs] cmds: - for: matrix: diff --git a/tools/spec_generator.py b/tools/spec_generator.py new file mode 100644 index 0000000..2625e4a --- /dev/null +++ b/tools/spec_generator.py @@ -0,0 +1,192 @@ +#!/usr/bin/env python3 +""" +Spec file generator for voicemeeter-compact builds. +Generates Python launcher files and PyInstaller spec files from templates. +""" + +import argparse +from pathlib import Path + +# Build configuration +THEMES = { + 'azure': ['azure-light', 'azure-dark'], + 'forest': ['forest-light', 'forest-dark'], + 'sunvalley': ['sunvalley'], # Single variant, no light/dark +} + +KINDS = ['basic', 'banana', 'potato'] + +# Templates +PYTHON_TEMPLATE = """import voicemeeterlib + +import vmcompact + + +def main(): + KIND_ID = '{kind}' + + with voicemeeterlib.api(KIND_ID) as vmr:{theme_arg} + app = vmcompact.connect(KIND_ID, vmr{theme_param}) + app.mainloop() + + +if __name__ == '__main__': + main() +""" + +SPEC_TEMPLATE = """# -*- mode: python ; coding: utf-8 -*- + + +block_cipher = None + +added_files = [ + ( '../../vmcompact/img', 'img' ),{theme_files} + ( '../../configs', 'configs' ), + ] + +a = Analysis( + ['{script_name}'], + 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}', +) +""" + + +def generate_python_file(theme_variant: str, kind: str, output_dir: Path) -> None: + """Generate a Python launcher file.""" + if theme_variant == 'sunvalley': + # Sunvalley doesn't use theme parameter + theme_arg = '' + theme_param = '' + else: + theme_arg = f"\n theme = '{theme_variant}'" + theme_param = ', theme=theme' + + content = PYTHON_TEMPLATE.format( + kind=kind, theme_arg=theme_arg, theme_param=theme_param + ) + + filename = f'{theme_variant}-{kind}.py' + output_path = output_dir / filename + + with open(output_path, 'w') as f: + f.write(content) + + print(f'Generated: {output_path}') + + +def generate_spec_file(theme_variant: str, kind: str, output_dir: Path) -> None: + """Generate a PyInstaller spec file.""" + script_name = f'{theme_variant}-{kind}.py' + + if theme_variant == 'sunvalley': + # Sunvalley doesn't include theme files + theme_files = '' + else: + theme_base = theme_variant.split('-')[0] # 'azure' from 'azure-dark' + theme_files = f"\n ( '../../theme/{theme_base}', 'theme' )," + + content = SPEC_TEMPLATE.format( + script_name=script_name, theme_files=theme_files, kind=kind + ) + + filename = f'{theme_variant}-{kind}.spec' + output_path = output_dir / filename + + with open(output_path, 'w') as f: + f.write(content) + + print(f'Generated: {output_path}') + + +def generate_all_files(output_base_dir: Path) -> None: + """Generate all Python and spec files for all theme/kind combinations.""" + for theme_family, theme_variants in THEMES.items(): + theme_dir = output_base_dir / theme_family + theme_dir.mkdir(parents=True, exist_ok=True) + + for theme_variant in theme_variants: + for kind in KINDS: + generate_python_file(theme_variant, kind, theme_dir) + generate_spec_file(theme_variant, kind, theme_dir) + + +def clean_existing_files(output_base_dir: Path) -> None: + """Remove all existing generated files.""" + for theme_family in THEMES.keys(): + theme_dir = output_base_dir / theme_family + if theme_dir.exists(): + for file in theme_dir.glob('*.py'): + file.unlink() + print(f'Removed: {file}') + for file in theme_dir.glob('*.spec'): + file.unlink() + print(f'Removed: {file}') + + +def main(): + parser = argparse.ArgumentParser( + description='Generate spec files for voicemeeter-compact' + ) + parser.add_argument( + '--clean', action='store_true', help='Clean existing files before generating' + ) + parser.add_argument( + '--output-dir', + type=Path, + default=Path('spec'), + help='Output directory for spec files (default: spec)', + ) + + args = parser.parse_args() + + if args.clean: + print('Cleaning existing files...') + clean_existing_files(args.output_dir) + + print('Generating spec files...') + generate_all_files(args.output_dir) + print('Done!') + + +if __name__ == '__main__': + main()