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:
parent
6213bbfa0a
commit
2b12ee2aef
6 changed files with 11945 additions and 33 deletions
File diff suppressed because one or more lines are too long
|
|
@ -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"
|
||||
|
|
|
|||
1
build/WebDropBridge_Files.wixobj
Normal file
1
build/WebDropBridge_Files.wixobj
Normal file
File diff suppressed because one or more lines are too long
11805
build/WebDropBridge_Files.wxs
Normal file
11805
build/WebDropBridge_Files.wxs
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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
24
test_msi.py
Normal 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}")
|
||||
Loading…
Add table
Add a link
Reference in a new issue