From fe1ba707df399588330dd78a4e3f0e47d499dafc Mon Sep 17 00:00:00 2001 From: Thomas Pellissier-Tanon Date: Mon, 30 Sep 2024 11:38:26 +0200 Subject: [PATCH] Draft: allows to use Maturin with UV --- .changelog/_unreleased.toml | 5 ++ ...turin-project.pyi => rust_pdm_project.pyi} | 0 ...in-project.pyi => rust_poetry_project.pyi} | 0 examples/rust-uv-project-consumer/.kraken.py | 12 ++++ .../rust-uv-project-consumer/pyproject.toml | 11 +++ .../src/rust_uv_project_consumer/__init__.py | 3 + examples/rust-uv-project/.kraken.py | 13 ++++ examples/rust-uv-project/Cargo.toml | 11 +++ examples/rust-uv-project/pyproject.toml | 10 +++ examples/rust-uv-project/rust_uv_project.pyi | 1 + examples/rust-uv-project/src/__init__.py | 0 examples/rust-uv-project/src/lib.rs | 10 +++ .../kraken/std/python/buildsystem/__init__.py | 8 ++- .../kraken/std/python/buildsystem/maturin.py | 69 +++++++++++++++---- .../integration/python/test_python.py | 10 ++- 15 files changed, 148 insertions(+), 15 deletions(-) create mode 100644 .changelog/_unreleased.toml rename examples/rust-pdm-project/{maturin-project.pyi => rust_pdm_project.pyi} (100%) rename examples/rust-poetry-project/{maturin-project.pyi => rust_poetry_project.pyi} (100%) create mode 100644 examples/rust-uv-project-consumer/.kraken.py create mode 100644 examples/rust-uv-project-consumer/pyproject.toml create mode 100644 examples/rust-uv-project-consumer/src/rust_uv_project_consumer/__init__.py create mode 100644 examples/rust-uv-project/.kraken.py create mode 100644 examples/rust-uv-project/Cargo.toml create mode 100644 examples/rust-uv-project/pyproject.toml create mode 100644 examples/rust-uv-project/rust_uv_project.pyi create mode 100644 examples/rust-uv-project/src/__init__.py create mode 100644 examples/rust-uv-project/src/lib.rs diff --git a/.changelog/_unreleased.toml b/.changelog/_unreleased.toml new file mode 100644 index 00000000..1f49e53e --- /dev/null +++ b/.changelog/_unreleased.toml @@ -0,0 +1,5 @@ +[[entries]] +id = "cbae7d73-09aa-442f-b099-27a4ae23b3c3" +type = "improvement" +description = "Allows to use uv with maturin" +author = "thomas.pellissier-tanon@helsing.ai" diff --git a/examples/rust-pdm-project/maturin-project.pyi b/examples/rust-pdm-project/rust_pdm_project.pyi similarity index 100% rename from examples/rust-pdm-project/maturin-project.pyi rename to examples/rust-pdm-project/rust_pdm_project.pyi diff --git a/examples/rust-poetry-project/maturin-project.pyi b/examples/rust-poetry-project/rust_poetry_project.pyi similarity index 100% rename from examples/rust-poetry-project/maturin-project.pyi rename to examples/rust-poetry-project/rust_poetry_project.pyi diff --git a/examples/rust-uv-project-consumer/.kraken.py b/examples/rust-uv-project-consumer/.kraken.py new file mode 100644 index 00000000..c6426d0a --- /dev/null +++ b/examples/rust-uv-project-consumer/.kraken.py @@ -0,0 +1,12 @@ +import os + +from kraken.std import python + +python.python_settings(always_use_managed_env=True).add_package_index( + alias="local", + index_url=os.environ["LOCAL_PACKAGE_INDEX"], + credentials=(os.environ["LOCAL_USER"], os.environ["LOCAL_PASSWORD"]), +) +python.install() +python.mypy(version_spec="==1.10.0") +python.update_pyproject_task() diff --git a/examples/rust-uv-project-consumer/pyproject.toml b/examples/rust-uv-project-consumer/pyproject.toml new file mode 100644 index 00000000..29c6509e --- /dev/null +++ b/examples/rust-uv-project-consumer/pyproject.toml @@ -0,0 +1,11 @@ +[project] +name = "rust-uv-project-consumer" +version = "0.1.0" +dependencies = [ + "rust-uv-project" +] +requires-python = "~=3.7" + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" diff --git a/examples/rust-uv-project-consumer/src/rust_uv_project_consumer/__init__.py b/examples/rust-uv-project-consumer/src/rust_uv_project_consumer/__init__.py new file mode 100644 index 00000000..4018b089 --- /dev/null +++ b/examples/rust-uv-project-consumer/src/rust_uv_project_consumer/__init__.py @@ -0,0 +1,3 @@ +from rust_uv_project import sum_as_string + +sum_as_string(1, 2) diff --git a/examples/rust-uv-project/.kraken.py b/examples/rust-uv-project/.kraken.py new file mode 100644 index 00000000..83ac3731 --- /dev/null +++ b/examples/rust-uv-project/.kraken.py @@ -0,0 +1,13 @@ +import os + +from kraken.std import python + +python.python_settings(always_use_managed_env=True).add_package_index( + alias="local", + index_url=os.environ["LOCAL_PACKAGE_INDEX"], + credentials=(os.environ["LOCAL_USER"], os.environ["LOCAL_PASSWORD"]), +) +python.install() +mypy_task = python.mypy(version_spec="==1.10.0") +python.mypy_stubtest(package="rust_uv_project", ignore_missing_stubs=True, mypy_pex_bin=mypy_task.mypy_pex_bin) +python.publish(package_index="local", distributions=python.build(as_version="0.1.0").output_files) diff --git a/examples/rust-uv-project/Cargo.toml b/examples/rust-uv-project/Cargo.toml new file mode 100644 index 00000000..5dd607ac --- /dev/null +++ b/examples/rust-uv-project/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "rust-uv-project" +version = "0.1.0" +edition = "2021" + +[lib] +name = "rust_uv_project" +crate-type = ["cdylib"] + +[dependencies] +pyo3 = { version = "0.19", features = ["extension-module"] } diff --git a/examples/rust-uv-project/pyproject.toml b/examples/rust-uv-project/pyproject.toml new file mode 100644 index 00000000..9927c3f7 --- /dev/null +++ b/examples/rust-uv-project/pyproject.toml @@ -0,0 +1,10 @@ +[project] +name = "rust-uv-project" +version = "0.1.0" +description = "" +authors = [] +requires-python = ">=3.8" + +[build-system] +requires = ["maturin~=1.0"] +build-backend = "maturin" diff --git a/examples/rust-uv-project/rust_uv_project.pyi b/examples/rust-uv-project/rust_uv_project.pyi new file mode 100644 index 00000000..5b51ba05 --- /dev/null +++ b/examples/rust-uv-project/rust_uv_project.pyi @@ -0,0 +1 @@ +def sum_as_string(a: int, b: int) -> str: ... diff --git a/examples/rust-uv-project/src/__init__.py b/examples/rust-uv-project/src/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/examples/rust-uv-project/src/lib.rs b/examples/rust-uv-project/src/lib.rs new file mode 100644 index 00000000..d144f499 --- /dev/null +++ b/examples/rust-uv-project/src/lib.rs @@ -0,0 +1,10 @@ +use pyo3::prelude::*; + +#[pymodule] +fn rust_uv_project(_py: Python<'_>, m: &PyModule) -> PyResult<()> { + #[pyfn(m)] + fn sum_as_string(a: usize, b: usize) -> String { + (a + b).to_string() + } + Ok(()) +} diff --git a/kraken-build/src/kraken/std/python/buildsystem/__init__.py b/kraken-build/src/kraken/std/python/buildsystem/__init__.py index 9307fcd5..97b0ae37 100644 --- a/kraken-build/src/kraken/std/python/buildsystem/__init__.py +++ b/kraken-build/src/kraken/std/python/buildsystem/__init__.py @@ -174,14 +174,18 @@ def detect_build_system(project_directory: Path) -> PythonBuildSystem | None: return PoetryPythonBuildSystem(project_directory) if "maturin" in pyproject_content: - if "[tool.poetry]" in pyproject_content: + if "[tool.poetry" in pyproject_content: from .maturin import MaturinPoetryPythonBuildSystem return MaturinPoetryPythonBuildSystem(project_directory) - else: + elif "[tool.pdm" in pyproject_content: from .maturin import MaturinPdmPythonBuildSystem return MaturinPdmPythonBuildSystem(project_directory) + else: + from .maturin import MaturinUvPythonBuildSystem + + return MaturinUvPythonBuildSystem(project_directory) if "pdm" in pyproject_content: from .pdm import PDMPythonBuildSystem diff --git a/kraken-build/src/kraken/std/python/buildsystem/maturin.py b/kraken-build/src/kraken/std/python/buildsystem/maturin.py index 1e5e4391..8b5b3eb1 100644 --- a/kraken-build/src/kraken/std/python/buildsystem/maturin.py +++ b/kraken-build/src/kraken/std/python/buildsystem/maturin.py @@ -8,6 +8,7 @@ import subprocess as sp from collections.abc import Callable, Collection from dataclasses import dataclass +from itertools import chain from pathlib import Path from kraken.common.path import is_relative_to @@ -18,6 +19,7 @@ from ..settings import PythonSettings from . import ManagedEnvironment from .pdm import PDMManagedEnvironment, PDMPythonBuildSystem +from .uv import UvPythonBuildSystem from .poetry import PoetryManagedEnvironment, PoetryPyprojectHandler, PoetryPythonBuildSystem logger = logging.getLogger(__name__) @@ -55,7 +57,10 @@ class MaturinZigTarget: class _MaturinBuilder: def __init__( - self, entry_point: str, get_pyproject_reader: Callable[[TomlFile], PyprojectHandler], project_directory: Path + self, + entry_point: Collection[str], + get_pyproject_reader: Callable[[TomlFile], PyprojectHandler], + project_directory: Path, ) -> None: self._entry_point = entry_point self._get_pyproject_reader = get_pyproject_reader @@ -86,13 +91,12 @@ def build(self, output_directory: Path) -> list[Path]: # We run the actual build build_env = {**os.environ, **self._build_env} if self._default_build: - command = [self._entry_point, "run", "maturin", "build", "--release"] + command = [*self._entry_point, "maturin", "build", "--release"] logger.info("%s", command) sp.check_call(command, cwd=self._project_directory, env=build_env) for target in self._zig_targets: command = [ - self._entry_point, - "run", + *self._entry_point, "maturin", "build", "--release", @@ -171,7 +175,7 @@ class MaturinPoetryPythonBuildSystem(PoetryPythonBuildSystem): def __init__(self, project_directory: Path) -> None: super().__init__(project_directory) - self._builder = _MaturinBuilder("poetry", self.get_pyproject_reader, self.project_directory) + self._builder = _MaturinBuilder(["poetry", "run"], self.get_pyproject_reader, self.project_directory) def disable_default_build(self) -> None: self._builder.disable_default_build() @@ -201,9 +205,6 @@ def update_pyproject(self, settings: PythonSettings, pyproject: TomlFile) -> Non def build(self, output_directory: Path) -> list[Path]: return self._builder.build(output_directory) - def get_lockfile(self) -> Path | None: - return self.project_directory / "poetry.lock" - class MaturinPoetryManagedEnvironment(PoetryManagedEnvironment): def install(self, settings: PythonSettings) -> None: @@ -233,7 +234,7 @@ class MaturinPdmPythonBuildSystem(PDMPythonBuildSystem): def __init__(self, project_directory: Path) -> None: super().__init__(project_directory) - self._builder = _MaturinBuilder("pdm", self.get_pyproject_reader, self.project_directory) + self._builder = _MaturinBuilder(["pdm", "run"], self.get_pyproject_reader, self.project_directory) def disable_default_build(self) -> None: self._builder.disable_default_build() @@ -253,10 +254,54 @@ def get_managed_environment(self) -> ManagedEnvironment: def build(self, output_directory: Path) -> list[Path]: return self._builder.build(output_directory) - def get_lockfile(self) -> Path | None: - return self.project_directory / "pdm.lock" - class MaturinPdmManagedEnvironment(PDMManagedEnvironment): def always_install(self) -> bool: return True + + +class MaturinUvPythonBuildSystem(UvPythonBuildSystem): + """A maturin-backed version of the UV build system, that invokes the maturin build-backend. + Can be enabled by adding the following to the local pyproject.yaml: + ```toml + [build-system] + requires = ["maturin~=1.0"] + build-backend = "maturin" + ``` + """ + + name = "Maturin UV" + + def __init__(self, project_directory: Path, uv_bin: Path | None = None) -> None: + super().__init__(project_directory, uv_bin) + # We use the build requirement to do custom Maturin builds + self._builder = _MaturinBuilder( + [ + self.uv_bin, + "tool", + "run", + *chain.from_iterable(("--with", r) for r in self._get_build_requirements()), + ], + self.get_pyproject_reader, + self.project_directory, + ) + + def disable_default_build(self) -> None: + self._builder.disable_default_build() + + def enable_zig_build(self, targets: Collection[MaturinZigTarget]) -> None: + """ + :param targets: Collection of MaturinTargets to cross-compile to using zig. + """ + self._builder.enable_zig_build(targets) + + def add_build_environment_variable(self, key: str, value: str) -> None: + self._builder.add_build_environment_variable(key, value) + + def build(self, output_directory: Path) -> list[Path]: + return self._builder.build(output_directory) + + def _get_build_requirements(self) -> Collection[str]: + pyproject_toml = self.project_directory / "pyproject.toml" + toml = TomlFile.read(pyproject_toml) + return toml.get("build-system", {}).get("requires", []) # type: ignore[no-any-return] diff --git a/kraken-build/tests/kraken_std/integration/python/test_python.py b/kraken-build/tests/kraken_std/integration/python/test_python.py index daa1ba7c..542c95c6 100644 --- a/kraken-build/tests/kraken_std/integration/python/test_python.py +++ b/kraken-build/tests/kraken_std/integration/python/test_python.py @@ -81,7 +81,15 @@ def pypiserver(docker_service_manager: DockerServiceManager) -> str: @pytest.mark.parametrize( "project_dir", - ["poetry-project", "slap-project", "pdm-project", "rust-poetry-project", "rust-pdm-project", "uv-project"], + [ + "poetry-project", + "slap-project", + "pdm-project", + "uv-project", + "rust-poetry-project", + "rust-pdm-project", + "rust-uv-project", + ], ) @unittest.mock.patch.dict(os.environ, {}) def test__python_project_install_lint_and_publish(