Files
Jackify/jackify/backend/services/dotnet10_installer.py
T

160 lines
6.2 KiB
Python

"""
.NET 10 SDK and Desktop Runtime installation for Synthesis patcher compilation.
Split out of tool_config_service.py to keep that file under the project's
line-count guardrail.
"""
import logging
import os
import subprocess
import urllib.request
import zipfile
from pathlib import Path
from typing import Callable, Optional
logger = logging.getLogger(__name__)
# .NET 10 SDK - ZIP distribution, extracted directly to avoid running an EXE under Wine.
# Synthesis requires the SDK (not just runtime) for patcher compilation. A newer SDK can
# still build projects targeting older TFMs (net6/7/8/9) as long as the matching runtime is
# present - which the native/winetricks component pipeline already installs separately - but
# an older SDK cannot target a newer TFM it doesn't recognize. Some Synthesis patchers target
# net10.0, so the SDK itself must be 10, matching Fluorine's confirmed-working configuration.
# ZIP distribution: Microsoft's officially supported xcopy-deployable install method, no
# installer-side effects the CLI depends on. Confirmed sufficient for net10.0 patcher builds
# by Styyx (2026-07-14).
_DOTNET_SDK_URL = "https://builds.dotnet.microsoft.com/dotnet/Sdk/10.0.301/dotnet-sdk-10.0.301-win-x64.zip"
_DOTNET_SDK_FILENAME = "dotnet-sdk-10.0.301-win-x64.zip"
# .NET Desktop Runtime 10 - provides NETCore.App + WindowsDesktop.App 10.0.2.
# Covers Synthesis patchers targeting .NET 10 runtime. ZIP distribution, same rationale
# as the SDK above - avoids running an EXE installer under Wine.
_DOTNET10_DESKTOP_URL = "https://builds.dotnet.microsoft.com/dotnet/WindowsDesktop/10.0.2/windowsdesktop-runtime-10.0.2-win-x64.zip"
_DOTNET10_DESKTOP_FILENAME = "windowsdesktop-runtime-10.0.2-win-x64.zip"
def install_dotnet_sdk(
prefix_path: Path,
wine_bin: str,
log: Callable[[str], None],
) -> bool:
"""
Download and extract the .NET 10 SDK ZIP into the Wine prefix.
Uses the standalone ZIP distribution to avoid running an EXE under Wine.
Synthesis requires the full SDK (Roslyn compiler) for patcher compilation.
"""
try:
from jackify.shared.paths import get_jackify_data_dir
cache_dir = get_jackify_data_dir() / "cache"
cache_dir.mkdir(parents=True, exist_ok=True)
sdk_zip = cache_dir / _DOTNET_SDK_FILENAME
if not sdk_zip.exists():
log(f"Downloading .NET 10 SDK ({_DOTNET_SDK_FILENAME})...")
urllib.request.urlretrieve(_DOTNET_SDK_URL, sdk_zip)
log(".NET 10 SDK downloaded")
else:
log(".NET 10 SDK already cached, skipping download")
dest = prefix_path / "drive_c" / "Program Files" / "dotnet"
dest.mkdir(parents=True, exist_ok=True)
log("Extracting .NET 10 SDK...")
with zipfile.ZipFile(sdk_zip) as zf:
zf.extractall(dest)
log(".NET 10 SDK extracted successfully")
return True
except Exception as e:
log(f"Failed to install .NET 10 SDK: {e}")
return False
def find_winetricks_bin() -> Optional[str]:
"""Locate the bundled winetricks binary, checking APPDIR for the AppImage case."""
module_dir = Path(__file__).parent.parent.parent
winetricks_bin = str(module_dir / "tools" / "winetricks")
if not os.path.exists(winetricks_bin):
appdir = os.environ.get("APPDIR", "")
if appdir:
winetricks_bin = os.path.join(appdir, "opt", "jackify", "tools", "winetricks")
return winetricks_bin if os.path.exists(winetricks_bin) else None
def install_dotnet10_desktop_runtime(
prefix_path: Path,
wine_bin: str,
log: Callable[[str], None],
) -> bool:
"""
Download and extract the .NET Desktop Runtime 10 ZIP into the Wine prefix.
Provides NETCore.App and WindowsDesktop.App 10.x for patchers targeting .NET 10.
Falls back to the bundled winetricks dotnetdesktop10 verb if the ZIP install fails.
"""
try:
from jackify.shared.paths import get_jackify_data_dir
cache_dir = get_jackify_data_dir() / "cache"
cache_dir.mkdir(parents=True, exist_ok=True)
runtime_zip = cache_dir / _DOTNET10_DESKTOP_FILENAME
if not runtime_zip.exists():
log(f"Downloading .NET Desktop Runtime 10 ({_DOTNET10_DESKTOP_FILENAME})...")
urllib.request.urlretrieve(_DOTNET10_DESKTOP_URL, runtime_zip)
log(".NET Desktop Runtime 10 downloaded")
else:
log(".NET Desktop Runtime 10 already cached, skipping download")
dest = prefix_path / "drive_c" / "Program Files" / "dotnet"
dest.mkdir(parents=True, exist_ok=True)
log("Extracting .NET Desktop Runtime 10...")
with zipfile.ZipFile(runtime_zip) as zf:
zf.extractall(dest)
log(".NET Desktop Runtime 10 extracted successfully")
return True
except Exception as e:
log(f"Failed to install .NET Desktop Runtime 10 via ZIP: {e}")
return _install_dotnet10_desktop_runtime_winetricks(prefix_path, wine_bin, log)
def _install_dotnet10_desktop_runtime_winetricks(
prefix_path: Path,
wine_bin: str,
log: Callable[[str], None],
) -> bool:
"""Fallback: install the .NET Desktop Runtime 10 via the bundled winetricks verb."""
winetricks_bin = find_winetricks_bin()
if not winetricks_bin:
log("Bundled winetricks not found - cannot fall back for .NET Desktop Runtime 10")
return False
try:
log("Falling back to winetricks dotnetdesktop10...")
env = os.environ.copy()
env["WINEPREFIX"] = str(prefix_path)
env["WINE"] = wine_bin
env["WINEDEBUG"] = "-all"
env["DISPLAY"] = env.get("DISPLAY", ":0")
result = subprocess.run(
[winetricks_bin, "-q", "dotnetdesktop10"],
env=env,
capture_output=True,
text=True,
timeout=300,
)
if result.returncode != 0:
log(f"winetricks dotnetdesktop10 exited with code {result.returncode}")
return False
log(".NET Desktop Runtime 10 installed via winetricks fallback")
return True
except subprocess.TimeoutExpired:
log("winetricks dotnetdesktop10 timed out")
return False
except Exception as e:
log(f"winetricks dotnetdesktop10 fallback failed: {e}")
return False