From 4fa74cd96adaeff0266fae7cc49cf72c64674150 Mon Sep 17 00:00:00 2001 From: Romain Merlet Date: Tue, 13 Jan 2026 15:25:39 +0100 Subject: [PATCH 1/6] Add Windows 3.14 build/test workflow --- .github/workflows/windows-build-test-3.14.yml | 53 +++++++++++++++++++ 1 file changed, 53 insertions(+) create mode 100644 .github/workflows/windows-build-test-3.14.yml diff --git a/.github/workflows/windows-build-test-3.14.yml b/.github/workflows/windows-build-test-3.14.yml new file mode 100644 index 000000000..09c00fafa --- /dev/null +++ b/.github/workflows/windows-build-test-3.14.yml @@ -0,0 +1,53 @@ +name: Windows Build and Test (3.14) + +on: + workflow_dispatch: + +jobs: + build-test: + name: Build and Test + runs-on: windows-latest + timeout-minutes: 15 + + steps: + - name: Checkout code + uses: actions/checkout@v6 + + - name: Setup .NET + uses: actions/setup-dotnet@v5 + with: + dotnet-version: '8.0.x' + + - name: Set up Python 3.14 + uses: astral-sh/setup-uv@v7 + with: + architecture: x64 + python-version: '3.14' + cache-python: true + activate-environment: true + enable-cache: true + + - name: Synchronize the virtual environment + run: uv sync --managed-python + + - name: Show pyvenv.cfg + run: cat .venv/pyvenv.cfg + + - name: Embedding tests (Mono/.NET Framework) + run: dotnet test --runtime any-x64 --framework net472 --logger "console;verbosity=detailed" src/embed_tests/ + if: always() + env: + MONO_THREADS_SUSPEND: preemptive # https://github.com/mono/mono/issues/21466 + + - name: Embedding tests (.NET Core) + run: dotnet test --runtime any-x64 --framework net8.0 --logger "console;verbosity=detailed" src/embed_tests/ + if: always() + + - name: Python Tests (.NET Core) + run: pytest --runtime coreclr + + - name: Python Tests (.NET Framework) + run: pytest --runtime netfx + + - name: Python tests run from .NET + run: dotnet test --runtime any-x64 src/python_tests_runner/ From 984a338f18df171ce7f287d589ef5aa785a4be74 Mon Sep 17 00:00:00 2001 From: Romain Merlet Date: Tue, 13 Jan 2026 15:30:13 +0100 Subject: [PATCH 2/6] Limit CI triggers to Windows 3.14 workflow --- .github/workflows/docs.yml | 3 ++- .github/workflows/main.yml | 5 +---- .github/workflows/windows-build-test-3.14.yml | 4 ++++ 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 3937d85e0..54024eaac 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -1,6 +1,7 @@ name: Documentation -on: [push, pull_request] +on: + workflow_dispatch: jobs: build: diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index e5e291169..83deb361b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,10 +1,7 @@ name: Main on: - push: - branches: - - master - pull_request: + workflow_dispatch: jobs: build-test: diff --git a/.github/workflows/windows-build-test-3.14.yml b/.github/workflows/windows-build-test-3.14.yml index 09c00fafa..119de2902 100644 --- a/.github/workflows/windows-build-test-3.14.yml +++ b/.github/workflows/windows-build-test-3.14.yml @@ -1,6 +1,10 @@ name: Windows Build and Test (3.14) on: + push: + branches: + - master + pull_request: workflow_dispatch: jobs: From cd53032b8ee0e07bfb8ad1448b71defd964ee45c Mon Sep 17 00:00:00 2001 From: Romain Merlet Date: Wed, 14 Jan 2026 09:14:22 +0100 Subject: [PATCH 3/6] Allow net8 embed tests to fail on Windows 3.14 --- .github/workflows/windows-build-test-3.14.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/windows-build-test-3.14.yml b/.github/workflows/windows-build-test-3.14.yml index 119de2902..d775935ef 100644 --- a/.github/workflows/windows-build-test-3.14.yml +++ b/.github/workflows/windows-build-test-3.14.yml @@ -46,6 +46,7 @@ jobs: - name: Embedding tests (.NET Core) run: dotnet test --runtime any-x64 --framework net8.0 --logger "console;verbosity=detailed" src/embed_tests/ if: always() + continue-on-error: true - name: Python Tests (.NET Core) run: pytest --runtime coreclr From 302f1d2e38568e388406109ec8e3946a2dda97c8 Mon Sep 17 00:00:00 2001 From: Romain Merlet Date: Wed, 14 Jan 2026 09:14:28 +0100 Subject: [PATCH 4/6] Skip Python GC on Windows 3.14 shutdown --- .github/workflows/windows-build-test-3.14.yml | 1 - src/runtime/PythonEngine.cs | 14 ++++++++ src/runtime/Runtime.cs | 35 +++++++++++++++---- 3 files changed, 43 insertions(+), 7 deletions(-) diff --git a/.github/workflows/windows-build-test-3.14.yml b/.github/workflows/windows-build-test-3.14.yml index d775935ef..119de2902 100644 --- a/.github/workflows/windows-build-test-3.14.yml +++ b/.github/workflows/windows-build-test-3.14.yml @@ -46,7 +46,6 @@ jobs: - name: Embedding tests (.NET Core) run: dotnet test --runtime any-x64 --framework net8.0 --logger "console;verbosity=detailed" src/embed_tests/ if: always() - continue-on-error: true - name: Python Tests (.NET Core) run: pytest --runtime coreclr diff --git a/src/runtime/PythonEngine.cs b/src/runtime/PythonEngine.cs index 13855adef..78c1e9db6 100644 --- a/src/runtime/PythonEngine.cs +++ b/src/runtime/PythonEngine.cs @@ -143,6 +143,20 @@ public static string Version get { return Marshal.PtrToStringAnsi(Runtime.Py_GetVersion()); } } + internal static Version GetPythonVersion() + { + string? versionText = Version; + if (string.IsNullOrWhiteSpace(versionText)) + { + return new Version(0, 0); + } + + string versionPart = versionText.Split(' ')[0]; + return Version.TryParse(versionPart, out Version? parsed) + ? parsed + : new Version(0, 0); + } + public static string BuildInfo { get { return Marshal.PtrToStringAnsi(Runtime.Py_GetBuildInfo()); } diff --git a/src/runtime/Runtime.cs b/src/runtime/Runtime.cs index 399608733..b13447249 100644 --- a/src/runtime/Runtime.cs +++ b/src/runtime/Runtime.cs @@ -262,7 +262,7 @@ internal static void Shutdown() if (!HostedInPython && !ProcessIsTerminating) { // avoid saving dead objects - TryCollectingGarbage(runs: 3); + TryCollectingGarbage(runs: 3, pythonGC: !ShouldSkipPythonGcOnShutdown); RuntimeData.Stash(); } @@ -275,7 +275,8 @@ internal static void Shutdown() RemoveClrRootModule(); TryCollectingGarbage(MaxCollectRetriesOnShutdown, forceBreakLoops: true, - obj: true, derived: false, buffer: false); + obj: true, derived: false, buffer: false, + pythonGC: !ShouldSkipPythonGcOnShutdown); CLRObject.creationBlocked = true; NullGCHandles(ExtensionType.loadedExtensions); @@ -294,8 +295,12 @@ internal static void Shutdown() DisposeLazyObject(hexCallable); PyObjectConversions.Reset(); - PyGC_Collect(); - bool everythingSeemsCollected = TryCollectingGarbage(MaxCollectRetriesOnShutdown); + if (!ShouldSkipPythonGcOnShutdown) + { + PyGC_Collect(); + } + bool everythingSeemsCollected = TryCollectingGarbage(MaxCollectRetriesOnShutdown, + pythonGC: !ShouldSkipPythonGcOnShutdown); Debug.Assert(everythingSeemsCollected); Finalizer.Shutdown(); @@ -328,7 +333,8 @@ internal static void Shutdown() const int MaxCollectRetriesOnShutdown = 20; internal static int _collected; static bool TryCollectingGarbage(int runs, bool forceBreakLoops, - bool obj = true, bool derived = true, bool buffer = true) + bool obj = true, bool derived = true, bool buffer = true, + bool pythonGC = true) { if (runs <= 0) throw new ArgumentOutOfRangeException(nameof(runs)); @@ -340,7 +346,10 @@ static bool TryCollectingGarbage(int runs, bool forceBreakLoops, { GC.Collect(); GC.WaitForPendingFinalizers(); - pyCollected += PyGC_Collect(); + if (pythonGC) + { + pyCollected += PyGC_Collect(); + } pyCollected += Finalizer.Instance.DisposeAll(disposeObj: obj, disposeDerived: derived, disposeBuffer: buffer); @@ -366,6 +375,20 @@ static bool TryCollectingGarbage(int runs, bool forceBreakLoops, public static bool TryCollectingGarbage(int runs) => TryCollectingGarbage(runs, forceBreakLoops: false); + static bool ShouldSkipPythonGcOnShutdown + { + get + { + if (!IsWindows) + { + return false; + } + + Version version = PythonEngine.GetPythonVersion(); + return version >= new Version(3, 14); + } + } + static void DisposeLazyObject(Lazy pyObject) { if (pyObject.IsValueCreated) From c2344920db5ebfd3e88db8f93f04dd19bbdb4c1a Mon Sep 17 00:00:00 2001 From: Romain Merlet Date: Wed, 14 Jan 2026 18:00:54 +0100 Subject: [PATCH 5/6] Parse pre-release Python versions for shutdown gating --- src/runtime/PythonEngine.cs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/runtime/PythonEngine.cs b/src/runtime/PythonEngine.cs index 78c1e9db6..8bc404991 100644 --- a/src/runtime/PythonEngine.cs +++ b/src/runtime/PythonEngine.cs @@ -2,6 +2,8 @@ using System.Collections.Generic; using System.ComponentModel; using System.Diagnostics; +using System.Globalization; +using System.Text.RegularExpressions; using System.Linq; using System.Reflection; using System.Runtime.InteropServices; @@ -151,10 +153,19 @@ internal static Version GetPythonVersion() return new Version(0, 0); } - string versionPart = versionText.Split(' ')[0]; - return Version.TryParse(versionPart, out Version? parsed) - ? parsed - : new Version(0, 0); + Match match = Regex.Match(versionText, @"^(\\d+)\\.(\\d+)(?:\\.(\\d+))?"); + if (!match.Success) + { + return new Version(0, 0); + } + + int major = int.Parse(match.Groups[1].Value, CultureInfo.InvariantCulture); + int minor = int.Parse(match.Groups[2].Value, CultureInfo.InvariantCulture); + int patch = match.Groups[3].Success + ? int.Parse(match.Groups[3].Value, CultureInfo.InvariantCulture) + : 0; + + return new Version(major, minor, patch); } public static string BuildInfo From 6cc744141e808d97179e14307338b523cf27067f Mon Sep 17 00:00:00 2001 From: Romain Merlet Date: Wed, 14 Jan 2026 18:01:00 +0100 Subject: [PATCH 6/6] Prefer public PyThreadState_GetUnchecked symbol --- src/runtime/Runtime.Delegates.cs | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/runtime/Runtime.Delegates.cs b/src/runtime/Runtime.Delegates.cs index dc4a4b0a9..b784cfeda 100644 --- a/src/runtime/Runtime.Delegates.cs +++ b/src/runtime/Runtime.Delegates.cs @@ -25,14 +25,13 @@ static Delegates() PyThreadState_Get = (delegate* unmanaged[Cdecl])GetFunctionByName(nameof(PyThreadState_Get), GetUnmanagedDll(_PythonDll)); try { - // Up until Python 3.13, this function was private and named - // slightly differently. - PyThreadState_GetUnchecked = (delegate* unmanaged[Cdecl])GetFunctionByName("_PyThreadState_UncheckedGet", GetUnmanagedDll(_PythonDll)); + PyThreadState_GetUnchecked = (delegate* unmanaged[Cdecl])GetFunctionByName(nameof(PyThreadState_GetUnchecked), GetUnmanagedDll(_PythonDll)); } catch (MissingMethodException) { - - PyThreadState_GetUnchecked = (delegate* unmanaged[Cdecl])GetFunctionByName(nameof(PyThreadState_GetUnchecked), GetUnmanagedDll(_PythonDll)); + // Up until Python 3.12, this function was private and named + // slightly differently. + PyThreadState_GetUnchecked = (delegate* unmanaged[Cdecl])GetFunctionByName("_PyThreadState_UncheckedGet", GetUnmanagedDll(_PythonDll)); } try {