Some checks are pending
Tests & Quality Checks / Test on Python 3.11 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.12 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.11-1 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.12-1 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.10 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.11-2 (push) Waiting to run
Tests & Quality Checks / Test on Python 3.12-2 (push) Waiting to run
Tests & Quality Checks / Build Artifacts (push) Blocked by required conditions
Tests & Quality Checks / Build Artifacts-1 (push) Blocked by required conditions
397 lines
14 KiB
Python
397 lines
14 KiB
Python
"""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
|
|
toolbar_icon_home: str
|
|
toolbar_icon_reload: str
|
|
toolbar_icon_open: str
|
|
toolbar_icon_openwith: str
|
|
|
|
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",
|
|
"toolbar_icon_home": "resources/icons/home.ico",
|
|
"toolbar_icon_reload": "resources/icons/reload.ico",
|
|
"toolbar_icon_open": "resources/icons/open.ico",
|
|
"toolbar_icon_openwith": "resources/icons/openwith.ico",
|
|
}
|
|
|
|
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"),
|
|
toolbar_icon_home=str(
|
|
values.get("toolbar_icon_home", DEFAULT_BRAND_VALUES["toolbar_icon_home"])
|
|
),
|
|
toolbar_icon_reload=str(
|
|
values.get("toolbar_icon_reload", DEFAULT_BRAND_VALUES["toolbar_icon_reload"])
|
|
),
|
|
toolbar_icon_open=str(
|
|
values.get("toolbar_icon_open", DEFAULT_BRAND_VALUES["toolbar_icon_open"])
|
|
),
|
|
toolbar_icon_openwith=str(
|
|
values.get("toolbar_icon_openwith", DEFAULT_BRAND_VALUES["toolbar_icon_openwith"])
|
|
),
|
|
)
|
|
|
|
|
|
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),
|
|
"WEBDROP_TOOLBAR_ICON_HOME": brand.toolbar_icon_home,
|
|
"WEBDROP_TOOLBAR_ICON_RELOAD": brand.toolbar_icon_reload,
|
|
"WEBDROP_TOOLBAR_ICON_OPEN": brand.toolbar_icon_open,
|
|
"WEBDROP_TOOLBAR_ICON_OPENWITH": brand.toolbar_icon_openwith,
|
|
}
|
|
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,
|
|
"toolbar_icon_home": brand.toolbar_icon_home,
|
|
"toolbar_icon_reload": brand.toolbar_icon_reload,
|
|
"toolbar_icon_open": brand.toolbar_icon_open,
|
|
"toolbar_icon_openwith": brand.toolbar_icon_openwith,
|
|
},
|
|
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())
|