diff --git a/CHANGELOG.md b/CHANGELOG.md index 022a7d30..ae6b7ffc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,8 +2,11 @@ ## Unreleased ### Packaging -- Include `py.typed` when packaging to denote that setuptools-rust includes type hints. [#338](https://github.com/PyO3/setuptools-rust/pull/338) - Remove direct imports from `distutils`. [#336](https://github.com/PyO3/setuptools-rust/pull/336) +- Include `py.typed` when packaging to denote that setuptools-rust includes type hints. [#338](https://github.com/PyO3/setuptools-rust/pull/338) + +### Added +- Add support for `pyproject.toml` configuration using `[tool.setuptools-rust]` options. [#348](https://github.com/PyO3/setuptools-rust/pull/348) ## 1.6.0 (2023-04-27) ### Changed diff --git a/examples/hello-world-pyprojecttoml/Cargo.lock b/examples/hello-world-pyprojecttoml/Cargo.lock new file mode 100644 index 00000000..d18dc500 --- /dev/null +++ b/examples/hello-world-pyprojecttoml/Cargo.lock @@ -0,0 +1,273 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "hello-world" +version = "0.1.0" +dependencies = [ + "pyo3", +] + +[[package]] +name = "indoc" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" + +[[package]] +name = "libc" +version = "0.2.147" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" + +[[package]] +name = "lock_api" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c1cc9717a20b1bb222f333e6a92fd32f7d8a18ddc5a3191a11af45dcbf4dcd16" +dependencies = [ + "autocfg", + "scopeguard", +] + +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + +[[package]] +name = "once_cell" +version = "1.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dd8b5dd2ae5ed71462c540258bedcb51965123ad7e7ccf4b9a8cafaa4a63576d" + +[[package]] +name = "parking_lot" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" +dependencies = [ + "lock_api", + "parking_lot_core", +] + +[[package]] +name = "parking_lot_core" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93f00c865fe7cabf650081affecd3871070f26767e7b2070a3ffae14c654b447" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall", + "smallvec", + "windows-targets", +] + +[[package]] +name = "proc-macro2" +version = "1.0.66" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "pyo3" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e681a6cfdc4adcc93b4d3cf993749a4552018ee0a9b65fc0ccfad74352c72a38" +dependencies = [ + "cfg-if", + "indoc", + "libc", + "memoffset", + "parking_lot", + "pyo3-build-config", + "pyo3-ffi", + "pyo3-macros", + "unindent", +] + +[[package]] +name = "pyo3-build-config" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "076c73d0bc438f7a4ef6fdd0c3bb4732149136abd952b110ac93e4edb13a6ba5" +dependencies = [ + "once_cell", + "target-lexicon", +] + +[[package]] +name = "pyo3-ffi" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e53cee42e77ebe256066ba8aa77eff722b3bb91f3419177cf4cd0f304d3284d9" +dependencies = [ + "libc", + "pyo3-build-config", +] + +[[package]] +name = "pyo3-macros" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dfeb4c99597e136528c6dd7d5e3de5434d1ceaf487436a3f03b2d56b6fc9efd1" +dependencies = [ + "proc-macro2", + "pyo3-macros-backend", + "quote", + "syn", +] + +[[package]] +name = "pyo3-macros-backend" +version = "0.19.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "947dc12175c254889edc0c02e399476c2f652b4b9ebd123aa655c224de259536" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + +[[package]] +name = "quote" +version = "1.0.32" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f3b39ccfb720540debaa0164757101c08ecb8d326b15358ce76a62c7e85965" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "redox_syscall" +version = "0.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "567664f262709473930a4bf9e51bf2ebf3348f2e748ccc50dea20646858f8f29" +dependencies = [ + "bitflags", +] + +[[package]] +name = "scopeguard" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" + +[[package]] +name = "smallvec" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62bb4feee49fdd9f707ef802e22365a35de4b7b299de4763d44bfea899442ff9" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "target-lexicon" +version = "0.12.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d0e916b1148c8e263850e1ebcbd046f333e0683c724876bb0da63ea4373dc8a" + +[[package]] +name = "unicode-ident" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" + +[[package]] +name = "unindent" +version = "0.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1766d682d402817b5ac4490b3c3002d91dfa0d22812f341609f97b08757359c" + +[[package]] +name = "windows-targets" +version = "0.48.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05d4b17490f70499f20b9e791dcf6a299785ce8af4d709018206dc5b4953e95f" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a" diff --git a/examples/hello-world-pyprojecttoml/Cargo.toml b/examples/hello-world-pyprojecttoml/Cargo.toml new file mode 100644 index 00000000..10d1242f --- /dev/null +++ b/examples/hello-world-pyprojecttoml/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "hello-world" +version = "0.1.0" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +pyo3 = { version = "0.19.2", features = ["extension-module"] } + +[profile.release-lto] +inherits = "release" +lto = true + +[lib] +# See https://github.com/PyO3/pyo3 for details +name = "_lib" # private module to be nested into Python package +crate-type = ["cdylib"] +path = "rust/lib.rs" + +[[bin]] +name = "print-hello" +path = "rust/print_hello.rs" diff --git a/examples/hello-world-pyprojecttoml/MANIFEST.in b/examples/hello-world-pyprojecttoml/MANIFEST.in new file mode 100644 index 00000000..84f0cfbb --- /dev/null +++ b/examples/hello-world-pyprojecttoml/MANIFEST.in @@ -0,0 +1,6 @@ +graft python +graft rust +graft tests +include Cargo.toml noxfile.py +global-exclude */__pycache__/* +global-exclude *.pyc diff --git a/examples/hello-world-pyprojecttoml/noxfile.py b/examples/hello-world-pyprojecttoml/noxfile.py new file mode 100644 index 00000000..7aa13342 --- /dev/null +++ b/examples/hello-world-pyprojecttoml/noxfile.py @@ -0,0 +1,20 @@ +from os.path import dirname + +import nox + +SETUPTOOLS_RUST = dirname(dirname(dirname(__file__))) + + +@nox.session() +def test(session: nox.Session): + session.install(SETUPTOOLS_RUST, "wheel", "build", "pytest") + # Ensure build works as intended + session.install("--no-build-isolation", ".") + # Test Rust binary + session.run("print-hello") + # Test script wrapper for Python entry-point + session.run("sum-cli", "5", "7") + session.run("rust-demo", "5", "7") + # Test library + session.run("pytest", "tests", *session.posargs) + session.run("python", "-c", "from hello_world import _lib; print(_lib)") diff --git a/examples/hello-world-pyprojecttoml/pyproject.toml b/examples/hello-world-pyprojecttoml/pyproject.toml new file mode 100644 index 00000000..84163491 --- /dev/null +++ b/examples/hello-world-pyprojecttoml/pyproject.toml @@ -0,0 +1,31 @@ +[build-system] +requires = ["setuptools", "setuptools-rust"] +build-backend = "setuptools.build_meta" + +[project] +name = "hello-world" +version = "1.0" + +[project.scripts] +# Python entry-point wrapper to be installed in `$venv/bin` +sum-cli = "hello_world.sum_cli:main" # Python function that uses Rust +rust-demo = "hello_world._lib:demo" # Rust function that uses Python + +[tool.setuptools.packages] +# Pure Python packages/modules +find = { where = ["python"] } + +[[tool.setuptools-rust.ext-modules]] +# Private Rust extension module to be nested into Python package +target = "hello_world._lib" # The last part of the name (e.g. "_lib") has to match lib.name in Cargo.toml, + # but you can add a prefix to nest it inside of a Python package. +py-limited-api = "auto" # Default value, can be omitted +binding = "PyO3" # Default value, can be omitted +# See reference for RustExtension in https://setuptools-rust.readthedocs.io/en/latest/reference.html + +[[tool.setuptools-rust.bins]] +# Rust executable to be installed in `$venv/bin` +target = "print-hello" # Needs to match bin.name in Cargo.toml +args = ["--profile", "release-lto"] # Extra args for Cargo +# See reference for RustBin in https://setuptools-rust.readthedocs.io/en/latest/reference.html +# Note that you can also use Python entry-points as alternative to Rust binaries diff --git a/examples/hello-world-pyprojecttoml/python/hello_world/__init__.py b/examples/hello-world-pyprojecttoml/python/hello_world/__init__.py new file mode 100644 index 00000000..30f12921 --- /dev/null +++ b/examples/hello-world-pyprojecttoml/python/hello_world/__init__.py @@ -0,0 +1,3 @@ +from ._lib import sum_as_string # export public parts of the binary extension + +__all__ = ["sum_as_string"] diff --git a/examples/hello-world-pyprojecttoml/python/hello_world/sum_cli.py b/examples/hello-world-pyprojecttoml/python/hello_world/sum_cli.py new file mode 100644 index 00000000..feaf7eb3 --- /dev/null +++ b/examples/hello-world-pyprojecttoml/python/hello_world/sum_cli.py @@ -0,0 +1,16 @@ +import argparse +import sys + +from ._lib import sum_as_string + + +def main(): + parser = argparse.ArgumentParser("sum 2 integers") + parser.add_argument("x", type=int) + parser.add_argument("y", type=int) + args = parser.parse_args() + print(f"{args.x} + {args.y} = {sum_as_string(args.x, args.y)}") + + +if __name__ == "__main__": + main() diff --git a/examples/hello-world-pyprojecttoml/rust/lib.rs b/examples/hello-world-pyprojecttoml/rust/lib.rs new file mode 100644 index 00000000..071c67bc --- /dev/null +++ b/examples/hello-world-pyprojecttoml/rust/lib.rs @@ -0,0 +1,33 @@ +use pyo3::prelude::*; +use std::env; + +/// Formats the sum of two numbers as string. +#[pyfunction] +fn sum_as_string(a: usize, b: usize) -> PyResult { + Ok((a + b).to_string()) +} + +/// Calls Python (see https://pyo3.rs for details) +#[pyfunction] +fn demo(py: Python) -> PyResult<()> { + let argv = env::args().collect::>(); + println!("argv = {:?}", argv); + // argv[0]: Python path, argv[1]: program name, argv[2..]: given args + + let numbers: Vec = argv[2..].iter().map(|s| s.parse().unwrap()).collect(); + + let python_sum = PyModule::import(py, "builtins")?.getattr("sum")?; + let total: i32 = python_sum.call1((numbers,))?.extract()?; + println!("sum({}) = {:?}", argv[2..].join(", "), total); + Ok(()) +} + +/// A Python module implemented in Rust. The name of this function must match +/// the `lib.name` setting in the `Cargo.toml`, else Python will not be able to +/// import the module. +#[pymodule] +fn _lib(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + m.add_function(wrap_pyfunction!(sum_as_string, m)?)?; + m.add_function(wrap_pyfunction!(demo, m)?)?; + Ok(()) +} diff --git a/examples/hello-world-pyprojecttoml/rust/print_hello.rs b/examples/hello-world-pyprojecttoml/rust/print_hello.rs new file mode 100644 index 00000000..e7a11a96 --- /dev/null +++ b/examples/hello-world-pyprojecttoml/rust/print_hello.rs @@ -0,0 +1,3 @@ +fn main() { + println!("Hello, world!"); +} diff --git a/examples/hello-world-pyprojecttoml/tests/test_hello_world.py b/examples/hello-world-pyprojecttoml/tests/test_hello_world.py new file mode 100644 index 00000000..9b7fe402 --- /dev/null +++ b/examples/hello-world-pyprojecttoml/tests/test_hello_world.py @@ -0,0 +1,5 @@ +import hello_world + + +def test_sum_as_string(): + assert hello_world.sum_as_string(5, 20) == "25" diff --git a/pyproject.toml b/pyproject.toml index 2c428874..8a903610 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -28,6 +28,7 @@ dependencies = [ "setuptools>=62.4", "semantic_version>=2.8.2,<3", "typing_extensions>=3.7.4.3", + 'tomli>=1.2.1; python_version<"3.11"' ] [project.entry-points."distutils.commands"] @@ -37,6 +38,9 @@ build_rust = "setuptools_rust:build_rust" [project.entry-points."distutils.setup_keywords"] rust_extensions = "setuptools_rust.setuptools_ext:rust_extensions" +[project.entry-points."setuptools.finalize_distribution_options"] +setuptools_rust = "setuptools_rust.setuptools_ext:pyprojecttoml_config" + [project.urls] repository = "https://github.com/PyO3/setuptools-rust" documentation = "https://setuptools-rust.readthedocs.io" diff --git a/setuptools_rust/setuptools_ext.py b/setuptools_rust/setuptools_ext.py index a024ba6c..0a5d16f8 100644 --- a/setuptools_rust/setuptools_ext.py +++ b/setuptools_rust/setuptools_ext.py @@ -1,9 +1,11 @@ import os import subprocess +import sys import sysconfig import logging -from typing import List, Set, Tuple, Type, cast +from typing import List, Set, Tuple, Type, TypeVar, cast +from functools import partial from setuptools.command.build_ext import build_ext @@ -14,15 +16,23 @@ from setuptools.dist import Distribution from typing_extensions import Literal -from .extension import RustBin, RustExtension +from .extension import Binding, RustBin, RustExtension, Strip try: from wheel.bdist_wheel import bdist_wheel except ImportError: bdist_wheel = None +if sys.version_info[:2] >= (3, 11): + from tomllib import load as toml_load +else: + from tomli import load as toml_load + + logger = logging.getLogger(__name__) +T = TypeVar("T", bound=RustExtension) + def add_rust_extension(dist: Distribution) -> None: sdist_base_class = cast(Type[sdist], dist.cmdclass.get("sdist", sdist)) @@ -287,6 +297,33 @@ def rust_extensions( add_rust_extension(dist) +def pyprojecttoml_config(dist: Distribution) -> None: + try: + with open("pyproject.toml", "rb") as f: + cfg = toml_load(f).get("tool", {}).get("setuptools-rust") + except FileNotFoundError: + return None + + if cfg: + modules = map(partial(_create, RustExtension), cfg.get("ext-modules", [])) + binaries = map(partial(_create, RustBin), cfg.get("bins", [])) + dist.rust_extensions = [*modules, *binaries] # type: ignore[attr-defined] + rust_extensions(dist, "rust_extensions", dist.rust_extensions) # type: ignore[attr-defined] + + +def _create(constructor: Type[T], config: dict) -> T: + kwargs = { + # PEP 517/621 convention: pyproject.toml uses dashes + k.replace("-", "_"): v + for k, v in config.items() + } + if "binding" in config: + kwargs["binding"] = Binding[config["binding"]] + if "strip" in config: + kwargs["strip"] = Strip[config["strip"]] + return constructor(**kwargs) + + _CARGO_VENDOR_CONFIG = b""" [source.crates-io] replace-with = "vendored-sources"