Installers for all platforms and related fixes #1

Merged
LeoVasanko merged 38 commits from installer into main 2026-09-23 17:41:21 +00:00
3 changed files with 55 additions and 20 deletions
Showing only changes of commit 5300fd0c9c - Show all commits
+2
View File
@@ -54,6 +54,8 @@ gui = [
"pywebview>=6.2.1; platform_system == 'Windows'", "pywebview>=6.2.1; platform_system == 'Windows'",
"qtpy>=2.4.1; platform_system == 'Darwin'", "qtpy>=2.4.1; platform_system == 'Darwin'",
"PyQt5>=5.15.11; 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'", "pythonnet>=3.1.0rc0; platform_system == 'Windows' and python_version >= '3.14'",
"pyinstaller>=6.0", "pyinstaller>=6.0",
] ]
+35 -5
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env -S uv run #!/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: Usage:
uv run scripts/winbuild.py uv run scripts/winbuild.py
@@ -13,7 +13,7 @@ This script:
3. On Windows, downloads the latest ffmpeg.exe for bundling 3. On Windows, downloads the latest ffmpeg.exe for bundling
4. On macOS arm64, downloads a prebuilt ffmpeg binary for bundling 4. On macOS arm64, downloads a prebuilt ffmpeg binary for bundling
4. Builds MediaHive using PyInstaller 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 import io
@@ -238,6 +238,33 @@ def create_zip(version: str) -> Path:
return zip_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: def main() -> None:
# Windows consoles default to cp1252, which can't encode ✓/✗ # Windows consoles default to cp1252, which can't encode ✓/✗
sys.stdout.reconfigure(errors="replace") sys.stdout.reconfigure(errors="replace")
@@ -258,10 +285,13 @@ def main() -> None:
) )
build_wheel() build_wheel()
build_executable() 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"✓ Built successfully: {artifact_path}")
print(f" Size: {zip_path.stat().st_size / (1024 * 1024):.1f} MB") print(f" Size: {artifact_path.stat().st_size / (1024 * 1024):.1f} MB")
except (FileNotFoundError, OSError, RuntimeError, ValueError) as e: except (FileNotFoundError, OSError, RuntimeError, ValueError) as e:
print(f"✗ Build failed: {e}", file=sys.stderr) print(f"✗ Build failed: {e}", file=sys.stderr)
sys.exit(1) sys.exit(1)
+18 -15
View File
@@ -67,17 +67,17 @@ def load_token() -> str:
# ZIP + dist helpers # 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 # 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]]: def find_releasable_artifacts() -> list[tuple[Path, str, str]]:
"""Return (path, version, platform_tag) for clean-versioned ZIPs in build/.""" """Return (path, version, platform_tag) for clean-versioned ZIPs/DMGs in build/."""
build_dir = REPO_ROOT / "build" build_dir = REPO_ROOT / "build"
results = [] results = []
for p in sorted(build_dir.glob("MediaHive-*.zip")): for p in sorted(build_dir.glob("MediaHive-*")):
m = _CLEAN_ZIP_RE.match(p.name) m = _CLEAN_ARTIFACT_RE.match(p.name)
if m: if m:
results.append((p, m.group(1), m.group(2))) results.append((p, m.group(1), m.group(2)))
return results return results
@@ -180,7 +180,10 @@ def upload_asset(
"""Upload a file to the release and return the download URL.""" """Upload a file to the release and return the download URL."""
url = f"{base_url}/api/v1/repos/{repo}/releases/{release_id}/assets" url = f"{base_url}/api/v1/repos/{repo}/releases/{release_id}/assets"
size_mb = path.stat().st_size / (1024 * 1024) 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) ...") print(f"Uploading {path.name} ({size_mb:.1f} MB) ...")
with Path(path).open("rb") as fh: with Path(path).open("rb") as fh:
resp = client.post( resp = client.post(
@@ -222,10 +225,10 @@ def main() -> None:
cfg = load_gitea_config() cfg = load_gitea_config()
token = load_token() token = load_token()
zips = find_releasable_zips() artifacts = find_releasable_artifacts()
if not zips: if not artifacts:
print( print(
"No clean-versioned ZIPs found in build/.\n" "No clean-versioned ZIPs/DMGs found in build/.\n"
"Run scripts/guibuild.py first.", "Run scripts/guibuild.py first.",
file=sys.stderr, file=sys.stderr,
) )
@@ -234,7 +237,7 @@ def main() -> None:
# Validate all dist files exist before touching Gitea # Validate all dist files exist before touching Gitea
dist_files: dict[str, list[Path]] = {} dist_files: dict[str, list[Path]] = {}
if not args.no_dist: if not args.no_dist:
for _, version, _platform_tag in zips: for _, version, _platform_tag in artifacts:
dist_files[version] = find_dist_files(version) dist_files[version] = find_dist_files(version)
base_url = cfg["url"].rstrip("/") base_url = cfg["url"].rstrip("/")
@@ -242,7 +245,7 @@ def main() -> None:
with httpx.Client(headers=gitea_headers(token)) as client: with httpx.Client(headers=gitea_headers(token)) as client:
releases: dict[str, tuple[int, set[str]]] = {} 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} ...") print(f"\nReleasing {version} ...")
tag = f"v{version}" tag = f"v{version}"
if version not in releases: if version not in releases:
@@ -257,11 +260,11 @@ def main() -> None:
upload_asset(client, base_url, repo, release_id, path) upload_asset(client, base_url, repo, release_id, path)
release_id, uploaded = releases[version] release_id, uploaded = releases[version]
if zip_path.name in uploaded: if artifact_path.name in uploaded:
print(f"Skipping {zip_path.name}, already on the release.") print(f"Skipping {artifact_path.name}, already on the release.")
continue continue
print(f"Uploading platform artifact: {platform_tag}") 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(f"{tag} published")
print("\nDone. To publish to PyPI, run:") print("\nDone. To publish to PyPI, run:")