Add test script for MSI creation using WindowsBuilder

This commit introduces a new test script, `test_msi.py`, which automates the process of creating an MSI installer. The script utilizes the `WindowsBuilder` class to generate the installer and checks for its successful creation, providing feedback on the result and the file size.
This commit is contained in:
claudi 2026-02-18 15:14:21 +01:00
parent 6213bbfa0a
commit 2b12ee2aef
6 changed files with 11945 additions and 33 deletions

File diff suppressed because one or more lines are too long

View file

@ -4,11 +4,11 @@
Manufacturer="HIM-Tools"
UpgradeCode="12345678-1234-1234-1234-123456789012">
<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />
<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" Platform="x64" />
<Media Id="1" Cabinet="WebDropBridge.cab" EmbedCab="yes" />
<Feature Id="ProductFeature" Title="WebDrop Bridge" Level="1">
<ComponentRef Id="MainExecutable" />
<ComponentGroupRef Id="AppFiles" />
<ComponentRef Id="ProgramMenuShortcut" />
</Feature>
@ -21,12 +21,6 @@
</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"

File diff suppressed because one or more lines are too long

11805
build/WebDropBridge_Files.wxs Normal file

File diff suppressed because it is too large Load diff

View file

@ -190,45 +190,76 @@ class WindowsBuilder:
print(" Or use: choco install wixtoolset")
return False
# Create WiX source file
# Create base WiX source file
if not self._create_wix_source():
return False
# Compile and link
# Harvest application files using Heat
print(f" Harvesting application files...")
dist_folder = self.dist_dir / "WebDropBridge"
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",
"-sfrag",
"-srd",
"-gg",
"-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)")
if result.stderr:
print(result.stderr[:200]) # Show first 200 chars of errors
else:
print(f" ✓ Harvested files")
# Compile both WiX files
wix_obj = self.build_dir / "WebDropBridge.wixobj"
wix_files_obj = self.build_dir / "WebDropBridge_Files.wixobj"
msi_output = self.dist_dir / f"WebDropBridge-{self.version}-Setup.msi"
# Run candle (compiler) - pass preprocessor variables
# Run candle compiler - make sure to use correct source directory
candle_cmd = [
str(candle_exe),
f"-dDistDir={self.dist_dir}",
"-o",
str(wix_obj),
f"-dSourceDir={self.dist_dir}\\WebDropBridge", # Set SourceDir for Heat-generated files
"-o", str(self.build_dir) + "\\",
str(self.build_dir / "WebDropBridge.wxs"),
]
if harvest_file.exists():
candle_cmd.append(str(harvest_file))
print(f" Compiling WiX source...")
result = subprocess.run(
candle_cmd,
text=True
)
result = subprocess.run(candle_cmd, text=True, cwd=str(self.build_dir))
if result.returncode != 0:
print("❌ WiX compilation failed")
return False
# Run light (linker)
# Link MSI - include both obj files if harvest was successful
light_cmd = [
str(light_exe),
"-o",
str(msi_output),
"-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
)
result = subprocess.run(light_cmd, text=True)
if result.returncode != 0:
print("❌ MSI linking failed")
return False
@ -251,11 +282,11 @@ class WindowsBuilder:
Manufacturer="HIM-Tools"
UpgradeCode="12345678-1234-1234-1234-123456789012">
<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />
<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" Platform="x64" />
<Media Id="1" Cabinet="WebDropBridge.cab" EmbedCab="yes" />
<Feature Id="ProductFeature" Title="WebDrop Bridge" Level="1">
<ComponentRef Id="MainExecutable" />
<ComponentGroupRef Id="AppFiles" />
<ComponentRef Id="ProgramMenuShortcut" />
</Feature>
@ -268,12 +299,6 @@ class WindowsBuilder:
</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"
@ -300,6 +325,69 @@ class WindowsBuilder:
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: 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)
elements.append(f'{indent_str}<File Id="{file_id}" Source="{file_path}" />')
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
)
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
"""
# Remove extension and invalid characters
safe_name = filename.rsplit(".", 1)[0] if "." in filename else filename
# Replace invalid characters with underscores
safe_name = "".join(c if c.isalnum() or c == "_" else "_" for c in safe_name)
# Ensure it starts with a letter or underscore
if safe_name and not (safe_name[0].isalpha() or safe_name[0] == "_"):
safe_name = f"_{safe_name}"
# Limit length to avoid WiX ID limits
return safe_name[:50] if len(safe_name) > 50 else safe_name
def sign_executable(self, cert_path: str, password: str) -> bool:
"""Sign executable with certificate (optional).

24
test_msi.py Normal file
View file

@ -0,0 +1,24 @@
#!/usr/bin/env python
"""Test MSI creation."""
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).parent / "build" / "scripts"))
from build_windows import WindowsBuilder
if __name__ == "__main__":
builder = WindowsBuilder()
print("Creating MSI installer...")
result = builder.create_msi()
print(f"MSI Creation Result: {result}")
# Check if MSI was created
msi_path = builder.dist_dir / f"WebDropBridge-{builder.version}-Setup.msi"
if msi_path.exists():
print(f"\n✅ MSI created successfully: {msi_path}")
print(f" Size: {msi_path.stat().st_size / 1024 / 1024:.1f} MB")
else:
print(f"\n❌ MSI not found: {msi_path}")