#!/usr/bin/env python3 """ Build script for Text Texture Generator addon. Creates ZIP packages for both full and free versions. """ import os import sys import json import shutil import zipfile import subprocess from pathlib import Path from typing import Dict, List, Optional import tempfile import argparse import fnmatch def get_project_root() -> Path: """Get the project root directory.""" return Path(__file__).parent.parent def load_config() -> Dict: """Load build configuration.""" config_path = get_project_root() / "build" / "config.json" with open(config_path, 'r') as f: return json.load(f) def load_exclusions_config() -> Dict: """Load file exclusions configuration.""" config_path = get_project_root() / "build" / "file_exclusions.json" with open(config_path, 'r') as f: return json.load(f) def clean_directory(path: Path): """Clean a directory by removing all contents.""" if path.exists(): shutil.rmtree(path) path.mkdir(parents=True, exist_ok=True) def should_exclude_file(file_path: Path, source_dir: Path, exclude_patterns: List[str], exclude_files: List[str]) -> bool: """Check if a file should be excluded based on patterns and specific files.""" file_str = str(file_path) name = file_path.name # Get relative path from source directory for exclusion checking try: rel_path = file_path.relative_to(source_dir) rel_path_str = str(rel_path) except ValueError: rel_path_str = str(file_path) # Check specific file exclusions for exclude_file in exclude_files: # Handle directory exclusions (ending with /) if exclude_file.endswith('/'): exclude_dir = exclude_file.rstrip('/') if rel_path_str.startswith(exclude_dir + '/') or rel_path_str == exclude_dir: return True # Handle exact file matches elif rel_path_str == exclude_file: return True # Check general patterns (for cache files, etc.) for pattern in exclude_patterns: if pattern in file_str or pattern in name: return True # Handle wildcards if pattern.startswith('*.'): extension = pattern[1:] if file_str.endswith(extension): return True # Handle glob patterns if fnmatch.fnmatch(name, pattern) or fnmatch.fnmatch(rel_path_str, pattern): return True return False def copy_source_files(source_dir: Path, dest_dir: Path, exclude_patterns: List[str], exclude_files: List[str] = None): """Copy source files to destination, excluding specified patterns and files.""" if exclude_files is None: exclude_files = [] print(f"Copying files from {source_dir} to {dest_dir}") print(f"Excluding files: {exclude_files}") print(f"Excluding patterns: {exclude_patterns}") copied_count = 0 excluded_count = 0 for item in source_dir.rglob('*'): if item.is_file(): if should_exclude_file(item, source_dir, exclude_patterns, exclude_files): rel_path = item.relative_to(source_dir) print(f" Excluded: {rel_path}") excluded_count += 1 else: # Calculate relative path rel_path = item.relative_to(source_dir) dest_file = dest_dir / rel_path # Create parent directories dest_file.parent.mkdir(parents=True, exist_ok=True) # Copy file shutil.copy2(item, dest_file) print(f" Copied: {rel_path}") copied_count += 1 print(f"Files copied: {copied_count}, excluded: {excluded_count}") def create_zip_package(source_dir: Path, output_file: Path, addon_name: str): """Create a ZIP package from source directory.""" print(f"Creating ZIP package: {output_file}") with zipfile.ZipFile(output_file, 'w', zipfile.ZIP_DEFLATED) as zip_file: for file_path in source_dir.rglob('*'): if file_path.is_file(): # Calculate archive path (include addon name as root folder) rel_path = file_path.relative_to(source_dir) archive_path = Path(addon_name) / rel_path zip_file.write(file_path, archive_path) print(f" Added to ZIP: {archive_path}") print(f"ZIP package created: {output_file}") def get_version_from_git() -> Optional[str]: """Get version from git tags.""" try: result = subprocess.run( ['python3', 'build/sync_version.py', '--version'], capture_output=True, text=True, cwd=get_project_root() ) if result.returncode == 0: # Extract version from output lines = result.stdout.strip().split('\n') for line in lines: if 'Synchronizing version:' in line: return line.split(':')[1].strip().split()[0] except Exception as e: print(f"Warning: Could not get version from git: {e}") return None def build_addon(version_type: str, version: Optional[str] = None, output_dir: Optional[Path] = None): """Build addon for specified version type.""" config = load_config() exclusions_config = load_exclusions_config() project_root = get_project_root() # Determine version if version is None: version = get_version_from_git() or "1.0.0" # Set up paths source_dir = project_root / config['build']['source_dir'] if output_dir is None: output_dir = project_root / config['build']['dist_dir'] temp_dir = project_root / config['build']['temp_dir'] # Clean and create directories clean_directory(temp_dir) output_dir.mkdir(parents=True, exist_ok=True) # Sync version files for the specific version type (simplified - no feature flags) print(f"Synchronizing version files for {version_type} version {version}") sync_result = subprocess.run([ 'python3', 'build/sync_version.py', '--version', version, '--type', version_type ], cwd=project_root) if sync_result.returncode != 0: raise RuntimeError(f"Version synchronization failed") # Get version-specific exclusions version_config = exclusions_config['version_configs'][version_type] exclude_files = version_config.get('exclude_files', []) exclude_patterns = config['build']['exclude_patterns'] + version_config.get('exclude_patterns', []) print(f"\nšŸ“‹ Build configuration for {version_type} version:") print(f"Max texture size: {version_config.get('max_texture_size', 'unlimited')}") print(f"Max concurrent operations: {version_config.get('max_concurrent_operations', 'unlimited')}") if exclude_files: print(f"Files to exclude: {exclude_files}") if version_config.get('exclude_patterns'): print(f"Additional patterns to exclude: {version_config.get('exclude_patterns')}") # Copy source files to temp directory with version-specific exclusions build_temp_dir = temp_dir / f"{config['project']['id']}_{version_type}" copy_source_files(source_dir, build_temp_dir, exclude_patterns, exclude_files) # Create output filename version_suffix = f"_{version_type}" if version_type == "free" else "" output_filename = f"{config['project']['id']}_v{version}{version_suffix}.zip" output_file = output_dir / output_filename # Create ZIP package create_zip_package(build_temp_dir, output_file, config['project']['id']) # Clean up temp directory shutil.rmtree(temp_dir) print(f"\nāœ… Build complete!") print(f"Package: {output_file}") print(f"Version: {version} ({version_type})") return output_file def build_all_versions(version: Optional[str] = None): """Build both full and free versions.""" print("Building all versions...") built_packages = [] # Build full version print("\n" + "="*50) print("BUILDING FULL VERSION") print("="*50) full_package = build_addon("full", version) built_packages.append(("full", full_package)) # Build free version print("\n" + "="*50) print("BUILDING FREE VERSION") print("="*50) free_package = build_addon("free", version) built_packages.append(("free", free_package)) print("\n" + "="*50) print("BUILD SUMMARY") print("="*50) for version_type, package_path in built_packages: file_size = package_path.stat().st_size / 1024 # KB print(f"{version_type.upper():>4}: {package_path.name} ({file_size:.1f} KB)") return built_packages def main(): """Main entry point.""" parser = argparse.ArgumentParser(description="Build Text Texture Generator addon") parser.add_argument("--version", help="Version to build (defaults to git tag)") parser.add_argument("--type", choices=["free", "full", "all"], default="all", help="Version type to build") parser.add_argument("--output", type=Path, help="Output directory") args = parser.parse_args() try: if args.type == "all": build_all_versions(args.version) else: build_addon(args.type, args.version, args.output) except Exception as e: print(f"āŒ Build failed: {e}") sys.exit(1) if __name__ == "__main__": main()