Refactor Windows build script for improved readability and consistency

- Cleaned up whitespace and formatting in build_windows.py for better readability.
- Consolidated environment variable setup for stdout and stderr.
- Streamlined subprocess command calls by removing unnecessary line breaks.
- Enhanced error handling and logging for better debugging.
- Updated comments for clarity and consistency.

Update updater.py to improve checksum verification logic

- Modified checksum verification to prioritize specific .sha256 files matching installer names.
- Added fallback logic to check for any .sha256 file if no specific match is found.

Enhance update manager UI with download progress dialog

- Introduced DownloadingDialog to provide feedback during update downloads.
- Updated MainWindow to manage the new downloading dialog and handle its lifecycle.
- Removed the skip version functionality from the update dialog as per new requirements.

Refactor update manager UI tests for clarity and maintainability

- Removed tests related to the skip version functionality.
- Updated test cases to reflect changes in the update manager UI.
- Ensured all tests are aligned with the new dialog structure and signal emissions.
This commit is contained in:
claudi 2026-02-25 14:38:33 +01:00
parent 88d9f200ab
commit 025e9c888c
10 changed files with 3066 additions and 3088 deletions

File diff suppressed because one or more lines are too long

View file

@ -2,7 +2,7 @@
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
xmlns:ui="http://schemas.microsoft.com/wix/2010/ui"
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
<Product Id="*" Name="WebDrop Bridge" Language="1033" Version="0.6.1"
<Product Id="*" Name="WebDrop Bridge" Language="1033" Version="0.6.2"
Manufacturer="HIM-Tools"
UpgradeCode="12345678-1234-1234-1234-123456789012">

File diff suppressed because one or more lines are too long

File diff suppressed because it is too large Load diff

View file

@ -10,7 +10,7 @@ Requirements:
Usage:
python build_windows.py [--msi] [--code-sign] [--env-file PATH]
Options:
--msi Create MSI installer (requires WiX Toolset)
--code-sign Sign executable (requires certificate)
@ -27,13 +27,10 @@ from typing import Optional
if sys.platform == "win32":
os.environ["PYTHONIOENCODING"] = "utf-8"
import io
# Reconfigure stdout/stderr for UTF-8 output
sys.stdout = io.TextIOWrapper(
sys.stdout.buffer, encoding="utf-8", errors="replace"
)
sys.stderr = io.TextIOWrapper(
sys.stderr.buffer, encoding="utf-8", errors="replace"
)
sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8", errors="replace")
sys.stderr = io.TextIOWrapper(sys.stderr.buffer, encoding="utf-8", errors="replace")
import subprocess
import shutil
@ -50,7 +47,7 @@ class WindowsBuilder:
def __init__(self, env_file: Path | None = None):
"""Initialize builder paths.
Args:
env_file: Path to .env file to bundle. If None, uses project root .env.
If that doesn't exist, raises error.
@ -61,20 +58,20 @@ class WindowsBuilder:
self.temp_dir = self.build_dir / "temp" / "windows"
self.spec_file = self.build_dir / "webdrop_bridge.spec"
self.version = get_current_version()
# Validate and set env file
if env_file is None:
env_file = self.project_root / ".env"
else:
env_file = Path(env_file).resolve()
if not env_file.exists():
raise FileNotFoundError(
f"Configuration file not found: {env_file}\n"
f"Please provide a .env file using --env-file parameter\n"
f"or ensure .env exists in project root"
)
self.env_file = env_file
print(f"📋 Using configuration: {self.env_file}")
@ -115,17 +112,12 @@ class WindowsBuilder:
]
print(f" Command: {' '.join(cmd)}")
# Set environment variable for spec file to use
env = os.environ.copy()
env["WEBDROP_ENV_FILE"] = str(self.env_file)
result = subprocess.run(
cmd,
cwd=str(self.project_root),
text=True,
env=env
)
result = subprocess.run(cmd, cwd=str(self.project_root), text=True, env=env)
if result.returncode != 0:
print("❌ PyInstaller build failed")
@ -139,9 +131,11 @@ class WindowsBuilder:
print("✅ Executable built successfully")
print(f"📦 Output: {exe_path}")
# Calculate total dist size
total_size = sum(f.stat().st_size for f in self.dist_dir.glob("WebDropBridge/**/*") if f.is_file())
total_size = sum(
f.stat().st_size for f in self.dist_dir.glob("WebDropBridge/**/*") if f.is_file()
)
if total_size > 0:
print(f" Total size: {total_size / 1024 / 1024:.1f} MB")
@ -174,15 +168,15 @@ class WindowsBuilder:
def set_exe_version(self, exe_path: Path) -> None:
"""Set executable file version for Windows.
This is important for MSI updates: Windows Installer compares file versions
to determine if files should be updated during a major upgrade.
Args:
exe_path: Path to the executable file
"""
print("\n🏷️ Setting executable version information...")
try:
import pefile
except ImportError:
@ -190,30 +184,30 @@ class WindowsBuilder:
print(" Note: Install with: pip install pefile")
print(" EXE version info will be blank (MSI updates may not work correctly)")
return
try:
pe = pefile.PE(str(exe_path))
# Parse version into 4-part format (Major, Minor, Build, Revision)
version_parts = self.version.split(".")
while len(version_parts) < 4:
version_parts.append("0")
file_version = tuple(int(v) for v in version_parts[:4])
# Set version resource if it exists
if hasattr(pe, "VS_FIXEDFILEINFO"):
pe.VS_FIXEDFILEINFO[0].FileVersionMS = (file_version[0] << 16) | file_version[1]
pe.VS_FIXEDFILEINFO[0].FileVersionLS = (file_version[2] << 16) | file_version[3]
pe.VS_FIXEDFILEINFO[0].ProductVersionMS = (file_version[0] << 16) | file_version[1]
pe.VS_FIXEDFILEINFO[0].ProductVersionLS = (file_version[2] << 16) | file_version[3]
# Write modified PE back to file
pe.write(filename=str(exe_path))
print(f"✅ Version set to {self.version}")
else:
print("⚠️ No version resource found in EXE")
except Exception as e:
print(f"⚠️ Could not set EXE version: {e}")
print(" MSI updates may not work correctly without file version info")
@ -255,22 +249,25 @@ class WindowsBuilder:
if not dist_folder.exists():
print(f"❌ Distribution folder not found: {dist_folder}")
return False
harvest_file = self.build_dir / "WebDropBridge_Files.wxs"
# Use Heat to harvest all files
heat_cmd = [
str(heat_exe),
"dir",
str(dist_folder),
"-cg", "AppFiles",
"-dr", "INSTALLFOLDER",
"-cg",
"AppFiles",
"-dr",
"INSTALLFOLDER",
"-sfrag",
"-srd",
"-gg",
"-o", str(harvest_file),
"-o",
str(harvest_file),
]
result = subprocess.run(heat_cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
if result.returncode != 0:
print("⚠️ Heat harvest warnings (may be non-critical)")
@ -278,15 +275,12 @@ class WindowsBuilder:
print(result.stderr[:200]) # Show first 200 chars of errors
else:
print(f" ✓ Harvested files")
# Post-process harvested file to mark components as 64-bit
if harvest_file.exists():
content = harvest_file.read_text()
# Add Win64="yes" to all Component tags
content = content.replace(
'<Component ',
'<Component Win64="yes" '
)
content = content.replace("<Component ", '<Component Win64="yes" ')
harvest_file.write_text(content)
print(f" ✓ Marked components as 64-bit")
@ -298,15 +292,18 @@ class WindowsBuilder:
# Run candle compiler - make sure to use correct source directory
candle_cmd = [
str(candle_exe),
"-ext", "WixUIExtension",
"-ext", "WixUtilExtension",
"-ext",
"WixUIExtension",
"-ext",
"WixUtilExtension",
f"-dDistDir={self.dist_dir}",
f"-dSourceDir={self.dist_dir}\\WebDropBridge", # Set SourceDir for Heat-generated files
f"-dResourcesDir={self.project_root}\\resources", # Set ResourcesDir for branding assets
"-o", str(self.build_dir) + "\\",
"-o",
str(self.build_dir) + "\\",
str(self.build_dir / "WebDropBridge.wxs"),
]
if harvest_file.exists():
candle_cmd.append(str(harvest_file))
@ -319,18 +316,24 @@ class WindowsBuilder:
# Link MSI - include both obj files if harvest was successful
light_cmd = [
str(light_exe),
"-ext", "WixUIExtension",
"-ext", "WixUtilExtension",
"-b", str(self.dist_dir / "WebDropBridge"), # Base path for source files
"-o", str(msi_output),
"-ext",
"WixUIExtension",
"-ext",
"WixUtilExtension",
"-b",
str(self.dist_dir / "WebDropBridge"), # Base path for source files
"-o",
str(msi_output),
str(wix_obj),
]
if wix_files_obj.exists():
light_cmd.append(str(wix_files_obj))
print(f" Linking MSI installer...")
result = subprocess.run(light_cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
result = subprocess.run(
light_cmd, text=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE
)
if result.returncode != 0:
print("❌ MSI linking failed")
if result.stdout:
@ -351,11 +354,11 @@ class WindowsBuilder:
def _create_wix_source(self) -> bool:
"""Create WiX source file for MSI generation.
Creates per-machine installation (Program Files).
Installation requires admin rights, but the app does not.
"""
wix_content = f'''<?xml version="1.0" encoding="UTF-8"?>
wix_content = f"""<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
xmlns:ui="http://schemas.microsoft.com/wix/2010/ui"
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
@ -443,38 +446,46 @@ class WindowsBuilder:
</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 _generate_file_elements(self, folder: Path, parent_dir_ref: str, parent_rel_path: str, indent: int = 8, file_counter: Optional[dict] = None) -> str:
def _generate_file_elements(
self,
folder: Path,
parent_dir_ref: str,
parent_rel_path: str,
indent: int = 8,
file_counter: Optional[dict] = None,
) -> str:
"""Generate WiX File elements for all files in a folder.
Args:
folder: Root folder to scan
parent_dir_ref: Parent WiX DirectoryRef ID
parent_rel_path: Relative path for component structure
indent: Indentation level
file_counter: Dictionary to track file IDs for uniqueness
Returns:
WiX XML string with all File elements
"""
if file_counter is None:
file_counter = {}
elements = []
indent_str = " " * indent
try:
# Get all files in current folder
for item in sorted(folder.iterdir()):
if item.is_file():
# Create unique File element ID using hash of full path
import hashlib
path_hash = hashlib.md5(str(item).encode()).hexdigest()[:8]
file_id = f"File_{path_hash}"
file_path = str(item)
@ -482,24 +493,21 @@ class WindowsBuilder:
elif item.is_dir() and item.name != "__pycache__":
# Recursively add files from subdirectories
sub_elements = self._generate_file_elements(
item, parent_dir_ref,
f"{parent_rel_path}/{item.name}",
indent,
file_counter
item, parent_dir_ref, f"{parent_rel_path}/{item.name}", indent, file_counter
)
if sub_elements:
elements.append(sub_elements)
except PermissionError:
print(f" ⚠️ Permission denied accessing {folder}")
return "\n".join(elements)
def _sanitize_id(self, filename: str) -> str:
"""Sanitize filename to be a valid WiX identifier.
Args:
filename: Filename to sanitize
Returns:
Sanitized identifier
"""
@ -544,10 +552,7 @@ class WindowsBuilder:
str(exe_path),
]
result = subprocess.run(
cmd,
text=True
)
result = subprocess.run(cmd, text=True)
if result.returncode != 0:
print("❌ Code signing failed")
return False
@ -594,9 +599,7 @@ class WindowsBuilder:
def main() -> int:
"""Build Windows MSI installer."""
parser = argparse.ArgumentParser(
description="Build WebDrop Bridge Windows installer"
)
parser = argparse.ArgumentParser(description="Build WebDrop Bridge Windows installer")
parser.add_argument(
"--msi",
action="store_true",
@ -613,9 +616,9 @@ def main() -> int:
default=None,
help="Path to .env file to bundle (default: project root .env)",
)
args = parser.parse_args()
print("🔄 Syncing version...")
do_sync_version()