Skip to content

Commit d3d2e54

Browse files
committed
Vendor generated-files handling from setuptools-rust
We need PyO3/setuptools-rust#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`.
1 parent acb0ff2 commit d3d2e54

2 files changed

Lines changed: 84 additions & 3 deletions

File tree

pyproject.toml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
# This is duplicated as `dependency-groups.build` for convenience.
33
requires = [
44
"setuptools>=77.0",
5-
"setuptools-rust",
5+
"setuptools-rust==1.12.0",
66
]
77
build-backend = "setuptools.build_meta"
88

@@ -184,7 +184,7 @@ default = "qiskit.transpiler.preset_passmanagers.builtin_plugins:DefaultScheduli
184184
# process. If you intended that, use `project.optional-dependencies` instead.
185185
[dependency-groups]
186186
# Sync with `build-system.requires`.
187-
build = ["setuptools>=77.0", "setuptools-rust"]
187+
build = ["setuptools>=77.0", "setuptools-rust==1.12.0"]
188188
# We use quite tight pins on pylint and astroid because they have a habit of adding new
189189
# on-by-default lints that are harder to deal with.
190190
lint = [

setup.py

Lines changed: 82 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,88 @@
1717
from setuptools import setup
1818
from setuptools_rust import Binding, RustExtension
1919

20+
# ==================================================================================================
21+
# Warning: this section is a horrendous monkey-patching hack that re-implements a version of
22+
# https://github.com/PyO3/setuptools-rust/pull/574 on top of `setuptools-rust==1.12.0`, which
23+
# contains just enough functionality for our own use.
24+
25+
from pathlib import Path
26+
import json
27+
import shutil
28+
import functools
29+
import setuptools_rust.build
30+
31+
# This function is only called once in our build, to find the `cdylib` artifact. It's passed the
32+
# `cargo` messages, which is what we need to locate the `OUT_DIR`; we use this as a hook point to
33+
# intercept them and leak them out for later use.
34+
find_cargo_artifacts_orig = setuptools_rust.build._find_cargo_artifacts
35+
install_extension_orig = setuptools_rust.build.build_rust.install_extension
36+
out_dir = None
37+
generated_files = {"include": "qiskit.capi"}
38+
39+
40+
@functools.wraps(find_cargo_artifacts_orig)
41+
def find_cargo_artifacts_patch(cargo_messages, *, package_id, kinds):
42+
global out_dir # noqa: PLW0603 (global) - we have to leak to get the monkeypatch to work.
43+
44+
# Chances are that the line we're looking for will be the third laste line in the
45+
# messages. The last is the completion report, the penultimate is generally the
46+
# build of the final artifact.
47+
for messsage in reversed(cargo_messages):
48+
if "build-script-executed" not in messsage or package_id not in messsage:
49+
continue
50+
parsed = json.loads(messsage)
51+
if parsed.get("package_id") == package_id:
52+
out_dir = parsed.get("out_dir")
53+
break
54+
55+
return find_cargo_artifacts_orig(cargo_messages, package_id=package_id, kinds=kinds)
56+
57+
58+
@functools.wraps(install_extension_orig)
59+
def install_extension_patch(self, ext, dylib_paths):
60+
install_extension_orig(self, ext, dylib_paths)
61+
62+
# `out_dir` and `generated_files` are captured from the outer scope.
63+
if out_dir is None:
64+
raise RuntimeError("setup sanity check failed: the cargo `out_dir` is not set")
65+
build_artifact_dirs = [Path(out_dir)]
66+
67+
# We'll delegate the finding of the package directories to Setuptools, so we
68+
# can be sure we're handling editable installs and other complex situations
69+
# correctly.
70+
build_ext = self.get_finalized_command("build_ext")
71+
build_py = self.get_finalized_command("build_py")
72+
73+
def get_package_dir(package: str) -> Path:
74+
if self.inplace:
75+
# If `inplace`, we have to ask `build_py` (like `build_ext` would).
76+
return Path(build_py.get_package_dir(package))
77+
# ... If not, `build_ext` knows where to put the package.
78+
return Path(build_ext.build_lib) / Path(*package.split("."))
79+
80+
for source, package in generated_files.items():
81+
dest = get_package_dir(package)
82+
dest.mkdir(mode=0o755, parents=True, exist_ok=True)
83+
for artifact_dir in build_artifact_dirs:
84+
source_full = artifact_dir / source
85+
dest_full = dest / source_full.name
86+
if source_full.is_file():
87+
# logger.info("Copying data file from %s to %s", source_full, dest_full)
88+
shutil.copy2(source_full, dest_full)
89+
elif source_full.is_dir():
90+
# logger.info("Copying data directory from %s to %s", source_full, dest_full)
91+
shutil.copytree(source_full, dest_full, dirs_exist_ok=True)
92+
# This tacitly makes "no match" a silent non-error.
93+
94+
95+
setuptools_rust.build._find_cargo_artifacts = find_cargo_artifacts_patch
96+
setuptools_rust.build.build_rust.install_extension = install_extension_patch
97+
98+
# Normal service resumes below here.
99+
# ==================================================================================================
100+
101+
20102
# Most of this configuration is managed by `pyproject.toml`. This only includes the extra bits to
21103
# configure `setuptools-rust`, because we do a little dynamic trick with the debug setting, and we
22104
# also want an explicit `setup.py` file to exist so we can manually call
@@ -64,7 +146,6 @@
64146
binding=Binding.PyO3,
65147
debug=rust_debug,
66148
features=features,
67-
data_files={"include": "qiskit.capi"},
68149
)
69150
],
70151
options={"bdist_wheel": {"py_limited_api": "cp310"}},

0 commit comments

Comments
 (0)