From acb0ff298109c534dfb94d854084fce820201f59 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Sat, 21 Feb 2026 22:28:32 +0000 Subject: [PATCH 1/5] Add `qiskit.capi` module This adds a basic `qiskit.capi` module for Python-space interaction with the C API. The two initial functions included are `get_include` and `get_lib`, to get the package include directory and shared-object library containing the C-API exported symbols, respectively. Note that while the included header files _are_ complete information to build against the C API, the file returned from `get_lib` is not generally useful as part of a _build_ system, but can be used to interact with the C API live through a Python session. I hope to follow this patch with two more: * provide a second "mode" for the packaged header files that allows building safe Python extension modules against the C API. This would mean that all the C API function names will be `#define`'d to function pointers looked up in a static table stored inside a `PyCapsule` that is initialised as part of `_accelerate`'s initialisation. * generate a `ctypes` wrapper file for the C API as part of the `_accelerate` (`pyext`) build script, which would expose all of the C API functions directly to Python space, potentially allowing them to be jitted through Numba, or allowing direct tests of C API functionality (without the manually-written file currently in our test suite). Co-authored-by: Max Rossmannek --- .gitignore | 2 + Cargo.lock | 2 + crates/bindgen/src/lib.rs | 27 +++++- crates/pyext/Cargo.toml | 5 ++ crates/pyext/build.rs | 41 +++++++++ docs/apidoc/capi.rst | 6 ++ docs/apidoc/index.rst | 1 + qiskit/__init__.py | 1 + qiskit/capi/__init__.py | 90 +++++++++++++++++++ .../header-file-include-aad5efd67ace6bad.yaml | 10 +++ setup.py | 1 + test/python/{c_api => capi}/__init__.py | 0 test/python/{c_api => capi}/ffi.py | 3 +- test/python/capi/test_capi.py | 28 ++++++ test/python/{c_api => capi}/test_transpile.py | 0 15 files changed, 211 insertions(+), 6 deletions(-) create mode 100644 crates/pyext/build.rs create mode 100644 docs/apidoc/capi.rst create mode 100644 qiskit/capi/__init__.py create mode 100644 releasenotes/notes/header-file-include-aad5efd67ace6bad.yaml rename test/python/{c_api => capi}/__init__.py (100%) rename test/python/{c_api => capi}/ffi.py (99%) create mode 100644 test/python/capi/test_capi.py rename test/python/{c_api => capi}/test_transpile.py (100%) diff --git a/.gitignore b/.gitignore index 8d7519e60e64..06225de69b5b 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,8 @@ __pycache__/ # C extensions *.so +# Generated C header files included in package. +/qiskit/capi/include # Distribution / packaging .Python diff --git a/Cargo.lock b/Cargo.lock index 185625aa1b12..50153afc48c4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1992,8 +1992,10 @@ dependencies = [ name = "qiskit-pyext" version = "2.4.0-dev" dependencies = [ + "anyhow", "pyo3", "qiskit-accelerate", + "qiskit-bindgen", "qiskit-cext", "qiskit-circuit", "qiskit-circuit-library", diff --git a/crates/bindgen/src/lib.rs b/crates/bindgen/src/lib.rs index 20c47a71662c..64eaa8e01bec 100644 --- a/crates/bindgen/src/lib.rs +++ b/crates/bindgen/src/lib.rs @@ -19,6 +19,20 @@ pub static GENERATED_FILE: &str = "qiskit.h"; pub static PYTHON_BINDING_FEATURE: &str = "python_binding"; pub static PYTHON_BINDING_DEFINE: &str = "QISKIT_C_PYTHON_INTERFACE"; +pub static COPYRIGHT: &str = "\ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at https://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. +"; + pub static INCLUDE_GUARD: &str = "QISKIT_H"; /// Crates that contain definitions of objects that are exposed through the C API. pub static QISKIT_PUBLIC_API_CRATES: &[&str] = @@ -100,6 +114,14 @@ fn manual_include_files() -> anyhow::Result> { /// Get the Qiskit configuration fn get_config() -> anyhow::Result { + // `Python.h` is required to be the first file included because it reserves the right to define + // preprocessor macros that affect standard-library includes. This causes it to be ahead of our + // include guard, but `Python.h` has its own, so we should be fine. + let header = Some(format!( + "{}\n{}", + COPYRIGHT, + guarded_python_import(PYTHON_BINDING_DEFINE) + )); let enumeration = cbindgen::EnumConfig { prefix_with_name: true, ..Default::default() @@ -158,10 +180,7 @@ fn get_config() -> anyhow::Result { }) .collect::, _>>()?; Ok(cbindgen::Config { - // `Python.h` is required to be the first file included because it reserves the right to - // define preprocessor macros that affect standard-library includes. This causes it to be - // ahead of our include guard, but `Python.h` has its own, so we should be fine. - header: Some(guarded_python_import(PYTHON_BINDING_DEFINE)), + header, language: cbindgen::Language::C, include_version: true, include_guard: Some(INCLUDE_GUARD.into()), diff --git a/crates/pyext/Cargo.toml b/crates/pyext/Cargo.toml index 6c4accd10874..be43a612287b 100644 --- a/crates/pyext/Cargo.toml +++ b/crates/pyext/Cargo.toml @@ -4,6 +4,7 @@ version.workspace = true edition.workspace = true rust-version.workspace = true license.workspace = true +build = "build.rs" [lib] name = "qiskit_pyext" @@ -22,6 +23,10 @@ workspace = true default = ["pyo3/extension-module"] cache_pygates = ["pyo3/extension-module", "qiskit-circuit/cache_pygates", "qiskit-accelerate/cache_pygates", "qiskit-transpiler/cache_pygates", "qiskit-cext/cache_pygates", "qiskit-qpy/cache_pygates"] +[build-dependencies] +anyhow.workspace = true +qiskit-bindgen.workspace = true + [dependencies] pyo3.workspace = true qiskit-accelerate.workspace = true diff --git a/crates/pyext/build.rs b/crates/pyext/build.rs new file mode 100644 index 000000000000..1015b5236695 --- /dev/null +++ b/crates/pyext/build.rs @@ -0,0 +1,41 @@ +// This code is part of Qiskit. +// +// (C) Copyright IBM 2026 +// +// This code is licensed under the Apache License, Version 2.0. You may +// obtain a copy of this license in the LICENSE.txt file in the root directory +// of this source tree or at https://www.apache.org/licenses/LICENSE-2.0. +// +// Any modifications or derivative works of this code must retain this +// copyright notice, and modified files need to carry a notice indicating +// that they have been altered from the originals. + +use anyhow::anyhow; +use std::path::Path; + +#[allow(clippy::print_stdout)] // We're a build script - we're _supposed_ to print to stdout. +fn main() -> anyhow::Result<()> { + let cext_path = { + let mut path = Path::new(env!("CARGO_MANIFEST_DIR")).to_path_buf(); + path.pop(); + path.push("cext"); + path + }; + println!( + "cargo::rerun-if-changed={}", + cext_path + .to_str() + .ok_or_else(|| anyhow!("cext path isn't unicode"))? + ); + let out_path = { + let out_dir = std::env::var("OUT_DIR").expect("cargo should set this for build scripts"); + let mut path = Path::new(&out_dir).to_path_buf(); + path.push("include"); + path + }; + let bindings = qiskit_bindgen::generate_bindings(&cext_path)?; + // We install the headers into our `OUT_DIR`, then we configure `setuptools-rust` to pick them + // up from there and put them into the Python package. + qiskit_bindgen::install_c_headers(&bindings, &out_path)?; + Ok(()) +} diff --git a/docs/apidoc/capi.rst b/docs/apidoc/capi.rst new file mode 100644 index 000000000000..feef8ba24f66 --- /dev/null +++ b/docs/apidoc/capi.rst @@ -0,0 +1,6 @@ +.. _qiskit-capi: + +.. automodule:: qiskit.capi + :no-members: + :no-inherited-members: + :no-special-members: diff --git a/docs/apidoc/index.rst b/docs/apidoc/index.rst index 4e7a184c8621..536d59c9d27d 100644 --- a/docs/apidoc/index.rst +++ b/docs/apidoc/index.rst @@ -75,6 +75,7 @@ Other: .. toctree:: :maxdepth: 1 + capi compiler exceptions root diff --git a/qiskit/__init__.py b/qiskit/__init__.py index 6cdda217535c..6e55a2c2a4d3 100644 --- a/qiskit/__init__.py +++ b/qiskit/__init__.py @@ -139,6 +139,7 @@ from qiskit.exceptions import QiskitError, MissingOptionalLibraryError +import qiskit.capi # The main qiskit operators from qiskit.circuit import ClassicalRegister diff --git a/qiskit/capi/__init__.py b/qiskit/capi/__init__.py new file mode 100644 index 000000000000..83c1c51015eb --- /dev/null +++ b/qiskit/capi/__init__.py @@ -0,0 +1,90 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2026. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at https://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +r""" +================================================ +C API interface from Python (:mod:`qiskit.capi`) +================================================ + +.. currentmodule:: qiskit.capi + +This module provides Python-space interactions with Qiskit's public C API. + +For documentation on the C API itself, see `Qiskit C API +`__. + +Build-system interaction +======================== + +The Python package :mod:`qiskit` contains all of the Qiskit C API header files, and a compiled +shared-object library that includes all of the C-API functions. You can access the locations of +these two objects with the functions :func:`get_include` and :func:`get_lib` respectively. + +.. warning:: + You typically *should not* link directly against the output of :func:`get_lib`, unless you know + what you are doing. In particular, directly linking against this objcet is not a safe way to + build a distributable Python extension module that uses Qiskit's C API. + + However, if you understand all the caveats of direct linking, you can use the function to get + the location of the library. + +.. autofunction:: get_include +.. autofunction:: get_lib +""" + +__all__ = ["get_include", "get_lib"] + +from pathlib import Path + +import qiskit._accelerate + + +def get_include() -> str: + """Get the directory containing the ``qiskit.h`` C header file and the internal + ``qiskit/*.h`` auxiliary files. + + When using Qiskit as a build dependency, you typically want to include this directory on the + include search path of your compiler, such as: + + .. code-block:: bash + + qiskit_include=$(python -c 'import qiskit.capi; print(qiskit.capi.get_include())') + gcc -I "$qiskit_include" my_bin.c -o my_bin + + The location of this directory within the Qiskit package data is not fixed, and may change + between Qiskit versions. You should always use this function to retrieve the directory. + + Returns: + an absolute path to the package include-files directory. + """ + return str(Path(__file__).parent.absolute() / "include") + + +def get_lib() -> str: + """Get the path to a shared-object library that contains all the C-API exported symbols. + + .. warning:: + You typically *should not* link directly against this object. In particular, directly + linking against this object is not a safe way to build a Python extension module that uses + Qiskit's C API. + + You can, if you choose, use :mod:`ctypes` to access the C API symbols contained in this object, + though beware that the C-API types declared in the header file are not interchangeable with the + Python objects that correspond to them. + + The location and name of this file within the Qiskit package data is not fixed, and may change + between Qiskit versions. + + Returns: + an absolute path to the shared-object library containing the C-API exported symbols. + """ + return str(Path(qiskit._accelerate.__file__).absolute()) diff --git a/releasenotes/notes/header-file-include-aad5efd67ace6bad.yaml b/releasenotes/notes/header-file-include-aad5efd67ace6bad.yaml new file mode 100644 index 000000000000..62a60fb3fc34 --- /dev/null +++ b/releasenotes/notes/header-file-include-aad5efd67ace6bad.yaml @@ -0,0 +1,10 @@ +--- +features_c: + - A new Python-space module, :mod:`qiskit.capi`, provides basic access to installation details + about the Qiskit C API bundled with the Python package. + - The C API header files (``qiskit.h`` and its auxiliary files) are now bundled inside the + Python-space ``qiskit`` package. You can find the location of the include directory to add to + you compiler's search path using :func:`qiskit.capi.get_include`. +build: + - Packages dependening on the Qiskit C API to build can now specify Qiskit as a Python-space build + dependency, then use :func:`qiskit.capi.get_include` to find the header files. diff --git a/setup.py b/setup.py index 55d1095d1778..da73c6881468 100644 --- a/setup.py +++ b/setup.py @@ -64,6 +64,7 @@ binding=Binding.PyO3, debug=rust_debug, features=features, + data_files={"include": "qiskit.capi"}, ) ], options={"bdist_wheel": {"py_limited_api": "cp310"}}, diff --git a/test/python/c_api/__init__.py b/test/python/capi/__init__.py similarity index 100% rename from test/python/c_api/__init__.py rename to test/python/capi/__init__.py diff --git a/test/python/c_api/ffi.py b/test/python/capi/ffi.py similarity index 99% rename from test/python/c_api/ffi.py rename to test/python/capi/ffi.py index 0a48e052a487..9b5a154f41a0 100644 --- a/test/python/c_api/ffi.py +++ b/test/python/capi/ffi.py @@ -18,8 +18,7 @@ import qiskit -LIB_PATH = qiskit._accelerate.__file__ -LIB = ctypes.PyDLL(LIB_PATH) +LIB = ctypes.PyDLL(qiskit.capi.get_lib()) class QkCircuit(ctypes.Structure): diff --git a/test/python/capi/test_capi.py b/test/python/capi/test_capi.py new file mode 100644 index 000000000000..fe61fe4bb79f --- /dev/null +++ b/test/python/capi/test_capi.py @@ -0,0 +1,28 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2026. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at https://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +# pylint: disable=missing-function-docstring,missing-class-docstring,missing-module-docstring + +from pathlib import Path + +from qiskit import capi +from test import QiskitTestCase # pylint: disable=wrong-import-order + + +class TestCAPI(QiskitTestCase): + def test_includes_exists(self): + path = Path(capi.get_include()) + self.assertIn("QISKIT_H", (path / "qiskit.h").read_text(encoding="utf-8")) + self.assertLess(set(), set((path / "qiskit").glob("*.h"))) + + def test_library_exists(self): + self.assertTrue(Path(capi.get_lib()).exists()) diff --git a/test/python/c_api/test_transpile.py b/test/python/capi/test_transpile.py similarity index 100% rename from test/python/c_api/test_transpile.py rename to test/python/capi/test_transpile.py From d3d2e54d572440d8d47198bcd6fd14cb47771915 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 10 Mar 2026 14:11:55 +0000 Subject: [PATCH 2/5] Vendor `generated-files` handling from `setuptools-rust` We need https://github.com/PyO3/setuptools-rust/pull/574 (or something akin to it) to handle the movement of the generated files from the build script to the output. This is not yet ready for merge, and is highly unlikely to be ready in time for the Qiskit 2.4.0 release (or at least 2.4.0rc1), so to avoid blocking ourselves, we vendor in a monkey-patching reimplementation of the code temporarily. The allowed range of `setuptools-rust` is _probably_ larger than the hard pin, but the monkey-patch is so invasive and v1.12.0 is sufficiently old (August 2025) that it's not worth the risk. There are no meaningful licensing concerns since the vendored code here was entirely written by me both here and in the `setuptools-rust` patch; there is no cross-contamination from that repository and no code inclusion from it. This commit should be reverted as soon as we can swap to a safe released version of `setuptools-rust`. --- pyproject.toml | 4 +-- setup.py | 83 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 84 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index c934c48b4919..5fb5b94afcdc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,7 +2,7 @@ # This is duplicated as `dependency-groups.build` for convenience. requires = [ "setuptools>=77.0", - "setuptools-rust", + "setuptools-rust==1.12.0", ] build-backend = "setuptools.build_meta" @@ -184,7 +184,7 @@ default = "qiskit.transpiler.preset_passmanagers.builtin_plugins:DefaultScheduli # process. If you intended that, use `project.optional-dependencies` instead. [dependency-groups] # Sync with `build-system.requires`. -build = ["setuptools>=77.0", "setuptools-rust"] +build = ["setuptools>=77.0", "setuptools-rust==1.12.0"] # We use quite tight pins on pylint and astroid because they have a habit of adding new # on-by-default lints that are harder to deal with. lint = [ diff --git a/setup.py b/setup.py index da73c6881468..c923618d47d6 100644 --- a/setup.py +++ b/setup.py @@ -17,6 +17,88 @@ from setuptools import setup from setuptools_rust import Binding, RustExtension +# ================================================================================================== +# Warning: this section is a horrendous monkey-patching hack that re-implements a version of +# https://github.com/PyO3/setuptools-rust/pull/574 on top of `setuptools-rust==1.12.0`, which +# contains just enough functionality for our own use. + +from pathlib import Path +import json +import shutil +import functools +import setuptools_rust.build + +# This function is only called once in our build, to find the `cdylib` artifact. It's passed the +# `cargo` messages, which is what we need to locate the `OUT_DIR`; we use this as a hook point to +# intercept them and leak them out for later use. +find_cargo_artifacts_orig = setuptools_rust.build._find_cargo_artifacts +install_extension_orig = setuptools_rust.build.build_rust.install_extension +out_dir = None +generated_files = {"include": "qiskit.capi"} + + +@functools.wraps(find_cargo_artifacts_orig) +def find_cargo_artifacts_patch(cargo_messages, *, package_id, kinds): + global out_dir # noqa: PLW0603 (global) - we have to leak to get the monkeypatch to work. + + # Chances are that the line we're looking for will be the third laste line in the + # messages. The last is the completion report, the penultimate is generally the + # build of the final artifact. + for messsage in reversed(cargo_messages): + if "build-script-executed" not in messsage or package_id not in messsage: + continue + parsed = json.loads(messsage) + if parsed.get("package_id") == package_id: + out_dir = parsed.get("out_dir") + break + + return find_cargo_artifacts_orig(cargo_messages, package_id=package_id, kinds=kinds) + + +@functools.wraps(install_extension_orig) +def install_extension_patch(self, ext, dylib_paths): + install_extension_orig(self, ext, dylib_paths) + + # `out_dir` and `generated_files` are captured from the outer scope. + if out_dir is None: + raise RuntimeError("setup sanity check failed: the cargo `out_dir` is not set") + build_artifact_dirs = [Path(out_dir)] + + # We'll delegate the finding of the package directories to Setuptools, so we + # can be sure we're handling editable installs and other complex situations + # correctly. + build_ext = self.get_finalized_command("build_ext") + build_py = self.get_finalized_command("build_py") + + def get_package_dir(package: str) -> Path: + if self.inplace: + # If `inplace`, we have to ask `build_py` (like `build_ext` would). + return Path(build_py.get_package_dir(package)) + # ... If not, `build_ext` knows where to put the package. + return Path(build_ext.build_lib) / Path(*package.split(".")) + + for source, package in generated_files.items(): + dest = get_package_dir(package) + dest.mkdir(mode=0o755, parents=True, exist_ok=True) + for artifact_dir in build_artifact_dirs: + source_full = artifact_dir / source + dest_full = dest / source_full.name + if source_full.is_file(): + # logger.info("Copying data file from %s to %s", source_full, dest_full) + shutil.copy2(source_full, dest_full) + elif source_full.is_dir(): + # logger.info("Copying data directory from %s to %s", source_full, dest_full) + shutil.copytree(source_full, dest_full, dirs_exist_ok=True) + # This tacitly makes "no match" a silent non-error. + + +setuptools_rust.build._find_cargo_artifacts = find_cargo_artifacts_patch +setuptools_rust.build.build_rust.install_extension = install_extension_patch + +# Normal service resumes below here. +# ================================================================================================== + + # Most of this configuration is managed by `pyproject.toml`. This only includes the extra bits to # configure `setuptools-rust`, because we do a little dynamic trick with the debug setting, and we # also want an explicit `setup.py` file to exist so we can manually call @@ -64,7 +146,6 @@ binding=Binding.PyO3, debug=rust_debug, features=features, - data_files={"include": "qiskit.capi"}, ) ], options={"bdist_wheel": {"py_limited_api": "cp310"}}, From e526d30676bfb28deb940739a90888dc488d4df8 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 10 Mar 2026 18:09:54 +0000 Subject: [PATCH 3/5] Remove pylint suppressions --- test/python/capi/test_capi.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/test/python/capi/test_capi.py b/test/python/capi/test_capi.py index fe61fe4bb79f..3fc684958f64 100644 --- a/test/python/capi/test_capi.py +++ b/test/python/capi/test_capi.py @@ -10,12 +10,10 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -# pylint: disable=missing-function-docstring,missing-class-docstring,missing-module-docstring - from pathlib import Path from qiskit import capi -from test import QiskitTestCase # pylint: disable=wrong-import-order +from test import QiskitTestCase class TestCAPI(QiskitTestCase): From 0c1645d49e032bb7b4004255230e87bead49ac94 Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 10 Mar 2026 18:41:29 +0000 Subject: [PATCH 4/5] Fix typos --- qiskit/capi/__init__.py | 2 +- setup.py | 8 +++----- 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/qiskit/capi/__init__.py b/qiskit/capi/__init__.py index 83c1c51015eb..88e857ecebcb 100644 --- a/qiskit/capi/__init__.py +++ b/qiskit/capi/__init__.py @@ -31,7 +31,7 @@ .. warning:: You typically *should not* link directly against the output of :func:`get_lib`, unless you know - what you are doing. In particular, directly linking against this objcet is not a safe way to + what you are doing. In particular, directly linking against this object is not a safe way to build a distributable Python extension module that uses Qiskit's C API. However, if you understand all the caveats of direct linking, you can use the function to get diff --git a/setup.py b/setup.py index c923618d47d6..84b4387da43f 100644 --- a/setup.py +++ b/setup.py @@ -44,10 +44,10 @@ def find_cargo_artifacts_patch(cargo_messages, *, package_id, kinds): # Chances are that the line we're looking for will be the third laste line in the # messages. The last is the completion report, the penultimate is generally the # build of the final artifact. - for messsage in reversed(cargo_messages): - if "build-script-executed" not in messsage or package_id not in messsage: + for message in reversed(cargo_messages): + if "build-script-executed" not in message or package_id not in message: continue - parsed = json.loads(messsage) + parsed = json.loads(message) if parsed.get("package_id") == package_id: out_dir = parsed.get("out_dir") break @@ -84,10 +84,8 @@ def get_package_dir(package: str) -> Path: source_full = artifact_dir / source dest_full = dest / source_full.name if source_full.is_file(): - # logger.info("Copying data file from %s to %s", source_full, dest_full) shutil.copy2(source_full, dest_full) elif source_full.is_dir(): - # logger.info("Copying data directory from %s to %s", source_full, dest_full) shutil.copytree(source_full, dest_full, dirs_exist_ok=True) # This tacitly makes "no match" a silent non-error. From b66bce6857dec66066dc2ca78a952c01b414624b Mon Sep 17 00:00:00 2001 From: Jake Lishman Date: Tue, 10 Mar 2026 18:43:14 +0000 Subject: [PATCH 5/5] Comment on hard-pin of `setuptools-rust` --- pyproject.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index 5fb5b94afcdc..2713f1742627 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -2,6 +2,9 @@ # This is duplicated as `dependency-groups.build` for convenience. requires = [ "setuptools>=77.0", + # This is hard-pinned to permit the awful monkeypatching in `setup.py`. + # It should be removed when we can use a released version of + # https://github.com/PyO3/setuptools-rust/pull/574 "setuptools-rust==1.12.0", ] build-backend = "setuptools.build_meta"