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