1
1
from __future__ import annotations
2
2
3
3
import glob
4
+ import json
4
5
import os
5
6
import platform
6
7
import shutil
15
16
DistutilsPlatformError ,
16
17
)
17
18
from distutils .sysconfig import get_config_var
18
- from typing import Dict , List , NamedTuple , Optional , Set , Tuple , cast
19
+ from pathlib import Path
20
+ from typing import Dict , Iterable , List , NamedTuple , Optional , Set , Tuple , cast
19
21
20
22
from setuptools .command .build import build as CommandBuild # type: ignore[import]
21
23
from setuptools .command .build_ext import build_ext as CommandBuildExt
@@ -139,11 +141,6 @@ def build_extension(
139
141
quiet = self .qbuild or ext .quiet
140
142
debug = self ._is_debug_build (ext )
141
143
142
- # Find where to put the temporary build files created by `cargo`
143
- target_dir = _base_cargo_target_dir (ext , quiet = quiet )
144
- if target_triple is not None :
145
- target_dir = os .path .join (target_dir , target_triple )
146
-
147
144
cargo_args = self ._cargo_args (
148
145
ext = ext , target_triple = target_triple , release = not debug , quiet = quiet
149
146
)
@@ -154,7 +151,14 @@ def build_extension(
154
151
rustflags .extend (["-C" , "linker=" + linker ])
155
152
156
153
if ext ._uses_exec_binding ():
157
- command = [self .cargo , "build" , "--manifest-path" , ext .path , * cargo_args ]
154
+ command = [
155
+ self .cargo ,
156
+ "build" ,
157
+ "--manifest-path" ,
158
+ ext .path ,
159
+ "--message-format=json-render-diagnostics" ,
160
+ * cargo_args ,
161
+ ]
158
162
159
163
else :
160
164
rustc_args = [
@@ -184,6 +188,7 @@ def build_extension(
184
188
self .cargo ,
185
189
"rustc" ,
186
190
"--lib" ,
191
+ "--message-format=json-render-diagnostics" ,
187
192
"--manifest-path" ,
188
193
ext .path ,
189
194
* cargo_args ,
@@ -209,13 +214,17 @@ def build_extension(
209
214
try :
210
215
# If quiet, capture all output and only show it in the exception
211
216
# If not quiet, forward all cargo output to stderr
212
- stdout = subprocess .PIPE if quiet else sys .stderr .fileno ()
213
217
stderr = subprocess .PIPE if quiet else None
214
- subprocess .run (
215
- command , env = env , stdout = stdout , stderr = stderr , text = True , check = True
218
+ cargo_messages = subprocess .check_output (
219
+ command ,
220
+ env = env ,
221
+ stderr = stderr ,
222
+ text = True ,
216
223
)
217
224
except subprocess .CalledProcessError as e :
218
- raise CompileError (format_called_process_error (e ))
225
+ # Don't include stdout in the formatted error as it is a huge dump
226
+ # of cargo json lines which aren't helpful for the end user.
227
+ raise CompileError (format_called_process_error (e , include_stdout = False ))
219
228
220
229
except OSError :
221
230
raise DistutilsExecError (
@@ -226,60 +235,64 @@ def build_extension(
226
235
# Find the shared library that cargo hopefully produced and copy
227
236
# it into the build directory as if it were produced by build_ext.
228
237
229
- profile = ext .get_cargo_profile ()
230
- if profile :
231
- # https://doc.rust-lang.org/cargo/reference/profiles.html
232
- if profile in {"dev" , "test" }:
233
- profile_dir = "debug"
234
- elif profile == "bench" :
235
- profile_dir = "release"
236
- else :
237
- profile_dir = profile
238
- else :
239
- profile_dir = "debug" if debug else "release"
240
- artifacts_dir = os .path .join (target_dir , profile_dir )
241
238
dylib_paths = []
239
+ package_id = ext .metadata (quiet = quiet )["resolve" ]["root" ]
242
240
243
241
if ext ._uses_exec_binding ():
242
+ # Find artifact from cargo messages
243
+ artifacts = _find_cargo_artifacts (
244
+ cargo_messages .splitlines (),
245
+ package_id = package_id ,
246
+ kind = "bin" ,
247
+ )
244
248
for name , dest in ext .target .items ():
245
249
if not name :
246
250
name = dest .split ("." )[- 1 ]
247
- exe = sysconfig .get_config_var ("EXE" )
248
- if exe is not None :
249
- name += exe
250
251
251
- path = os .path .join (artifacts_dir , name )
252
- if os .access (path , os .X_OK ):
253
- dylib_paths .append (_BuiltModule (dest , path ))
254
- else :
252
+ try :
253
+ artifact_path = next (
254
+ artifact
255
+ for artifact in artifacts
256
+ if Path (artifact ).with_suffix ("" ).name == name
257
+ )
258
+ except StopIteration :
255
259
raise DistutilsExecError (
256
- "Rust build failed; "
257
- f"unable to find executable '{ name } ' in '{ artifacts_dir } '"
260
+ f"Rust build failed; unable to locate executable '{ name } '"
258
261
)
259
- else :
260
- platform = sysconfig .get_platform ()
261
- if "win" in platform :
262
- dylib_ext = "dll"
263
- elif platform .startswith ("macosx" ):
264
- dylib_ext = "dylib"
265
- elif "wasm32" in platform :
266
- dylib_ext = "wasm"
267
- else :
268
- dylib_ext = "so"
269
-
270
- wildcard_so = "*{}.{}" .format (ext .get_lib_name (quiet = quiet ), dylib_ext )
271
262
272
- try :
273
- dylib_paths .append (
274
- _BuiltModule (
275
- ext .name ,
276
- next (glob .iglob (os .path .join (artifacts_dir , wildcard_so ))),
263
+ if os .environ .get ("CARGO" ) == "cross" :
264
+ artifact_path = _replace_cross_target_dir (
265
+ artifact_path , ext , quiet = quiet
277
266
)
267
+
268
+ dylib_paths .append (_BuiltModule (dest , artifact_path ))
269
+ else :
270
+ # Find artifact from cargo messages
271
+ artifacts = tuple (
272
+ _find_cargo_artifacts (
273
+ cargo_messages .splitlines (),
274
+ package_id = package_id ,
275
+ kind = "cdylib" ,
278
276
)
279
- except StopIteration :
277
+ )
278
+ if len (artifacts ) == 0 :
279
+ raise DistutilsExecError (
280
+ "Rust build failed; unable to find any build artifacts"
281
+ )
282
+ elif len (artifacts ) > 1 :
280
283
raise DistutilsExecError (
281
- f"Rust build failed; unable to find any { wildcard_so } in { artifacts_dir } "
284
+ f"Rust build failed; expected only one build artifact but found { artifacts } "
285
+ )
286
+
287
+ artifact_path = artifacts [0 ]
288
+
289
+ if os .environ .get ("CARGO" ) == "cross" :
290
+ artifact_path = _replace_cross_target_dir (
291
+ artifact_path , ext , quiet = quiet
282
292
)
293
+
294
+ # guaranteed to be just one element after checks above
295
+ dylib_paths .append (_BuiltModule (ext .name , artifact_path ))
283
296
return dylib_paths
284
297
285
298
def install_extension (
@@ -668,23 +681,9 @@ def _prepare_build_environment(cross_lib: Optional[str]) -> Dict[str, str]:
668
681
if cross_lib :
669
682
env .setdefault ("PYO3_CROSS_LIB_DIR" , cross_lib )
670
683
671
- env .pop ("CARGO" , None )
672
684
return env
673
685
674
686
675
- def _base_cargo_target_dir (ext : RustExtension , * , quiet : bool ) -> str :
676
- """Returns the root target directory cargo will use.
677
-
678
- If --target is passed to cargo in the command line, the target directory
679
- will have the target appended as a child.
680
- """
681
- target_directory = ext ._metadata (quiet = quiet )["target_directory" ]
682
- assert isinstance (
683
- target_directory , str
684
- ), "expected cargo metadata to contain a string target directory"
685
- return target_directory
686
-
687
-
688
687
def _is_py_limited_api (
689
688
ext_setting : Literal ["auto" , True , False ],
690
689
wheel_setting : Optional [_PyLimitedApi ],
@@ -771,3 +770,64 @@ def _split_platform_and_extension(ext_path: str) -> Tuple[str, str, str]:
771
770
# rust.cpython-38-x86_64-linux-gnu to (rust, .cpython-38-x86_64-linux-gnu)
772
771
ext_path , platform_tag = os .path .splitext (ext_path )
773
772
return (ext_path , platform_tag , extension )
773
+
774
+
775
+ def _find_cargo_artifacts (
776
+ cargo_messages : List [str ],
777
+ * ,
778
+ package_id : str ,
779
+ kind : str ,
780
+ ) -> Iterable [str ]:
781
+ """Identifies cargo artifacts built for the given `package_id` from the
782
+ provided cargo_messages.
783
+
784
+ >>> list(_find_cargo_artifacts(
785
+ ... [
786
+ ... '{"some_irrelevant_message": []}',
787
+ ... '{"reason":"compiler-artifact","package_id":"some_id","target":{"kind":["cdylib"]},"filenames":["/some/path/baz.so"]}',
788
+ ... '{"reason":"compiler-artifact","package_id":"some_id","target":{"kind":["cdylib", "rlib"]},"filenames":["/file/two/baz.dylib", "/file/two/baz.rlib"]}',
789
+ ... '{"reason":"compiler-artifact","package_id":"some_other_id","target":{"kind":["cdylib"]},"filenames":["/not/this.so"]}',
790
+ ... ],
791
+ ... package_id="some_id",
792
+ ... kind="cdylib",
793
+ ... ))
794
+ ['/some/path/baz.so', '/file/two/baz.dylib']
795
+ >>> list(_find_cargo_artifacts(
796
+ ... [
797
+ ... '{"some_irrelevant_message": []}',
798
+ ... '{"reason":"compiler-artifact","package_id":"some_id","target":{"kind":["cdylib"]},"filenames":["/some/path/baz.so"]}',
799
+ ... '{"reason":"compiler-artifact","package_id":"some_id","target":{"kind":["cdylib", "rlib"]},"filenames":["/file/two/baz.dylib", "/file/two/baz.rlib"]}',
800
+ ... '{"reason":"compiler-artifact","package_id":"some_other_id","target":{"kind":["cdylib"]},"filenames":["/not/this.so"]}',
801
+ ... ],
802
+ ... package_id="some_id",
803
+ ... kind="rlib",
804
+ ... ))
805
+ ['/file/two/baz.rlib']
806
+ """
807
+ for message in cargo_messages :
808
+ # only bother parsing messages that look like a match
809
+ if "compiler-artifact" in message and package_id in message and kind in message :
810
+ parsed = json .loads (message )
811
+ # verify the message is correct
812
+ if (
813
+ parsed .get ("reason" ) == "compiler-artifact"
814
+ and parsed .get ("package_id" ) == package_id
815
+ ):
816
+ for artifact_kind , filename in zip (
817
+ parsed ["target" ]["kind" ], parsed ["filenames" ]
818
+ ):
819
+ if artifact_kind == kind :
820
+ yield filename
821
+
822
+
823
+ def _replace_cross_target_dir (path : str , ext : RustExtension , * , quiet : bool ) -> str :
824
+ """Replaces target director from `cross` docker build with the correct
825
+ local path.
826
+
827
+ Cross artifact messages and metadata contain paths from inside the
828
+ dockerfile; invoking `cargo metadata` we can work out the correct local
829
+ target directory.
830
+ """
831
+ cross_target_dir = ext ._metadata (cargo = "cross" , quiet = quiet )["target_directory" ]
832
+ local_target_dir = ext ._metadata (cargo = "cargo" , quiet = quiet )["target_directory" ]
833
+ return path .replace (cross_target_dir , local_target_dir )
0 commit comments