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"
|
Manufacturer="HIM-Tools"
|
||||||
UpgradeCode="12345678-1234-1234-1234-123456789012">
|
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" />
|
<Media Id="1" Cabinet="WebDropBridge.cab" EmbedCab="yes" />
|
||||||
|
|
||||||
<Feature Id="ProductFeature" Title="WebDrop Bridge" Level="1">
|
<Feature Id="ProductFeature" Title="WebDrop Bridge" Level="1">
|
||||||
<ComponentRef Id="MainExecutable" />
|
<ComponentGroupRef Id="AppFiles" />
|
||||||
<ComponentRef Id="ProgramMenuShortcut" />
|
<ComponentRef Id="ProgramMenuShortcut" />
|
||||||
</Feature>
|
</Feature>
|
||||||
|
|
||||||
|
|
@ -21,12 +21,6 @@
|
||||||
</Directory>
|
</Directory>
|
||||||
</Directory>
|
</Directory>
|
||||||
|
|
||||||
<DirectoryRef Id="INSTALLFOLDER">
|
|
||||||
<Component Id="MainExecutable" Guid="*">
|
|
||||||
<File Id="WebDropBridgeExe" Source="$(var.DistDir)\WebDropBridge.exe" KeyPath="yes"/>
|
|
||||||
</Component>
|
|
||||||
</DirectoryRef>
|
|
||||||
|
|
||||||
<DirectoryRef Id="ApplicationProgramsFolder">
|
<DirectoryRef Id="ApplicationProgramsFolder">
|
||||||
<Component Id="ProgramMenuShortcut" Guid="*">
|
<Component Id="ProgramMenuShortcut" Guid="*">
|
||||||
<Shortcut Id="ApplicationStartMenuShortcut"
|
<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")
|
print(" Or use: choco install wixtoolset")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Create WiX source file
|
# Create base WiX source file
|
||||||
if not self._create_wix_source():
|
if not self._create_wix_source():
|
||||||
return False
|
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_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"
|
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 = [
|
candle_cmd = [
|
||||||
str(candle_exe),
|
str(candle_exe),
|
||||||
f"-dDistDir={self.dist_dir}",
|
f"-dDistDir={self.dist_dir}",
|
||||||
"-o",
|
f"-dSourceDir={self.dist_dir}\\WebDropBridge", # Set SourceDir for Heat-generated files
|
||||||
str(wix_obj),
|
"-o", str(self.build_dir) + "\\",
|
||||||
str(self.build_dir / "WebDropBridge.wxs"),
|
str(self.build_dir / "WebDropBridge.wxs"),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if harvest_file.exists():
|
||||||
|
candle_cmd.append(str(harvest_file))
|
||||||
|
|
||||||
print(f" Compiling WiX source...")
|
print(f" Compiling WiX source...")
|
||||||
result = subprocess.run(
|
result = subprocess.run(candle_cmd, text=True, cwd=str(self.build_dir))
|
||||||
candle_cmd,
|
|
||||||
text=True
|
|
||||||
)
|
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
print("❌ WiX compilation failed")
|
print("❌ WiX compilation failed")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
# Run light (linker)
|
# Link MSI - include both obj files if harvest was successful
|
||||||
light_cmd = [
|
light_cmd = [
|
||||||
str(light_exe),
|
str(light_exe),
|
||||||
"-o",
|
"-b", str(self.dist_dir / "WebDropBridge"), # Base path for source files
|
||||||
str(msi_output),
|
"-o", str(msi_output),
|
||||||
str(wix_obj),
|
str(wix_obj),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
if wix_files_obj.exists():
|
||||||
|
light_cmd.append(str(wix_files_obj))
|
||||||
|
|
||||||
print(f" Linking MSI installer...")
|
print(f" Linking MSI installer...")
|
||||||
result = subprocess.run(
|
result = subprocess.run(light_cmd, text=True)
|
||||||
light_cmd,
|
|
||||||
text=True
|
|
||||||
)
|
|
||||||
if result.returncode != 0:
|
if result.returncode != 0:
|
||||||
print("❌ MSI linking failed")
|
print("❌ MSI linking failed")
|
||||||
return False
|
return False
|
||||||
|
|
@ -251,11 +282,11 @@ class WindowsBuilder:
|
||||||
Manufacturer="HIM-Tools"
|
Manufacturer="HIM-Tools"
|
||||||
UpgradeCode="12345678-1234-1234-1234-123456789012">
|
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" />
|
<Media Id="1" Cabinet="WebDropBridge.cab" EmbedCab="yes" />
|
||||||
|
|
||||||
<Feature Id="ProductFeature" Title="WebDrop Bridge" Level="1">
|
<Feature Id="ProductFeature" Title="WebDrop Bridge" Level="1">
|
||||||
<ComponentRef Id="MainExecutable" />
|
<ComponentGroupRef Id="AppFiles" />
|
||||||
<ComponentRef Id="ProgramMenuShortcut" />
|
<ComponentRef Id="ProgramMenuShortcut" />
|
||||||
</Feature>
|
</Feature>
|
||||||
|
|
||||||
|
|
@ -268,12 +299,6 @@ class WindowsBuilder:
|
||||||
</Directory>
|
</Directory>
|
||||||
</Directory>
|
</Directory>
|
||||||
|
|
||||||
<DirectoryRef Id="INSTALLFOLDER">
|
|
||||||
<Component Id="MainExecutable" Guid="*">
|
|
||||||
<File Id="WebDropBridgeExe" Source="$(var.DistDir)\\WebDropBridge.exe" KeyPath="yes"/>
|
|
||||||
</Component>
|
|
||||||
</DirectoryRef>
|
|
||||||
|
|
||||||
<DirectoryRef Id="ApplicationProgramsFolder">
|
<DirectoryRef Id="ApplicationProgramsFolder">
|
||||||
<Component Id="ProgramMenuShortcut" Guid="*">
|
<Component Id="ProgramMenuShortcut" Guid="*">
|
||||||
<Shortcut Id="ApplicationStartMenuShortcut"
|
<Shortcut Id="ApplicationStartMenuShortcut"
|
||||||
|
|
@ -300,6 +325,69 @@ class WindowsBuilder:
|
||||||
print(f" Created WiX source: {wix_file}")
|
print(f" Created WiX source: {wix_file}")
|
||||||
return True
|
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:
|
def sign_executable(self, cert_path: str, password: str) -> bool:
|
||||||
"""Sign executable with certificate (optional).
|
"""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