feat: Implement default welcome page for missing web application
- Added a professional HTML welcome page displayed when no web application is configured. - Enhanced `_load_webapp()` method to support improved path resolution for both development and bundled modes. - Updated error handling to show the welcome page instead of a bare error message when the webapp file is not found. - Modified unit tests to verify the welcome page is displayed in error scenarios. build: Complete Windows and macOS build scripts - Created `build_windows.py` for building Windows executable and optional MSI installer using PyInstaller. - Developed `build_macos.sh` for creating macOS application bundle and DMG image. - Added logging and error handling to build scripts for better user feedback. docs: Add build and icon requirements documentation - Created `PHASE_3_BUILD_SUMMARY.md` detailing the build process, results, and next steps. - Added `resources/icons/README.md` outlining icon requirements and creation guidelines. chore: Sync remotes script for repository maintenance - Introduced `sync_remotes.ps1` PowerShell script to fetch updates from origin and upstream remotes.
This commit is contained in:
parent
90dc09eb4d
commit
f0c96f15b8
10 changed files with 1415 additions and 39 deletions
321
build/scripts/build_windows.py
Normal file
321
build/scripts/build_windows.py
Normal file
|
|
@ -0,0 +1,321 @@
|
|||
"""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'''<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
|
||||
<Product Id="*" Name="WebDrop Bridge" Language="1033" Version="{self.version}"
|
||||
Manufacturer="HIM-Tools"
|
||||
UpgradeCode="12345678-1234-1234-1234-123456789012">
|
||||
|
||||
<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />
|
||||
<Media Id="1" Cabinet="WebDropBridge.cab" EmbedCab="yes" />
|
||||
|
||||
<Feature Id="ProductFeature" Title="WebDrop Bridge" Level="1">
|
||||
<ComponentRef Id="MainExecutable" />
|
||||
<ComponentRef Id="ProgramMenuShortcut" />
|
||||
</Feature>
|
||||
|
||||
<Directory Id="TARGETDIR" Name="SourceDir">
|
||||
<Directory Id="ProgramFilesFolder">
|
||||
<Directory Id="INSTALLFOLDER" Name="WebDrop Bridge" />
|
||||
</Directory>
|
||||
<Directory Id="ProgramMenuFolder">
|
||||
<Directory Id="ApplicationProgramsFolder" Name="WebDrop Bridge"/>
|
||||
</Directory>
|
||||
</Directory>
|
||||
|
||||
<DirectoryRef Id="INSTALLFOLDER">
|
||||
<Component Id="MainExecutable" Guid="*">
|
||||
<File Id="WebDropBridgeExe" Source="$(var.DistDir)\\WebDropBridge.exe" KeyPath="yes"/>
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
|
||||
<DirectoryRef Id="ApplicationProgramsFolder">
|
||||
<Component Id="ProgramMenuShortcut" Guid="*">
|
||||
<Shortcut Id="ApplicationStartMenuShortcut"
|
||||
Name="WebDrop Bridge"
|
||||
Description="Web Drag-and-Drop Bridge"
|
||||
Target="[INSTALLFOLDER]WebDropBridge.exe"
|
||||
WorkingDirectory="INSTALLFOLDER" />
|
||||
<RemoveFolder Id="ApplicationProgramsFolderRemove"
|
||||
On="uninstall" />
|
||||
<RegistryValue Root="HKCU"
|
||||
Key="Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\WebDropBridge"
|
||||
Name="installed"
|
||||
Type="integer"
|
||||
Value="1"
|
||||
KeyPath="yes" />
|
||||
</Component>
|
||||
</DirectoryRef>
|
||||
</Product>
|
||||
</Wix>
|
||||
'''
|
||||
|
||||
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())
|
||||
Loading…
Add table
Add a link
Reference in a new issue