Bundle PyQtWebEngine on macOS and ship the app as a DMG
release / gui-build (linux) (push) Successful in 1m13s
release / gui-build (macos) (push) Successful in 1m15s
release / gui-build (windows) (push) Failing after 1s

pywebview[qt] no longer depends on PyQtWebEngine, so fresh CI builds
produced a macOS app without its Chromium engine that crashed on launch.
Package MediaHive.app as a compressed DMG instead of zipping the raw
onedir folder; release.py accepts .dmg artifacts.
This commit is contained in:
2026-09-22 04:07:30 +00:00
parent ff10c86e84
commit 5300fd0c9c
3 changed files with 55 additions and 20 deletions
+2
View File
@@ -54,6 +54,8 @@ gui = [
"pywebview>=6.2.1; platform_system == 'Windows'",
"qtpy>=2.4.1; platform_system == 'Darwin'",
"PyQt5>=5.15.11; platform_system == 'Darwin'",
# pywebview[qt] no longer pulls this in; the macOS Qt backend needs it
"PyQtWebEngine>=5.15.7; platform_system == 'Darwin'",
"pythonnet>=3.1.0rc0; platform_system == 'Windows' and python_version >= '3.14'",
"pyinstaller>=6.0",
]
+35 -5
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env -S uv run
"""Build the desktop GUI application and package it as a version-numbered ZIP.
"""Build the desktop GUI application and package it as a versioned ZIP/DMG.
Usage:
uv run scripts/winbuild.py
@@ -13,7 +13,7 @@ This script:
3. On Windows, downloads the latest ffmpeg.exe for bundling
4. On macOS arm64, downloads a prebuilt ffmpeg binary for bundling
4. Builds MediaHive using PyInstaller
5. Creates a ZIP file with the version number
5. Creates a versioned ZIP (Windows/Linux) or DMG (macOS) artifact
"""
import io
@@ -238,6 +238,33 @@ def create_zip(version: str) -> Path:
return zip_path
def create_dmg(version: str) -> Path:
"""Create a version-numbered DMG containing MediaHive.app (macOS only)."""
repo_root = _REPO_ROOT
app_bundle = repo_root / "build" / "MediaHive.app"
if not app_bundle.exists():
raise FileNotFoundError(f"App bundle not found: {app_bundle}")
dmg_path = repo_root / "build" / f"MediaHive-{version}-{_platform_zip_suffix()}.dmg"
dmg_path.parent.mkdir(parents=True, exist_ok=True)
dmg_path.unlink(missing_ok=True)
cmd = [
"hdiutil", "create",
"-volname", "MediaHive",
"-srcfolder", str(app_bundle),
"-ov",
"-format", "UDZO",
str(dmg_path),
]
print(f"Running: {' '.join(cmd)}")
result = subprocess.run(cmd, cwd=repo_root)
if result.returncode != 0:
raise RuntimeError(f"hdiutil failed with exit code {result.returncode}")
return dmg_path
def main() -> None:
# Windows consoles default to cp1252, which can't encode ✓/✗
sys.stdout.reconfigure(errors="replace")
@@ -258,10 +285,13 @@ def main() -> None:
)
build_wheel()
build_executable()
zip_path = create_zip(version)
if sys.platform == "darwin":
artifact_path = create_dmg(version)
else:
artifact_path = create_zip(version)
print(f"✓ Built successfully: {zip_path}")
print(f" Size: {zip_path.stat().st_size / (1024 * 1024):.1f} MB")
print(f"✓ Built successfully: {artifact_path}")
print(f" Size: {artifact_path.stat().st_size / (1024 * 1024):.1f} MB")
except (FileNotFoundError, OSError, RuntimeError, ValueError) as e:
print(f"✗ Build failed: {e}", file=sys.stderr)
sys.exit(1)
+18 -15
View File
@@ -67,17 +67,17 @@ def load_token() -> str:
# ZIP + dist helpers
# ---------------------------------------------------------------------------
# Matches MediaHive-1.2.3-win64.zip, MediaHive-1.2.3-macos-arm64.zip, etc.
# Matches MediaHive-1.2.3-win64.zip, MediaHive-1.2.3-macos-arm64.dmg, etc.
# Rejects dev/dirty versions like MediaHive-1.2.3.dev0+gabcd-win64.zip
_CLEAN_ZIP_RE = re.compile(r"^MediaHive-(\d+(?:\.\d+)*)-([A-Za-z0-9._-]+)\.zip$")
_CLEAN_ARTIFACT_RE = re.compile(r"^MediaHive-(\d+(?:\.\d+)*)-([A-Za-z0-9._-]+)\.(?:zip|dmg)$")
def find_releasable_zips() -> list[tuple[Path, str, str]]:
"""Return (path, version, platform_tag) for clean-versioned ZIPs in build/."""
def find_releasable_artifacts() -> list[tuple[Path, str, str]]:
"""Return (path, version, platform_tag) for clean-versioned ZIPs/DMGs in build/."""
build_dir = REPO_ROOT / "build"
results = []
for p in sorted(build_dir.glob("MediaHive-*.zip")):
m = _CLEAN_ZIP_RE.match(p.name)
for p in sorted(build_dir.glob("MediaHive-*")):
m = _CLEAN_ARTIFACT_RE.match(p.name)
if m:
results.append((p, m.group(1), m.group(2)))
return results
@@ -180,7 +180,10 @@ def upload_asset(
"""Upload a file to the release and return the download URL."""
url = f"{base_url}/api/v1/repos/{repo}/releases/{release_id}/assets"
size_mb = path.stat().st_size / (1024 * 1024)
mime = "application/zip" if path.suffix == ".zip" else "application/octet-stream"
mime = {
".zip": "application/zip",
".dmg": "application/x-apple-diskimage",
}.get(path.suffix, "application/octet-stream")
print(f"Uploading {path.name} ({size_mb:.1f} MB) ...")
with Path(path).open("rb") as fh:
resp = client.post(
@@ -222,10 +225,10 @@ def main() -> None:
cfg = load_gitea_config()
token = load_token()
zips = find_releasable_zips()
if not zips:
artifacts = find_releasable_artifacts()
if not artifacts:
print(
"No clean-versioned ZIPs found in build/.\n"
"No clean-versioned ZIPs/DMGs found in build/.\n"
"Run scripts/guibuild.py first.",
file=sys.stderr,
)
@@ -234,7 +237,7 @@ def main() -> None:
# Validate all dist files exist before touching Gitea
dist_files: dict[str, list[Path]] = {}
if not args.no_dist:
for _, version, _platform_tag in zips:
for _, version, _platform_tag in artifacts:
dist_files[version] = find_dist_files(version)
base_url = cfg["url"].rstrip("/")
@@ -242,7 +245,7 @@ def main() -> None:
with httpx.Client(headers=gitea_headers(token)) as client:
releases: dict[str, tuple[int, set[str]]] = {}
for zip_path, version, platform_tag in zips:
for artifact_path, version, platform_tag in artifacts:
print(f"\nReleasing {version} ...")
tag = f"v{version}"
if version not in releases:
@@ -257,11 +260,11 @@ def main() -> None:
upload_asset(client, base_url, repo, release_id, path)
release_id, uploaded = releases[version]
if zip_path.name in uploaded:
print(f"Skipping {zip_path.name}, already on the release.")
if artifact_path.name in uploaded:
print(f"Skipping {artifact_path.name}, already on the release.")
continue
print(f"Uploading platform artifact: {platform_tag}")
upload_asset(client, base_url, repo, release_id, zip_path)
upload_asset(client, base_url, repo, release_id, artifact_path)
print(f"{tag} published")
print("\nDone. To publish to PyPI, run:")