Skip to content

Commit b27fbfd

Browse files
committed
Fix bundled .NET runtime config
1 parent 87d4297 commit b27fbfd

4 files changed

Lines changed: 52 additions & 17 deletions

File tree

RELEASE_NOTES.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
## 🔧 Main App
2-
* Added rename asset action.
2+
* Fixed Cleanup failing to open when the .NET Desktop Runtime config was missing.
3+
* Improved .NET runtime checks for bundled stable builds.

makefile.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,11 @@ def build_app_pyinstaller(fast=False, channel='stable') -> None:
221221
_generate_pycparser_tables()
222222
tables = find_pycparser_tables()
223223

224+
runtime_config_dir = os.path.join(cur_dir, 'src', 'external', 'dotnet')
225+
runtime_config_path = os.path.join(runtime_config_dir, 'Hammer5Tools.runtimeconfig.json')
226+
if channel == 'stable':
227+
generate_runtime_config(runtime_config_dir)
228+
224229
pyinstaller_cmd = [
225230
sys.executable, '-m', 'PyInstaller',
226231
'--name=Hammer5Tools_Core',
@@ -269,14 +274,10 @@ def build_app_pyinstaller(fast=False, channel='stable') -> None:
269274
external,
270275
*( [f'--add-binary=src{os.sep}external{os.sep}{dll};external{os.sep}{dll}' for dll, _ in dotnet_dlls] if channel == 'stable' else [] ),
271276
*( get_dotnet_runtime_data() if channel == 'stable' else [] ),
272-
'--add-data=src/external/dotnet;dotnet/' if os.path.exists('src/external/dotnet') else '',
277+
f'--add-data={runtime_config_path};dotnet' if channel == 'stable' and os.path.exists(runtime_config_path) else '',
273278
'src/main.py'
274279
]
275280
pyinstaller_cmd = [arg for arg in pyinstaller_cmd if arg]
276-
277-
# Ensure runtime config exists for bundling
278-
if channel == 'stable':
279-
generate_runtime_config(os.path.join(cur_dir, 'src', 'external', 'dotnet'))
280281

281282
if tables:
282283
lextab_path, yacctab_path = tables

src/common.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def generate_unique_name(base_name: str, existing_names: Set[str], separator: st
4646
from pathlib import Path
4747

4848
# Versions
49-
app_version = '5.1.1'
49+
app_version = '5.1.2'
5050

5151
def get_channel() -> str:
5252
"""
@@ -385,4 +385,4 @@ def fast_deepcopy(obj):
385385

386386
def get_cs2_path():
387387
from src.settings.common import get_cs2_path as _get_cs2_path
388-
return _get_cs2_path()
388+
return _get_cs2_path()

src/dotnet.py

Lines changed: 42 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
from PySide6.QtWidgets import QMessageBox
1717

1818
tests_path = Path(__file__).parent.parent / 'tests'
19+
RUNTIME_CONFIG_NAME = 'Hammer5Tools.runtimeconfig.json'
1920

2021

2122
class DotNetPaths:
@@ -57,21 +58,28 @@ def _init_pythonnet(self):
5758

5859
try:
5960
from pythonnet import load
60-
61+
62+
runtime_config = None
63+
6164
# Check for bundled runtime in frozen (PyInstaller) state
6265
if getattr(sys, 'frozen', False):
6366
bundled_dotnet = os.path.join(sys._MEIPASS, 'dotnet')
6467
if os.path.exists(bundled_dotnet):
6568
# Set DOTNET_ROOT to help clr_loader find the bundled runtime
6669
os.environ["DOTNET_ROOT"] = bundled_dotnet
70+
os.environ["DOTNET_ROOT_X64"] = bundled_dotnet
6771
# Point to the runtime config if it exists
68-
runtime_config = os.path.join(bundled_dotnet, 'Hammer5Tools.runtimeconfig.json')
69-
if os.path.exists(runtime_config):
70-
load("coreclr", runtime_config=runtime_config)
71-
else:
72-
load("coreclr")
73-
else:
74-
load("coreclr")
72+
bundled_config = os.path.join(bundled_dotnet, RUNTIME_CONFIG_NAME)
73+
if os.path.exists(bundled_config):
74+
runtime_config = bundled_config
75+
76+
if runtime_config is None:
77+
local_config = Path(__file__).parent / 'external' / 'dotnet' / RUNTIME_CONFIG_NAME
78+
if local_config.exists():
79+
runtime_config = str(local_config)
80+
81+
if runtime_config:
82+
load("coreclr", runtime_config=runtime_config)
7583
else:
7684
load("coreclr")
7785

@@ -80,6 +88,11 @@ def _init_pythonnet(self):
8088
self._initialized = True
8189
except ImportError as e:
8290
raise RuntimeError("Python.NET not available. Install with: pip install pythonnet") from e
91+
except Exception as e:
92+
raise RuntimeError(
93+
".NET Desktop Runtime 9.0 or newer is required for this tool. "
94+
"Install it from https://dotnet.microsoft.com/download/dotnet/9.0"
95+
) from e
8396

8497
def _load_assembly(self, path: Path) -> None:
8598
"""Load a .NET assembly with error handling."""
@@ -347,7 +360,7 @@ def check_runtime(self, show_dialog: bool = True) -> bool:
347360
# 1. Check for bundled runtime first (in frozen state)
348361
if getattr(sys, 'frozen', False):
349362
bundled_dotnet = os.path.join(sys._MEIPASS, 'dotnet')
350-
if os.path.exists(bundled_dotnet):
363+
if self._bundled_runtime_is_complete(bundled_dotnet):
351364
return True
352365

353366
try:
@@ -384,6 +397,26 @@ def check_runtime(self, show_dialog: bool = True) -> bool:
384397
setup_keyvalues2()
385398
return False
386399

400+
def _bundled_runtime_is_complete(self, bundled_dotnet: str) -> bool:
401+
"""Validate the bundled runtime enough to avoid pythonnet hostfxr crashes."""
402+
if not os.path.isdir(bundled_dotnet):
403+
return False
404+
405+
runtime_config = os.path.join(bundled_dotnet, RUNTIME_CONFIG_NAME)
406+
if not os.path.isfile(runtime_config):
407+
return False
408+
409+
shared = os.path.join(bundled_dotnet, "shared")
410+
for framework in ("Microsoft.NETCore.App", "Microsoft.WindowsDesktop.App"):
411+
framework_dir = os.path.join(shared, framework)
412+
if not os.path.isdir(framework_dir):
413+
return False
414+
versions = [v for v in os.listdir(framework_dir) if v.startswith(self.min_version)]
415+
if not versions:
416+
return False
417+
418+
return True
419+
387420
def _show_download_dialog(self):
388421
"""Show dialog to download .NET runtime."""
389422
try:

0 commit comments

Comments
 (0)