Velopack packaging on all platforms with in-app auto-updates
- macOS: .pkg installer replaces the DMG; Linux: .AppImage replaces the ZIP - winmain runs velopack.App() first (proper hook handling) and checks for updates in the background; downloads are applied on next launch - release.py uploads the vpk update feed (releases.<channel>.json, nupkgs) so GiteaSource finds updates on the latest release - guibuild.py bootstraps a .NET runtime into build/dotnet when the runner host lacks one
This commit is contained in:
@@ -4,12 +4,13 @@
|
|||||||
|
|
||||||
Netflix style browsing of your local media archive. Supports keyboard, mouse and gamepad navigation. Uses your favorite movie player.
|
Netflix style browsing of your local media archive. Supports keyboard, mouse and gamepad navigation. Uses your favorite movie player.
|
||||||
|
|
||||||
**[Windows and Mac portable ZIP downloads](https://git.zi.fi/LeoVasanko/mediahive/releases)**
|
**[Windows, Mac and Linux downloads](https://git.zi.fi/LeoVasanko/mediahive/releases)**
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
- Windows and macOS: Download the portable ZIP from the releases page, extract it anywhere, and run `MediaHive`.
|
- Windows: Download `*-win64-setup.exe` from the releases page and run it (no admin needed; auto-updates included). A `-win64-portable.zip` is also available.
|
||||||
- Linux and other platforms: Install [UV](https://docs.astral.sh/uv/getting-started/installation/) and run directly with `uvx --from mediahive[gui] mediahive`.
|
- macOS: Download `*-macos-arm64-setup.pkg` and install (auto-updates included).
|
||||||
|
- Linux: Download the `.AppImage`, `chmod +x` it, and run. Alternatively install [UV](https://docs.astral.sh/uv/getting-started/installation/) and run directly with `uvx --from mediahive[gui] mediahive`.
|
||||||
|
|
||||||
## What It Does
|
## What It Does
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -1,6 +1,6 @@
|
|||||||
# Development
|
# Development
|
||||||
|
|
||||||
This document covers the developer-facing ways to run MediaHive locally. The main [README.md](../README.md) covers end-user startup across platforms (portable ZIPs on Windows/macOS, `uvx --from mediahive[gui] mediahive` on Linux/other).
|
This document covers the developer-facing ways to run MediaHive locally. The main [README.md](../README.md) covers end-user startup across platforms (installer/AppImage downloads, `uvx --from mediahive[gui] mediahive` on Linux/other).
|
||||||
|
|
||||||
## Requirements
|
## Requirements
|
||||||
|
|
||||||
@@ -44,8 +44,8 @@ This launches the same pywebview-based desktop flow used by the Windows build.
|
|||||||
|
|
||||||
The helper scripts are directly executable via their `uv run` shebang (on Windows, run them with `uv run scripts/<name>.py`):
|
The helper scripts are directly executable via their `uv run` shebang (on Windows, run them with `uv run scripts/<name>.py`):
|
||||||
|
|
||||||
- `./scripts/guibuild.py` builds the PyInstaller desktop app and a versioned portable ZIP under `build/`.
|
- `./scripts/guibuild.py` builds the PyInstaller desktop app and packages it with Velopack under `build/`: per-user `Setup.exe` (Windows), `.pkg` installer (macOS), `.AppImage` (Linux), plus the update feed in `build/velopack/`. On Windows it also creates a `-win64-portable.zip` (no auto-updates). `vpk` (and a .NET runtime, if the host lacks one) are downloaded into `build/` automatically.
|
||||||
- `./scripts/release.py` publishes a release to the Gitea releases page.
|
- `./scripts/release.py` publishes a release to the Gitea releases page, uploading the platform artifacts and the Velopack update feed files — installed apps auto-update from the latest release.
|
||||||
|
|
||||||
Python packaging builds the frontend automatically through the hatch build hook `scripts/fastapi-vue/buildhook.py` (see `pyproject.toml`), so wheels and sdists always ship a fresh `mediahive/frontend-build`.
|
Python packaging builds the frontend automatically through the hatch build hook `scripts/fastapi-vue/buildhook.py` (see `pyproject.toml`), so wheels and sdists always ship a fresh `mediahive/frontend-build`.
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 197 KiB |
+48
-4
@@ -39,6 +39,7 @@ HEALTH_TIMEOUT = 2 # seconds
|
|||||||
BACKEND_HEALTH_REQUEST_TIMEOUT = 2 # seconds
|
BACKEND_HEALTH_REQUEST_TIMEOUT = 2 # seconds
|
||||||
BACKEND_HEALTH_POLL_SECONDS = 0.25
|
BACKEND_HEALTH_POLL_SECONDS = 0.25
|
||||||
MPC_BE_URL = "http://127.0.0.1:13579"
|
MPC_BE_URL = "http://127.0.0.1:13579"
|
||||||
|
VELOPACK_REPO_URL = "https://git.zi.fi/LeoVasanko/mediahive"
|
||||||
GAMEPAD_REPEAT_SECONDS = 0.008
|
GAMEPAD_REPEAT_SECONDS = 0.008
|
||||||
GAMEPAD_POLL_SECONDS = 0.008
|
GAMEPAD_POLL_SECONDS = 0.008
|
||||||
MPC_BE_FRAME_REPEAT_SECONDS = 0.016
|
MPC_BE_FRAME_REPEAT_SECONDS = 0.016
|
||||||
@@ -976,12 +977,51 @@ def _show_fatal_error(exc: BaseException) -> None:
|
|||||||
logger.exception("Could not display the error window")
|
logger.exception("Could not display the error window")
|
||||||
|
|
||||||
|
|
||||||
|
def _velopack_startup() -> None:
|
||||||
|
"""Handle Velopack install/update/uninstall hooks and pending updates.
|
||||||
|
|
||||||
|
Must be the first thing at startup: when Velopack launches the app with
|
||||||
|
--veloapp-* hook arguments (during install/update/uninstall), run()
|
||||||
|
executes the hook and exits the process, so the GUI never starts.
|
||||||
|
Applies downloaded-but-pending updates. No-op in development and
|
||||||
|
portable-ZIP runs.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import velopack
|
||||||
|
except ImportError:
|
||||||
|
return
|
||||||
|
try:
|
||||||
|
velopack.App().run()
|
||||||
|
except Exception:
|
||||||
|
logger.exception("Velopack startup hook failed")
|
||||||
|
|
||||||
|
|
||||||
|
def _check_for_updates() -> None:
|
||||||
|
"""Download available updates in the background.
|
||||||
|
|
||||||
|
Downloaded updates are applied automatically by Velopack on the next app
|
||||||
|
start (via _velopack_startup), so the running session is never
|
||||||
|
interrupted.
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
import velopack
|
||||||
|
|
||||||
|
mgr = velopack.UpdateManager(velopack.GiteaSource(VELOPACK_REPO_URL))
|
||||||
|
info = mgr.check_for_updates()
|
||||||
|
if info is None:
|
||||||
|
logger.info("Velopack: no update available")
|
||||||
|
return
|
||||||
|
version = info.TargetFullRelease.Version
|
||||||
|
logger.info("Velopack: downloading update %s", version)
|
||||||
|
mgr.download_updates(info)
|
||||||
|
logger.info("Velopack: update %s staged, applies on next launch", version)
|
||||||
|
except Exception as exc: # not a Velopack install (dev/portable), network, ...
|
||||||
|
logger.info("Velopack update check skipped: %s", exc)
|
||||||
|
|
||||||
|
|
||||||
def gui_main() -> None:
|
def gui_main() -> None:
|
||||||
"""Run the GUI, rendering fatal exceptions as a TraceRite HTML window."""
|
"""Run the GUI, rendering fatal exceptions as a TraceRite HTML window."""
|
||||||
# Velopack runs the app with --veloapp-* hook arguments during install,
|
_velopack_startup()
|
||||||
# update, and uninstall; these must exit fast instead of starting the GUI.
|
|
||||||
if any(arg.startswith("--veloapp-") for arg in sys.argv[1:]):
|
|
||||||
return
|
|
||||||
try:
|
try:
|
||||||
winmain()
|
winmain()
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -1250,6 +1290,10 @@ def winmain() -> None:
|
|||||||
server.should_exit = True
|
server.should_exit = True
|
||||||
raise RuntimeError(f"Backend did not become ready within {HEALTH_TIMEOUT}s")
|
raise RuntimeError(f"Backend did not become ready within {HEALTH_TIMEOUT}s")
|
||||||
|
|
||||||
|
threading.Thread(
|
||||||
|
target=_check_for_updates, daemon=True, name="mediahive-update-check"
|
||||||
|
).start()
|
||||||
|
|
||||||
api = JsApi()
|
api = JsApi()
|
||||||
logger.info("Configured pywebview backend: %s", _selected_webview_backend())
|
logger.info("Configured pywebview backend: %s", _selected_webview_backend())
|
||||||
window = webview.create_window(
|
window = webview.create_window(
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ parse-torrent-title = { git = "https://github.com/platelminto/parse-torrent-titl
|
|||||||
gui = [
|
gui = [
|
||||||
"pywebview[qt]>=6.2.1; platform_system != 'Windows'",
|
"pywebview[qt]>=6.2.1; platform_system != 'Windows'",
|
||||||
"pywebview>=6.2.1; platform_system == 'Windows'",
|
"pywebview>=6.2.1; platform_system == 'Windows'",
|
||||||
|
"velopack>=1.2",
|
||||||
"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
|
# pywebview[qt] no longer pulls this in; the macOS Qt backend needs it
|
||||||
|
|||||||
+129
-65
@@ -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 versioned ZIP/DMG.
|
"""Build the desktop GUI application and package it with Velopack.
|
||||||
|
|
||||||
Usage:
|
Usage:
|
||||||
uv run scripts/winbuild.py
|
uv run scripts/winbuild.py
|
||||||
@@ -10,10 +10,12 @@ are available via pyproject.toml.
|
|||||||
This script:
|
This script:
|
||||||
1. Reads the version from pyproject.toml
|
1. Reads the version from pyproject.toml
|
||||||
2. Runs `uv build` to produce the wheel/sdist
|
2. Runs `uv build` to produce the wheel/sdist
|
||||||
3. On Windows, downloads the latest ffmpeg.exe for bundling
|
3. On Windows/macOS, downloads the 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 versioned ZIP (Windows/Linux) or DMG (macOS) artifact
|
5. Packages with Velopack: Setup.exe (Windows), .pkg (macOS),
|
||||||
|
.AppImage (Linux), plus the update feed in build/velopack/
|
||||||
|
that release.py uploads for in-app auto-updates
|
||||||
|
6. On Windows, also creates a portable ZIP (no auto-updates)
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import io
|
import io
|
||||||
@@ -23,6 +25,7 @@ import shutil
|
|||||||
import stat
|
import stat
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
|
import tarfile
|
||||||
import urllib.request
|
import urllib.request
|
||||||
import zipfile
|
import zipfile
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
@@ -215,36 +218,119 @@ def fetch_vpk() -> Path:
|
|||||||
return vpk_dll
|
return vpk_dll
|
||||||
|
|
||||||
|
|
||||||
def _dotnet() -> str:
|
_DOTNET_STAGING = _REPO_ROOT / "build" / "dotnet"
|
||||||
"""Resolve the dotnet host with an SDK/modern runtime.
|
# aka.ms latest-runtime archives per platform (vpk needs .NET >= 8).
|
||||||
|
_DOTNET_RUNTIME_URLS = {
|
||||||
|
("win32", "x64"): "https://aka.ms/dotnet/10.0/dotnet-runtime-win-x64.zip",
|
||||||
|
("darwin", "arm64"): "https://aka.ms/dotnet/10.0/dotnet-runtime-osx-arm64.tar.gz",
|
||||||
|
("linux", "x64"): "https://aka.ms/dotnet/10.0/dotnet-runtime-linux-x64.tar.gz",
|
||||||
|
}
|
||||||
|
|
||||||
A plain `dotnet` may resolve to a runtime-only installation (e.g.
|
|
||||||
C:\\Program Files\\dotnet), and long-running services (CI runners) may
|
def _dotnet_has_runtime(exe: Path) -> bool:
|
||||||
carry a stale environment without DOTNET_ROOT or scoop paths — so probe
|
"""Check that `exe` runs and has Microsoft.NETCore.App >= 8."""
|
||||||
known locations explicitly.
|
try:
|
||||||
|
result = subprocess.run(
|
||||||
|
[str(exe), "--list-runtimes"], capture_output=True, text=True, timeout=30
|
||||||
|
)
|
||||||
|
except (OSError, subprocess.TimeoutExpired):
|
||||||
|
return False
|
||||||
|
if result.returncode != 0:
|
||||||
|
return False
|
||||||
|
for line in result.stdout.splitlines():
|
||||||
|
parts = line.split()
|
||||||
|
if len(parts) >= 2 and parts[0] == "Microsoft.NETCore.App":
|
||||||
|
try:
|
||||||
|
if int(parts[1].split(".")[0]) >= 8:
|
||||||
|
return True
|
||||||
|
except ValueError:
|
||||||
|
continue
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def fetch_dotnet() -> str:
|
||||||
|
"""Resolve a dotnet host with a modern (>= 8) runtime, bootstrapping one
|
||||||
|
into build/dotnet/ if none is found.
|
||||||
|
|
||||||
|
A plain `dotnet` may resolve to a runtime-only installation, and
|
||||||
|
long-running services (CI runners) may carry a stale environment without
|
||||||
|
DOTNET_ROOT or scoop paths — so probe known locations explicitly.
|
||||||
"""
|
"""
|
||||||
candidates = []
|
exe_name = "dotnet.exe" if sys.platform == "win32" else "dotnet"
|
||||||
|
candidates: list[Path] = []
|
||||||
root = os.environ.get("DOTNET_ROOT")
|
root = os.environ.get("DOTNET_ROOT")
|
||||||
if root:
|
if root:
|
||||||
candidates.append(Path(root))
|
candidates.append(Path(root) / exe_name)
|
||||||
if sys.platform == "win32":
|
if sys.platform == "win32":
|
||||||
candidates.append(Path(r"C:\ProgramData\scoop\apps\dotnet-sdk\current"))
|
candidates.append(
|
||||||
exe_name = "dotnet.exe" if sys.platform == "win32" else "dotnet"
|
Path(r"C:\ProgramData\scoop\apps\dotnet-sdk\current") / exe_name
|
||||||
for base in candidates:
|
)
|
||||||
exe = base / exe_name
|
which = shutil.which("dotnet")
|
||||||
if exe.exists():
|
if which:
|
||||||
|
candidates.append(Path(which))
|
||||||
|
staged = _DOTNET_STAGING / exe_name
|
||||||
|
candidates.append(staged)
|
||||||
|
|
||||||
|
for exe in candidates:
|
||||||
|
if exe.exists() and _dotnet_has_runtime(exe):
|
||||||
|
print(f"Using dotnet at {exe}")
|
||||||
return str(exe)
|
return str(exe)
|
||||||
return "dotnet"
|
|
||||||
|
machine = platform.machine().lower()
|
||||||
|
arch = {"x86_64": "x64", "amd64": "x64", "arm64": "arm64", "aarch64": "arm64"}.get(
|
||||||
|
machine, machine
|
||||||
|
)
|
||||||
|
url = _DOTNET_RUNTIME_URLS.get((sys.platform, arch))
|
||||||
|
if url is None:
|
||||||
|
raise RuntimeError(f"No dotnet runtime download for {sys.platform}/{arch}")
|
||||||
|
|
||||||
|
print(f"Downloading dotnet runtime from {url} ...")
|
||||||
|
with urllib.request.urlopen(url) as resp:
|
||||||
|
data = resp.read()
|
||||||
|
|
||||||
|
_DOTNET_STAGING.mkdir(parents=True, exist_ok=True)
|
||||||
|
if url.endswith(".zip"):
|
||||||
|
with zipfile.ZipFile(io.BytesIO(data)) as zf:
|
||||||
|
zf.extractall(_DOTNET_STAGING)
|
||||||
|
else:
|
||||||
|
with tarfile.open(io.BytesIO(data), "r:gz") as tf:
|
||||||
|
tf.extractall(_DOTNET_STAGING)
|
||||||
|
staged.chmod(staged.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH)
|
||||||
|
|
||||||
|
if not _dotnet_has_runtime(staged):
|
||||||
|
raise RuntimeError(f"Bootstrapped dotnet at {staged} failed runtime check")
|
||||||
|
print(f"dotnet staged at {_DOTNET_STAGING}")
|
||||||
|
return str(staged)
|
||||||
|
|
||||||
|
|
||||||
def build_setup(version: str) -> Path:
|
def build_velopack(version: str) -> Path:
|
||||||
"""Build a Velopack per-user Setup.exe from the PyInstaller output folder.
|
"""Build the Velopack installer/bundle for this platform.
|
||||||
|
|
||||||
Velopack installs to %LOCALAPPDATA% (no admin) and installed files carry
|
Windows: per-user Setup.exe. macOS: .pkg installer. Linux: .AppImage.
|
||||||
no Mark-of-the-Web, so the .NET CLR loads pythonnet/pywebview assemblies
|
Also produces the update feed (releases.<channel>.json, *.nupkg) in
|
||||||
that it refuses from a downloaded ZIP.
|
build/velopack/ for release.py to upload — in-app auto-updates read it
|
||||||
|
from the Gitea release. Velopack installs carry no Mark-of-the-Web, so
|
||||||
|
the .NET CLR loads pythonnet/pywebview assemblies that it refuses from
|
||||||
|
a downloaded ZIP.
|
||||||
"""
|
"""
|
||||||
|
if sys.platform == "darwin":
|
||||||
|
dist_folder = _REPO_ROOT / "build" / "MediaHive.app"
|
||||||
|
icon = _ASSETS_DIR / "mediahive.icns"
|
||||||
|
rid = "osx-arm64"
|
||||||
|
main_exe = "MediaHive"
|
||||||
|
artifact_ext = ".pkg"
|
||||||
|
elif sys.platform == "win32":
|
||||||
dist_folder = _REPO_ROOT / "build" / "MediaHive"
|
dist_folder = _REPO_ROOT / "build" / "MediaHive"
|
||||||
|
icon = _ASSETS_DIR / "mediahive.ico"
|
||||||
|
rid = "win-x64"
|
||||||
|
main_exe = "MediaHive.exe"
|
||||||
|
artifact_ext = ".exe"
|
||||||
|
else:
|
||||||
|
dist_folder = _REPO_ROOT / "build" / "MediaHive"
|
||||||
|
icon = _ASSETS_DIR / "mediahive.png"
|
||||||
|
rid = "linux-x64"
|
||||||
|
main_exe = "MediaHive"
|
||||||
|
artifact_ext = ".AppImage"
|
||||||
if not dist_folder.exists():
|
if not dist_folder.exists():
|
||||||
raise FileNotFoundError(f"Distribution folder not found: {dist_folder}")
|
raise FileNotFoundError(f"Distribution folder not found: {dist_folder}")
|
||||||
|
|
||||||
@@ -252,7 +338,7 @@ def build_setup(version: str) -> Path:
|
|||||||
releases_dir = _REPO_ROOT / "build" / "velopack"
|
releases_dir = _REPO_ROOT / "build" / "velopack"
|
||||||
|
|
||||||
cmd = [
|
cmd = [
|
||||||
_dotnet(),
|
fetch_dotnet(),
|
||||||
str(vpk_dll),
|
str(vpk_dll),
|
||||||
"pack",
|
"pack",
|
||||||
"--packId",
|
"--packId",
|
||||||
@@ -262,13 +348,15 @@ def build_setup(version: str) -> Path:
|
|||||||
"--packDir",
|
"--packDir",
|
||||||
str(dist_folder),
|
str(dist_folder),
|
||||||
"--mainExe",
|
"--mainExe",
|
||||||
"MediaHive.exe",
|
main_exe,
|
||||||
"--packAuthors",
|
"--packAuthors",
|
||||||
"MediaHive",
|
"MediaHive",
|
||||||
"--packTitle",
|
"--packTitle",
|
||||||
"MediaHive",
|
"MediaHive",
|
||||||
"--icon",
|
"--icon",
|
||||||
str(_ASSETS_DIR / "mediahive.ico"),
|
str(icon),
|
||||||
|
"--runtime",
|
||||||
|
rid,
|
||||||
"--outputDir",
|
"--outputDir",
|
||||||
str(releases_dir),
|
str(releases_dir),
|
||||||
]
|
]
|
||||||
@@ -283,10 +371,16 @@ def build_setup(version: str) -> Path:
|
|||||||
f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}"
|
f"stdout:\n{result.stdout}\nstderr:\n{result.stderr}"
|
||||||
)
|
)
|
||||||
|
|
||||||
setup = next(releases_dir.glob("*-win-Setup.exe"), None)
|
setup = next(iter(sorted(releases_dir.glob(f"*Setup*{artifact_ext}"))), None)
|
||||||
if setup is None:
|
if setup is None:
|
||||||
raise RuntimeError(f"vpk produced no Setup.exe in {releases_dir}")
|
setup = next(iter(sorted(releases_dir.glob(f"*{artifact_ext}"))), None)
|
||||||
artifact = _REPO_ROOT / "build" / f"MediaHive-{version}-{_platform_zip_suffix()}-setup.exe"
|
if setup is None:
|
||||||
|
raise RuntimeError(f"vpk produced no *{artifact_ext} in {releases_dir}")
|
||||||
|
artifact = (
|
||||||
|
_REPO_ROOT
|
||||||
|
/ "build"
|
||||||
|
/ f"MediaHive-{version}-{_platform_zip_suffix()}-setup{artifact_ext}"
|
||||||
|
)
|
||||||
artifact.unlink(missing_ok=True)
|
artifact.unlink(missing_ok=True)
|
||||||
setup.rename(artifact)
|
setup.rename(artifact)
|
||||||
return artifact
|
return artifact
|
||||||
@@ -353,33 +447,6 @@ 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")
|
||||||
@@ -400,18 +467,15 @@ def main() -> None:
|
|||||||
)
|
)
|
||||||
build_wheel()
|
build_wheel()
|
||||||
build_executable()
|
build_executable()
|
||||||
if sys.platform == "darwin":
|
|
||||||
artifact_path = create_dmg(version)
|
|
||||||
else:
|
|
||||||
artifact_path = create_zip(version)
|
|
||||||
|
|
||||||
|
artifacts = [build_velopack(version)]
|
||||||
|
if sys.platform == "win32":
|
||||||
|
# Velopack-less plain-folder distribution (with MOTW strip)
|
||||||
|
artifacts.append(create_zip(version))
|
||||||
|
|
||||||
|
for artifact_path in artifacts:
|
||||||
print(f"✓ Built successfully: {artifact_path}")
|
print(f"✓ Built successfully: {artifact_path}")
|
||||||
print(f" Size: {artifact_path.stat().st_size / (1024 * 1024):.1f} MB")
|
print(f" Size: {artifact_path.stat().st_size / (1024 * 1024):.1f} MB")
|
||||||
|
|
||||||
if sys.platform == "win32":
|
|
||||||
setup_path = build_setup(version)
|
|
||||||
print(f"✓ Built successfully: {setup_path}")
|
|
||||||
print(f" Size: {setup_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)
|
||||||
|
|||||||
+28
-4
@@ -67,10 +67,12 @@ def load_token() -> str:
|
|||||||
# ZIP + dist helpers
|
# ZIP + dist helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
# Matches MediaHive-1.2.3-win64.zip, MediaHive-1.2.3-win64-setup.exe,
|
# Matches MediaHive-1.2.3-win64-portable.zip, MediaHive-1.2.3-win64-setup.exe,
|
||||||
# MediaHive-1.2.3-macos-arm64.dmg, etc.
|
# MediaHive-1.2.3-macos-arm64-setup.pkg, MediaHive-1.2.3-linux-x64.AppImage, 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-portable.zip
|
||||||
_CLEAN_ARTIFACT_RE = re.compile(r"^MediaHive-(\d+(?:\.\d+)*)-([A-Za-z0-9._-]+)\.(?:zip|dmg|exe)$")
|
_CLEAN_ARTIFACT_RE = re.compile(
|
||||||
|
r"^MediaHive-(\d+(?:\.\d+)*)-([A-Za-z0-9._-]+)\.(?:zip|dmg|exe|pkg|AppImage)$"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def find_releasable_artifacts() -> list[tuple[Path, str, str]]:
|
def find_releasable_artifacts() -> list[tuple[Path, str, str]]:
|
||||||
@@ -113,6 +115,21 @@ def find_dist_files(version: str) -> list[Path]:
|
|||||||
return [wheel, sdist]
|
return [wheel, sdist]
|
||||||
|
|
||||||
|
|
||||||
|
def find_velopack_feed_files() -> list[Path]:
|
||||||
|
"""Velopack update feed files produced by vpk pack in build/velopack/.
|
||||||
|
|
||||||
|
These keep their original names — the feed JSONs reference them — and
|
||||||
|
in-app updates (GiteaSource) download them from the latest release.
|
||||||
|
"""
|
||||||
|
releases_dir = REPO_ROOT / "build" / "velopack"
|
||||||
|
if not releases_dir.exists():
|
||||||
|
return []
|
||||||
|
files: list[Path] = []
|
||||||
|
for pattern in ("RELEASES", "releases.*.json", "assets.*.json", "*.nupkg"):
|
||||||
|
files.extend(sorted(releases_dir.glob(pattern)))
|
||||||
|
return files
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# Gitea API helpers
|
# Gitea API helpers
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
@@ -266,6 +283,13 @@ def main() -> None:
|
|||||||
continue
|
continue
|
||||||
print(f"Uploading platform artifact: {platform_tag}")
|
print(f"Uploading platform artifact: {platform_tag}")
|
||||||
upload_asset(client, base_url, repo, release_id, artifact_path)
|
upload_asset(client, base_url, repo, release_id, artifact_path)
|
||||||
|
uploaded.add(artifact_path.name)
|
||||||
|
for feed_file in find_velopack_feed_files():
|
||||||
|
if feed_file.name in uploaded:
|
||||||
|
print(f"Skipping {feed_file.name}, already on the release.")
|
||||||
|
continue
|
||||||
|
upload_asset(client, base_url, repo, release_id, feed_file)
|
||||||
|
uploaded.add(feed_file.name)
|
||||||
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