From 0b79dc4addd0ab707390ffcc44d4a5b19efa6b0d Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Fri, 10 Jan 2025 14:51:55 -0500 Subject: [PATCH] Add nanobind and pybind support --- README.md | 2 +- hatch_cpp/structs.py | 42 +++++++++++++--- .../cpp/{basic-project => project}/basic.cpp | 2 +- .../cpp/{basic-project => project}/basic.hpp | 0 .../{basic_project => project}/__init__.py | 0 .../tests/test_project_basic/pyproject.toml | 40 +++------------ .../cpp/project/basic.cpp | 2 + .../cpp/project/basic.hpp | 7 +++ .../project}/__init__.py | 0 .../test_project_nanobind/pyproject.toml | 35 +++++++++++++ .../cpp/{basic-project => project}/basic.cpp | 2 +- .../cpp/{basic-project => project}/basic.hpp | 0 .../project/__init__.py | 0 .../pyproject.toml | 40 +++------------ .../test_project_pybind/cpp/project/basic.cpp | 6 +++ .../test_project_pybind/cpp/project/basic.hpp | 9 ++++ .../test_project_pybind/project/__init__.py | 0 .../tests/test_project_pybind/pyproject.toml | 35 +++++++++++++ hatch_cpp/tests/test_projects.py | 50 ++++++++----------- pyproject.toml | 2 + 20 files changed, 166 insertions(+), 108 deletions(-) rename hatch_cpp/tests/test_project_basic/cpp/{basic-project => project}/basic.cpp (71%) rename hatch_cpp/tests/test_project_basic/cpp/{basic-project => project}/basic.hpp (100%) rename hatch_cpp/tests/test_project_basic/{basic_project => project}/__init__.py (100%) create mode 100644 hatch_cpp/tests/test_project_nanobind/cpp/project/basic.cpp create mode 100644 hatch_cpp/tests/test_project_nanobind/cpp/project/basic.hpp rename hatch_cpp/tests/{test_project_override_classes/basic_project => test_project_nanobind/project}/__init__.py (100%) create mode 100644 hatch_cpp/tests/test_project_nanobind/pyproject.toml rename hatch_cpp/tests/test_project_override_classes/cpp/{basic-project => project}/basic.cpp (71%) rename hatch_cpp/tests/test_project_override_classes/cpp/{basic-project => project}/basic.hpp (100%) create mode 100644 hatch_cpp/tests/test_project_override_classes/project/__init__.py create mode 100644 hatch_cpp/tests/test_project_pybind/cpp/project/basic.cpp create mode 100644 hatch_cpp/tests/test_project_pybind/cpp/project/basic.hpp create mode 100644 hatch_cpp/tests/test_project_pybind/project/__init__.py create mode 100644 hatch_cpp/tests/test_project_pybind/pyproject.toml diff --git a/README.md b/README.md index 0850415..1f26618 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,7 @@ A simple, extensible C++ build plugin for [hatch](https://hatch.pypa.io/latest/) ```toml [tool.hatch.build.hooks.hatch-cpp] libraries = [ - {name = "basic_project/extension", sources = ["cpp/basic-project/basic.cpp"], include-dirs = ["cpp"]} + {name = "project/extension", sources = ["cpp/project/basic.cpp"], include-dirs = ["cpp"]} ] ``` diff --git a/hatch_cpp/structs.py b/hatch_cpp/structs.py index 5bdf707..26f846d 100644 --- a/hatch_cpp/structs.py +++ b/hatch_cpp/structs.py @@ -19,6 +19,7 @@ BuildType = Literal["debug", "release"] CompilerToolchain = Literal["gcc", "clang", "msvc"] Language = Literal["c", "c++"] +Binding = Literal["cpython", "pybind11", "nanobind"] Platform = Literal["linux", "darwin", "win32"] PlatformDefaults = { "linux": {"CC": "gcc", "CXX": "g++", "LD": "ld"}, @@ -33,12 +34,18 @@ class HatchCppLibrary(BaseModel): name: str sources: List[str] language: Language = "c++" + + binding: Binding = "cpython" + std: Optional[str] = None + include_dirs: List[str] = Field(default_factory=list, alias="include-dirs") library_dirs: List[str] = Field(default_factory=list, alias="library-dirs") libraries: List[str] = Field(default_factory=list) + extra_compile_args: List[str] = Field(default_factory=list, alias="extra-compile-args") extra_link_args: List[str] = Field(default_factory=list, alias="extra-link-args") extra_objects: List[str] = Field(default_factory=list, alias="extra-objects") + define_macros: List[str] = Field(default_factory=list, alias="define-macros") undef_macros: List[str] = Field(default_factory=list, alias="undef-macros") @@ -82,22 +89,42 @@ def default() -> HatchCppPlatform: def get_compile_flags(self, library: HatchCppLibrary, build_type: BuildType = "release") -> str: flags = "" + + # Python.h + library.include_dirs.append(get_path("include")) + + if library.binding == "pybind11": + import pybind11 + + library.include_dirs.append(pybind11.get_include()) + if not library.std: + library.std = "c++11" + elif library.binding == "nanobind": + import nanobind + + library.include_dirs.append(nanobind.include_dir()) + if not library.std: + library.std = "c++17" + library.sources.append(str(Path(nanobind.include_dir()).parent / "src" / "nb_combined.cpp")) + library.include_dirs.append(str((Path(nanobind.include_dir()).parent / "ext" / "robin_map" / "include"))) + if self.toolchain == "gcc": - flags = f"-I{get_path('include')}" flags += " " + " ".join(f"-I{d}" for d in library.include_dirs) flags += " -fPIC" flags += " " + " ".join(library.extra_compile_args) flags += " " + " ".join(f"-D{macro}" for macro in library.define_macros) flags += " " + " ".join(f"-U{macro}" for macro in library.undef_macros) + if library.std: + flags += f" -std={library.std}" elif self.toolchain == "clang": - flags = f"-I{get_path('include')} " flags += " ".join(f"-I{d}" for d in library.include_dirs) flags += " -fPIC" flags += " " + " ".join(library.extra_compile_args) flags += " " + " ".join(f"-D{macro}" for macro in library.define_macros) flags += " " + " ".join(f"-U{macro}" for macro in library.undef_macros) + if library.std: + flags += f" -std={library.std}" elif self.toolchain == "msvc": - flags = f"/I{get_path('include')} " flags += " ".join(f"/I{d}" for d in library.include_dirs) flags += " " + " ".join(library.extra_compile_args) flags += " " + " ".join(library.extra_link_args) @@ -105,6 +132,8 @@ def get_compile_flags(self, library: HatchCppLibrary, build_type: BuildType = "r flags += " " + " ".join(f"/D{macro}" for macro in library.define_macros) flags += " " + " ".join(f"/U{macro}" for macro in library.undef_macros) flags += " /EHsc /DWIN32" + if library.std: + flags += f" /std:{library.std}" # clean while flags.count(" "): flags = flags.replace(" ", " ") @@ -142,7 +171,6 @@ def get_link_flags(self, library: HatchCppLibrary, build_type: BuildType = "rele flags += " " + " ".join(library.extra_link_args) flags += " " + " ".join(library.extra_objects) flags += " /LD" - flags += f" /Fo:{library.name}.obj" flags += f" /Fe:{library.name}.pyd" flags += " /link /DLL" if (Path(executable).parent / "libs").exists(): @@ -178,10 +206,8 @@ def execute(self): def cleanup(self): if self.platform.platform == "win32": - for library in self.libraries: - temp_obj = Path(f"{library.name}.obj") - if temp_obj.exists(): - temp_obj.unlink() + for temp_obj in Path(".").glob("*.obj"): + temp_obj.unlink() class HatchCppBuildConfig(BaseModel): diff --git a/hatch_cpp/tests/test_project_basic/cpp/basic-project/basic.cpp b/hatch_cpp/tests/test_project_basic/cpp/project/basic.cpp similarity index 71% rename from hatch_cpp/tests/test_project_basic/cpp/basic-project/basic.cpp rename to hatch_cpp/tests/test_project_basic/cpp/project/basic.cpp index a7e840e..db4432a 100644 --- a/hatch_cpp/tests/test_project_basic/cpp/basic-project/basic.cpp +++ b/hatch_cpp/tests/test_project_basic/cpp/project/basic.cpp @@ -1,4 +1,4 @@ -#include "basic-project/basic.hpp" +#include "project/basic.hpp" PyObject* hello(PyObject*, PyObject*) { return PyUnicode_FromString("A string"); diff --git a/hatch_cpp/tests/test_project_basic/cpp/basic-project/basic.hpp b/hatch_cpp/tests/test_project_basic/cpp/project/basic.hpp similarity index 100% rename from hatch_cpp/tests/test_project_basic/cpp/basic-project/basic.hpp rename to hatch_cpp/tests/test_project_basic/cpp/project/basic.hpp diff --git a/hatch_cpp/tests/test_project_basic/basic_project/__init__.py b/hatch_cpp/tests/test_project_basic/project/__init__.py similarity index 100% rename from hatch_cpp/tests/test_project_basic/basic_project/__init__.py rename to hatch_cpp/tests/test_project_basic/project/__init__.py diff --git a/hatch_cpp/tests/test_project_basic/pyproject.toml b/hatch_cpp/tests/test_project_basic/pyproject.toml index aea842d..d51683e 100644 --- a/hatch_cpp/tests/test_project_basic/pyproject.toml +++ b/hatch_cpp/tests/test_project_basic/pyproject.toml @@ -14,50 +14,22 @@ dependencies = [ [tool.hatch.build] artifacts = [ - "basic_project/*.dll", - "basic_project/*.dylib", - "basic_project/*.so", + "project/*.dll", + "project/*.dylib", + "project/*.so", ] [tool.hatch.build.sources] src = "/" [tool.hatch.build.targets.sdist] -packages = ["basic_project"] +packages = ["project"] [tool.hatch.build.targets.wheel] -packages = ["basic_project"] +packages = ["project"] [tool.hatch.build.hooks.hatch-cpp] verbose = true libraries = [ - {name = "basic_project/extension", sources = ["cpp/basic-project/basic.cpp"], include-dirs = ["cpp"]} + {name = "project/extension", sources = ["cpp/project/basic.cpp"], include-dirs = ["cpp"]} ] - -# build-function = "hatch_cpp.cpp_builder" - -# [tool.hatch.build.hooks.defaults] -# build-type = "release" - -# [tool.hatch.build.hooks.env-vars] -# TODO: these will all be available via -# CLI after https://github.com/pypa/hatch/pull/1743 -# e.g. --hatch-cpp-build-type=debug -# build-type = "BUILD_TYPE" -# ccache = "USE_CCACHE" -# manylinux = "MANYLINUX" -# vcpkg = "USE_VCPKG" - -# [tool.hatch.build.hooks.cmake] - -# [tool.hatch.build.hooks.vcpkg] -# triplets = {linux="x64-linux", macos="x64-osx", windows="x64-windows-static-md"} -# clone = true -# update = true - -# [tool.hatch.build.hooks.hatch-cpp.build-kwargs] -# path = "cpp" - -[tool.pytest.ini_options] -asyncio_mode = "strict" -testpaths = "basic_project/tests" diff --git a/hatch_cpp/tests/test_project_nanobind/cpp/project/basic.cpp b/hatch_cpp/tests/test_project_nanobind/cpp/project/basic.cpp new file mode 100644 index 0000000..2ac7d56 --- /dev/null +++ b/hatch_cpp/tests/test_project_nanobind/cpp/project/basic.cpp @@ -0,0 +1,2 @@ +#include "project/basic.hpp" + diff --git a/hatch_cpp/tests/test_project_nanobind/cpp/project/basic.hpp b/hatch_cpp/tests/test_project_nanobind/cpp/project/basic.hpp new file mode 100644 index 0000000..1afa022 --- /dev/null +++ b/hatch_cpp/tests/test_project_nanobind/cpp/project/basic.hpp @@ -0,0 +1,7 @@ +#pragma once +#include +#include + +NB_MODULE(extension, m) { + m.def("hello", []() { return "A string"; }); +} diff --git a/hatch_cpp/tests/test_project_override_classes/basic_project/__init__.py b/hatch_cpp/tests/test_project_nanobind/project/__init__.py similarity index 100% rename from hatch_cpp/tests/test_project_override_classes/basic_project/__init__.py rename to hatch_cpp/tests/test_project_nanobind/project/__init__.py diff --git a/hatch_cpp/tests/test_project_nanobind/pyproject.toml b/hatch_cpp/tests/test_project_nanobind/pyproject.toml new file mode 100644 index 0000000..6a8f632 --- /dev/null +++ b/hatch_cpp/tests/test_project_nanobind/pyproject.toml @@ -0,0 +1,35 @@ +[build-system] +requires = ["hatchling>=1.20"] +build-backend = "hatchling.build" + +[project] +name = "hatch-cpp-test-project-basic" +description = "Basic test project for hatch-cpp" +version = "0.1.0" +requires-python = ">=3.9" +dependencies = [ + "hatchling>=1.20", + "hatch-cpp", +] + +[tool.hatch.build] +artifacts = [ + "project/*.dll", + "project/*.dylib", + "project/*.so", +] + +[tool.hatch.build.sources] +src = "/" + +[tool.hatch.build.targets.sdist] +packages = ["project"] + +[tool.hatch.build.targets.wheel] +packages = ["project"] + +[tool.hatch.build.hooks.hatch-cpp] +verbose = true +libraries = [ + {name = "project/extension", sources = ["cpp/project/basic.cpp"], include-dirs = ["cpp"], binding = "nanobind"}, +] diff --git a/hatch_cpp/tests/test_project_override_classes/cpp/basic-project/basic.cpp b/hatch_cpp/tests/test_project_override_classes/cpp/project/basic.cpp similarity index 71% rename from hatch_cpp/tests/test_project_override_classes/cpp/basic-project/basic.cpp rename to hatch_cpp/tests/test_project_override_classes/cpp/project/basic.cpp index a7e840e..db4432a 100644 --- a/hatch_cpp/tests/test_project_override_classes/cpp/basic-project/basic.cpp +++ b/hatch_cpp/tests/test_project_override_classes/cpp/project/basic.cpp @@ -1,4 +1,4 @@ -#include "basic-project/basic.hpp" +#include "project/basic.hpp" PyObject* hello(PyObject*, PyObject*) { return PyUnicode_FromString("A string"); diff --git a/hatch_cpp/tests/test_project_override_classes/cpp/basic-project/basic.hpp b/hatch_cpp/tests/test_project_override_classes/cpp/project/basic.hpp similarity index 100% rename from hatch_cpp/tests/test_project_override_classes/cpp/basic-project/basic.hpp rename to hatch_cpp/tests/test_project_override_classes/cpp/project/basic.hpp diff --git a/hatch_cpp/tests/test_project_override_classes/project/__init__.py b/hatch_cpp/tests/test_project_override_classes/project/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hatch_cpp/tests/test_project_override_classes/pyproject.toml b/hatch_cpp/tests/test_project_override_classes/pyproject.toml index d7ffab9..57fd83e 100644 --- a/hatch_cpp/tests/test_project_override_classes/pyproject.toml +++ b/hatch_cpp/tests/test_project_override_classes/pyproject.toml @@ -14,52 +14,24 @@ dependencies = [ [tool.hatch.build] artifacts = [ - "basic_project/*.dll", - "basic_project/*.dylib", - "basic_project/*.so", + "project/*.dll", + "project/*.dylib", + "project/*.so", ] [tool.hatch.build.sources] src = "/" [tool.hatch.build.targets.sdist] -packages = ["basic_project"] +packages = ["project"] [tool.hatch.build.targets.wheel] -packages = ["basic_project"] +packages = ["project"] [tool.hatch.build.hooks.hatch-cpp] build-config-class = "hatch_cpp.HatchCppBuildConfig" build-plan-class = "hatch_cpp.HatchCppBuildPlan" verbose = true libraries = [ - {name = "basic_project/extension", sources = ["cpp/basic-project/basic.cpp"], include-dirs = ["cpp"]} + {name = "project/extension", sources = ["cpp/project/basic.cpp"], include-dirs = ["cpp"]} ] - -# build-function = "hatch_cpp.cpp_builder" - -# [tool.hatch.build.hooks.defaults] -# build-type = "release" - -# [tool.hatch.build.hooks.env-vars] -# TODO: these will all be available via -# CLI after https://github.com/pypa/hatch/pull/1743 -# e.g. --hatch-cpp-build-type=debug -# build-type = "BUILD_TYPE" -# ccache = "USE_CCACHE" -# manylinux = "MANYLINUX" -# vcpkg = "USE_VCPKG" - -# [tool.hatch.build.hooks.cmake] - -# [tool.hatch.build.hooks.vcpkg] -# triplets = {linux="x64-linux", macos="x64-osx", windows="x64-windows-static-md"} -# clone = true -# update = true - -# [tool.hatch.build.hooks.hatch-cpp.build-kwargs] -# path = "cpp" - -[tool.pytest.ini_options] -asyncio_mode = "strict" -testpaths = "basic_project/tests" diff --git a/hatch_cpp/tests/test_project_pybind/cpp/project/basic.cpp b/hatch_cpp/tests/test_project_pybind/cpp/project/basic.cpp new file mode 100644 index 0000000..ebe96f8 --- /dev/null +++ b/hatch_cpp/tests/test_project_pybind/cpp/project/basic.cpp @@ -0,0 +1,6 @@ +#include "project/basic.hpp" + +std::string hello() { + return "A string"; +} + diff --git a/hatch_cpp/tests/test_project_pybind/cpp/project/basic.hpp b/hatch_cpp/tests/test_project_pybind/cpp/project/basic.hpp new file mode 100644 index 0000000..86053b2 --- /dev/null +++ b/hatch_cpp/tests/test_project_pybind/cpp/project/basic.hpp @@ -0,0 +1,9 @@ +#pragma once +#include +#include + +std::string hello(); + +PYBIND11_MODULE(extension, m) { + m.def("hello", &hello); +} \ No newline at end of file diff --git a/hatch_cpp/tests/test_project_pybind/project/__init__.py b/hatch_cpp/tests/test_project_pybind/project/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hatch_cpp/tests/test_project_pybind/pyproject.toml b/hatch_cpp/tests/test_project_pybind/pyproject.toml new file mode 100644 index 0000000..b24e6cd --- /dev/null +++ b/hatch_cpp/tests/test_project_pybind/pyproject.toml @@ -0,0 +1,35 @@ +[build-system] +requires = ["hatchling>=1.20"] +build-backend = "hatchling.build" + +[project] +name = "hatch-cpp-test-project-basic" +description = "Basic test project for hatch-cpp" +version = "0.1.0" +requires-python = ">=3.9" +dependencies = [ + "hatchling>=1.20", + "hatch-cpp", +] + +[tool.hatch.build] +artifacts = [ + "project/*.dll", + "project/*.dylib", + "project/*.so", +] + +[tool.hatch.build.sources] +src = "/" + +[tool.hatch.build.targets.sdist] +packages = ["project"] + +[tool.hatch.build.targets.wheel] +packages = ["project"] + +[tool.hatch.build.hooks.hatch-cpp] +verbose = true +libraries = [ + {name = "project/extension", sources = ["cpp/project/basic.cpp"], include-dirs = ["cpp"], binding="pybind11"}, +] diff --git a/hatch_cpp/tests/test_projects.py b/hatch_cpp/tests/test_projects.py index d05755a..34e5bf7 100644 --- a/hatch_cpp/tests/test_projects.py +++ b/hatch_cpp/tests/test_projects.py @@ -2,48 +2,40 @@ from pathlib import Path from shutil import rmtree from subprocess import check_call -from sys import path, platform +from sys import modules, path, platform + +import pytest class TestProject: - def test_basic(self): - rmtree("hatch_cpp/tests/test_project_basic/basic_project/extension.so", ignore_errors=True) - rmtree("hatch_cpp/tests/test_project_basic/basic_project/extension.pyd", ignore_errors=True) + @pytest.mark.parametrize("project", ["test_project_basic", "test_project_override_classes", "test_project_pybind", "test_project_nanobind"]) + def test_basic(self, project): + # cleanup + rmtree(f"hatch_cpp/tests/{project}/project/extension.so", ignore_errors=True) + rmtree(f"hatch_cpp/tests/{project}/project/extension.pyd", ignore_errors=True) + modules.pop("project", None) + modules.pop("project.extension", None) + + # compile check_call( [ "hatchling", "build", "--hooks-only", ], - cwd="hatch_cpp/tests/test_project_basic", + cwd=f"hatch_cpp/tests/{project}", ) - if platform == "win32": - assert "extension.pyd" in listdir("hatch_cpp/tests/test_project_basic/basic_project") - else: - assert "extension.so" in listdir("hatch_cpp/tests/test_project_basic/basic_project") - here = Path(__file__).parent / "test_project_basic" - path.insert(0, str(here)) - import basic_project.extension - assert basic_project.extension.hello() == "A string" + # assert built - def test_override_classes(self): - rmtree("hatch_cpp/tests/test_project_override_classes/basic_project/extension.so", ignore_errors=True) - rmtree("hatch_cpp/tests/test_project_override_classes/basic_project/extension.pyd", ignore_errors=True) - check_call( - [ - "hatchling", - "build", - "--hooks-only", - ], - cwd="hatch_cpp/tests/test_project_override_classes", - ) if platform == "win32": - assert "extension.pyd" in listdir("hatch_cpp/tests/test_project_override_classes/basic_project") + assert "extension.pyd" in listdir(f"hatch_cpp/tests/{project}/project") else: - assert "extension.so" in listdir("hatch_cpp/tests/test_project_override_classes/basic_project") - here = Path(__file__).parent / "test_project_override_classes" + assert "extension.so" in listdir(f"hatch_cpp/tests/{project}/project") + + # import + here = Path(__file__).parent / project path.insert(0, str(here)) - import basic_project.extension + import project.extension - assert basic_project.extension.hello() == "A string" + assert project.extension.hello() == "A string" diff --git a/pyproject.toml b/pyproject.toml index 0218232..174e84e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -45,6 +45,8 @@ develop = [ "twine", "wheel", # test + "nanobind", + "pybind11", "pytest", "pytest-cov", ]