From 63cf5a790ea39d2c618be54f549a0c62b3f277aa Mon Sep 17 00:00:00 2001 From: Metadorius Date: Fri, 27 Mar 2026 00:05:05 +0200 Subject: [PATCH 1/5] Support .NET Framework 4.6.1 --- setup.py | 2 +- src/compat/Python.Runtime.Compat.csproj | 13 ++++++++ src/runtime/AppConfig.cs | 44 +++++++++++++++++++++++++ src/runtime/Loader.cs | 17 ++++++++++ 4 files changed, 75 insertions(+), 1 deletion(-) create mode 100644 src/compat/Python.Runtime.Compat.csproj create mode 100644 src/runtime/AppConfig.cs diff --git a/setup.py b/setup.py index 7c02b7710..678aeba2e 100644 --- a/setup.py +++ b/setup.py @@ -127,7 +127,7 @@ def finalize_options(self): dotnet_libs = [ DotnetLib( "python-runtime", - "src/runtime/Python.Runtime.csproj", + "src/compat/Python.Runtime.Compat.csproj", output="pythonnet/runtime", ) ] diff --git a/src/compat/Python.Runtime.Compat.csproj b/src/compat/Python.Runtime.Compat.csproj new file mode 100644 index 000000000..5c607e157 --- /dev/null +++ b/src/compat/Python.Runtime.Compat.csproj @@ -0,0 +1,13 @@ + + + + net461 + Library + + false + false + + + + + diff --git a/src/runtime/AppConfig.cs b/src/runtime/AppConfig.cs new file mode 100644 index 000000000..2318c3360 --- /dev/null +++ b/src/runtime/AppConfig.cs @@ -0,0 +1,44 @@ +using System; +using System.Linq; +using System.Reflection; + +namespace Python.Runtime +{ + static class AppConfig + { + /// + /// Override the AppDomain's config file path and reset the + /// ConfigurationManager cache so the CLR re-reads binding redirects. + /// Uses reflection to avoid a compile-time dependency on System.Configuration. + /// + public static void SetConfigFile(string path) + { + AppDomain.CurrentDomain.SetData("APP_CONFIG_FILE", path); + ResetConfigMechanism(); + } + + private static void ResetConfigMechanism() + { + var configManager = Type.GetType( + "System.Configuration.ConfigurationManager, System.Configuration, " + + "Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); + + if (configManager == null) + return; + + configManager + .GetField("s_initState", BindingFlags.NonPublic | BindingFlags.Static) + ?.SetValue(null, 0); + + configManager + .GetField("s_configSystem", BindingFlags.NonPublic | BindingFlags.Static) + ?.SetValue(null, null); + + configManager.Assembly + .GetTypes() + .FirstOrDefault(x => x.FullName == "System.Configuration.ClientConfigPaths") + ?.GetField("s_current", BindingFlags.NonPublic | BindingFlags.Static) + ?.SetValue(null, null); + } + } +} diff --git a/src/runtime/Loader.cs b/src/runtime/Loader.cs index c0e964abc..5f76a0c05 100644 --- a/src/runtime/Loader.cs +++ b/src/runtime/Loader.cs @@ -1,4 +1,5 @@ using System; +using System.IO; using System.Text; namespace Python.Runtime @@ -12,6 +13,22 @@ public unsafe static int Initialize(IntPtr data, int size) { try { + // On .NET Framework, the host is python.exe which has no binding + // redirects for netstandard2.0 shims (e.g. RuntimeInformation + // Version=0.0.0.0 vs the 4.0.2.0 shim on disk). Binding redirects + // via config files can't be injected after AppDomain creation, so + // resolve assemblies from our runtime directory directly. + AppDomain.CurrentDomain.AssemblyResolve += (_, args) => + { + var name = new System.Reflection.AssemblyName(args.Name); + var dir = Path.GetDirectoryName(typeof(Loader).Assembly.Location); + var path = Path.Combine(dir, name.Name + ".dll"); + + return File.Exists(path) + ? System.Reflection.Assembly.LoadFrom(path) + : null; + }; + var dllPath = Encodings.UTF8.GetString((byte*)data.ToPointer(), size); if (!string.IsNullOrEmpty(dllPath)) From d2db7c7e70c0652a515808c2bd19238967d6523a Mon Sep 17 00:00:00 2001 From: Metadorius Date: Sat, 28 Mar 2026 00:16:12 +0200 Subject: [PATCH 2/5] Add a PyInstaller hook to handle files correctly in PyInstaller --- pyproject.toml | 3 +++ pythonnet/_pyinstaller/__init__.py | 4 ++++ pythonnet/_pyinstaller/hook-clr.py | 9 +++++++++ 3 files changed, 16 insertions(+) create mode 100644 pythonnet/_pyinstaller/__init__.py create mode 100644 pythonnet/_pyinstaller/hook-clr.py diff --git a/pyproject.toml b/pyproject.toml index 59d4d107a..d9b26f63c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,6 +49,9 @@ email = "pythonnet@python.org" Homepage = "https://pythonnet.github.io/" Sources = "https://github.com/pythonnet/pythonnet" +[project.entry-points.pyinstaller40] +hook-dirs = "pythonnet._pyinstaller:get_hook_dirs" + [tool.setuptools] zip-safe = false py-modules = ["clr"] diff --git a/pythonnet/_pyinstaller/__init__.py b/pythonnet/_pyinstaller/__init__.py new file mode 100644 index 000000000..2ed816a4e --- /dev/null +++ b/pythonnet/_pyinstaller/__init__.py @@ -0,0 +1,4 @@ +import os + +def get_hook_dirs(): + return [os.path.dirname(__file__)] diff --git a/pythonnet/_pyinstaller/hook-clr.py b/pythonnet/_pyinstaller/hook-clr.py new file mode 100644 index 000000000..f0b058681 --- /dev/null +++ b/pythonnet/_pyinstaller/hook-clr.py @@ -0,0 +1,9 @@ +from PyInstaller.utils.hooks import collect_data_files, collect_dynamic_libs + +try: + binaries = collect_dynamic_libs("pythonnet") + datas = collect_data_files("pythonnet") +except Exception: + # name conflict with https://pypi.org/project/clr/, do not crash if just clr is present + binaries = [] + datas = [] From 8c6b4cae4cc0a9a6a8743d9de9dfc14716766aeb Mon Sep 17 00:00:00 2001 From: Metadorius Date: Sat, 28 Mar 2026 01:30:43 +0200 Subject: [PATCH 3/5] docs --- AUTHORS.md | 1 + CHANGELOG.md | 3 ++- doc/source/python.rst | 2 +- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/AUTHORS.md b/AUTHORS.md index 7ea639059..96e58ff46 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -89,3 +89,4 @@ - Ehsan Iran-Nejad ([@eirannejad](https://github.com/eirannejad)) - ([@legomanww](https://github.com/legomanww)) - ([@gertdreyer](https://github.com/gertdreyer)) +- Kerbiter ([@Metadorius](https://github.com/Metadorius)) diff --git a/CHANGELOG.md b/CHANGELOG.md index df68fbb39..73a81368f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,12 +10,13 @@ This document follows the conventions laid out in [Keep a CHANGELOG][]. ### Added - Support `del obj[...]` for types derived from `IList` and `IDictionary` +- Support for .NET Framework 4.6.1 (#2701) ### Changed ### Fixed - Fixed crash when trying to `del clrObj[...]` for non-arrays -- ci: properly exclude job (#2542) +- ci: properly exclude job (#2542) ## [3.0.5](https://github.com/pythonnet/pythonnet/releases/tag/v3.0.5) - 2024-12-13 diff --git a/doc/source/python.rst b/doc/source/python.rst index a9228537c..89f90eb07 100644 --- a/doc/source/python.rst +++ b/doc/source/python.rst @@ -45,7 +45,7 @@ Mono (``mono``) .NET Framework (``netfx``) Default on Windows and also only supported there. Must be at least version - 4.7.2. + 4.6.1, with 4.7.2 or later recommended. .NET Core (``coreclr``) Self-contained is not supported, must be at least version 3.1. From ba57bb5b696c7d482c661a6da70a13d6a0559184 Mon Sep 17 00:00:00 2001 From: Metadorius Date: Sat, 28 Mar 2026 01:37:47 +0200 Subject: [PATCH 4/5] remove leftover --- src/runtime/AppConfig.cs | 44 ---------------------------------------- 1 file changed, 44 deletions(-) delete mode 100644 src/runtime/AppConfig.cs diff --git a/src/runtime/AppConfig.cs b/src/runtime/AppConfig.cs deleted file mode 100644 index 2318c3360..000000000 --- a/src/runtime/AppConfig.cs +++ /dev/null @@ -1,44 +0,0 @@ -using System; -using System.Linq; -using System.Reflection; - -namespace Python.Runtime -{ - static class AppConfig - { - /// - /// Override the AppDomain's config file path and reset the - /// ConfigurationManager cache so the CLR re-reads binding redirects. - /// Uses reflection to avoid a compile-time dependency on System.Configuration. - /// - public static void SetConfigFile(string path) - { - AppDomain.CurrentDomain.SetData("APP_CONFIG_FILE", path); - ResetConfigMechanism(); - } - - private static void ResetConfigMechanism() - { - var configManager = Type.GetType( - "System.Configuration.ConfigurationManager, System.Configuration, " - + "Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a"); - - if (configManager == null) - return; - - configManager - .GetField("s_initState", BindingFlags.NonPublic | BindingFlags.Static) - ?.SetValue(null, 0); - - configManager - .GetField("s_configSystem", BindingFlags.NonPublic | BindingFlags.Static) - ?.SetValue(null, null); - - configManager.Assembly - .GetTypes() - .FirstOrDefault(x => x.FullName == "System.Configuration.ClientConfigPaths") - ?.GetField("s_current", BindingFlags.NonPublic | BindingFlags.Static) - ?.SetValue(null, null); - } - } -} From ef2c11aec496538e357da95d39366ae35be15f1d Mon Sep 17 00:00:00 2001 From: Metadorius Date: Fri, 3 Apr 2026 20:48:48 +0300 Subject: [PATCH 5/5] unbreak non-FW builds --- src/runtime/Loader.cs | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/src/runtime/Loader.cs b/src/runtime/Loader.cs index 5f76a0c05..08c0be7cb 100644 --- a/src/runtime/Loader.cs +++ b/src/runtime/Loader.cs @@ -18,16 +18,22 @@ public unsafe static int Initialize(IntPtr data, int size) // Version=0.0.0.0 vs the 4.0.2.0 shim on disk). Binding redirects // via config files can't be injected after AppDomain creation, so // resolve assemblies from our runtime directory directly. - AppDomain.CurrentDomain.AssemblyResolve += (_, args) => + // Only needed on .NET Framework; on .NET (Core) this causes + // duplicate assembly loads, as .deps.json is respected and + // the correct assembly is already found. + if (typeof(object).Assembly.GetName().Name == "mscorlib") { - var name = new System.Reflection.AssemblyName(args.Name); - var dir = Path.GetDirectoryName(typeof(Loader).Assembly.Location); - var path = Path.Combine(dir, name.Name + ".dll"); + AppDomain.CurrentDomain.AssemblyResolve += (_, args) => + { + var name = new System.Reflection.AssemblyName(args.Name); + var dir = Path.GetDirectoryName(typeof(Loader).Assembly.Location); + var path = Path.Combine(dir, name.Name + ".dll"); - return File.Exists(path) - ? System.Reflection.Assembly.LoadFrom(path) - : null; - }; + return File.Exists(path) + ? System.Reflection.Assembly.LoadFrom(path) + : null; + }; + } var dllPath = Encodings.UTF8.GetString((byte*)data.ToPointer(), size);