Installers for all platforms and related fixes #1
@@ -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
@@ -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
@@ -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:")
|
||||
|
||||
Reference in New Issue
Block a user