"""Brand-aware build configuration helpers.""" from __future__ import annotations import argparse import json from dataclasses import dataclass from pathlib import Path from typing import Any @dataclass(frozen=True) class BrandConfig: """Packaging metadata for a branded build.""" brand_id: str display_name: str asset_prefix: str exe_name: str manufacturer: str install_dir_name: str shortcut_description: str bundle_identifier: str config_dir_name: str msi_upgrade_code: str update_channel: str icon_ico: Path icon_icns: Path dialog_bmp: Path banner_bmp: Path license_rtf: Path def windows_installer_name(self, version: str) -> str: return f"{self.asset_prefix}-{version}-win-x64.msi" def macos_installer_name(self, version: str) -> str: return f"{self.asset_prefix}-{version}-macos-universal.dmg" @property def app_bundle_name(self) -> str: return f"{self.asset_prefix}.app" DEFAULT_BRAND_VALUES: dict[str, Any] = { "brand_id": "webdrop_bridge", "display_name": "WebDrop Bridge", "asset_prefix": "WebDropBridge", "exe_name": "WebDropBridge", "manufacturer": "HIM-Tools", "install_dir_name": "WebDrop Bridge", "shortcut_description": "Web Drag-and-Drop Bridge", "bundle_identifier": "de.him_tools.webdrop-bridge", "config_dir_name": "webdrop_bridge", "msi_upgrade_code": "12345678-1234-1234-1234-123456789012", "update_channel": "stable", "icon_ico": "resources/icons/app.ico", "icon_icns": "resources/icons/app.icns", "dialog_bmp": "resources/icons/background.bmp", "banner_bmp": "resources/icons/banner.bmp", "license_rtf": "resources/license.rtf", } def project_root() -> Path: return Path(__file__).resolve().parents[2] def brands_dir(root: Path | None = None) -> Path: base = root or project_root() return base / "build" / "brands" def load_brand_config( brand: str | None = None, *, root: Path | None = None, manifest_path: Path | None = None, ) -> BrandConfig: """Load a brand manifest with defaults and asset fallbacks.""" base = root or project_root() values = dict(DEFAULT_BRAND_VALUES) if manifest_path is None and brand: manifest_path = brands_dir(base) / f"{brand}.json" if manifest_path and manifest_path.exists(): values.update(json.loads(manifest_path.read_text(encoding="utf-8"))) elif manifest_path and not manifest_path.exists(): raise FileNotFoundError(f"Brand manifest not found: {manifest_path}") def resolve_asset(key: str) -> Path: candidate = base / str(values.get(key, DEFAULT_BRAND_VALUES[key])) if candidate.exists(): return candidate return base / str(DEFAULT_BRAND_VALUES[key]) return BrandConfig( brand_id=str(values["brand_id"]), display_name=str(values["display_name"]), asset_prefix=str(values["asset_prefix"]), exe_name=str(values["exe_name"]), manufacturer=str(values["manufacturer"]), install_dir_name=str(values["install_dir_name"]), shortcut_description=str(values["shortcut_description"]), bundle_identifier=str(values["bundle_identifier"]), config_dir_name=str(values["config_dir_name"]), msi_upgrade_code=str(values["msi_upgrade_code"]), update_channel=str(values.get("update_channel", "stable")), icon_ico=resolve_asset("icon_ico"), icon_icns=resolve_asset("icon_icns"), dialog_bmp=resolve_asset("dialog_bmp"), banner_bmp=resolve_asset("banner_bmp"), license_rtf=resolve_asset("license_rtf"), ) def generate_release_manifest( version: str, brands: list[str], *, output_path: Path, root: Path | None = None, ) -> Path: """Generate a shared release-manifest.json from local build outputs.""" base = root or project_root() manifest: dict[str, Any] = { "version": version, "channel": "stable", "brands": {}, } for brand_name in brands: brand = load_brand_config(brand_name, root=base) manifest["channel"] = brand.update_channel entries: dict[str, dict[str, str]] = {} windows_dir = base / "build" / "dist" / "windows" / brand.brand_id windows_installer = windows_dir / brand.windows_installer_name(version) windows_checksum = windows_dir / f"{windows_installer.name}.sha256" if windows_installer.exists(): entries["windows-x64"] = { "installer": windows_installer.name, "checksum": windows_checksum.name if windows_checksum.exists() else "", } macos_dir = base / "build" / "dist" / "macos" / brand.brand_id macos_installer = macos_dir / brand.macos_installer_name(version) macos_checksum = macos_dir / f"{macos_installer.name}.sha256" if macos_installer.exists(): entries["macos-universal"] = { "installer": macos_installer.name, "checksum": macos_checksum.name if macos_checksum.exists() else "", } if entries: manifest["brands"][brand.brand_id] = entries output_path.parent.mkdir(parents=True, exist_ok=True) output_path.write_text(json.dumps(manifest, indent=2), encoding="utf-8") return output_path def cli_env(args: argparse.Namespace) -> int: brand = load_brand_config(args.brand) assignments = { "WEBDROP_BRAND_ID": brand.brand_id, "WEBDROP_APP_DISPLAY_NAME": brand.display_name, "WEBDROP_ASSET_PREFIX": brand.asset_prefix, "WEBDROP_EXE_NAME": brand.exe_name, "WEBDROP_BUNDLE_ID": brand.bundle_identifier, "WEBDROP_CONFIG_DIR_NAME": brand.config_dir_name, "WEBDROP_ICON_ICO": str(brand.icon_ico), "WEBDROP_ICON_ICNS": str(brand.icon_icns), } for key, value in assignments.items(): print(f'export {key}="{value}"') return 0 def cli_manifest(args: argparse.Namespace) -> int: output = generate_release_manifest( args.version, args.brands, output_path=Path(args.output).resolve(), ) print(output) return 0 def cli_show(args: argparse.Namespace) -> int: brand = load_brand_config(args.brand) print( json.dumps( { "brand_id": brand.brand_id, "display_name": brand.display_name, "asset_prefix": brand.asset_prefix, "exe_name": brand.exe_name, "manufacturer": brand.manufacturer, "install_dir_name": brand.install_dir_name, "shortcut_description": brand.shortcut_description, "bundle_identifier": brand.bundle_identifier, "config_dir_name": brand.config_dir_name, "msi_upgrade_code": brand.msi_upgrade_code, "update_channel": brand.update_channel, }, indent=2, ) ) return 0 def main() -> int: parser = argparse.ArgumentParser(description="Brand-aware build configuration") subparsers = parser.add_subparsers(dest="command", required=True) env_parser = subparsers.add_parser("env") env_parser.add_argument("--brand", required=True) env_parser.set_defaults(func=cli_env) manifest_parser = subparsers.add_parser("release-manifest") manifest_parser.add_argument("--version", required=True) manifest_parser.add_argument("--output", required=True) manifest_parser.add_argument("--brands", nargs="+", required=True) manifest_parser.set_defaults(func=cli_manifest) show_parser = subparsers.add_parser("show") show_parser.add_argument("--brand", required=True) show_parser.set_defaults(func=cli_show) args = parser.parse_args() return args.func(args) if __name__ == "__main__": raise SystemExit(main())