From 7847adacda5d35582c9f3e5720199ae001681781 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 13 Dec 2025 17:04:12 -0800 Subject: [PATCH 01/18] Limit busy-wait loops in per-subinterpreter GIL test Add explicit timeouts to the busy-wait coordination loops in the Per-Subinterpreter GIL test in tests/test_with_catch/test_subinterpreter.cpp. Previously those loops spun indefinitely waiting for shared atomics like `started` and `sync` to change, which is fine when CPython's free-threading and per-interpreter GIL behavior matches the test's expectations but becomes pathologically bad when that behavior regresses: the `test_with_catch` executable can then hang forever, causing our 3.14t CI jobs to time out after 90 minutes. This change keeps the structure and intent of the test but adds a std::chrono::steady_clock deadline to each of the coordination loops, using a conservative 10 second bound. Worker threads record a failure and return if they hit the timeout, while the main thread fails the test via Catch2 instead of hanging. That way, if future CPython free-threading patches change the semantics again, the test will fail quickly and produced a diagnosable error instead of wedging the CI job. --- tests/test_with_catch/test_subinterpreter.cpp | 62 ++++++++++++++++--- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/tests/test_with_catch/test_subinterpreter.cpp b/tests/test_with_catch/test_subinterpreter.cpp index 3c7c35be19..928557d683 100644 --- a/tests/test_with_catch/test_subinterpreter.cpp +++ b/tests/test_with_catch/test_subinterpreter.cpp @@ -7,6 +7,7 @@ PYBIND11_WARNING_DISABLE_MSVC(4996) # include +# include # include # include # include @@ -309,6 +310,9 @@ TEST_CASE("Per-Subinterpreter GIL") { sync = 0; failure = 0; + using clock = std::chrono::steady_clock; + constexpr auto wait_timeout = std::chrono::seconds(10); + // REQUIRE throws on failure, so we can't use it within the thread # define T_REQUIRE(status) \ do { \ @@ -318,8 +322,16 @@ TEST_CASE("Per-Subinterpreter GIL") { } while (0) auto &&thread_main = [&](int num) { - while (started == 0) - std::this_thread::sleep_for(std::chrono::microseconds(1)); + { + auto deadline = clock::now() + wait_timeout; + while (started == 0) { + if (clock::now() > deadline) { + T_REQUIRE(false); + return; + } + std::this_thread::sleep_for(std::chrono::microseconds(1)); + } + } ++started; py::gil_scoped_acquire gil; @@ -370,15 +382,31 @@ TEST_CASE("Per-Subinterpreter GIL") { // wait for something to set sync to our thread number // we are holding our subinterpreter's GIL - while (sync != num) - std::this_thread::sleep_for(std::chrono::microseconds(1)); + { + auto deadline = clock::now() + wait_timeout; + while (sync != num) { + if (clock::now() > deadline) { + T_REQUIRE(false); + return; + } + std::this_thread::sleep_for(std::chrono::microseconds(1)); + } + } // now change it so the next thread can move on ++sync; // but keep holding the GIL until after the next thread moves on as well - while (sync == num + 1) - std::this_thread::sleep_for(std::chrono::microseconds(1)); + { + auto deadline = clock::now() + wait_timeout; + while (sync == num + 1) { + if (clock::now() > deadline) { + T_REQUIRE(false); + return; + } + std::this_thread::sleep_for(std::chrono::microseconds(1)); + } + } // one last check before quitting the thread, the internals should be different auto sub_int @@ -395,8 +423,15 @@ TEST_CASE("Per-Subinterpreter GIL") { ++started; // ok now wait for the threads to start - while (started != 3) - std::this_thread::sleep_for(std::chrono::microseconds(1)); + { + auto deadline = clock::now() + wait_timeout; + while (started != 3) { + if (clock::now() > deadline) { + FAIL("Timeout while waiting for worker threads to start"); + } + std::this_thread::sleep_for(std::chrono::microseconds(1)); + } + } // we still hold the main GIL, at this point both threads are waiting on the main GIL // IN THE CASE of free threading, the threads are waiting on sync (because there is no GIL) @@ -414,8 +449,15 @@ TEST_CASE("Per-Subinterpreter GIL") { sync = 1; // wait for thread 2 to advance - while (sync != 3) - std::this_thread::sleep_for(std::chrono::microseconds(1)); + { + auto deadline = clock::now() + wait_timeout; + while (sync != 3) { + if (clock::now() > deadline) { + FAIL("Timeout while waiting for sync to reach 3"); + } + std::this_thread::sleep_for(std::chrono::microseconds(1)); + } + } // we know now that thread 1 has run and may be finishing // and thread 2 is waiting for permission to advance From 32725f761b9ca88cd215c4fa4b60bfe8faab5b61 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 13 Dec 2025 19:04:35 -0800 Subject: [PATCH 02/18] Revert "Limit busy-wait loops in per-subinterpreter GIL test" This reverts commit 7847adacda5d35582c9f3e5720199ae001681781. --- tests/test_with_catch/test_subinterpreter.cpp | 62 +++---------------- 1 file changed, 10 insertions(+), 52 deletions(-) diff --git a/tests/test_with_catch/test_subinterpreter.cpp b/tests/test_with_catch/test_subinterpreter.cpp index 928557d683..3c7c35be19 100644 --- a/tests/test_with_catch/test_subinterpreter.cpp +++ b/tests/test_with_catch/test_subinterpreter.cpp @@ -7,7 +7,6 @@ PYBIND11_WARNING_DISABLE_MSVC(4996) # include -# include # include # include # include @@ -310,9 +309,6 @@ TEST_CASE("Per-Subinterpreter GIL") { sync = 0; failure = 0; - using clock = std::chrono::steady_clock; - constexpr auto wait_timeout = std::chrono::seconds(10); - // REQUIRE throws on failure, so we can't use it within the thread # define T_REQUIRE(status) \ do { \ @@ -322,16 +318,8 @@ TEST_CASE("Per-Subinterpreter GIL") { } while (0) auto &&thread_main = [&](int num) { - { - auto deadline = clock::now() + wait_timeout; - while (started == 0) { - if (clock::now() > deadline) { - T_REQUIRE(false); - return; - } - std::this_thread::sleep_for(std::chrono::microseconds(1)); - } - } + while (started == 0) + std::this_thread::sleep_for(std::chrono::microseconds(1)); ++started; py::gil_scoped_acquire gil; @@ -382,31 +370,15 @@ TEST_CASE("Per-Subinterpreter GIL") { // wait for something to set sync to our thread number // we are holding our subinterpreter's GIL - { - auto deadline = clock::now() + wait_timeout; - while (sync != num) { - if (clock::now() > deadline) { - T_REQUIRE(false); - return; - } - std::this_thread::sleep_for(std::chrono::microseconds(1)); - } - } + while (sync != num) + std::this_thread::sleep_for(std::chrono::microseconds(1)); // now change it so the next thread can move on ++sync; // but keep holding the GIL until after the next thread moves on as well - { - auto deadline = clock::now() + wait_timeout; - while (sync == num + 1) { - if (clock::now() > deadline) { - T_REQUIRE(false); - return; - } - std::this_thread::sleep_for(std::chrono::microseconds(1)); - } - } + while (sync == num + 1) + std::this_thread::sleep_for(std::chrono::microseconds(1)); // one last check before quitting the thread, the internals should be different auto sub_int @@ -423,15 +395,8 @@ TEST_CASE("Per-Subinterpreter GIL") { ++started; // ok now wait for the threads to start - { - auto deadline = clock::now() + wait_timeout; - while (started != 3) { - if (clock::now() > deadline) { - FAIL("Timeout while waiting for worker threads to start"); - } - std::this_thread::sleep_for(std::chrono::microseconds(1)); - } - } + while (started != 3) + std::this_thread::sleep_for(std::chrono::microseconds(1)); // we still hold the main GIL, at this point both threads are waiting on the main GIL // IN THE CASE of free threading, the threads are waiting on sync (because there is no GIL) @@ -449,15 +414,8 @@ TEST_CASE("Per-Subinterpreter GIL") { sync = 1; // wait for thread 2 to advance - { - auto deadline = clock::now() + wait_timeout; - while (sync != 3) { - if (clock::now() > deadline) { - FAIL("Timeout while waiting for sync to reach 3"); - } - std::this_thread::sleep_for(std::chrono::microseconds(1)); - } - } + while (sync != 3) + std::this_thread::sleep_for(std::chrono::microseconds(1)); // we know now that thread 1 has run and may be finishing // and thread 2 is waiting for permission to advance From 179a66f606ed921064370aec736bab84f459b68b Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 13 Dec 2025 19:05:01 -0800 Subject: [PATCH 03/18] Add progress reporter for test_with_catch Catch runner Introduce a custom Catch2 reporter for tests/test_with_catch that prints a simple one-line status for each test case as it starts and ends, and wire the cpptest CMake target to invoke test_with_catch with -r progress. This makes it much easier to see where the embedded/interpreter test binary is spending its time in CI logs, and in particular to pinpoint which test case is stuck when the free-threading builds hang. Compared to adding ad hoc timeouts around potentially infinite busy-wait loops in individual tests, a progress reporter is a more general and robust approach: it gives visibility into all tests (including future ones) without changing their behavior, and turns otherwise opaque 90-minute timeouts into locatable issues in the Catch output. --- tests/test_with_catch/catch.cpp | 45 +++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) diff --git a/tests/test_with_catch/catch.cpp b/tests/test_with_catch/catch.cpp index 5bd8b3880e..aa5de2f3c5 100644 --- a/tests/test_with_catch/catch.cpp +++ b/tests/test_with_catch/catch.cpp @@ -13,10 +13,55 @@ PYBIND11_WARNING_DISABLE_MSVC(4996) #endif #define CATCH_CONFIG_RUNNER +#define CATCH_CONFIG_DEFAULT_REPORTER "progress" #include namespace py = pybind11; +// Simple progress reporter that prints a line per test case. +namespace { + +class ProgressReporter : public Catch::CumulativeReporterBase { +public: + using CumulativeReporterBase::CumulativeReporterBase; + + static std::string getDescription() { return "Simple progress reporter (one line per test)"; } + + void testCaseStarting(Catch::TestCaseInfo const &testInfo) override { + stream << "[ RUN ] " << testInfo.name << '\n'; + stream.flush(); + CumulativeReporterBase::testCaseStarting(testInfo); + } + + void testCaseEnded(Catch::TestCaseStats const &testCaseStats) override { + auto const &info = testCaseStats.testInfo; + bool failed = (testCaseStats.totals.assertions.failed > 0); + stream << (failed ? "[ FAILED ] " : "[ OK ] ") << info.name << '\n'; + stream.flush(); + CumulativeReporterBase::testCaseEnded(testCaseStats); + } + + static std::set getSupportedVerbosities() { + return {Catch::Verbosity::Normal}; + } + + void testRunEndedCumulative() override {} + + void noMatchingTestCases(std::string const &spec) override { + stream << "[ NO TEST ] no matching test cases for spec: " << spec << '\n'; + stream.flush(); + } + + void reportInvalidArguments(std::string const &arg) override { + stream << "[ ERROR ] invalid Catch2 arguments: " << arg << '\n'; + stream.flush(); + } +}; + +} // namespace + +CATCH_REGISTER_REPORTER("progress", ProgressReporter) + int main(int argc, char *argv[]) { // Setup for TEST_CASE in test_interpreter.cpp, tagging on a large random number: std::string updated_pythonpath("pybind11_test_with_catch_PYTHONPATH_2099743835476552"); From 60ae0e8f744904ba2499cf44a43fdf91049f6e4a Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 13 Dec 2025 19:14:44 -0800 Subject: [PATCH 04/18] Temporarily limit CI to Python 3.14t free-threading jobs --- .github/workflows/ci.yml | 131 ++++++--------------------------------- 1 file changed, 18 insertions(+), 113 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d00ee6d27e..0653b2adcd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,27 +35,9 @@ jobs: fail-fast: false matrix: include: - - runs-on: ubuntu-22.04 - python-version: '3.8' - cmake-args: -DPYBIND11_FINDPYTHON=OFF -DPYBIND11_NUMPY_1_ONLY=ON - - runs-on: ubuntu-latest - python-version: '3.13' - cmake-args: -DCMAKE_CXX_STANDARD=23 -DPYBIND11_SIMPLE_GIL_MANAGEMENT=ON - runs-on: ubuntu-latest python-version: '3.14t' cmake-args: -DCMAKE_CXX_STANDARD=17 -DPYBIND11_TEST_SMART_HOLDER=ON - - runs-on: ubuntu-latest - python-version: 'pypy3.11' - cmake-args: -DCMAKE_CXX_STANDARD=17 - - runs-on: ubuntu-latest - python-version: 'graalpy-24.2' - cmake-args: -DCMAKE_CXX_STANDARD=20 - - runs-on: macos-latest - python-version: '3.14' - cmake-args: -DCMAKE_CXX_STANDARD=14 - - runs-on: windows-2022 - python-version: '3.8' - cmake-args: -DPYBIND11_FINDPYTHON=OFF name: 🐍 @@ -71,90 +53,12 @@ jobs: fail-fast: false matrix: include: - - runs-on: ubuntu-latest - python-version: '3.8' - cmake-args: -DPYBIND11_FINDPYTHON=ON -DCMAKE_CXX_STANDARD=17 - - runs-on: ubuntu-latest - python-version: '3.10' - cmake-args: -DCMAKE_CXX_STANDARD=20 - - runs-on: ubuntu-latest - python-version: '3.11' - cmake-args: -DPYBIND11_TEST_SMART_HOLDER=ON -DCMAKE_CXX_STANDARD=17 - - runs-on: ubuntu-latest - python-version: '3.12' - cmake-args: -DPYBIND11_TEST_SMART_HOLDER=ON -DPYBIND11_SIMPLE_GIL_MANAGEMENT=ON - - runs-on: ubuntu-latest - python-version: '3.13t' - cmake-args: -DCMAKE_CXX_STANDARD=20 -DPYBIND11_DISABLE_HANDLE_TYPE_NAME_DEFAULT_IMPLEMENTATION=ON - - runs-on: ubuntu-latest - python-version: '3.14' - cmake-args: -DCMAKE_CXX_STANDARD=14 -DCMAKE_CXX_FLAGS="-DPYBIND11_HAS_SUBINTERPRETER_SUPPORT=0" - - runs-on: ubuntu-latest - python-version: 'pypy-3.10' - cmake-args: -DCMAKE_CXX_STANDARD=14 - - runs-on: ubuntu-latest - python-version: 'graalpy-24.1' - - # No SciPy for macOS ARM - - runs-on: macos-15-intel - python-version: '3.8' - cmake-args: -DCMAKE_CXX_STANDARD=14 - - runs-on: macos-15-intel - python-version: '3.11' - cmake-args: -DPYBIND11_TEST_SMART_HOLDER=ON - - runs-on: macos-latest - python-version: '3.12' - cmake-args: -DCMAKE_CXX_STANDARD=17 -DPYBIND11_DISABLE_HANDLE_TYPE_NAME_DEFAULT_IMPLEMENTATION=ON - - runs-on: macos-15-intel - python-version: '3.13t' - cmake-args: -DCMAKE_CXX_STANDARD=11 - runs-on: macos-latest python-version: '3.14t' cmake-args: -DCMAKE_CXX_STANDARD=20 - - runs-on: macos-15-intel - python-version: 'pypy-3.10' - cmake-args: -DCMAKE_CXX_STANDARD=17 - - runs-on: macos-latest - python-version: 'pypy-3.11' - - runs-on: macos-latest - python-version: 'graalpy-24.2' - - - runs-on: windows-latest - python-version: '3.9' - cmake-args: -DPYBIND11_TEST_SMART_HOLDER=ON - - runs-on: windows-2022 - python-version: '3.8' - cmake-args: -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded -DPYBIND11_NUMPY_1_ONLY=ON - - runs-on: windows-2022 - python-version: '3.9' - cmake-args: -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDLL -DCMAKE_CXX_STANDARD=14 - # This needs a python built with MTd - # - runs-on: windows-2022 - # python-version: '3.11' - # cmake-args: -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDebug - - runs-on: windows-2022 - python-version: '3.10' - cmake-args: -DPYBIND11_TEST_SMART_HOLDER=ON -DCMAKE_CXX_FLAGS="/GR /EHsc" - - runs-on: windows-2022 - python-version: '3.13' - cmake-args: -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDebugDLL - - runs-on: windows-latest - python-version: '3.13t' - cmake-args: -DCMAKE_CXX_STANDARD=17 - - runs-on: windows-latest - python-version: '3.14' - cmake-args: -DCMAKE_CXX_STANDARD=20 - runs-on: windows-latest python-version: '3.14t' cmake-args: -DCMAKE_CXX_STANDARD=23 - - runs-on: windows-latest - python-version: 'pypy-3.10' - cmake-args: -DCMAKE_CXX_STANDARD=17 - - runs-on: windows-latest - python-version: 'pypy3.11' - cmake-args: -DCMAKE_CXX_STANDARD=20 - # The setup-python action currently doesn't have graalpy for windows - # See https://github.com/actions/setup-python/pull/880 name: 🐍 uses: ./.github/workflows/reusable-standard.yml @@ -165,7 +69,7 @@ jobs: # This checks inplace builds with C++11 inplace: - if: github.event.pull_request.draft == false + if: false strategy: fail-fast: false matrix: @@ -240,7 +144,7 @@ jobs: manylinux: name: Manylinux on 🐍 3.13t • GIL - if: github.event.pull_request.draft == false + if: false runs-on: ubuntu-latest timeout-minutes: 40 container: quay.io/pypa/musllinux_1_2_x86_64:latest @@ -265,7 +169,7 @@ jobs: run: cmake --build --preset testsvenv -t pytest deadsnakes: - if: github.event.pull_request.draft == false + if: false strategy: fail-fast: false matrix: @@ -346,7 +250,7 @@ jobs: # Testing on clang using the excellent silkeh clang docker images clang: - if: github.event.pull_request.draft == false + if: false runs-on: ubuntu-latest strategy: fail-fast: false @@ -403,6 +307,7 @@ jobs: # Testing NVCC; forces sources to behave like .cu files cuda: + if: false runs-on: ubuntu-latest name: "🐍 3.10 • CUDA 12.2 • Ubuntu 22.04" container: nvidia/cuda:12.2.0-devel-ubuntu22.04 @@ -471,7 +376,7 @@ jobs: # Testing on Ubuntu + NVHPC (previous PGI) compilers, which seems to require more workarounds ubuntu-nvhpc7: - if: github.event.pull_request.draft == false + if: false runs-on: ubuntu-22.04 name: "🐍 3 • NVHPC 23.5 • C++17 • x64" timeout-minutes: 90 @@ -526,7 +431,7 @@ jobs: # Testing on GCC using the GCC docker images (only recent images supported) gcc: - if: github.event.pull_request.draft == false + if: false runs-on: ubuntu-latest strategy: fail-fast: false @@ -599,7 +504,7 @@ jobs: # Testing on ICC using the oneAPI apt repo icc: - if: github.event.pull_request.draft == false + if: false runs-on: ubuntu-22.04 timeout-minutes: 90 @@ -705,7 +610,7 @@ jobs: # Testing on CentOS (manylinux uses a centos base). centos: - if: github.event.pull_request.draft == false + if: false runs-on: ubuntu-latest strategy: fail-fast: false @@ -771,7 +676,7 @@ jobs: # This tests an "install" with the CMake tools install-classic: - if: github.event.pull_request.draft == false + if: false name: "🐍 3.9 • Debian • x86 • Install" runs-on: ubuntu-latest container: i386/debian:bullseye @@ -817,7 +722,7 @@ jobs: # This verifies that the documentation is not horribly broken, and does a # basic validation check on the SDist. doxygen: - if: github.event.pull_request.draft == false + if: false name: "Documentation build test" runs-on: ubuntu-latest timeout-minutes: 90 @@ -853,7 +758,7 @@ jobs: diff -rq $installed ./pybind11 win32: - if: github.event.pull_request.draft == false + if: false strategy: fail-fast: false matrix: @@ -908,7 +813,7 @@ jobs: run: cmake --build build -t pytest win32-debug: - if: github.event.pull_request.draft == false + if: false strategy: fail-fast: false matrix: @@ -960,7 +865,7 @@ jobs: windows-2022: - if: github.event.pull_request.draft == false + if: false strategy: fail-fast: false matrix: @@ -1024,7 +929,7 @@ jobs: run: cmake --build build_partial --target pytest mingw: - if: github.event.pull_request.draft == false + if: false name: "🐍 3 • windows-latest • ${{ matrix.sys }}" runs-on: windows-latest timeout-minutes: 90 @@ -1133,7 +1038,7 @@ jobs: run: PYTHONHOME=/${{matrix.sys}} PYTHONPATH=/${{matrix.sys}} cmake --build build3 --target test_cross_module_rtti windows_clang: - if: github.event.pull_request.draft == false + if: false strategy: matrix: @@ -1208,7 +1113,7 @@ jobs: # Clang with MSVC/Windows SDK toolchain + python.org CPython (Windows ARM) windows_arm_clang_msvc: - if: github.event.pull_request.draft == false + if: false strategy: fail-fast: false @@ -1267,7 +1172,7 @@ jobs: # Clang in MSYS2/MinGW-w64 CLANGARM64 toolchain + MSYS2 Python (Windows ARM) windows_arm_clang_msys2: - if: github.event.pull_request.draft == false + if: false strategy: fail-fast: false From 0fe6a42a0406f32178429fa8b50de206b6db37d2 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 13 Dec 2025 19:19:54 -0800 Subject: [PATCH 05/18] Temporarily remove non-CI GitHub workflow files --- .github/workflows/configure.yml | 85 ---------------------- .github/workflows/docs-link.yml | 41 ----------- .github/workflows/format.yml | 54 -------------- .github/workflows/labeler.yml | 25 ------- .github/workflows/nightlies.yml | 59 ---------------- .github/workflows/pip.yml | 118 ------------------------------- .github/workflows/tests-cibw.yml | 95 ------------------------- .github/workflows/upstream.yml | 116 ------------------------------ 8 files changed, 593 deletions(-) delete mode 100644 .github/workflows/configure.yml delete mode 100644 .github/workflows/docs-link.yml delete mode 100644 .github/workflows/format.yml delete mode 100644 .github/workflows/labeler.yml delete mode 100644 .github/workflows/nightlies.yml delete mode 100644 .github/workflows/pip.yml delete mode 100644 .github/workflows/tests-cibw.yml delete mode 100644 .github/workflows/upstream.yml diff --git a/.github/workflows/configure.yml b/.github/workflows/configure.yml deleted file mode 100644 index cd034c883f..0000000000 --- a/.github/workflows/configure.yml +++ /dev/null @@ -1,85 +0,0 @@ -name: Config - -on: - workflow_dispatch: - pull_request: - types: - - opened - - synchronize - - reopened - - ready_for_review - push: - branches: - - master - - stable - - v* - -permissions: - contents: read - -jobs: - # This tests various versions of CMake in various combinations, to make sure - # the configure step passes. - cmake: - if: github.event.pull_request.draft == false - strategy: - fail-fast: false - matrix: - include: - - runs-on: ubuntu-22.04 - cmake: "3.15" - - - runs-on: ubuntu-24.04 - cmake: "3.26" - - - runs-on: ubuntu-24.04 - cmake: "3.29" - - - runs-on: macos-15-intel - cmake: "3.15" - - - runs-on: macos-14 - cmake: "4.0" - - - runs-on: windows-latest - cmake: "4.0" - - name: 🐍 3.11 • CMake ${{ matrix.cmake }} • ${{ matrix.runs-on }} - runs-on: ${{ matrix.runs-on }} - - steps: - - uses: actions/checkout@v6 - - - name: Setup Python 3.11 - uses: actions/setup-python@v6 - with: - python-version: 3.11 - - - name: Install uv - uses: astral-sh/setup-uv@v7 - - - name: Prepare env - run: uv pip install --python=python --system -r tests/requirements.txt - - # An action for adding a specific version of CMake: - # https://github.com/jwlawson/actions-setup-cmake - - name: Setup CMake ${{ matrix.cmake }} - uses: jwlawson/actions-setup-cmake@v2.0 - with: - cmake-version: ${{ matrix.cmake }} - - # These steps use a directory with a space in it intentionally - - name: Configure - shell: bash - run: cmake -S. -B"build dir" -DPYBIND11_WERROR=ON -DDOWNLOAD_CATCH=ON - - # Only build and test if this was manually triggered in the GitHub UI - - name: Build - working-directory: build dir - if: github.event_name == 'workflow_dispatch' - run: cmake --build . --config Release - - - name: Test - working-directory: build dir - if: github.event_name == 'workflow_dispatch' - run: cmake --build . --config Release --target check diff --git a/.github/workflows/docs-link.yml b/.github/workflows/docs-link.yml deleted file mode 100644 index ea25410cb9..0000000000 --- a/.github/workflows/docs-link.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Read the Docs PR preview - -on: - pull_request_target: - types: - - opened - - synchronize - -permissions: - contents: read - pull-requests: write - -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - documentation-links: - runs-on: ubuntu-latest - if: github.event.repository.fork == false - steps: - - uses: actions/checkout@v6 - - - name: Check for docs changes - id: docs_changes - run: | - # Fetch the PR head - git fetch origin pull/${{ github.event.pull_request.number }}/head:pr-head - - # Show diff between base (current checkout) and PR head - if git diff --name-only HEAD pr-head | grep -q '^docs/'; then - echo "docs_changed=true" >> "$GITHUB_OUTPUT" - else - echo "docs_changed=false" >> "$GITHUB_OUTPUT" - fi - - - uses: readthedocs/actions/preview@v1 - if: steps.docs_changes.outputs.docs_changed == 'true' - with: - project-slug: "pybind11" - single-version: "true" diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml deleted file mode 100644 index 6bf77324a9..0000000000 --- a/.github/workflows/format.yml +++ /dev/null @@ -1,54 +0,0 @@ -# This is a format job. Pre-commit has a first-party GitHub action, so we use -# that: https://github.com/pre-commit/action - -name: Format - -on: - workflow_dispatch: - pull_request: - push: - branches: - - master - - stable - - "v*" - -permissions: - contents: read - -env: - FORCE_COLOR: 3 - # For cmake: - VERBOSE: 1 - -jobs: - pre-commit: - name: Format - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - - uses: actions/setup-python@v6 - with: - python-version: "3.x" - - name: Add matchers - run: echo "::add-matcher::$GITHUB_WORKSPACE/.github/matchers/pylint.json" - - uses: pre-commit/action@v3.0.1 - - clang-tidy: - # When making changes here, please also review the "Clang-Tidy" section - # in .github/CONTRIBUTING.md and update as needed. - name: Clang-Tidy - runs-on: ubuntu-latest - container: silkeh/clang:20 - steps: - - uses: actions/checkout@v6 - - - name: Install requirements - run: apt-get update && apt-get install -y git python3-dev python3-pytest ninja-build - - - name: Configure - run: cmake --preset tidy - - name: Build - run: cmake --build --preset tidy - - - name: Embedded - run: cmake --build --preset tidy -t cpptest diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml deleted file mode 100644 index f5b618ba82..0000000000 --- a/.github/workflows/labeler.yml +++ /dev/null @@ -1,25 +0,0 @@ -name: Labeler -on: - pull_request_target: - types: [closed] - -permissions: {} - -jobs: - label: - name: Labeler - runs-on: ubuntu-latest - permissions: - contents: read - pull-requests: write - steps: - - - uses: actions/labeler@v6 - if: > - github.event.pull_request.merged == true && - !startsWith(github.event.pull_request.title, 'chore(deps):') && - !startsWith(github.event.pull_request.title, 'ci(fix):') && - !startsWith(github.event.pull_request.title, 'docs(changelog):') - with: - repo-token: ${{ secrets.GITHUB_TOKEN }} - configuration-path: .github/labeler_merged.yml diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml deleted file mode 100644 index ad4a351521..0000000000 --- a/.github/workflows/nightlies.yml +++ /dev/null @@ -1,59 +0,0 @@ -name: Upload nightly wheels to Anaconda Cloud - -on: - # Run daily at 2:34 UTC to upload nightly wheels to Anaconda Cloud - schedule: - - cron: "34 2 * * *" - # Run on demand with workflow dispatch - workflow_dispatch: - -permissions: - actions: read - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build_wheel: - name: Build and upload wheel - if: github.repository_owner == 'pybind' - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - fetch-depth: 0 - - - name: Install uv - uses: astral-sh/setup-uv@v7 - - - name: Build SDist and wheels - run: | - uv tool install nox - nox -s build - nox -s build_global - - - uses: actions/upload-artifact@v5 - with: - name: Packages - path: dist/* - - upload_nightly_wheels: - name: Upload nightly wheels to Anaconda Cloud - if: github.repository_owner == 'pybind' - needs: [build_wheel] - runs-on: ubuntu-latest - steps: - - uses: actions/download-artifact@v6 - with: - name: Packages - path: dist - - - name: List wheel to be deployed - run: ls -lha dist/*.whl - - - name: Upload wheel to Anaconda Cloud as nightly - uses: scientific-python/upload-nightly-action@b36e8c0c10dbcfd2e05bf95f17ef8c14fd708dbf # 0.6.2 - with: - artifacts_path: dist - anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }} diff --git a/.github/workflows/pip.yml b/.github/workflows/pip.yml deleted file mode 100644 index 8df91a00fa..0000000000 --- a/.github/workflows/pip.yml +++ /dev/null @@ -1,118 +0,0 @@ -name: Pip - -on: - workflow_dispatch: - pull_request: - push: - branches: - - master - - stable - - v* - release: - types: - - published - -permissions: - contents: read - -jobs: - # This builds the sdists and wheels and makes sure the files are exactly as - # expected. - test-packaging: - name: 🐍 3.8 • 📦 tests • windows-latest - runs-on: windows-latest - - steps: - - uses: actions/checkout@v6 - - - name: Setup 🐍 3.8 - uses: actions/setup-python@v6 - with: - python-version: 3.8 - - - name: Install uv - uses: astral-sh/setup-uv@v7 - - - name: Prepare env - run: uv pip install --system -r tests/requirements.txt - - - name: Python Packaging tests - run: pytest tests/extra_python_package/ - - - # This runs the packaging tests and also builds and saves the packages as - # artifacts. - packaging: - name: 🐍 3.8 • 📦 & 📦 tests • ubuntu-latest - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v6 - - - name: Setup 🐍 3.8 - uses: actions/setup-python@v6 - with: - python-version: 3.8 - - - name: Install uv - uses: astral-sh/setup-uv@v7 - - - name: Prepare env - run: uv pip install --system -r tests/requirements.txt twine nox - - - name: Python Packaging tests - run: pytest tests/extra_python_package/ - - - name: Build SDist and wheels - run: | - nox -s build - nox -s build_global - - - name: Check metadata - run: twine check dist/* - - - name: Save standard package - uses: actions/upload-artifact@v5 - with: - name: standard - path: dist/pybind11-* - - - name: Save global package - uses: actions/upload-artifact@v5 - with: - name: global - path: dist/*global-* - - - - # When a GitHub release is made, upload the artifacts to PyPI - upload: - name: Upload to PyPI - runs-on: ubuntu-latest - if: github.event_name == 'release' && github.event.action == 'published' - needs: [packaging] - environment: - name: pypi - url: https://pypi.org/p/pybind11 - permissions: - id-token: write - attestations: write - - steps: - # Downloads all to directories matching the artifact names - - uses: actions/download-artifact@v6 - - - name: Generate artifact attestation for sdist and wheel - uses: actions/attest-build-provenance@v3 - with: - subject-path: "*/pybind11*" - - - name: Publish standard package - uses: pypa/gh-action-pypi-publish@release/v1 - with: - packages-dir: standard/ - - - name: Publish global package - uses: pypa/gh-action-pypi-publish@release/v1 - with: - packages-dir: global/ diff --git a/.github/workflows/tests-cibw.yml b/.github/workflows/tests-cibw.yml deleted file mode 100644 index 5dfb5dc940..0000000000 --- a/.github/workflows/tests-cibw.yml +++ /dev/null @@ -1,95 +0,0 @@ -name: CIBW - -on: - workflow_dispatch: - pull_request: - branches: - - master - - stable - - v* - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build-wasm-emscripten: - name: Pyodide wheel - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v6 - with: - submodules: true - fetch-depth: 0 - - - uses: pypa/cibuildwheel@v3.3 - env: - PYODIDE_BUILD_EXPORTS: whole_archive - with: - package-dir: tests - only: cp312-pyodide_wasm32 - - build-ios: - name: iOS wheel ${{ matrix.runs-on }} - runs-on: ${{ matrix.runs-on }} - strategy: - fail-fast: false - matrix: - runs-on: [macos-14, macos-15-intel] - steps: - - uses: actions/checkout@v6 - with: - submodules: true - fetch-depth: 0 - - # We have to uninstall first because GH is now using a local tap to build cmake<4, iOS needs cmake>=4 - - run: brew uninstall cmake && brew install cmake - - - uses: pypa/cibuildwheel@v3.3 - env: - CIBW_PLATFORM: ios - CIBW_SKIP: cp314-* # https://github.com/pypa/cibuildwheel/issues/2494 - with: - package-dir: tests - - build-android: - name: Android wheel ${{ matrix.runs-on }} - runs-on: ${{ matrix.runs-on }} - strategy: - fail-fast: false - matrix: - runs-on: [macos-latest, macos-15-intel, ubuntu-latest] - steps: - - uses: actions/checkout@v6 - with: - submodules: true - fetch-depth: 0 - - # GitHub Actions can't currently run the Android emulator on macOS. - - name: Skip Android tests on macOS - if: contains(matrix.runs-on, 'macos') - run: echo "CIBW_TEST_COMMAND=" >> "$GITHUB_ENV" - - # Temporarily disable Android tests on ubuntu-latest due to emulator issues. - # See https://github.com/pybind/pybind11/pull/5914. - - name: "NOTE: Android tests are disabled on ubuntu-latest" - if: contains(matrix.runs-on, 'ubuntu') - run: | - echo "CIBW_TEST_COMMAND=" >> "$GITHUB_ENV" - echo '::warning::Android cibuildwheel tests are disabled on ubuntu-latest (CIBW_TEST_COMMAND is empty). See PR 5914.' - - # https://github.blog/changelog/2024-04-02-github-actions-hardware-accelerated-android-virtualization-now-available/ - - name: Enable KVM for Android emulator - if: contains(matrix.runs-on, 'ubuntu') - run: | - echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules - sudo udevadm control --reload-rules - sudo udevadm trigger --name-match=kvm - - - run: pipx install patchelf - - - uses: pypa/cibuildwheel@v3.3 - env: - CIBW_PLATFORM: android - with: - package-dir: tests diff --git a/.github/workflows/upstream.yml b/.github/workflows/upstream.yml deleted file mode 100644 index 15ede7a856..0000000000 --- a/.github/workflows/upstream.yml +++ /dev/null @@ -1,116 +0,0 @@ -name: Upstream - -on: - workflow_dispatch: - pull_request: - -permissions: - contents: read - -concurrency: - group: upstream-${{ github.ref }} - cancel-in-progress: true - -env: - PIP_BREAK_SYSTEM_PACKAGES: 1 - # For cmake: - VERBOSE: 1 - -jobs: - standard: - name: "🐍 3.13 latest • ubuntu-latest • x64" - runs-on: ubuntu-latest - # Only runs when the 'python dev' label is selected - if: "contains(github.event.pull_request.labels.*.name, 'python dev')" - - steps: - - uses: actions/checkout@v6 - - - name: Setup Python 3.13 - uses: actions/setup-python@v6 - with: - python-version: "3.13" - allow-prereleases: true - - - name: Setup Boost - run: sudo apt-get install libboost-dev - - - name: Update CMake - uses: jwlawson/actions-setup-cmake@v2.0 - - - name: Run pip installs - run: | - python -m pip install --upgrade pip - python -m pip install -r tests/requirements.txt - - - name: Show platform info - run: | - python -m platform - cmake --version - pip list - - # First build - C++11 mode and inplace - - name: Configure C++11 - run: > - cmake -S . -B build11 - -DPYBIND11_WERROR=ON - -DDOWNLOAD_CATCH=ON - -DDOWNLOAD_EIGEN=ON - -DCMAKE_CXX_STANDARD=11 - -DCMAKE_BUILD_TYPE=Debug - - - name: Build C++11 - run: cmake --build build11 -j 2 - - - name: Python tests C++11 - run: cmake --build build11 --target pytest -j 2 - - - name: C++11 tests - run: cmake --build build11 --target cpptest -j 2 - - - name: Interface test C++11 - run: cmake --build build11 --target test_cmake_build - - # Second build - C++17 mode and in a build directory - - name: Configure C++17 - run: > - cmake -S . -B build17 - -DPYBIND11_WERROR=ON - -DDOWNLOAD_CATCH=ON - -DDOWNLOAD_EIGEN=ON - -DCMAKE_CXX_STANDARD=17 - - - name: Build C++17 - run: cmake --build build17 -j 2 - - - name: Python tests C++17 - run: cmake --build build17 --target pytest - - - name: C++17 tests - run: cmake --build build17 --target cpptest - - # Third build - C++17 mode with unstable ABI - - name: Configure (unstable ABI) - run: > - cmake -S . -B build17max - -DPYBIND11_WERROR=ON - -DDOWNLOAD_CATCH=ON - -DDOWNLOAD_EIGEN=ON - -DCMAKE_CXX_STANDARD=17 - -DPYBIND11_INTERNALS_VERSION=10000000 - - - name: Build (unstable ABI) - run: cmake --build build17max -j 2 - - - name: Python tests (unstable ABI) - run: cmake --build build17max --target pytest - - - name: Interface test (unstable ABI) - run: cmake --build build17max --target test_cmake_build - - # This makes sure the setup_helpers module can build packages using - # setuptools - - name: Setuptools helpers test - run: | - pip install setuptools - pytest tests/extra_setuptools From ed112926365544bfdb6d5153f8947474d8c4caf2 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 13 Dec 2025 19:25:39 -0800 Subject: [PATCH 06/18] Temporarily disable AppVeyor builds via skip_commits --- .appveyor.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.appveyor.yml b/.appveyor.yml index 391cf1071c..f1a0bf1048 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -2,6 +2,8 @@ version: 1.0.{build} image: - Visual Studio 2017 test: off +skip_commits: + message: /.*/ # Skip ALL commits. skip_branch_with_pr: true build: parallel: true From ad3e1c34ceeb072c5812f385f38360387fbbcf68 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 13 Dec 2025 20:21:35 -0800 Subject: [PATCH 07/18] Add DEBUG_LOOK in TEST_CASE("Move Subinterpreter") --- tests/test_with_catch/test_subinterpreter.cpp | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/test_with_catch/test_subinterpreter.cpp b/tests/test_with_catch/test_subinterpreter.cpp index 3c7c35be19..466af4cd33 100644 --- a/tests/test_with_catch/test_subinterpreter.cpp +++ b/tests/test_with_catch/test_subinterpreter.cpp @@ -7,6 +7,7 @@ PYBIND11_WARNING_DISABLE_MSVC(4996) # include +# include # include # include # include @@ -16,6 +17,16 @@ PYBIND11_WARNING_DISABLE_MSVC(4996) namespace py = pybind11; using namespace py::literals; +namespace { +inline void debug_look(const char *file, int line) { + fflush(stderr); + std::fprintf(stdout, "\nLOOOK %s:%d\n", file, line); + fflush(stdout); +} +} // namespace + +# define DEBUG_LOOK() debug_look(__FILE__, __LINE__) + bool has_state_dict_internals_obj(); uintptr_t get_details_as_uintptr(); @@ -92,30 +103,48 @@ TEST_CASE("Single Subinterpreter") { # if PY_VERSION_HEX >= 0x030D0000 TEST_CASE("Move Subinterpreter") { + DEBUG_LOOK(); std::unique_ptr sub(new py::subinterpreter(py::subinterpreter::create())); + DEBUG_LOOK(); // on this thread, use the subinterpreter and import some non-trivial junk { + DEBUG_LOOK(); py::subinterpreter_scoped_activate activate(*sub); + DEBUG_LOOK(); py::list(py::module_::import("sys").attr("path")).append(py::str(".")); + DEBUG_LOOK(); py::module_::import("datetime"); + DEBUG_LOOK(); py::module_::import("threading"); + DEBUG_LOOK(); py::module_::import("external_module"); + DEBUG_LOOK(); } + DEBUG_LOOK(); std::thread([&]() { + DEBUG_LOOK(); // Use it again { + DEBUG_LOOK(); py::subinterpreter_scoped_activate activate(*sub); + DEBUG_LOOK(); py::module_::import("external_module"); + DEBUG_LOOK(); } + DEBUG_LOOK(); sub.reset(); + DEBUG_LOOK(); }).join(); + DEBUG_LOOK(); REQUIRE(!sub); + DEBUG_LOOK(); unsafe_reset_internals_for_single_interpreter(); + DEBUG_LOOK(); } # endif From 48725893c63502ac0da8a10e608197eecf4d5f83 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 13 Dec 2025 23:25:33 -0800 Subject: [PATCH 08/18] Add Python version banner to Catch progress reporter Print the CPython version once at the start of the Catch-based interpreter tests using Py_GetVersion(). This makes it trivial to confirm which free-threaded build a failing run is using when inspecting CI or local logs. --- tests/test_with_catch/catch.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/test_with_catch/catch.cpp b/tests/test_with_catch/catch.cpp index aa5de2f3c5..171d1df626 100644 --- a/tests/test_with_catch/catch.cpp +++ b/tests/test_with_catch/catch.cpp @@ -21,6 +21,8 @@ namespace py = pybind11; // Simple progress reporter that prints a line per test case. namespace { +bool g_printed_python_version = false; + class ProgressReporter : public Catch::CumulativeReporterBase { public: using CumulativeReporterBase::CumulativeReporterBase; @@ -28,6 +30,12 @@ class ProgressReporter : public Catch::CumulativeReporterBase static std::string getDescription() { return "Simple progress reporter (one line per test)"; } void testCaseStarting(Catch::TestCaseInfo const &testInfo) override { + if (!g_printed_python_version) { + g_printed_python_version = true; + const char *version = Py_GetVersion(); + stream << "[ PYTHON ] " << version << '\n'; + stream.flush(); + } stream << "[ RUN ] " << testInfo.name << '\n'; stream.flush(); CumulativeReporterBase::testCaseStarting(testInfo); From b13e218bfa2c2f6c7546f3e577385a3161caeb8d Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 13 Dec 2025 23:26:04 -0800 Subject: [PATCH 09/18] Revert "Add DEBUG_LOOK in TEST_CASE("Move Subinterpreter")" This reverts commit ad3e1c34ceeb072c5812f385f38360387fbbcf68. --- tests/test_with_catch/test_subinterpreter.cpp | 29 ------------------- 1 file changed, 29 deletions(-) diff --git a/tests/test_with_catch/test_subinterpreter.cpp b/tests/test_with_catch/test_subinterpreter.cpp index 466af4cd33..3c7c35be19 100644 --- a/tests/test_with_catch/test_subinterpreter.cpp +++ b/tests/test_with_catch/test_subinterpreter.cpp @@ -7,7 +7,6 @@ PYBIND11_WARNING_DISABLE_MSVC(4996) # include -# include # include # include # include @@ -17,16 +16,6 @@ PYBIND11_WARNING_DISABLE_MSVC(4996) namespace py = pybind11; using namespace py::literals; -namespace { -inline void debug_look(const char *file, int line) { - fflush(stderr); - std::fprintf(stdout, "\nLOOOK %s:%d\n", file, line); - fflush(stdout); -} -} // namespace - -# define DEBUG_LOOK() debug_look(__FILE__, __LINE__) - bool has_state_dict_internals_obj(); uintptr_t get_details_as_uintptr(); @@ -103,48 +92,30 @@ TEST_CASE("Single Subinterpreter") { # if PY_VERSION_HEX >= 0x030D0000 TEST_CASE("Move Subinterpreter") { - DEBUG_LOOK(); std::unique_ptr sub(new py::subinterpreter(py::subinterpreter::create())); - DEBUG_LOOK(); // on this thread, use the subinterpreter and import some non-trivial junk { - DEBUG_LOOK(); py::subinterpreter_scoped_activate activate(*sub); - DEBUG_LOOK(); py::list(py::module_::import("sys").attr("path")).append(py::str(".")); - DEBUG_LOOK(); py::module_::import("datetime"); - DEBUG_LOOK(); py::module_::import("threading"); - DEBUG_LOOK(); py::module_::import("external_module"); - DEBUG_LOOK(); } - DEBUG_LOOK(); std::thread([&]() { - DEBUG_LOOK(); // Use it again { - DEBUG_LOOK(); py::subinterpreter_scoped_activate activate(*sub); - DEBUG_LOOK(); py::module_::import("external_module"); - DEBUG_LOOK(); } - DEBUG_LOOK(); sub.reset(); - DEBUG_LOOK(); }).join(); - DEBUG_LOOK(); REQUIRE(!sub); - DEBUG_LOOK(); unsafe_reset_internals_for_single_interpreter(); - DEBUG_LOOK(); } # endif From 5281e1c20c7477878df53fd6a09395f8bb6dcc09 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sat, 13 Dec 2025 23:27:51 -0800 Subject: [PATCH 10/18] Pin CI free-threaded runs to CPython 3.14.0t Update the standard-small and standard-large GitHub Actions jobs to request python-version 3.14.0t instead of 3.14t. This forces setup-python to use the last-known-good 3.14.0 free-threaded build rather than the newer 3.14.1+ builds where subinterpreter finalization regressed. --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0653b2adcd..f51b734a1b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: matrix: include: - runs-on: ubuntu-latest - python-version: '3.14t' + python-version: '3.14.0t' cmake-args: -DCMAKE_CXX_STANDARD=17 -DPYBIND11_TEST_SMART_HOLDER=ON @@ -54,10 +54,10 @@ jobs: matrix: include: - runs-on: macos-latest - python-version: '3.14t' + python-version: '3.14.0t' cmake-args: -DCMAKE_CXX_STANDARD=20 - runs-on: windows-latest - python-version: '3.14t' + python-version: '3.14.0t' cmake-args: -DCMAKE_CXX_STANDARD=23 name: 🐍 From 8f4753ee78c6d122e3572445b8df5bb65dc24ba3 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 14 Dec 2025 06:54:38 -0800 Subject: [PATCH 11/18] Revert "Pin CI free-threaded runs to CPython 3.14.0t" This reverts commit 5281e1c20c7477878df53fd6a09395f8bb6dcc09. --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f51b734a1b..0653b2adcd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,7 +36,7 @@ jobs: matrix: include: - runs-on: ubuntu-latest - python-version: '3.14.0t' + python-version: '3.14t' cmake-args: -DCMAKE_CXX_STANDARD=17 -DPYBIND11_TEST_SMART_HOLDER=ON @@ -54,10 +54,10 @@ jobs: matrix: include: - runs-on: macos-latest - python-version: '3.14.0t' + python-version: '3.14t' cmake-args: -DCMAKE_CXX_STANDARD=20 - runs-on: windows-latest - python-version: '3.14.0t' + python-version: '3.14t' cmake-args: -DCMAKE_CXX_STANDARD=23 name: 🐍 From f1d349b98fe9108af94d7ce2fe73a6ffcfcb6297 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 14 Dec 2025 06:54:56 -0800 Subject: [PATCH 12/18] Revert "Temporarily disable AppVeyor builds via skip_commits" This reverts commit ed112926365544bfdb6d5153f8947474d8c4caf2. --- .appveyor.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index f1a0bf1048..391cf1071c 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -2,8 +2,6 @@ version: 1.0.{build} image: - Visual Studio 2017 test: off -skip_commits: - message: /.*/ # Skip ALL commits. skip_branch_with_pr: true build: parallel: true From 35472e5706891f5946c48ac7f6630d97aab6665f Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 14 Dec 2025 06:55:13 -0800 Subject: [PATCH 13/18] Revert "Temporarily remove non-CI GitHub workflow files" This reverts commit 0fe6a42a0406f32178429fa8b50de206b6db37d2. --- .github/workflows/configure.yml | 85 ++++++++++++++++++++++ .github/workflows/docs-link.yml | 41 +++++++++++ .github/workflows/format.yml | 54 ++++++++++++++ .github/workflows/labeler.yml | 25 +++++++ .github/workflows/nightlies.yml | 59 ++++++++++++++++ .github/workflows/pip.yml | 118 +++++++++++++++++++++++++++++++ .github/workflows/tests-cibw.yml | 95 +++++++++++++++++++++++++ .github/workflows/upstream.yml | 116 ++++++++++++++++++++++++++++++ 8 files changed, 593 insertions(+) create mode 100644 .github/workflows/configure.yml create mode 100644 .github/workflows/docs-link.yml create mode 100644 .github/workflows/format.yml create mode 100644 .github/workflows/labeler.yml create mode 100644 .github/workflows/nightlies.yml create mode 100644 .github/workflows/pip.yml create mode 100644 .github/workflows/tests-cibw.yml create mode 100644 .github/workflows/upstream.yml diff --git a/.github/workflows/configure.yml b/.github/workflows/configure.yml new file mode 100644 index 0000000000..cd034c883f --- /dev/null +++ b/.github/workflows/configure.yml @@ -0,0 +1,85 @@ +name: Config + +on: + workflow_dispatch: + pull_request: + types: + - opened + - synchronize + - reopened + - ready_for_review + push: + branches: + - master + - stable + - v* + +permissions: + contents: read + +jobs: + # This tests various versions of CMake in various combinations, to make sure + # the configure step passes. + cmake: + if: github.event.pull_request.draft == false + strategy: + fail-fast: false + matrix: + include: + - runs-on: ubuntu-22.04 + cmake: "3.15" + + - runs-on: ubuntu-24.04 + cmake: "3.26" + + - runs-on: ubuntu-24.04 + cmake: "3.29" + + - runs-on: macos-15-intel + cmake: "3.15" + + - runs-on: macos-14 + cmake: "4.0" + + - runs-on: windows-latest + cmake: "4.0" + + name: 🐍 3.11 • CMake ${{ matrix.cmake }} • ${{ matrix.runs-on }} + runs-on: ${{ matrix.runs-on }} + + steps: + - uses: actions/checkout@v6 + + - name: Setup Python 3.11 + uses: actions/setup-python@v6 + with: + python-version: 3.11 + + - name: Install uv + uses: astral-sh/setup-uv@v7 + + - name: Prepare env + run: uv pip install --python=python --system -r tests/requirements.txt + + # An action for adding a specific version of CMake: + # https://github.com/jwlawson/actions-setup-cmake + - name: Setup CMake ${{ matrix.cmake }} + uses: jwlawson/actions-setup-cmake@v2.0 + with: + cmake-version: ${{ matrix.cmake }} + + # These steps use a directory with a space in it intentionally + - name: Configure + shell: bash + run: cmake -S. -B"build dir" -DPYBIND11_WERROR=ON -DDOWNLOAD_CATCH=ON + + # Only build and test if this was manually triggered in the GitHub UI + - name: Build + working-directory: build dir + if: github.event_name == 'workflow_dispatch' + run: cmake --build . --config Release + + - name: Test + working-directory: build dir + if: github.event_name == 'workflow_dispatch' + run: cmake --build . --config Release --target check diff --git a/.github/workflows/docs-link.yml b/.github/workflows/docs-link.yml new file mode 100644 index 0000000000..ea25410cb9 --- /dev/null +++ b/.github/workflows/docs-link.yml @@ -0,0 +1,41 @@ +name: Read the Docs PR preview + +on: + pull_request_target: + types: + - opened + - synchronize + +permissions: + contents: read + pull-requests: write + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + documentation-links: + runs-on: ubuntu-latest + if: github.event.repository.fork == false + steps: + - uses: actions/checkout@v6 + + - name: Check for docs changes + id: docs_changes + run: | + # Fetch the PR head + git fetch origin pull/${{ github.event.pull_request.number }}/head:pr-head + + # Show diff between base (current checkout) and PR head + if git diff --name-only HEAD pr-head | grep -q '^docs/'; then + echo "docs_changed=true" >> "$GITHUB_OUTPUT" + else + echo "docs_changed=false" >> "$GITHUB_OUTPUT" + fi + + - uses: readthedocs/actions/preview@v1 + if: steps.docs_changes.outputs.docs_changed == 'true' + with: + project-slug: "pybind11" + single-version: "true" diff --git a/.github/workflows/format.yml b/.github/workflows/format.yml new file mode 100644 index 0000000000..6bf77324a9 --- /dev/null +++ b/.github/workflows/format.yml @@ -0,0 +1,54 @@ +# This is a format job. Pre-commit has a first-party GitHub action, so we use +# that: https://github.com/pre-commit/action + +name: Format + +on: + workflow_dispatch: + pull_request: + push: + branches: + - master + - stable + - "v*" + +permissions: + contents: read + +env: + FORCE_COLOR: 3 + # For cmake: + VERBOSE: 1 + +jobs: + pre-commit: + name: Format + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - uses: actions/setup-python@v6 + with: + python-version: "3.x" + - name: Add matchers + run: echo "::add-matcher::$GITHUB_WORKSPACE/.github/matchers/pylint.json" + - uses: pre-commit/action@v3.0.1 + + clang-tidy: + # When making changes here, please also review the "Clang-Tidy" section + # in .github/CONTRIBUTING.md and update as needed. + name: Clang-Tidy + runs-on: ubuntu-latest + container: silkeh/clang:20 + steps: + - uses: actions/checkout@v6 + + - name: Install requirements + run: apt-get update && apt-get install -y git python3-dev python3-pytest ninja-build + + - name: Configure + run: cmake --preset tidy + - name: Build + run: cmake --build --preset tidy + + - name: Embedded + run: cmake --build --preset tidy -t cpptest diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml new file mode 100644 index 0000000000..f5b618ba82 --- /dev/null +++ b/.github/workflows/labeler.yml @@ -0,0 +1,25 @@ +name: Labeler +on: + pull_request_target: + types: [closed] + +permissions: {} + +jobs: + label: + name: Labeler + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + steps: + + - uses: actions/labeler@v6 + if: > + github.event.pull_request.merged == true && + !startsWith(github.event.pull_request.title, 'chore(deps):') && + !startsWith(github.event.pull_request.title, 'ci(fix):') && + !startsWith(github.event.pull_request.title, 'docs(changelog):') + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + configuration-path: .github/labeler_merged.yml diff --git a/.github/workflows/nightlies.yml b/.github/workflows/nightlies.yml new file mode 100644 index 0000000000..ad4a351521 --- /dev/null +++ b/.github/workflows/nightlies.yml @@ -0,0 +1,59 @@ +name: Upload nightly wheels to Anaconda Cloud + +on: + # Run daily at 2:34 UTC to upload nightly wheels to Anaconda Cloud + schedule: + - cron: "34 2 * * *" + # Run on demand with workflow dispatch + workflow_dispatch: + +permissions: + actions: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build_wheel: + name: Build and upload wheel + if: github.repository_owner == 'pybind' + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + + - name: Install uv + uses: astral-sh/setup-uv@v7 + + - name: Build SDist and wheels + run: | + uv tool install nox + nox -s build + nox -s build_global + + - uses: actions/upload-artifact@v5 + with: + name: Packages + path: dist/* + + upload_nightly_wheels: + name: Upload nightly wheels to Anaconda Cloud + if: github.repository_owner == 'pybind' + needs: [build_wheel] + runs-on: ubuntu-latest + steps: + - uses: actions/download-artifact@v6 + with: + name: Packages + path: dist + + - name: List wheel to be deployed + run: ls -lha dist/*.whl + + - name: Upload wheel to Anaconda Cloud as nightly + uses: scientific-python/upload-nightly-action@b36e8c0c10dbcfd2e05bf95f17ef8c14fd708dbf # 0.6.2 + with: + artifacts_path: dist + anaconda_nightly_upload_token: ${{ secrets.ANACONDA_ORG_UPLOAD_TOKEN }} diff --git a/.github/workflows/pip.yml b/.github/workflows/pip.yml new file mode 100644 index 0000000000..8df91a00fa --- /dev/null +++ b/.github/workflows/pip.yml @@ -0,0 +1,118 @@ +name: Pip + +on: + workflow_dispatch: + pull_request: + push: + branches: + - master + - stable + - v* + release: + types: + - published + +permissions: + contents: read + +jobs: + # This builds the sdists and wheels and makes sure the files are exactly as + # expected. + test-packaging: + name: 🐍 3.8 • 📦 tests • windows-latest + runs-on: windows-latest + + steps: + - uses: actions/checkout@v6 + + - name: Setup 🐍 3.8 + uses: actions/setup-python@v6 + with: + python-version: 3.8 + + - name: Install uv + uses: astral-sh/setup-uv@v7 + + - name: Prepare env + run: uv pip install --system -r tests/requirements.txt + + - name: Python Packaging tests + run: pytest tests/extra_python_package/ + + + # This runs the packaging tests and also builds and saves the packages as + # artifacts. + packaging: + name: 🐍 3.8 • 📦 & 📦 tests • ubuntu-latest + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + + - name: Setup 🐍 3.8 + uses: actions/setup-python@v6 + with: + python-version: 3.8 + + - name: Install uv + uses: astral-sh/setup-uv@v7 + + - name: Prepare env + run: uv pip install --system -r tests/requirements.txt twine nox + + - name: Python Packaging tests + run: pytest tests/extra_python_package/ + + - name: Build SDist and wheels + run: | + nox -s build + nox -s build_global + + - name: Check metadata + run: twine check dist/* + + - name: Save standard package + uses: actions/upload-artifact@v5 + with: + name: standard + path: dist/pybind11-* + + - name: Save global package + uses: actions/upload-artifact@v5 + with: + name: global + path: dist/*global-* + + + + # When a GitHub release is made, upload the artifacts to PyPI + upload: + name: Upload to PyPI + runs-on: ubuntu-latest + if: github.event_name == 'release' && github.event.action == 'published' + needs: [packaging] + environment: + name: pypi + url: https://pypi.org/p/pybind11 + permissions: + id-token: write + attestations: write + + steps: + # Downloads all to directories matching the artifact names + - uses: actions/download-artifact@v6 + + - name: Generate artifact attestation for sdist and wheel + uses: actions/attest-build-provenance@v3 + with: + subject-path: "*/pybind11*" + + - name: Publish standard package + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: standard/ + + - name: Publish global package + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: global/ diff --git a/.github/workflows/tests-cibw.yml b/.github/workflows/tests-cibw.yml new file mode 100644 index 0000000000..5dfb5dc940 --- /dev/null +++ b/.github/workflows/tests-cibw.yml @@ -0,0 +1,95 @@ +name: CIBW + +on: + workflow_dispatch: + pull_request: + branches: + - master + - stable + - v* + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + build-wasm-emscripten: + name: Pyodide wheel + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + with: + submodules: true + fetch-depth: 0 + + - uses: pypa/cibuildwheel@v3.3 + env: + PYODIDE_BUILD_EXPORTS: whole_archive + with: + package-dir: tests + only: cp312-pyodide_wasm32 + + build-ios: + name: iOS wheel ${{ matrix.runs-on }} + runs-on: ${{ matrix.runs-on }} + strategy: + fail-fast: false + matrix: + runs-on: [macos-14, macos-15-intel] + steps: + - uses: actions/checkout@v6 + with: + submodules: true + fetch-depth: 0 + + # We have to uninstall first because GH is now using a local tap to build cmake<4, iOS needs cmake>=4 + - run: brew uninstall cmake && brew install cmake + + - uses: pypa/cibuildwheel@v3.3 + env: + CIBW_PLATFORM: ios + CIBW_SKIP: cp314-* # https://github.com/pypa/cibuildwheel/issues/2494 + with: + package-dir: tests + + build-android: + name: Android wheel ${{ matrix.runs-on }} + runs-on: ${{ matrix.runs-on }} + strategy: + fail-fast: false + matrix: + runs-on: [macos-latest, macos-15-intel, ubuntu-latest] + steps: + - uses: actions/checkout@v6 + with: + submodules: true + fetch-depth: 0 + + # GitHub Actions can't currently run the Android emulator on macOS. + - name: Skip Android tests on macOS + if: contains(matrix.runs-on, 'macos') + run: echo "CIBW_TEST_COMMAND=" >> "$GITHUB_ENV" + + # Temporarily disable Android tests on ubuntu-latest due to emulator issues. + # See https://github.com/pybind/pybind11/pull/5914. + - name: "NOTE: Android tests are disabled on ubuntu-latest" + if: contains(matrix.runs-on, 'ubuntu') + run: | + echo "CIBW_TEST_COMMAND=" >> "$GITHUB_ENV" + echo '::warning::Android cibuildwheel tests are disabled on ubuntu-latest (CIBW_TEST_COMMAND is empty). See PR 5914.' + + # https://github.blog/changelog/2024-04-02-github-actions-hardware-accelerated-android-virtualization-now-available/ + - name: Enable KVM for Android emulator + if: contains(matrix.runs-on, 'ubuntu') + run: | + echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules + sudo udevadm control --reload-rules + sudo udevadm trigger --name-match=kvm + + - run: pipx install patchelf + + - uses: pypa/cibuildwheel@v3.3 + env: + CIBW_PLATFORM: android + with: + package-dir: tests diff --git a/.github/workflows/upstream.yml b/.github/workflows/upstream.yml new file mode 100644 index 0000000000..15ede7a856 --- /dev/null +++ b/.github/workflows/upstream.yml @@ -0,0 +1,116 @@ +name: Upstream + +on: + workflow_dispatch: + pull_request: + +permissions: + contents: read + +concurrency: + group: upstream-${{ github.ref }} + cancel-in-progress: true + +env: + PIP_BREAK_SYSTEM_PACKAGES: 1 + # For cmake: + VERBOSE: 1 + +jobs: + standard: + name: "🐍 3.13 latest • ubuntu-latest • x64" + runs-on: ubuntu-latest + # Only runs when the 'python dev' label is selected + if: "contains(github.event.pull_request.labels.*.name, 'python dev')" + + steps: + - uses: actions/checkout@v6 + + - name: Setup Python 3.13 + uses: actions/setup-python@v6 + with: + python-version: "3.13" + allow-prereleases: true + + - name: Setup Boost + run: sudo apt-get install libboost-dev + + - name: Update CMake + uses: jwlawson/actions-setup-cmake@v2.0 + + - name: Run pip installs + run: | + python -m pip install --upgrade pip + python -m pip install -r tests/requirements.txt + + - name: Show platform info + run: | + python -m platform + cmake --version + pip list + + # First build - C++11 mode and inplace + - name: Configure C++11 + run: > + cmake -S . -B build11 + -DPYBIND11_WERROR=ON + -DDOWNLOAD_CATCH=ON + -DDOWNLOAD_EIGEN=ON + -DCMAKE_CXX_STANDARD=11 + -DCMAKE_BUILD_TYPE=Debug + + - name: Build C++11 + run: cmake --build build11 -j 2 + + - name: Python tests C++11 + run: cmake --build build11 --target pytest -j 2 + + - name: C++11 tests + run: cmake --build build11 --target cpptest -j 2 + + - name: Interface test C++11 + run: cmake --build build11 --target test_cmake_build + + # Second build - C++17 mode and in a build directory + - name: Configure C++17 + run: > + cmake -S . -B build17 + -DPYBIND11_WERROR=ON + -DDOWNLOAD_CATCH=ON + -DDOWNLOAD_EIGEN=ON + -DCMAKE_CXX_STANDARD=17 + + - name: Build C++17 + run: cmake --build build17 -j 2 + + - name: Python tests C++17 + run: cmake --build build17 --target pytest + + - name: C++17 tests + run: cmake --build build17 --target cpptest + + # Third build - C++17 mode with unstable ABI + - name: Configure (unstable ABI) + run: > + cmake -S . -B build17max + -DPYBIND11_WERROR=ON + -DDOWNLOAD_CATCH=ON + -DDOWNLOAD_EIGEN=ON + -DCMAKE_CXX_STANDARD=17 + -DPYBIND11_INTERNALS_VERSION=10000000 + + - name: Build (unstable ABI) + run: cmake --build build17max -j 2 + + - name: Python tests (unstable ABI) + run: cmake --build build17max --target pytest + + - name: Interface test (unstable ABI) + run: cmake --build build17max --target test_cmake_build + + # This makes sure the setup_helpers module can build packages using + # setuptools + - name: Setuptools helpers test + run: | + pip install setuptools + pytest tests/extra_setuptools From fd07d33685b278def31af70ffb85bc8da10ad36f Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 14 Dec 2025 06:55:31 -0800 Subject: [PATCH 14/18] Revert "Temporarily limit CI to Python 3.14t free-threading jobs" This reverts commit 60ae0e8f744904ba2499cf44a43fdf91049f6e4a. --- .github/workflows/ci.yml | 131 +++++++++++++++++++++++++++++++++------ 1 file changed, 113 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0653b2adcd..d00ee6d27e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -35,9 +35,27 @@ jobs: fail-fast: false matrix: include: + - runs-on: ubuntu-22.04 + python-version: '3.8' + cmake-args: -DPYBIND11_FINDPYTHON=OFF -DPYBIND11_NUMPY_1_ONLY=ON + - runs-on: ubuntu-latest + python-version: '3.13' + cmake-args: -DCMAKE_CXX_STANDARD=23 -DPYBIND11_SIMPLE_GIL_MANAGEMENT=ON - runs-on: ubuntu-latest python-version: '3.14t' cmake-args: -DCMAKE_CXX_STANDARD=17 -DPYBIND11_TEST_SMART_HOLDER=ON + - runs-on: ubuntu-latest + python-version: 'pypy3.11' + cmake-args: -DCMAKE_CXX_STANDARD=17 + - runs-on: ubuntu-latest + python-version: 'graalpy-24.2' + cmake-args: -DCMAKE_CXX_STANDARD=20 + - runs-on: macos-latest + python-version: '3.14' + cmake-args: -DCMAKE_CXX_STANDARD=14 + - runs-on: windows-2022 + python-version: '3.8' + cmake-args: -DPYBIND11_FINDPYTHON=OFF name: 🐍 @@ -53,12 +71,90 @@ jobs: fail-fast: false matrix: include: + - runs-on: ubuntu-latest + python-version: '3.8' + cmake-args: -DPYBIND11_FINDPYTHON=ON -DCMAKE_CXX_STANDARD=17 + - runs-on: ubuntu-latest + python-version: '3.10' + cmake-args: -DCMAKE_CXX_STANDARD=20 + - runs-on: ubuntu-latest + python-version: '3.11' + cmake-args: -DPYBIND11_TEST_SMART_HOLDER=ON -DCMAKE_CXX_STANDARD=17 + - runs-on: ubuntu-latest + python-version: '3.12' + cmake-args: -DPYBIND11_TEST_SMART_HOLDER=ON -DPYBIND11_SIMPLE_GIL_MANAGEMENT=ON + - runs-on: ubuntu-latest + python-version: '3.13t' + cmake-args: -DCMAKE_CXX_STANDARD=20 -DPYBIND11_DISABLE_HANDLE_TYPE_NAME_DEFAULT_IMPLEMENTATION=ON + - runs-on: ubuntu-latest + python-version: '3.14' + cmake-args: -DCMAKE_CXX_STANDARD=14 -DCMAKE_CXX_FLAGS="-DPYBIND11_HAS_SUBINTERPRETER_SUPPORT=0" + - runs-on: ubuntu-latest + python-version: 'pypy-3.10' + cmake-args: -DCMAKE_CXX_STANDARD=14 + - runs-on: ubuntu-latest + python-version: 'graalpy-24.1' + + # No SciPy for macOS ARM + - runs-on: macos-15-intel + python-version: '3.8' + cmake-args: -DCMAKE_CXX_STANDARD=14 + - runs-on: macos-15-intel + python-version: '3.11' + cmake-args: -DPYBIND11_TEST_SMART_HOLDER=ON + - runs-on: macos-latest + python-version: '3.12' + cmake-args: -DCMAKE_CXX_STANDARD=17 -DPYBIND11_DISABLE_HANDLE_TYPE_NAME_DEFAULT_IMPLEMENTATION=ON + - runs-on: macos-15-intel + python-version: '3.13t' + cmake-args: -DCMAKE_CXX_STANDARD=11 - runs-on: macos-latest python-version: '3.14t' cmake-args: -DCMAKE_CXX_STANDARD=20 + - runs-on: macos-15-intel + python-version: 'pypy-3.10' + cmake-args: -DCMAKE_CXX_STANDARD=17 + - runs-on: macos-latest + python-version: 'pypy-3.11' + - runs-on: macos-latest + python-version: 'graalpy-24.2' + + - runs-on: windows-latest + python-version: '3.9' + cmake-args: -DPYBIND11_TEST_SMART_HOLDER=ON + - runs-on: windows-2022 + python-version: '3.8' + cmake-args: -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreaded -DPYBIND11_NUMPY_1_ONLY=ON + - runs-on: windows-2022 + python-version: '3.9' + cmake-args: -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDLL -DCMAKE_CXX_STANDARD=14 + # This needs a python built with MTd + # - runs-on: windows-2022 + # python-version: '3.11' + # cmake-args: -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDebug + - runs-on: windows-2022 + python-version: '3.10' + cmake-args: -DPYBIND11_TEST_SMART_HOLDER=ON -DCMAKE_CXX_FLAGS="/GR /EHsc" + - runs-on: windows-2022 + python-version: '3.13' + cmake-args: -DCMAKE_MSVC_RUNTIME_LIBRARY=MultiThreadedDebugDLL + - runs-on: windows-latest + python-version: '3.13t' + cmake-args: -DCMAKE_CXX_STANDARD=17 + - runs-on: windows-latest + python-version: '3.14' + cmake-args: -DCMAKE_CXX_STANDARD=20 - runs-on: windows-latest python-version: '3.14t' cmake-args: -DCMAKE_CXX_STANDARD=23 + - runs-on: windows-latest + python-version: 'pypy-3.10' + cmake-args: -DCMAKE_CXX_STANDARD=17 + - runs-on: windows-latest + python-version: 'pypy3.11' + cmake-args: -DCMAKE_CXX_STANDARD=20 + # The setup-python action currently doesn't have graalpy for windows + # See https://github.com/actions/setup-python/pull/880 name: 🐍 uses: ./.github/workflows/reusable-standard.yml @@ -69,7 +165,7 @@ jobs: # This checks inplace builds with C++11 inplace: - if: false + if: github.event.pull_request.draft == false strategy: fail-fast: false matrix: @@ -144,7 +240,7 @@ jobs: manylinux: name: Manylinux on 🐍 3.13t • GIL - if: false + if: github.event.pull_request.draft == false runs-on: ubuntu-latest timeout-minutes: 40 container: quay.io/pypa/musllinux_1_2_x86_64:latest @@ -169,7 +265,7 @@ jobs: run: cmake --build --preset testsvenv -t pytest deadsnakes: - if: false + if: github.event.pull_request.draft == false strategy: fail-fast: false matrix: @@ -250,7 +346,7 @@ jobs: # Testing on clang using the excellent silkeh clang docker images clang: - if: false + if: github.event.pull_request.draft == false runs-on: ubuntu-latest strategy: fail-fast: false @@ -307,7 +403,6 @@ jobs: # Testing NVCC; forces sources to behave like .cu files cuda: - if: false runs-on: ubuntu-latest name: "🐍 3.10 • CUDA 12.2 • Ubuntu 22.04" container: nvidia/cuda:12.2.0-devel-ubuntu22.04 @@ -376,7 +471,7 @@ jobs: # Testing on Ubuntu + NVHPC (previous PGI) compilers, which seems to require more workarounds ubuntu-nvhpc7: - if: false + if: github.event.pull_request.draft == false runs-on: ubuntu-22.04 name: "🐍 3 • NVHPC 23.5 • C++17 • x64" timeout-minutes: 90 @@ -431,7 +526,7 @@ jobs: # Testing on GCC using the GCC docker images (only recent images supported) gcc: - if: false + if: github.event.pull_request.draft == false runs-on: ubuntu-latest strategy: fail-fast: false @@ -504,7 +599,7 @@ jobs: # Testing on ICC using the oneAPI apt repo icc: - if: false + if: github.event.pull_request.draft == false runs-on: ubuntu-22.04 timeout-minutes: 90 @@ -610,7 +705,7 @@ jobs: # Testing on CentOS (manylinux uses a centos base). centos: - if: false + if: github.event.pull_request.draft == false runs-on: ubuntu-latest strategy: fail-fast: false @@ -676,7 +771,7 @@ jobs: # This tests an "install" with the CMake tools install-classic: - if: false + if: github.event.pull_request.draft == false name: "🐍 3.9 • Debian • x86 • Install" runs-on: ubuntu-latest container: i386/debian:bullseye @@ -722,7 +817,7 @@ jobs: # This verifies that the documentation is not horribly broken, and does a # basic validation check on the SDist. doxygen: - if: false + if: github.event.pull_request.draft == false name: "Documentation build test" runs-on: ubuntu-latest timeout-minutes: 90 @@ -758,7 +853,7 @@ jobs: diff -rq $installed ./pybind11 win32: - if: false + if: github.event.pull_request.draft == false strategy: fail-fast: false matrix: @@ -813,7 +908,7 @@ jobs: run: cmake --build build -t pytest win32-debug: - if: false + if: github.event.pull_request.draft == false strategy: fail-fast: false matrix: @@ -865,7 +960,7 @@ jobs: windows-2022: - if: false + if: github.event.pull_request.draft == false strategy: fail-fast: false matrix: @@ -929,7 +1024,7 @@ jobs: run: cmake --build build_partial --target pytest mingw: - if: false + if: github.event.pull_request.draft == false name: "🐍 3 • windows-latest • ${{ matrix.sys }}" runs-on: windows-latest timeout-minutes: 90 @@ -1038,7 +1133,7 @@ jobs: run: PYTHONHOME=/${{matrix.sys}} PYTHONPATH=/${{matrix.sys}} cmake --build build3 --target test_cross_module_rtti windows_clang: - if: false + if: github.event.pull_request.draft == false strategy: matrix: @@ -1113,7 +1208,7 @@ jobs: # Clang with MSVC/Windows SDK toolchain + python.org CPython (Windows ARM) windows_arm_clang_msvc: - if: false + if: github.event.pull_request.draft == false strategy: fail-fast: false @@ -1172,7 +1267,7 @@ jobs: # Clang in MSYS2/MinGW-w64 CLANGARM64 toolchain + MSYS2 Python (Windows ARM) windows_arm_clang_msys2: - if: false + if: github.event.pull_request.draft == false strategy: fail-fast: false From c8880a9b0fdd3dc071cb17724fc449bda0b8c167 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 14 Dec 2025 06:57:21 -0800 Subject: [PATCH 15/18] Pin CI free-threaded runs to CPython 3.14.0t Update the standard-small and standard-large GitHub Actions jobs to request python-version 3.14.0t instead of 3.14t. This forces setup-python to use the last-known-good 3.14.0 free-threaded build rather than the newer 3.14.1+ builds where subinterpreter finalization regressed. --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d00ee6d27e..ac0a7000e5 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -42,7 +42,7 @@ jobs: python-version: '3.13' cmake-args: -DCMAKE_CXX_STANDARD=23 -DPYBIND11_SIMPLE_GIL_MANAGEMENT=ON - runs-on: ubuntu-latest - python-version: '3.14t' + python-version: '3.14.0t' cmake-args: -DCMAKE_CXX_STANDARD=17 -DPYBIND11_TEST_SMART_HOLDER=ON - runs-on: ubuntu-latest python-version: 'pypy3.11' @@ -109,7 +109,7 @@ jobs: python-version: '3.13t' cmake-args: -DCMAKE_CXX_STANDARD=11 - runs-on: macos-latest - python-version: '3.14t' + python-version: '3.14.0t' cmake-args: -DCMAKE_CXX_STANDARD=20 - runs-on: macos-15-intel python-version: 'pypy-3.10' @@ -145,7 +145,7 @@ jobs: python-version: '3.14' cmake-args: -DCMAKE_CXX_STANDARD=20 - runs-on: windows-latest - python-version: '3.14t' + python-version: '3.14.0t' cmake-args: -DCMAKE_CXX_STANDARD=23 - runs-on: windows-latest python-version: 'pypy-3.10' From d118d9d549e999f7cfdcba314f9ab4ba10dd0df8 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 14 Dec 2025 20:33:24 -0800 Subject: [PATCH 16/18] Replace Catch2 CumulativeReporterBase with StreamingReporterBase The progress reporter emits output as test cases start and finish and flushes immediately to keep CI logs current with progress (so that we can see immediately where tests hang). StreamingReporterBase matches this behavior directly, whereas CumulativeReporterBase is meant for reporters that collect results and emit output at the end of the run. --- tests/test_with_catch/catch.cpp | 62 ++++++++++++++++++--------------- 1 file changed, 33 insertions(+), 29 deletions(-) diff --git a/tests/test_with_catch/catch.cpp b/tests/test_with_catch/catch.cpp index 171d1df626..c615afa71f 100644 --- a/tests/test_with_catch/catch.cpp +++ b/tests/test_with_catch/catch.cpp @@ -21,49 +21,53 @@ namespace py = pybind11; // Simple progress reporter that prints a line per test case. namespace { -bool g_printed_python_version = false; - -class ProgressReporter : public Catch::CumulativeReporterBase { +class ProgressReporter : public Catch::StreamingReporterBase { public: - using CumulativeReporterBase::CumulativeReporterBase; + using StreamingReporterBase::StreamingReporterBase; static std::string getDescription() { return "Simple progress reporter (one line per test)"; } void testCaseStarting(Catch::TestCaseInfo const &testInfo) override { - if (!g_printed_python_version) { - g_printed_python_version = true; - const char *version = Py_GetVersion(); - stream << "[ PYTHON ] " << version << '\n'; - stream.flush(); - } - stream << "[ RUN ] " << testInfo.name << '\n'; - stream.flush(); - CumulativeReporterBase::testCaseStarting(testInfo); - } - - void testCaseEnded(Catch::TestCaseStats const &testCaseStats) override { - auto const &info = testCaseStats.testInfo; - bool failed = (testCaseStats.totals.assertions.failed > 0); - stream << (failed ? "[ FAILED ] " : "[ OK ] ") << info.name << '\n'; - stream.flush(); - CumulativeReporterBase::testCaseEnded(testCaseStats); + print_python_version_once(); + auto &os = Catch::cout(); + os << "[ RUN ] " << testInfo.name << '\n'; + os.flush(); } - static std::set getSupportedVerbosities() { - return {Catch::Verbosity::Normal}; + void testCaseEnded(Catch::TestCaseStats const &stats) override { + bool failed = stats.totals.assertions.failed > 0; + auto &os = Catch::cout(); + os << (failed ? "[ FAILED ] " : "[ OK ] ") << stats.testInfo.name << '\n'; + os.flush(); } - void testRunEndedCumulative() override {} - void noMatchingTestCases(std::string const &spec) override { - stream << "[ NO TEST ] no matching test cases for spec: " << spec << '\n'; - stream.flush(); + auto &os = Catch::cout(); + os << "[ NO TEST ] no matching test cases for spec: " << spec << '\n'; + os.flush(); } void reportInvalidArguments(std::string const &arg) override { - stream << "[ ERROR ] invalid Catch2 arguments: " << arg << '\n'; - stream.flush(); + auto &os = Catch::cout(); + os << "[ ERROR ] invalid Catch2 arguments: " << arg << '\n'; + os.flush(); } + + void assertionStarting(Catch::AssertionInfo const &) override {} + + bool assertionEnded(Catch::AssertionStats const &) override { return false; } + +private: + void print_python_version_once() { + if (printed_) + return; + printed_ = true; + auto &os = Catch::cout(); + os << "[ PYTHON ] " << Py_GetVersion() << '\n'; + os.flush(); + } + + bool printed_ = false; }; } // namespace From 29b718ef171f06b4632a08f1382a268797649d89 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 14 Dec 2025 21:21:58 -0800 Subject: [PATCH 17/18] Resolve clang-tidy readability-braces-around-statements error: /__w/pybind11/pybind11/tests/test_with_catch/catch.cpp:62:22: error: statement should be inside braces [readability-braces-around-statements,-warnings-as-errors] 62 | if (printed_) | ^ | { 63 | return; | --- tests/test_with_catch/catch.cpp | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/test_with_catch/catch.cpp b/tests/test_with_catch/catch.cpp index c615afa71f..5dbc01f677 100644 --- a/tests/test_with_catch/catch.cpp +++ b/tests/test_with_catch/catch.cpp @@ -59,8 +59,9 @@ class ProgressReporter : public Catch::StreamingReporterBase { private: void print_python_version_once() { - if (printed_) + if (printed_) { return; + } printed_ = true; auto &os = Catch::cout(); os << "[ PYTHON ] " << Py_GetVersion() << '\n'; From 6b0eb1daf8f12760ce880dc34b20129aed504ec6 Mon Sep 17 00:00:00 2001 From: "Ralf W. Grosse-Kunstleve" Date: Sun, 14 Dec 2025 23:18:25 -0800 Subject: [PATCH 18/18] Add move_subinterpreter_redux subdirectory (NOT meant for merging) --- move_subinterpreter_redux/build_and_run.sh | 60 ++++++ .../move_subinterpreter_redux.c | 203 ++++++++++++++++++ 2 files changed, 263 insertions(+) create mode 100755 move_subinterpreter_redux/build_and_run.sh create mode 100644 move_subinterpreter_redux/move_subinterpreter_redux.c diff --git a/move_subinterpreter_redux/build_and_run.sh b/move_subinterpreter_redux/build_and_run.sh new file mode 100755 index 0000000000..f32aba967f --- /dev/null +++ b/move_subinterpreter_redux/build_and_run.sh @@ -0,0 +1,60 @@ +#!/bin/bash +set -euo pipefail + +# Build and run CPython 3.14t move_subinterpreter_redux.c +# +# Usage: +# ./build_and_run.sh /path/to/python3.14t-config +# +# Example: +# ./build_and_run.sh "$HOME/wrk/cpython_installs/v3.14_57e0d177c26/bin/python3.14t-config" + +if [ "$#" -ne 1 ]; then + echo "Usage: $0 /full/path/to/pythonX.Y[t]-config" >&2 + exit 1 +fi + +PYTHON_CONFIG="$1" +if [ ! -x "$PYTHON_CONFIG" ]; then + echo "Error: $PYTHON_CONFIG is not executable" >&2 + exit 1 +fi + +CC="${CC:-gcc}" +CFLAGS="$($PYTHON_CONFIG --cflags)" +LDFLAGS="$($PYTHON_CONFIG --embed --ldflags)" +LIBS="$($PYTHON_CONFIG --embed --libs)" + +src_dir="$(cd "$(dirname "$0")" && pwd)" +cd "$src_dir" + +rm -f move_subinterpreter_redux + +echo "Building move_subinterpreter_redux with: $PYTHON_CONFIG" >&2 +set -x +# shellcheck disable=SC2086 # CFLAGS/LDFLAGS/LIBS need word splitting +"$CC" -O0 -g -Wall -Wextra -o move_subinterpreter_redux move_subinterpreter_redux.c $CFLAGS $LDFLAGS $LIBS -lpthread +set -x + +prefix="$($PYTHON_CONFIG --prefix)" +export LD_LIBRARY_PATH="$prefix/lib${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}" + +echo "Running move_subinterpreter_redux..." >&2 +set -x + +# Temporarily disable 'exit on error' so we can inspect the exit code. +set +e +timeout 3s ./move_subinterpreter_redux +status=$? +set -e +set +x + +if [ "$status" -eq 124 ]; then + echo "move_subinterpreter_redux: TIMED OUT after 3s" >&2 +elif [ "$status" -eq 0 ]; then + echo "move_subinterpreter_redux: finished successfully (exit code 0)" >&2 +else + echo "move_subinterpreter_redux: finished with exit code $status" >&2 +fi + +exit "$status" diff --git a/move_subinterpreter_redux/move_subinterpreter_redux.c b/move_subinterpreter_redux/move_subinterpreter_redux.c new file mode 100644 index 0000000000..735d3e3439 --- /dev/null +++ b/move_subinterpreter_redux/move_subinterpreter_redux.c @@ -0,0 +1,203 @@ +// Minimal CPython 3.14 free-threading Move Subinterpreter redux. +// +// This version is intentionally modeled more closely after pybind11's +// `subinterpreter` + "Move Subinterpreter" test: +// +// - Create a subinterpreter via Py_NewInterpreterFromConfig with +// PyInterpreterConfig_OWN_GIL and allow_threads=1. +// - On the main thread, temporarily activate the subinterpreter and +// import some non-trivial modules. +// - On a worker thread, activate the same subinterpreter again, run +// some code, and then *destroy* the subinterpreter from that thread +// using Py_EndInterpreter with a fresh PyThreadState created on +// that thread (mirroring pybind11's destructor on 3.13+). +// +// Critical differences from the original pybind11 test: +// - We do not keep a permanent PyThreadState* for the subinterpreter. +// Each thread creates a temporary thread state while it is using +// the subinterpreter, then clears and deletes it again (similar +// to pybind11::subinterpreter_scoped_activate). +// - When destroying the subinterpreter we create a new thread state +// on the worker thread and pass that to Py_EndInterpreter, with no +// other live thread states for that interpreter, matching the +// intended CPython contract for Py_EndInterpreter. +// +// Build against a free-threaded CPython 3.14 installation using the +// accompanying shell script. + +#define PY_SSIZE_T_CLEAN +#include +#include +#include +#include +#include + +// Global handle to the subinterpreter's state (mirrors pybind11::subinterpreter::istate_). +static PyInterpreterState *sub_interp = NULL; + +static void fatal(const char *msg) { + fprintf(stderr, "FATAL: %s\n", msg); + fflush(stderr); + exit(1); +} + +// Helper: run some Python code inside the subinterpreter on the current thread, +// creating a temporary PyThreadState and then cleaning it up again. +static void run_in_subinterpreter(const char *label, const char *code) { + if (sub_interp == NULL) { + fatal("run_in_subinterpreter called with sub_interp == NULL"); + } + + PyThreadState *tstate = PyThreadState_New(sub_interp); + if (tstate == NULL) { + fatal("PyThreadState_New failed in run_in_subinterpreter"); + } + + fprintf(stderr, "%s: activating subinterpreter on this thread\n", label); + PyThreadState_Swap(tstate); + + if (PyRun_SimpleString(code) != 0) { + PyErr_Print(); + fatal("PyRun_SimpleString failed in subinterpreter"); + } + + fprintf(stderr, "%s: finished running code in subinterpreter\n", label); + + // Clean up the temporary thread state. After this, the current thread + // no longer has an active thread state for any interpreter. + PyThreadState_Clear(tstate); + PyThreadState_DeleteCurrent(); +} + +// Helper: create the subinterpreter with a configuration similar to +// pybind11::subinterpreter::create(). +static void create_subinterpreter(void) { + if (sub_interp != NULL) { + fatal("create_subinterpreter called twice"); + } + + PyInterpreterConfig cfg; + memset(&cfg, 0, sizeof(cfg)); + cfg.allow_threads = 1; + cfg.check_multi_interp_extensions = 1; + cfg.gil = PyInterpreterConfig_OWN_GIL; + + PyThreadState *creation_tstate = NULL; + PyStatus status = Py_NewInterpreterFromConfig(&creation_tstate, &cfg); + if (PyStatus_Exception(status)) { + Py_ExitStatusException(status); + } + if (creation_tstate == NULL || creation_tstate->interp == NULL) { + fatal("Py_NewInterpreterFromConfig returned NULL interpreter"); + } + + sub_interp = creation_tstate->interp; + + // On 3.13+ pybind11 clears and deletes the creation thread state right away. +#if PY_VERSION_HEX >= 0x030D0000 + PyThreadState_Clear(creation_tstate); + PyThreadState_DeleteCurrent(); +#endif + + fprintf(stderr, "Subinterpreter created.\n"); +} + +// Helper: destroy the subinterpreter from the current thread, mirroring +// pybind11::subinterpreter::~subinterpreter() on 3.13+. +static void destroy_subinterpreter_from_current_thread(const char *label) { + if (sub_interp == NULL) { + fatal("destroy_subinterpreter_from_current_thread called with sub_interp == NULL"); + } + + PyThreadState *destroy_tstate = PyThreadState_New(sub_interp); + if (destroy_tstate == NULL) { + fatal("PyThreadState_New failed in destroy_subinterpreter_from_current_thread"); + } + + PyThreadState *old_tstate = PyThreadState_Swap(destroy_tstate); + + fprintf(stderr, "%s: calling Py_EndInterpreter on subinterpreter\n", label); + Py_EndInterpreter(destroy_tstate); + fprintf(stderr, "%s: returned from Py_EndInterpreter\n", label); + + // If there was a previous thread state belonging to a different interpreter, + // restore it (this should normally be the main interpreter). + if (old_tstate != NULL && old_tstate->interp != sub_interp) { + PyThreadState_Swap(old_tstate); + } + + sub_interp = NULL; +} + +static void *worker_thread(void *arg) { + (void) arg; + + // Use the subinterpreter again from this worker thread. + run_in_subinterpreter("worker", + "import datetime\n" + "import threading\n" + "print('worker: ran code in subinterpreter')\n"); + + // Now destroy the subinterpreter from this worker thread. + destroy_subinterpreter_from_current_thread("worker"); + + return NULL; +} + +int main(int argc, char **argv) { + (void) argc; + (void) argv; + + // Initialize the main interpreter. + PyStatus status; + PyConfig config; + PyConfig_InitPythonConfig(&config); + config.isolated = 0; + config.install_signal_handlers = 0; + + status = Py_InitializeFromConfig(&config); + if (PyStatus_Exception(status)) { + Py_ExitStatusException(status); + } + + // First line of output: the Python version. + fprintf(stderr, "Python version: %s\n", Py_GetVersion()); + + PyThreadState *main_tstate = PyThreadState_Get(); + if (main_tstate == NULL) { + fatal("PyThreadState_Get returned NULL"); + } + + fprintf(stderr, "Main interpreter initialized.\n"); + + // Create a subinterpreter with its own GIL, similar to pybind11::subinterpreter::create(). + create_subinterpreter(); + + // On the main thread, activate the subinterpreter and import some modules. + run_in_subinterpreter("main", + "import sys\n" + "import datetime\n" + "import threading\n" + "print('main: ran code in subinterpreter')\n"); + + fprintf(stderr, "Subinterpreter imports on main thread done.\n"); + + // Start a worker thread that uses the same subinterpreter and then destroys it. + pthread_t th; + if (pthread_create(&th, NULL, worker_thread, NULL) != 0) { + fatal("pthread_create failed"); + } + + if (pthread_join(th, NULL) != 0) { + fatal("pthread_join failed"); + } + + fprintf(stderr, "Worker thread joined.\n"); + + // At this point the subinterpreter should be gone. Finalize the main interpreter. + PyThreadState_Swap(main_tstate); + int rc = Py_FinalizeEx(); + fprintf(stderr, "Py_FinalizeEx() returned %d.\n", rc); + + return (rc == 0) ? 0 : 1; +}