feat: Implement brand-aware release creation for Agravity
Some checks failed
Tests & Quality Checks / Test on Python 3.11 (push) Has been cancelled
Tests & Quality Checks / Test on Python 3.12 (push) Has been cancelled
Tests & Quality Checks / Test on Python 3.11-1 (push) Has been cancelled
Tests & Quality Checks / Test on Python 3.12-1 (push) Has been cancelled
Tests & Quality Checks / Test on Python 3.10 (push) Has been cancelled
Tests & Quality Checks / Test on Python 3.11-2 (push) Has been cancelled
Tests & Quality Checks / Test on Python 3.12-2 (push) Has been cancelled
Tests & Quality Checks / Build Artifacts (push) Has been cancelled
Tests & Quality Checks / Build Artifacts-1 (push) Has been cancelled

- Added support for multiple brands in release scripts, allowing for branded artifacts.
- Introduced brand configuration management with JSON files for each brand.
- Created a new `brand_config.py` script to handle brand-specific logic and asset resolution.
- Updated `create_release.ps1` and `create_release.sh` scripts to utilize brand configurations and generate release manifests.
- Added unit tests for brand configuration loading and release manifest generation.
- Introduced `agravity` brand with its specific configuration in `agravity.json`.
This commit is contained in:
claudi 2026-03-10 16:18:28 +01:00
parent b988532aaa
commit fd69996c53
8 changed files with 552 additions and 409 deletions

View file

@ -39,13 +39,14 @@ from pathlib import Path
from datetime import datetime
# Import shared version utilities
from brand_config import load_brand_config
from sync_version import get_current_version, do_sync_version
class WindowsBuilder:
"""Build Windows installer using PyInstaller."""
def __init__(self, env_file: Path | None = None):
def __init__(self, env_file: Path | None = None, brand: str | None = None):
"""Initialize builder paths.
Args:
@ -53,10 +54,12 @@ class WindowsBuilder:
If that doesn't exist, raises error.
"""
self.project_root = Path(__file__).parent.parent.parent
self.brand = load_brand_config(brand, root=self.project_root)
self.build_dir = self.project_root / "build"
self.dist_dir = self.build_dir / "dist" / "windows"
self.temp_dir = self.build_dir / "temp" / "windows"
self.dist_dir = self.build_dir / "dist" / "windows" / self.brand.brand_id
self.temp_dir = self.build_dir / "temp" / "windows" / self.brand.brand_id
self.spec_file = self.build_dir / "webdrop_bridge.spec"
self.wix_template = self.build_dir / "WebDropBridge.wxs"
self.version = get_current_version()
# Validate and set env file
@ -74,6 +77,7 @@ class WindowsBuilder:
self.env_file = env_file
print(f"📋 Using configuration: {self.env_file}")
print(f"🏷️ Building brand: {self.brand.display_name} ({self.brand.brand_id})")
def _get_version(self) -> str:
"""Get version from __init__.py.
@ -116,6 +120,15 @@ class WindowsBuilder:
# Set environment variable for spec file to use
env = os.environ.copy()
env["WEBDROP_ENV_FILE"] = str(self.env_file)
env["WEBDROP_BRAND_ID"] = self.brand.brand_id
env["WEBDROP_APP_DISPLAY_NAME"] = self.brand.display_name
env["WEBDROP_ASSET_PREFIX"] = self.brand.asset_prefix
env["WEBDROP_EXE_NAME"] = self.brand.exe_name
env["WEBDROP_BUNDLE_ID"] = self.brand.bundle_identifier
env["WEBDROP_CONFIG_DIR_NAME"] = self.brand.config_dir_name
env["WEBDROP_ICON_ICO"] = str(self.brand.icon_ico)
env["WEBDROP_ICON_ICNS"] = str(self.brand.icon_icns)
env["WEBDROP_VERSION"] = self.version
result = subprocess.run(cmd, cwd=str(self.project_root), text=True, env=env)
@ -123,8 +136,8 @@ class WindowsBuilder:
print("❌ PyInstaller build failed")
return False
# Check if executable exists (now in WebDropBridge/WebDropBridge.exe due to COLLECT)
exe_path = self.dist_dir / "WebDropBridge" / "WebDropBridge.exe"
# Check if executable exists (inside the COLLECT directory)
exe_path = self.dist_dir / self.brand.exe_name / f"{self.brand.exe_name}.exe"
if not exe_path.exists():
print(f"❌ Executable not found at {exe_path}")
return False
@ -134,7 +147,9 @@ class WindowsBuilder:
# Calculate total dist size
total_size = sum(
f.stat().st_size for f in self.dist_dir.glob("WebDropBridge/**/*") if f.is_file()
f.stat().st_size
for f in self.dist_dir.glob(f"{self.brand.exe_name}/**/*")
if f.is_file()
)
if total_size > 0:
print(f" Total size: {total_size / 1024 / 1024:.1f} MB")
@ -249,7 +264,7 @@ class WindowsBuilder:
# Harvest application files using Heat
print(f" Harvesting application files...")
dist_folder = self.dist_dir / "WebDropBridge"
dist_folder = self.dist_dir / self.brand.exe_name
if not dist_folder.exists():
print(f"❌ Distribution folder not found: {dist_folder}")
return False
@ -291,7 +306,7 @@ class WindowsBuilder:
# 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"
msi_output = self.dist_dir / self.brand.windows_installer_name(self.version)
# Run candle compiler - make sure to use correct source directory
candle_cmd = [
@ -301,11 +316,11 @@ class WindowsBuilder:
"-ext",
"WixUtilExtension",
f"-dDistDir={self.dist_dir}",
f"-dSourceDir={self.dist_dir}\\WebDropBridge", # Set SourceDir for Heat-generated files
f"-dSourceDir={self.dist_dir}\{self.brand.exe_name}", # Set SourceDir for Heat-generated files
f"-dResourcesDir={self.project_root}\\resources", # Set ResourcesDir for branding assets
"-o",
str(self.build_dir) + "\\",
str(self.build_dir / "WebDropBridge.wxs"),
str(self.build_dir / "WebDropBridge.generated.wxs"),
]
if harvest_file.exists():
@ -325,7 +340,7 @@ class WindowsBuilder:
"-ext",
"WixUtilExtension",
"-b",
str(self.dist_dir / "WebDropBridge"), # Base path for source files
str(self.dist_dir / self.brand.exe_name), # Base path for source files
"-o",
str(msi_output),
str(wix_obj),
@ -353,6 +368,7 @@ class WindowsBuilder:
print("✅ MSI installer created successfully")
print(f"📦 Output: {msi_output}")
print(f" Size: {msi_output.stat().st_size / 1024 / 1024:.1f} MB")
self.generate_checksum(msi_output)
return True
@ -363,7 +379,7 @@ class WindowsBuilder:
even if a previous PyInstaller run omitted them.
"""
src_icons_dir = self.project_root / "resources" / "icons"
bundle_icons_dir = self.dist_dir / "WebDropBridge" / "_internal" / "resources" / "icons"
bundle_icons_dir = self.dist_dir / self.brand.exe_name / "_internal" / "resources" / "icons"
required_icons = ["home.ico", "reload.ico", "open.ico", "openwith.ico"]
try:
@ -392,97 +408,23 @@ class WindowsBuilder:
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 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="{self.version}"
Manufacturer="HIM-Tools"
UpgradeCode="12345678-1234-1234-1234-123456789012">
wix_template = self.wix_template.read_text(encoding="utf-8")
wix_content = wix_template.format(
product_name=self.brand.display_name,
version=self.version,
manufacturer=self.brand.manufacturer,
upgrade_code=self.brand.msi_upgrade_code,
asset_prefix=self.brand.asset_prefix,
icon_ico=str(self.brand.icon_ico),
dialog_bmp=str(self.brand.dialog_bmp),
banner_bmp=str(self.brand.banner_bmp),
license_rtf=str(self.brand.license_rtf),
exe_name=self.brand.exe_name,
install_dir_name=self.brand.install_dir_name,
shortcut_description=self.brand.shortcut_description,
)
<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" Platform="x64" />
<Media Id="1" Cabinet="WebDropBridge.cab" EmbedCab="yes" />
<!-- Required property for WixUI_InstallDir dialog set -->
<Property Id="WIXUI_INSTALLDIR" Value="INSTALLFOLDER" />
<!-- Application Icon -->
<Icon Id="AppIcon.ico" SourceFile="$(var.ResourcesDir)\\icons\\app.ico" />
<!-- Custom branding for InstallDir dialog set -->
<WixVariable Id="WixUIDialogBmp" Value="$(var.ResourcesDir)\\icons\\background.bmp" />
<WixVariable Id="WixUIBannerBmp" Value="$(var.ResourcesDir)\\icons\\banner.bmp" />
<WixVariable Id="WixUILicenseRtf" Value="$(var.ResourcesDir)\\license.rtf" />
<!-- Installation UI dialogs -->
<UIRef Id="WixUI_InstallDir" />
<UIRef Id="WixUI_ErrorProgressText" />
<!-- Close running application before installation -->
<util:CloseApplication
Target="WebDropBridge.exe"
CloseMessage="yes"
RebootPrompt="no"
ElevatedCloseMessage="no" />
<Feature Id="ProductFeature" Title="WebDrop Bridge" Level="1">
<ComponentGroupRef Id="AppFiles" />
<ComponentRef Id="ProgramMenuShortcut" />
<ComponentRef Id="DesktopShortcut" />
</Feature>
<Directory Id="TARGETDIR" Name="SourceDir">
<Directory Id="ProgramFiles64Folder">
<Directory Id="INSTALLFOLDER" Name="WebDrop Bridge" />
</Directory>
<Directory Id="ProgramMenuFolder">
<Directory Id="ApplicationProgramsFolder" Name="WebDrop Bridge"/>
</Directory>
<Directory Id="DesktopFolder" />
</Directory>
<DirectoryRef Id="ApplicationProgramsFolder">
<Component Id="ProgramMenuShortcut" Guid="*">
<Shortcut Id="ApplicationStartMenuShortcut"
Name="WebDrop Bridge"
Description="Web Drag-and-Drop Bridge"
Target="[INSTALLFOLDER]WebDropBridge.exe"
Icon="AppIcon.ico"
IconIndex="0"
WorkingDirectory="INSTALLFOLDER" />
<RemoveFolder Id="ApplicationProgramsFolderRemove"
On="uninstall" />
<RegistryValue Root="HKCU"
Key="Software\\Microsoft\\Windows\\CurrentVersion\\Uninstall\\WebDropBridge"
Name="installed"
Type="integer"
Value="1"
KeyPath="yes" />
</Component>
</DirectoryRef>
<DirectoryRef Id="DesktopFolder">
<Component Id="DesktopShortcut" Guid="*">
<Shortcut Id="DesktopApplicationShortcut"
Name="WebDrop Bridge"
Description="Web Drag-and-Drop Bridge"
Target="[INSTALLFOLDER]WebDropBridge.exe"
Icon="AppIcon.ico"
IconIndex="0"
WorkingDirectory="INSTALLFOLDER" />
<RegistryValue Root="HKCU"
Key="Software\\WebDropBridge"
Name="DesktopShortcut"
Type="integer"
Value="1"
KeyPath="yes" />
</Component>
</DirectoryRef>
</Product>
</Wix>
"""
wix_file = self.build_dir / "WebDropBridge.wxs"
wix_file = self.build_dir / "WebDropBridge.generated.wxs"
wix_file.write_text(wix_content)
print(f" Created WiX source: {wix_file}")
return True
@ -573,7 +515,7 @@ class WindowsBuilder:
print(" Skipping code signing")
return True
exe_path = self.dist_dir / "WebDropBridge.exe"
exe_path = self.dist_dir / self.brand.exe_name / f"{self.brand.exe_name}.exe"
cmd = [
signtool,
"sign",
@ -606,7 +548,7 @@ class WindowsBuilder:
"""
start_time = datetime.now()
print("=" * 60)
print("🚀 WebDrop Bridge Windows Build")
print(f"🚀 {self.brand.display_name} Windows Build")
print("=" * 60)
self.clean()
@ -650,6 +592,12 @@ def main() -> int:
default=None,
help="Path to .env file to bundle (default: project root .env)",
)
parser.add_argument(
"--brand",
type=str,
default=None,
help="Brand manifest name from build/brands (e.g. agravity)",
)
args = parser.parse_args()
@ -657,7 +605,7 @@ def main() -> int:
do_sync_version()
try:
builder = WindowsBuilder(env_file=args.env_file)
builder = WindowsBuilder(env_file=args.env_file, brand=args.brand)
except FileNotFoundError as e:
print(f"❌ Build failed: {e}")
return 1