Fix doubled platformdirs paths; pick newest dotnet runtime
release / gui-build (linux, bash) (push) Successful in 54s
release / gui-build (windows, cmd) (push) Successful in 1m24s
release / gui-build (macos, bash) (push) Successful in 1m47s

appauthor=False/opinion=False avoids mediahive\mediahive\Cache style
paths on Windows. fetch_dotnet now probes all known dotnet locations and
selects the highest Microsoft.NETCore.App major, requiring the version
vpk's TFM targets (10) — PATH on the Windows CI runner resolves to a
runtime-only .NET 8 while scoop holds the SDK 10.
This commit is contained in:
2026-09-23 00:49:38 +00:00
parent e514829737
commit 3142bc2cb2
3 changed files with 40 additions and 24 deletions
+6 -4
View File
@@ -22,13 +22,15 @@ class Config(msgspec.Struct, omit_defaults=True):
def config_dir() -> Path: def config_dir() -> Path:
# Local (non-roaming) on Windows: config is machine-specific state, # appauthor=False: avoid the doubled %LOCALAPPDATA%\mediahive\mediahive.
# not something to sync across a domain profile. # roaming=False: config is machine-specific state, not something to sync
return user_config_path("mediahive", roaming=False) # across a domain profile.
return user_config_path("mediahive", appauthor=False, roaming=False)
def log_dir() -> Path: def log_dir() -> Path:
return user_log_path("mediahive", opinion=False) # opinion=False: no extra Logs/ subdir; mediahive.log sits beside config.
return user_log_path("mediahive", appauthor=False, opinion=False)
def config_path() -> Path: def config_path() -> Path:
+3 -1
View File
@@ -24,7 +24,9 @@ _icon_mac = _pkg / "assets" / "mediahive.icns"
# scripts/guibuild.py); fall back to the legacy build/ffmpeg location. # scripts/guibuild.py); fall back to the legacy build/ffmpeg location.
from platformdirs import user_cache_path from platformdirs import user_cache_path
_tools_dir = user_cache_path("mediahive-build") / "ffmpeg" _tools_dir = (
user_cache_path("mediahive-build", appauthor=False, opinion=False) / "ffmpeg"
)
if not _tools_dir.exists(): if not _tools_dir.exists():
_tools_dir = Path(SPECPATH).parent / "build" / "ffmpeg" _tools_dir = Path(SPECPATH).parent / "build" / "ffmpeg"
_tool_names = ["ffmpeg.exe"] if sys.platform == "win32" else ["ffmpeg"] _tool_names = ["ffmpeg.exe"] if sys.platform == "win32" else ["ffmpeg"]
+31 -19
View File
@@ -47,7 +47,7 @@ _ASSETS_DIR = _REPO_ROOT / "mediahive" / "assets"
def _build_cache_dir() -> Path: def _build_cache_dir() -> Path:
"""Return the persistent cross-build cache dir for downloaded tools (CI wipes build/).""" """Return the persistent cross-build cache dir for downloaded tools (CI wipes build/)."""
return user_cache_path("mediahive-build") return user_cache_path("mediahive-build", appauthor=False, opinion=False)
_FFMPEG_STAGING = _build_cache_dir() / "ffmpeg" _FFMPEG_STAGING = _build_cache_dir() / "ffmpeg"
@@ -202,12 +202,16 @@ def ensure_macos_icon() -> Path:
return icon_icns return icon_icns
_VPK_TFM = "net10.0"
_VPK_REQUIRED_DOTNET_MAJOR = int(re.fullmatch(r"net(\d+)\.0", _VPK_TFM).group(1))
def fetch_vpk() -> Path: def fetch_vpk() -> Path:
"""Download the Velopack CLI package into the persistent build cache. """Download the Velopack CLI package into the persistent build cache.
Returns the path to vpk.dll, runnable with `dotnet vpk.dll ...`. Returns the path to vpk.dll, runnable with `dotnet vpk.dll ...`.
""" """
vpk_dll = _VPK_STAGING / "tools" / "net10.0" / "any" / "vpk.dll" vpk_dll = _VPK_STAGING / "tools" / _VPK_TFM / "any" / "vpk.dll"
if vpk_dll.exists(): if vpk_dll.exists():
print(f"vpk already staged at {_VPK_STAGING}, skipping download.") print(f"vpk already staged at {_VPK_STAGING}, skipping download.")
return vpk_dll return vpk_dll
@@ -226,35 +230,35 @@ def fetch_vpk() -> Path:
return vpk_dll return vpk_dll
def _dotnet_has_runtime(exe: Path) -> bool: def _dotnet_runtime_major(exe: Path) -> int | None:
"""Check that `exe` runs and has Microsoft.NETCore.App >= 8.""" """Return the highest installed Microsoft.NETCore.App major version, or None."""
try: try:
result = subprocess.run( result = subprocess.run(
[str(exe), "--list-runtimes"], capture_output=True, text=True, timeout=30 [str(exe), "--list-runtimes"], capture_output=True, text=True, timeout=30
) )
except (OSError, subprocess.TimeoutExpired): except (OSError, subprocess.TimeoutExpired):
return False return None
if result.returncode != 0: if result.returncode != 0:
return False return None
majors = []
for line in result.stdout.splitlines(): for line in result.stdout.splitlines():
parts = line.split() parts = line.split()
if len(parts) >= 2 and parts[0] == "Microsoft.NETCore.App": if len(parts) >= 2 and parts[0] == "Microsoft.NETCore.App":
try: try:
if int(parts[1].split(".")[0]) >= 8: majors.append(int(parts[1].split(".")[0]))
return True
except ValueError: except ValueError:
continue continue
return False return max(majors, default=None)
def fetch_dotnet() -> str: def fetch_dotnet() -> str:
"""Resolve a dotnet host with a modern (>= 8) runtime from the system. """Resolve a system dotnet host able to run vpk (needs .NET >= 10).
The .NET SDK is a build prerequisite installed on the build machine — 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 downloading a runtime per build is slow and flaky. Several dotnet
resolve to a runtime-only installation, and long-running services (CI installations may coexist (PATH may resolve to a runtime-only .NET 8
runners) may carry a stale environment without DOTNET_ROOT or scoop while scoop holds the SDK 10), so probe known locations and pick the
paths — so probe known locations explicitly. newest runtime rather than the first that runs.
""" """
exe_name = "dotnet.exe" if sys.platform == "win32" else "dotnet" exe_name = "dotnet.exe" if sys.platform == "win32" else "dotnet"
candidates: list[Path] = [] candidates: list[Path] = []
@@ -281,14 +285,22 @@ def fetch_dotnet() -> str:
Path.home() / ".dotnet" / exe_name, Path.home() / ".dotnet" / exe_name,
] ]
best: tuple[int, Path] | None = None
for exe in candidates: for exe in candidates:
if exe.exists() and _dotnet_has_runtime(exe): if not exe.exists():
print(f"Using dotnet at {exe}") continue
return str(exe) major = _dotnet_runtime_major(exe)
if major is not None and (best is None or major > best[0]):
best = (major, exe)
if best is not None and best[0] >= _VPK_REQUIRED_DOTNET_MAJOR:
print(f"Using dotnet at {best[1]} (.NET {best[0]})")
return str(best[1])
found = f"newest found is .NET {best[0]} at {best[1]}" if best else "none found"
raise RuntimeError( raise RuntimeError(
"No dotnet with Microsoft.NETCore.App >= 8 found. " f"vpk requires Microsoft.NETCore.App >= {_VPK_REQUIRED_DOTNET_MAJOR} ({found}). "
"Install the .NET SDK on this build machine " "Install the current .NET SDK on this build machine "
"(Windows: `scoop install dotnet-sdk`; macOS: `brew install dotnet-sdk`; " "(Windows: `scoop install dotnet-sdk`; macOS: `brew install dotnet-sdk`; "
"Linux: distro `dotnet-sdk` package or the dotnet-install script)." "Linux: distro `dotnet-sdk` package or the dotnet-install script)."
) )