1616from PySide6 .QtWidgets import QMessageBox
1717
1818tests_path = Path (__file__ ).parent .parent / 'tests'
19+ RUNTIME_CONFIG_NAME = 'Hammer5Tools.runtimeconfig.json'
1920
2021
2122class 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