"""Build Windows installer (MSI) using PyInstaller. This script builds the WebDrop Bridge application for Windows using PyInstaller. It creates both a standalone executable and optionally an MSI installer. Requirements: - PyInstaller 6.0+ - Python 3.10+ - For MSI: WiX Toolset (optional, requires separate installation) Usage: python build_windows.py [--msi] [--code-sign] """ import sys import subprocess import os import shutil from pathlib import Path from datetime import datetime # Fix Unicode output on Windows if sys.platform == "win32": import io sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8") class WindowsBuilder: """Build Windows installer using PyInstaller.""" def __init__(self): """Initialize builder paths.""" self.project_root = Path(__file__).parent.parent.parent self.build_dir = self.project_root / "build" self.dist_dir = self.build_dir / "dist" / "windows" self.temp_dir = self.build_dir / "temp" / "windows" self.spec_file = self.build_dir / "webdrop_bridge.spec" self.version = self._get_version() def _get_version(self) -> str: """Get version from config.py.""" config_file = self.project_root / "src" / "webdrop_bridge" / "config.py" for line in config_file.read_text().split("\n"): if "app_version" in line and "1.0.0" in line: # Extract default version from config return "1.0.0" return "1.0.0" def clean(self): """Clean previous builds.""" print("๐Ÿงน Cleaning previous builds...") for path in [self.dist_dir, self.temp_dir]: if path.exists(): shutil.rmtree(path) print(f" Removed {path}") def build_executable(self) -> bool: """Build executable using PyInstaller.""" print("\n๐Ÿ”จ Building Windows executable with PyInstaller...") self.dist_dir.mkdir(parents=True, exist_ok=True) self.temp_dir.mkdir(parents=True, exist_ok=True) # PyInstaller command using spec file cmd = [ sys.executable, "-m", "PyInstaller", "--distpath", str(self.dist_dir), "--workpath", str(self.temp_dir), str(self.spec_file), ] print(f" Command: {' '.join(cmd)}") result = subprocess.run(cmd, cwd=str(self.project_root)) if result.returncode != 0: print("โŒ PyInstaller build failed") return False exe_path = self.dist_dir / "WebDropBridge.exe" if not exe_path.exists(): print(f"โŒ Executable not found at {exe_path}") return False print("โœ… Executable built successfully") print(f"๐Ÿ“ฆ Output: {exe_path}") print(f" Size: {exe_path.stat().st_size / 1024 / 1024:.1f} MB") return True def create_msi(self) -> bool: """Create MSI installer using WiX Toolset. This requires WiX Toolset to be installed: https://wixtoolset.org/releases/ """ print("\n๐Ÿ“ฆ Creating MSI installer with WiX...") # Check if WiX is installed heat_exe = shutil.which("heat.exe") candle_exe = shutil.which("candle.exe") light_exe = shutil.which("light.exe") if not all([heat_exe, candle_exe, light_exe]): print("โš ๏ธ WiX Toolset not found in PATH") print(" Install from: https://wixtoolset.org/releases/") print(" Or use: choco install wixtoolset") return False # Create WiX source file if not self._create_wix_source(): return False # Compile and link wix_obj = self.build_dir / "WebDropBridge.wixobj" msi_output = self.dist_dir / f"WebDropBridge-{self.version}-Setup.msi" # Run candle (compiler) candle_cmd = [ str(candle_exe), "-o", str(wix_obj), str(self.build_dir / "WebDropBridge.wxs"), ] print(f" Compiling WiX source...") result = subprocess.run(candle_cmd) if result.returncode != 0: print("โŒ WiX compilation failed") return False # Run light (linker) light_cmd = [ str(light_exe), "-o", str(msi_output), str(wix_obj), ] print(f" Linking MSI installer...") result = subprocess.run(light_cmd) if result.returncode != 0: print("โŒ MSI linking failed") return False if not msi_output.exists(): print(f"โŒ MSI not found at {msi_output}") return False print("โœ… MSI installer created successfully") print(f"๐Ÿ“ฆ Output: {msi_output}") print(f" Size: {msi_output.stat().st_size / 1024 / 1024:.1f} MB") return True def _create_wix_source(self) -> bool: """Create WiX source file for MSI generation.""" wix_content = f''' ''' wix_file = self.build_dir / "WebDropBridge.wxs" wix_file.write_text(wix_content) print(f" Created WiX source: {wix_file}") return True def sign_executable(self, cert_path: str, password: str) -> bool: """Sign executable with certificate (optional). Args: cert_path: Path to code signing certificate password: Certificate password Returns: True if signing successful """ print("\n๐Ÿ” Signing executable...") signtool = shutil.which("signtool.exe") if not signtool: print("โš ๏ธ signtool.exe not found (part of Windows SDK)") print(" Skipping code signing") return True exe_path = self.dist_dir / "WebDropBridge.exe" cmd = [ signtool, "sign", "/f", cert_path, "/p", password, "/t", "http://timestamp.comodoca.com/authenticode", str(exe_path), ] result = subprocess.run(cmd) if result.returncode != 0: print("โŒ Code signing failed") return False print("โœ… Executable signed successfully") return True def build(self, create_msi: bool = False, sign: bool = False) -> bool: """Run complete build process. Args: create_msi: Whether to create MSI installer sign: Whether to sign executable (requires certificate) Returns: True if build successful """ start_time = datetime.now() print("=" * 60) print("๐Ÿš€ WebDrop Bridge Windows Build") print("=" * 60) self.clean() if not self.build_executable(): return False if create_msi: if not self.create_msi(): print("โš ๏ธ MSI creation failed, but executable is available") if sign: # Would need certificate path from environment cert_path = os.getenv("CODE_SIGN_CERT") if cert_path: self.sign_executable(cert_path, os.getenv("CODE_SIGN_PASSWORD", "")) elapsed = (datetime.now() - start_time).total_seconds() print("\n" + "=" * 60) print(f"โœ… Build completed in {elapsed:.1f}s") print("=" * 60) return True def main(): """Main entry point.""" import argparse parser = argparse.ArgumentParser( description="Build WebDrop Bridge for Windows" ) parser.add_argument( "--msi", action="store_true", help="Create MSI installer (requires WiX Toolset)", ) parser.add_argument( "--sign", action="store_true", help="Sign executable (requires CODE_SIGN_CERT environment variable)", ) args = parser.parse_args() builder = WindowsBuilder() success = builder.build(create_msi=args.msi, sign=args.sign) return 0 if success else 1 if __name__ == "__main__": sys.exit(main())