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:
parent
88d9f200ab
commit
025e9c888c
10 changed files with 3066 additions and 3088 deletions
File diff suppressed because one or more lines are too long
|
|
@ -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
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue