Skip to content

Commit 14732c2

Browse files
committed
build: use cargo messages to locate built cdylib
1 parent e8d4149 commit 14732c2

File tree

6 files changed

+187
-111
lines changed

6 files changed

+187
-111
lines changed

CHANGELOG.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,15 @@
11
# Changelog
22

3-
## 1.4.1 (2022-07-05)
3+
## Unreleased
4+
### Changed
5+
- Locate cdylib artifacts by handling messages from cargo instead of searching target dir (fixes build on MSYS2). [#267](https://github.com/PyO3/setuptools-rust/pull/267)
6+
47

8+
## 1.4.1 (2022-07-05)
59
### Fixed
610
- Fix crash when checking Rust version. [#263](https://github.com/PyO3/setuptools-rust/pull/263)
711

812
## 1.4.0 (2022-07-05)
9-
1013
### Packaging
1114
- Increase minimum `setuptools` version to 62.4. [#246](https://github.com/PyO3/setuptools-rust/pull/246)
1215

@@ -25,7 +28,6 @@
2528
- If the sysconfig for `BLDSHARED` has no flags, `setuptools-rust` won't crash anymore. [#241](https://github.com/PyO3/setuptools-rust/pull/241)
2629

2730
## 1.3.0 (2022-04-26)
28-
2931
### Packaging
3032
- Increase minimum `setuptools` version to 58. [#222](https://github.com/PyO3/setuptools-rust/pull/222)
3133

examples/namespace_package/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ version = "0.1.0"
44
edition = "2018"
55

66
[lib]
7-
crate-type = ["cdylib"]
7+
crate-type = ["cdylib", "rlib"]
88

99
[dependencies]
1010
pyo3 = { version = "0.16.5", features = ["extension-module"] }

setuptools_rust/_utils.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
import subprocess
22

33

4-
def format_called_process_error(e: subprocess.CalledProcessError) -> str:
4+
def format_called_process_error(
5+
e: subprocess.CalledProcessError,
6+
*,
7+
include_stdout: bool = True,
8+
include_stderr: bool = True,
9+
) -> str:
510
"""Helper to convert a CalledProcessError to an error message.
611
12+
If `include_stdout` or `include_stderr` are True (the default), the
13+
respective output stream will be added to the error message (if
14+
present in the exception).
715
816
>>> format_called_process_error(subprocess.CalledProcessError(
917
... 777, ['ls', '-la'], None, None
@@ -14,18 +22,22 @@ def format_called_process_error(e: subprocess.CalledProcessError) -> str:
1422
... ))
1523
"`cargo 'foo bar'` failed with code 1\\n-- Output captured from stdout:\\nmessage"
1624
>>> format_called_process_error(subprocess.CalledProcessError(
25+
... 1, ['cargo', 'foo bar'], 'message', None
26+
... ), include_stdout=False)
27+
"`cargo 'foo bar'` failed with code 1"
28+
>>> format_called_process_error(subprocess.CalledProcessError(
1729
... -1, ['cargo'], 'stdout', 'stderr'
1830
... ))
1931
'`cargo` failed with code -1\\n-- Output captured from stdout:\\nstdout\\n-- Output captured from stderr:\\nstderr'
2032
"""
2133
command = " ".join(_quote_whitespace(arg) for arg in e.cmd)
2234
message = f"`{command}` failed with code {e.returncode}"
23-
if e.stdout is not None:
35+
if include_stdout and e.stdout is not None:
2436
message += f"""
2537
-- Output captured from stdout:
2638
{e.stdout}"""
2739

28-
if e.stderr is not None:
40+
if include_stderr and e.stderr is not None:
2941
message += f"""
3042
-- Output captured from stderr:
3143
{e.stderr}"""

setuptools_rust/build.py

Lines changed: 125 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from __future__ import annotations
22

33
import glob
4+
import json
45
import os
56
import platform
67
import shutil
@@ -15,7 +16,8 @@
1516
DistutilsPlatformError,
1617
)
1718
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
1921

2022
from setuptools.command.build import build as CommandBuild # type: ignore[import]
2123
from setuptools.command.build_ext import build_ext as CommandBuildExt
@@ -139,11 +141,6 @@ def build_extension(
139141
quiet = self.qbuild or ext.quiet
140142
debug = self._is_debug_build(ext)
141143

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-
147144
cargo_args = self._cargo_args(
148145
ext=ext, target_triple=target_triple, release=not debug, quiet=quiet
149146
)
@@ -154,7 +151,14 @@ def build_extension(
154151
rustflags.extend(["-C", "linker=" + linker])
155152

156153
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+
]
158162

159163
else:
160164
rustc_args = [
@@ -184,6 +188,7 @@ def build_extension(
184188
self.cargo,
185189
"rustc",
186190
"--lib",
191+
"--message-format=json-render-diagnostics",
187192
"--manifest-path",
188193
ext.path,
189194
*cargo_args,
@@ -209,13 +214,17 @@ def build_extension(
209214
try:
210215
# If quiet, capture all output and only show it in the exception
211216
# If not quiet, forward all cargo output to stderr
212-
stdout = subprocess.PIPE if quiet else sys.stderr.fileno()
213217
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,
216223
)
217224
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))
219228

220229
except OSError:
221230
raise DistutilsExecError(
@@ -226,60 +235,64 @@ def build_extension(
226235
# Find the shared library that cargo hopefully produced and copy
227236
# it into the build directory as if it were produced by build_ext.
228237

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)
241238
dylib_paths = []
239+
package_id = ext.metadata(quiet=quiet)["resolve"]["root"]
242240

243241
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+
)
244248
for name, dest in ext.target.items():
245249
if not name:
246250
name = dest.split(".")[-1]
247-
exe = sysconfig.get_config_var("EXE")
248-
if exe is not None:
249-
name += exe
250251

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:
255259
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}'"
258261
)
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)
271262

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
277266
)
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",
278276
)
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:
280283
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
282292
)
293+
294+
# guaranteed to be just one element after checks above
295+
dylib_paths.append(_BuiltModule(ext.name, artifact_path))
283296
return dylib_paths
284297

285298
def install_extension(
@@ -668,23 +681,9 @@ def _prepare_build_environment(cross_lib: Optional[str]) -> Dict[str, str]:
668681
if cross_lib:
669682
env.setdefault("PYO3_CROSS_LIB_DIR", cross_lib)
670683

671-
env.pop("CARGO", None)
672684
return env
673685

674686

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-
688687
def _is_py_limited_api(
689688
ext_setting: Literal["auto", True, False],
690689
wheel_setting: Optional[_PyLimitedApi],
@@ -771,3 +770,64 @@ def _split_platform_and_extension(ext_path: str) -> Tuple[str, str, str]:
771770
# rust.cpython-38-x86_64-linux-gnu to (rust, .cpython-38-x86_64-linux-gnu)
772771
ext_path, platform_tag = os.path.splitext(ext_path)
773772
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

Comments
 (0)