Require system .NET SDK; cache vpk/ffmpeg persistently
Per-build downloads of the dotnet runtime (~80 MB) were slow and flaky (IncompleteRead killed a CI run). The .NET SDK is now a build-host prerequisite; vpk and ffmpeg download once into a user-level cache (~/.cache/mediahive-build, %LOCALAPPDATA%\mediahive-build) that survives the per-run build dir.
This commit is contained in:
+1
-1
@@ -44,7 +44,7 @@ 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`):
|
||||
|
||||
- `./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/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). Requires the .NET SDK (>= 8 runtime) installed on the build host; `vpk` and ffmpeg are downloaded once into a persistent user cache (`~/.cache/mediahive-build`, `%LOCALAPPDATA%\mediahive-build` on Windows).
|
||||
- `./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`.
|
||||
|
||||
+11
-2
@@ -5,8 +5,9 @@
|
||||
# pyinstaller --noconfirm --clean scripts/MediaHive.spec
|
||||
#
|
||||
# Or use the build script (recommended—handles versioning and packaging):
|
||||
# uv run scripts/winbuild.py
|
||||
# uv run scripts/guibuild.py
|
||||
|
||||
import os
|
||||
import sys
|
||||
import mediahive.winmain
|
||||
import mediahive.server
|
||||
@@ -20,7 +21,15 @@ _frontend_build = _pkg / "frontend-build"
|
||||
_logo_webp = _pkg / "assets" / "mediahive.webp"
|
||||
_icon_win = _pkg / "assets" / "mediahive.ico"
|
||||
_icon_mac = _pkg / "assets" / "mediahive.icns"
|
||||
_tools_dir = Path(SPECPATH).parent / "build" / "ffmpeg"
|
||||
# ffmpeg staging lives in the persistent build cache (same logic as
|
||||
# scripts/guibuild.py); fall back to the legacy build/ffmpeg location.
|
||||
if sys.platform == "win32":
|
||||
_cache_base = Path(os.environ.get("LOCALAPPDATA", Path.home() / "AppData" / "Local"))
|
||||
else:
|
||||
_cache_base = Path(os.environ.get("XDG_CACHE_HOME", Path.home() / ".cache"))
|
||||
_tools_dir = _cache_base / "mediahive-build" / "ffmpeg"
|
||||
if not _tools_dir.exists():
|
||||
_tools_dir = Path(SPECPATH).parent / "build" / "ffmpeg"
|
||||
_tool_names = ["ffmpeg.exe"] if sys.platform == "win32" else ["ffmpeg"]
|
||||
|
||||
_binaries = []
|
||||
|
||||
+44
-50
@@ -2,7 +2,7 @@
|
||||
"""Build the desktop GUI application and package it with Velopack.
|
||||
|
||||
Usage:
|
||||
uv run scripts/winbuild.py
|
||||
uv run scripts/guibuild.py
|
||||
|
||||
This runs in the project environment where dependencies
|
||||
are available via pyproject.toml.
|
||||
@@ -26,7 +26,6 @@ import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
import sys
|
||||
import tarfile
|
||||
import urllib.request
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
@@ -41,10 +40,21 @@ _FFMPEG_URL = (
|
||||
_MACOS_ARM64_TOOL_URLS = {
|
||||
"ffmpeg": "https://www.osxexperts.net/ffmpeg81arm.zip",
|
||||
}
|
||||
_FFMPEG_STAGING = Path(__file__).parent.parent / "build" / "ffmpeg"
|
||||
_REPO_ROOT = Path(__file__).parent.parent
|
||||
_ASSETS_DIR = _REPO_ROOT / "mediahive" / "assets"
|
||||
|
||||
|
||||
def _build_cache_dir() -> Path:
|
||||
"""Return the persistent cross-build cache dir for downloaded tools (CI wipes build/)."""
|
||||
if sys.platform == "win32":
|
||||
base = os.environ.get("LOCALAPPDATA") or (Path.home() / "AppData" / "Local")
|
||||
return Path(base) / "mediahive-build"
|
||||
base = os.environ.get("XDG_CACHE_HOME") or (Path.home() / ".cache")
|
||||
return Path(base) / "mediahive-build"
|
||||
|
||||
|
||||
_FFMPEG_STAGING = _build_cache_dir() / "ffmpeg"
|
||||
|
||||
# Velopack CLI (dotnet tool package). Runs on the machine's .NET runtime; the
|
||||
# produced Setup.exe/Update.exe are native and need no runtime on end-user
|
||||
# machines. Pin a version whose tools target an installed .NET major.
|
||||
@@ -53,7 +63,7 @@ _VPK_URL = (
|
||||
f"https://api.nuget.org/v3-flatcontainer/vpk/{_VPK_VERSION}"
|
||||
f"/vpk.{_VPK_VERSION}.nupkg"
|
||||
)
|
||||
_VPK_STAGING = _REPO_ROOT / "build" / "vpk"
|
||||
_VPK_STAGING = _build_cache_dir() / f"vpk-{_VPK_VERSION}"
|
||||
|
||||
|
||||
def _platform_zip_suffix() -> str:
|
||||
@@ -73,7 +83,7 @@ def _platform_zip_suffix() -> str:
|
||||
|
||||
|
||||
def fetch_ffmpeg() -> Path:
|
||||
"""Download latest ffmpeg.exe from BtbN builds into build/ffmpeg/."""
|
||||
"""Download latest ffmpeg.exe from BtbN builds into the persistent build cache."""
|
||||
dest = _FFMPEG_STAGING / "ffmpeg.exe"
|
||||
if dest.exists():
|
||||
print(f"ffmpeg already staged at {dest}, skipping download.")
|
||||
@@ -98,7 +108,7 @@ def fetch_ffmpeg() -> Path:
|
||||
|
||||
|
||||
def fetch_macos_arm64_binaries() -> dict[str, Path]:
|
||||
"""Download prebuilt macOS arm64 ffmpeg binary into build/ffmpeg/."""
|
||||
"""Download prebuilt macOS arm64 ffmpeg binary into the persistent build cache."""
|
||||
if sys.platform != "darwin" or platform.machine().lower() not in {
|
||||
"arm64",
|
||||
"aarch64",
|
||||
@@ -196,7 +206,7 @@ def ensure_macos_icon() -> Path:
|
||||
|
||||
|
||||
def fetch_vpk() -> Path:
|
||||
"""Download the Velopack CLI package into build/vpk/ (cached).
|
||||
"""Download the Velopack CLI package into the persistent build cache.
|
||||
|
||||
Returns the path to vpk.dll, runnable with `dotnet vpk.dll ...`.
|
||||
"""
|
||||
@@ -219,15 +229,6 @@ def fetch_vpk() -> Path:
|
||||
return vpk_dll
|
||||
|
||||
|
||||
_DOTNET_STAGING = _REPO_ROOT / "build" / "dotnet"
|
||||
# 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",
|
||||
}
|
||||
|
||||
|
||||
def _dotnet_has_runtime(exe: Path) -> bool:
|
||||
"""Check that `exe` runs and has Microsoft.NETCore.App >= 8."""
|
||||
try:
|
||||
@@ -250,57 +251,50 @@ def _dotnet_has_runtime(exe: Path) -> bool:
|
||||
|
||||
|
||||
def fetch_dotnet() -> str:
|
||||
"""Resolve a dotnet host with a modern (>= 8) runtime, bootstrapping one into build/dotnet/ if none is found.
|
||||
"""Resolve a dotnet host with a modern (>= 8) runtime from the system.
|
||||
|
||||
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.
|
||||
The .NET SDK is a build prerequisite installed on the build machine —
|
||||
downloading a runtime per build is slow and flaky. 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.
|
||||
"""
|
||||
exe_name = "dotnet.exe" if sys.platform == "win32" else "dotnet"
|
||||
candidates: list[Path] = []
|
||||
root = os.environ.get("DOTNET_ROOT")
|
||||
if root:
|
||||
candidates.append(Path(root) / exe_name)
|
||||
if sys.platform == "win32":
|
||||
candidates.append(
|
||||
Path(r"C:\ProgramData\scoop\apps\dotnet-sdk\current") / exe_name
|
||||
)
|
||||
which = shutil.which("dotnet")
|
||||
if which:
|
||||
candidates.append(Path(which))
|
||||
staged = _DOTNET_STAGING / exe_name
|
||||
candidates.append(staged)
|
||||
if sys.platform == "win32":
|
||||
candidates += [
|
||||
Path(r"C:\ProgramData\scoop\apps\dotnet-sdk\current") / exe_name,
|
||||
Path(r"C:\Program Files\dotnet") / exe_name,
|
||||
]
|
||||
elif sys.platform == "darwin":
|
||||
candidates += [
|
||||
Path("/opt/homebrew/bin") / exe_name,
|
||||
Path("/usr/local/share/dotnet") / exe_name,
|
||||
]
|
||||
else:
|
||||
candidates += [
|
||||
Path("/usr/share/dotnet") / exe_name,
|
||||
Path("/usr/lib/dotnet") / exe_name,
|
||||
Path.home() / ".dotnet" / exe_name,
|
||||
]
|
||||
|
||||
for exe in candidates:
|
||||
if exe.exists() and _dotnet_has_runtime(exe):
|
||||
print(f"Using dotnet at {exe}")
|
||||
return str(exe)
|
||||
|
||||
machine = platform.machine().lower()
|
||||
arch = {"x86_64": "x64", "amd64": "x64", "arm64": "arm64", "aarch64": "arm64"}.get(
|
||||
machine, machine
|
||||
raise RuntimeError(
|
||||
"No dotnet with Microsoft.NETCore.App >= 8 found. "
|
||||
"Install the .NET SDK on this build machine "
|
||||
"(Windows: `scoop install dotnet-sdk`; macOS: `brew install dotnet-sdk`; "
|
||||
"Linux: distro `dotnet-sdk` package or the dotnet-install script)."
|
||||
)
|
||||
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(fileobj=io.BytesIO(data), mode="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_velopack(version: str) -> Path:
|
||||
|
||||
Reference in New Issue
Block a user