"""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", } DEFAULT_BRAND_ID = str(DEFAULT_BRAND_VALUES["brand_id"]) 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 available_brand_names(root: Path | None = None) -> list[str]: """Return all supported brand names, including the default build.""" base = root or project_root() names = [DEFAULT_BRAND_ID] manifest_dir = brands_dir(base) if manifest_dir.exists(): for manifest in sorted(manifest_dir.glob("*.json")): if manifest.stem not in names: names.append(manifest.stem) return names 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 and brand != DEFAULT_BRAND_ID: 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 merge_release_manifests( base_manifest: dict[str, Any], overlay_manifest: dict[str, Any] ) -> dict[str, Any]: """Merge two release manifests, preserving previously uploaded platforms.""" merged: dict[str, Any] = { "version": overlay_manifest.get("version") or base_manifest.get("version", ""), "channel": overlay_manifest.get("channel") or base_manifest.get("channel", "stable"), "brands": dict(base_manifest.get("brands", {})), } for brand_id, entries in overlay_manifest.get("brands", {}).items(): brand_entry = dict(merged["brands"].get(brand_id, {})) for platform_key, platform_value in entries.items(): if platform_value: brand_entry[platform_key] = platform_value merged["brands"][brand_id] = brand_entry return merged def collect_local_release_data( version: str, *, platform: str, root: Path | None = None, brands: list[str] | None = None, ) -> dict[str, Any]: """Collect local artifacts and manifest entries for the requested platform.""" base = root or project_root() selected_brands = brands or available_brand_names(base) release_manifest: dict[str, Any] = { "version": version, "channel": "stable", "brands": {}, } artifacts: list[str] = [] found_brands: list[str] = [] for brand_name in selected_brands: brand = load_brand_config(brand_name, root=base) release_manifest["channel"] = brand.update_channel if platform == "windows": artifact_dir = base / "build" / "dist" / "windows" / brand.brand_id installer = artifact_dir / brand.windows_installer_name(version) checksum = artifact_dir / f"{installer.name}.sha256" platform_key = "windows-x64" elif platform == "macos": artifact_dir = base / "build" / "dist" / "macos" / brand.brand_id installer = artifact_dir / brand.macos_installer_name(version) checksum = artifact_dir / f"{installer.name}.sha256" platform_key = "macos-universal" if not installer.exists() and brand.brand_id == DEFAULT_BRAND_ID: legacy_installer = (base / "build" / "dist" / "macos") / brand.macos_installer_name( version ) legacy_checksum = legacy_installer.parent / f"{legacy_installer.name}.sha256" if legacy_installer.exists(): installer = legacy_installer checksum = legacy_checksum else: raise ValueError(f"Unsupported platform: {platform}") if not installer.exists(): continue found_brands.append(brand.brand_id) artifacts.append(str(installer)) if checksum.exists(): artifacts.append(str(checksum)) release_manifest["brands"].setdefault(brand.brand_id, {})[platform_key] = { "installer": installer.name, "checksum": checksum.name if checksum.exists() else "", } return { "version": version, "platform": platform, "brands": found_brands, "artifacts": artifacts, "manifest": release_manifest, } 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_UPDATE_CHANNEL": brand.update_channel, "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_local_release_data(args: argparse.Namespace) -> int: data = collect_local_release_data( args.version, platform=args.platform, brands=args.brands, ) print(json.dumps(data, indent=2)) return 0 def cli_merge_manifests(args: argparse.Namespace) -> int: base_manifest = json.loads(Path(args.base).read_text(encoding="utf-8")) overlay_manifest = json.loads(Path(args.overlay).read_text(encoding="utf-8")) merged = merge_release_manifests(base_manifest, overlay_manifest) output_path = Path(args.output) output_path.write_text(json.dumps(merged, indent=2), encoding="utf-8") print(output_path) 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) local_parser = subparsers.add_parser("local-release-data") local_parser.add_argument("--version", required=True) local_parser.add_argument("--platform", choices=["windows", "macos"], required=True) local_parser.add_argument("--brands", nargs="+") local_parser.set_defaults(func=cli_local_release_data) merge_parser = subparsers.add_parser("merge-manifests") merge_parser.add_argument("--base", required=True) merge_parser.add_argument("--overlay", required=True) merge_parser.add_argument("--output", required=True) merge_parser.set_defaults(func=cli_merge_manifests) 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())