From 6f59e855f9bff6288ef4d9b8f8e3d0d41c714d9c Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Fri, 10 Jan 2025 13:00:39 -0500 Subject: [PATCH 1/5] Switch from raw dataclasses to pydantic for validation --- hatch_cpp/plugin.py | 51 +++----------------------------- hatch_cpp/structs.py | 70 +++++++++++++++++++++----------------------- pyproject.toml | 1 + 3 files changed, 38 insertions(+), 84 deletions(-) diff --git a/hatch_cpp/plugin.py b/hatch_cpp/plugin.py index 0de620f..6b57f2d 100644 --- a/hatch_cpp/plugin.py +++ b/hatch_cpp/plugin.py @@ -3,11 +3,10 @@ import logging import os import typing as t -from dataclasses import fields from hatchling.builders.hooks.plugin.interface import BuildHookInterface -from .structs import HatchCppBuildConfig, HatchCppBuildPlan, HatchCppLibrary, HatchCppPlatform +from .structs import HatchCppBuildConfig, HatchCppBuildPlan __all__ = ("HatchCppBuildHook",) @@ -30,18 +29,10 @@ def initialize(self, version: str, _: dict[str, t.Any]) -> None: self._logger.info("Skipping the build hook since SKIP_HATCH_CPP was set") return - kwargs = {k.replace("-", "_"): v if not isinstance(v, bool) else str(v) for k, v in self.config.items()} - available_fields = [f.name for f in fields(HatchCppBuildConfig)] - for key in list(kwargs): - if key not in available_fields: - del kwargs[key] - config = HatchCppBuildConfig(**kwargs) + config = HatchCppBuildConfig(**self.config) - library_kwargs = [ - {k.replace("-", "_"): v if not isinstance(v, bool) else str(v) for k, v in library_kwargs.items()} for library_kwargs in config.libraries - ] - libraries = [HatchCppLibrary(**library_kwargs) for library_kwargs in library_kwargs] - platform = HatchCppPlatform.default() + libraries = config.libraries + platform = config.platform if config.toolchain == "raw": build_plan = HatchCppBuildPlan(libraries=libraries, platform=platform) build_plan.generate() @@ -51,39 +42,5 @@ def initialize(self, version: str, _: dict[str, t.Any]) -> None: build_plan.execute() build_plan.cleanup() - # build_kwargs = config.build_kwargs - # if version == "editable": - # build_kwargs = config.editable_build_kwargs or build_kwargs - - # should_skip_build = False - # if not config.build_function: - # log.warning("No build function found") - # should_skip_build = True - - # elif config.skip_if_exists and version == "standard": - # should_skip_build = should_skip(config.skip_if_exists) - # if should_skip_build: - # log.info("Skip-if-exists file(s) found") - - # # Get build function and call it with normalized parameter names. - # if not should_skip_build and config.build_function: - # build_func = get_build_func(config.build_function) - # build_kwargs = normalize_kwargs(build_kwargs) - # log.info("Building with %s", config.build_function) - # log.info("With kwargs: %s", build_kwargs) - # try: - # build_func(self.target_name, version, **build_kwargs) - # except Exception as e: - # if version == "editable" and config.optional_editable_build.lower() == "true": - # warnings.warn(f"Encountered build error:\n{e}", stacklevel=2) - # else: - # raise e - # else: - # log.info("Skipping build") - - # # Ensure targets in distributable dists. - # if version == "standard": - # ensure_targets(config.ensured_targets) - self._logger.info("Finished running hatch-cpp") return diff --git a/hatch_cpp/structs.py b/hatch_cpp/structs.py index cc9c99c..4241a7b 100644 --- a/hatch_cpp/structs.py +++ b/hatch_cpp/structs.py @@ -1,13 +1,12 @@ from __future__ import annotations -from dataclasses import dataclass, field from os import environ, system from pathlib import Path from sys import executable, platform as sys_platform from sysconfig import get_path -from typing import Literal +from typing import List, Literal, Optional -from hatchling.builders.config import BuilderConfig +from pydantic import BaseModel, Field __all__ = ( "HatchCppBuildConfig", @@ -25,42 +24,26 @@ } -@dataclass -class HatchCppBuildConfig(BuilderConfig): - """Build config values for Hatch C++ Builder.""" - - toolchain: str | None = field(default="raw") - libraries: list[dict[str, str]] = field(default_factory=list) - verbose: bool | None = field(default=False) - # build_function: str | None = None - # build_kwargs: t.Mapping[str, str] = field(default_factory=dict) - # editable_build_kwargs: t.Mapping[str, str] = field(default_factory=dict) - # ensured_targets: list[str] = field(default_factory=list) - # skip_if_exists: list[str] = field(default_factory=list) - - -@dataclass -class HatchCppLibrary(object): +class HatchCppLibrary(BaseModel): """A C++ library.""" name: str - sources: list[str] + sources: List[str] - include_dirs: list[str] = field(default_factory=list) - library_dirs: list[str] = field(default_factory=list) - libraries: list[str] = field(default_factory=list) - extra_compile_args: list[str] = field(default_factory=list) - extra_link_args: list[str] = field(default_factory=list) - extra_objects: list[str] = field(default_factory=list) - define_macros: list[str] = field(default_factory=list) - undef_macros: list[str] = field(default_factory=list) + 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") - export_symbols: list[str] = field(default_factory=list) - depends: list[str] = field(default_factory=list) + export_symbols: List[str] = Field(default_factory=list, alias="export-symbols") + depends: List[str] = Field(default_factory=list) -@dataclass -class HatchCppPlatform(object): +class HatchCppPlatform(BaseModel): cc: str cxx: str platform: Platform @@ -133,11 +116,10 @@ def get_link_flags(self, library: HatchCppLibrary) -> str: return flags -@dataclass -class HatchCppBuildPlan(object): - libraries: list[HatchCppLibrary] = field(default_factory=list) - platform: HatchCppPlatform = field(default_factory=HatchCppPlatform.default) - commands: list[str] = field(default_factory=list) +class HatchCppBuildPlan(BaseModel): + libraries: List[HatchCppLibrary] = Field(default_factory=list) + platform: HatchCppPlatform = Field(default_factory=HatchCppPlatform.default) + commands: List[str] = Field(default_factory=list) def generate(self): self.commands = [] @@ -157,3 +139,17 @@ def cleanup(self): temp_obj = Path(f"{library.name}.obj") if temp_obj.exists(): temp_obj.unlink() + + +class HatchCppBuildConfig(BaseModel): + """Build config values for Hatch C++ Builder.""" + + toolchain: Optional[str] = Field(default="raw") + libraries: List[HatchCppLibrary] = Field(default_factory=list) + verbose: Optional[bool] = Field(default=False) + platform: Optional[HatchCppPlatform] = Field(default_factory=HatchCppPlatform.default) + # build_function: str | None = None + # build_kwargs: t.Mapping[str, str] = field(default_factory=dict) + # editable_build_kwargs: t.Mapping[str, str] = field(default_factory=dict) + # ensured_targets: list[str] = field(default_factory=list) + # skip_if_exists: list[str] = field(default_factory=list) diff --git a/pyproject.toml b/pyproject.toml index 451b580..7e45b50 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -33,6 +33,7 @@ classifiers = [ dependencies = [ "hatchling>=1.20", + "pydantic", ] [project.optional-dependencies] From 65a8618fd15aa4015208b99a60740602c51cc5e3 Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Fri, 10 Jan 2025 13:30:19 -0500 Subject: [PATCH 2/5] Add overrides to config and build plan --- hatch_cpp/__init__.py | 4 ++ hatch_cpp/__main__.py | 4 -- hatch_cpp/plugin.py | 45 +++++++++---- hatch_cpp/structs.py | 5 +- .../basic_project/__init__.py | 0 .../cpp/basic-project/basic.cpp | 5 ++ .../cpp/basic-project/basic.hpp | 17 +++++ .../pyproject.toml | 65 +++++++++++++++++++ ...test_project_basic.py => test_projects.py} | 21 ++++++ hatch_cpp/utils.py | 12 ++++ pyproject.toml | 4 +- 11 files changed, 162 insertions(+), 20 deletions(-) delete mode 100644 hatch_cpp/__main__.py create mode 100644 hatch_cpp/tests/test_project_override_classes/basic_project/__init__.py create mode 100644 hatch_cpp/tests/test_project_override_classes/cpp/basic-project/basic.cpp create mode 100644 hatch_cpp/tests/test_project_override_classes/cpp/basic-project/basic.hpp create mode 100644 hatch_cpp/tests/test_project_override_classes/pyproject.toml rename hatch_cpp/tests/{test_project_basic.py => test_projects.py} (52%) diff --git a/hatch_cpp/__init__.py b/hatch_cpp/__init__.py index 485f44a..88f1932 100644 --- a/hatch_cpp/__init__.py +++ b/hatch_cpp/__init__.py @@ -1 +1,5 @@ __version__ = "0.1.1" + +from .hooks import hatch_register_build_hook +from .plugin import HatchCppBuildHook +from .structs import * diff --git a/hatch_cpp/__main__.py b/hatch_cpp/__main__.py deleted file mode 100644 index 9ae637f..0000000 --- a/hatch_cpp/__main__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .cli import main - -if __name__ == "__main__": - main() diff --git a/hatch_cpp/plugin.py b/hatch_cpp/plugin.py index 6b57f2d..9ad7fb1 100644 --- a/hatch_cpp/plugin.py +++ b/hatch_cpp/plugin.py @@ -7,6 +7,7 @@ from hatchling.builders.hooks.plugin.interface import BuildHookInterface from .structs import HatchCppBuildConfig, HatchCppBuildPlan +from .utils import import_string __all__ = ("HatchCppBuildHook",) @@ -19,28 +20,48 @@ class HatchCppBuildHook(BuildHookInterface[HatchCppBuildConfig]): def initialize(self, version: str, _: dict[str, t.Any]) -> None: """Initialize the plugin.""" + # Log some basic information + self._logger.info("Initializing hatch-cpp plugin version %s", version) self._logger.info("Running hatch-cpp") + # Only run if creating wheel + # TODO: Add support for specify sdist-plan if self.target_name != "wheel": self._logger.info("ignoring target name %s", self.target_name) return + # Skip if SKIP_HATCH_CPP is set + # TODO: Support CLI once https://github.com/pypa/hatch/pull/1743 if os.getenv("SKIP_HATCH_CPP"): self._logger.info("Skipping the build hook since SKIP_HATCH_CPP was set") return - config = HatchCppBuildConfig(**self.config) + # Get build config class or use default + build_config_class = import_string(self.config["build-config-class"]) if "build-config-class" in self.config else HatchCppBuildConfig + # Instantiate build config + config = build_config_class(**self.config) + + # Grab libraries and platform libraries = config.libraries platform = config.platform - if config.toolchain == "raw": - build_plan = HatchCppBuildPlan(libraries=libraries, platform=platform) - build_plan.generate() - if config.verbose: - for command in build_plan.commands: - self._logger.info(command) - build_plan.execute() - build_plan.cleanup() - - self._logger.info("Finished running hatch-cpp") - return + + # Get build plan class or use default + build_plan_class = import_string(self.config["build-plan-class"]) if "build-plan-class" in self.config else HatchCppBuildPlan + + # Instantiate builder + build_plan = build_plan_class(libraries=libraries, platform=platform) + + # Generate commands + build_plan.generate() + + # Log commands if in verbose mode + if config.verbose: + for command in build_plan.commands: + self._logger.info(command) + + # Execute build plan + build_plan.execute() + + # Perform any cleanup actions + build_plan.cleanup() diff --git a/hatch_cpp/structs.py b/hatch_cpp/structs.py index 4241a7b..461fb8e 100644 --- a/hatch_cpp/structs.py +++ b/hatch_cpp/structs.py @@ -112,6 +112,7 @@ def get_compile_flags(self, library: HatchCppLibrary) -> str: return flags def get_link_flags(self, library: HatchCppLibrary) -> str: + # TODO flags = "" return flags @@ -144,10 +145,10 @@ def cleanup(self): class HatchCppBuildConfig(BaseModel): """Build config values for Hatch C++ Builder.""" - toolchain: Optional[str] = Field(default="raw") - libraries: List[HatchCppLibrary] = Field(default_factory=list) verbose: Optional[bool] = Field(default=False) + libraries: List[HatchCppLibrary] = Field(default_factory=list) platform: Optional[HatchCppPlatform] = Field(default_factory=HatchCppPlatform.default) + # build_function: str | None = None # build_kwargs: t.Mapping[str, str] = field(default_factory=dict) # editable_build_kwargs: t.Mapping[str, str] = field(default_factory=dict) diff --git a/hatch_cpp/tests/test_project_override_classes/basic_project/__init__.py b/hatch_cpp/tests/test_project_override_classes/basic_project/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/hatch_cpp/tests/test_project_override_classes/cpp/basic-project/basic.cpp b/hatch_cpp/tests/test_project_override_classes/cpp/basic-project/basic.cpp new file mode 100644 index 0000000..a7e840e --- /dev/null +++ b/hatch_cpp/tests/test_project_override_classes/cpp/basic-project/basic.cpp @@ -0,0 +1,5 @@ +#include "basic-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/basic-project/basic.hpp new file mode 100644 index 0000000..65cb62e --- /dev/null +++ b/hatch_cpp/tests/test_project_override_classes/cpp/basic-project/basic.hpp @@ -0,0 +1,17 @@ +#pragma once +#include "Python.h" + +PyObject* hello(PyObject*, PyObject*); + +static PyMethodDef extension_methods[] = { + {"hello", (PyCFunction)hello, METH_NOARGS}, + {nullptr, nullptr, 0, nullptr} +}; + +static PyModuleDef extension_module = { + PyModuleDef_HEAD_INIT, "extension", "extension", -1, extension_methods}; + +PyMODINIT_FUNC PyInit_extension(void) { + Py_Initialize(); + return PyModule_Create(&extension_module); +} diff --git a/hatch_cpp/tests/test_project_override_classes/pyproject.toml b/hatch_cpp/tests/test_project_override_classes/pyproject.toml new file mode 100644 index 0000000..d7ffab9 --- /dev/null +++ b/hatch_cpp/tests/test_project_override_classes/pyproject.toml @@ -0,0 +1,65 @@ +[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 = [ + "basic_project/*.dll", + "basic_project/*.dylib", + "basic_project/*.so", +] + +[tool.hatch.build.sources] +src = "/" + +[tool.hatch.build.targets.sdist] +packages = ["basic_project"] + +[tool.hatch.build.targets.wheel] +packages = ["basic_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"]} +] + +# 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_basic.py b/hatch_cpp/tests/test_projects.py similarity index 52% rename from hatch_cpp/tests/test_project_basic.py rename to hatch_cpp/tests/test_projects.py index 03de9f9..4d5a39c 100644 --- a/hatch_cpp/tests/test_project_basic.py +++ b/hatch_cpp/tests/test_projects.py @@ -26,3 +26,24 @@ def test_basic(self): import basic_project.extension assert basic_project.extension.hello() == "A string" + + 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_output( + [ + "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") + else: + assert "extension.so" in listdir("hatch_cpp/tests/test_project_override_classes/basic_project") + here = Path(__file__).parent / "test_project_override_classes" + path.insert(0, str(here)) + import basic_project.extension + + assert basic_project.extension.hello() == "A string" diff --git a/hatch_cpp/utils.py b/hatch_cpp/utils.py index f95bf5e..fb209b2 100644 --- a/hatch_cpp/utils.py +++ b/hatch_cpp/utils.py @@ -1,5 +1,17 @@ from __future__ import annotations +from functools import lru_cache + +from pydantic import ImportString, TypeAdapter + +_import_string_adapter = TypeAdapter(ImportString) + + +@lru_cache(maxsize=None) +def import_string(input_string: str): + return _import_string_adapter.validate_python(input_string) + + # import multiprocessing # import os # import os.path diff --git a/pyproject.toml b/pyproject.toml index 7e45b50..452e4af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -52,8 +52,8 @@ develop = [ [project.entry-points.hatch] cpp = "hatch_cpp.hooks" -[project.scripts] -hatch-cpp = "hatch_cpp.cli:main" +# [project.scripts] +# hatch-cpp = "hatch_cpp.cli:main" [project.urls] Repository = "https://github.com/python-project-templates/hatch-cpp" From 8d0af69c44c06d95cee56a2d7b215935860bc0d1 Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Fri, 10 Jan 2025 14:10:27 -0500 Subject: [PATCH 3/5] Add support for other spawners and linkers --- README.md | 22 +++++++ hatch_cpp/plugin.py | 2 +- hatch_cpp/structs.py | 102 +++++++++++++++++++++---------- hatch_cpp/tests/test_projects.py | 6 +- pyproject.toml | 2 +- 5 files changed, 96 insertions(+), 38 deletions(-) diff --git a/README.md b/README.md index d47e19c..0850415 100644 --- a/README.md +++ b/README.md @@ -9,5 +9,27 @@ Hatch plugin for C++ builds ## Overview +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"]} +] +``` + +For more complete systems, see: +- [scikit-build-core](https://github.com/scikit-build/scikit-build-core) +- [setuptools](https://setuptools.pypa.io/en/latest/userguide/ext_modules.html) + +## Environment Variables +| Name | Default | Description | +|:-----|:--------|:------------| +|`CC`| | | +|`CXX`| | | +|`LD`| | | +|`HATCH_CPP_PLATFORM`| | | +|`HATCH_CPP_DISABLE_CCACHE`| | | + > [!NOTE] > This library was generated using [copier](https://copier.readthedocs.io/en/stable/) from the [Base Python Project Template repository](https://github.com/python-project-templates/base). diff --git a/hatch_cpp/plugin.py b/hatch_cpp/plugin.py index 9ad7fb1..469ee39 100644 --- a/hatch_cpp/plugin.py +++ b/hatch_cpp/plugin.py @@ -58,7 +58,7 @@ def initialize(self, version: str, _: dict[str, t.Any]) -> None: # Log commands if in verbose mode if config.verbose: for command in build_plan.commands: - self._logger.info(command) + self._logger.warning(command) # Execute build plan build_plan.execute() diff --git a/hatch_cpp/structs.py b/hatch_cpp/structs.py index 461fb8e..5bdf707 100644 --- a/hatch_cpp/structs.py +++ b/hatch_cpp/structs.py @@ -2,6 +2,7 @@ from os import environ, system from pathlib import Path +from shutil import which from sys import executable, platform as sys_platform from sysconfig import get_path from typing import List, Literal, Optional @@ -15,12 +16,14 @@ "HatchCppBuildPlan", ) -Platform = Literal["linux", "darwin", "win32"] +BuildType = Literal["debug", "release"] CompilerToolchain = Literal["gcc", "clang", "msvc"] +Language = Literal["c", "c++"] +Platform = Literal["linux", "darwin", "win32"] PlatformDefaults = { - "linux": {"CC": "gcc", "CXX": "g++"}, - "darwin": {"CC": "clang", "CXX": "clang++"}, - "win32": {"CC": "cl", "CXX": "cl"}, + "linux": {"CC": "gcc", "CXX": "g++", "LD": "ld"}, + "darwin": {"CC": "clang", "CXX": "clang++", "LD": "ld"}, + "win32": {"CC": "cl", "CXX": "cl", "LD": "link"}, } @@ -29,7 +32,7 @@ class HatchCppLibrary(BaseModel): name: str sources: List[str] - + language: Language = "c++" 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) @@ -46,6 +49,7 @@ class HatchCppLibrary(BaseModel): class HatchCppPlatform(BaseModel): cc: str cxx: str + ld: str platform: Platform toolchain: CompilerToolchain @@ -54,6 +58,7 @@ def default() -> HatchCppPlatform: platform = environ.get("HATCH_CPP_PLATFORM", sys_platform) CC = environ.get("CC", PlatformDefaults[platform]["CC"]) CXX = environ.get("CXX", PlatformDefaults[platform]["CXX"]) + LD = environ.get("LD", PlatformDefaults[platform]["LD"]) if "gcc" in CC and "g++" in CXX: toolchain = "gcc" elif "clang" in CC and "clang++" in CXX: @@ -62,34 +67,35 @@ def default() -> HatchCppPlatform: toolchain = "msvc" else: raise Exception(f"Unrecognized toolchain: {CC}, {CXX}") - return HatchCppPlatform(cc=CC, cxx=CXX, platform=platform, toolchain=toolchain) - def get_compile_flags(self, library: HatchCppLibrary) -> str: + # Customizations + if which("ccache") and not environ.get("HATCH_CPP_DISABLE_CCACHE"): + CC = f"ccache {CC}" + CXX = f"ccache {CXX}" + + # https://github.com/rui314/mold/issues/647 + # if which("ld.mold"): + # LD = which("ld.mold") + # elif which("ld.lld"): + # LD = which("ld.lld") + return HatchCppPlatform(cc=CC, cxx=CXX, ld=LD, platform=platform, toolchain=toolchain) + + def get_compile_flags(self, library: HatchCppLibrary, build_type: BuildType = "release") -> str: flags = "" if self.toolchain == "gcc": flags = f"-I{get_path('include')}" flags += " " + " ".join(f"-I{d}" for d in library.include_dirs) - flags += " -fPIC -shared" + flags += " -fPIC" flags += " " + " ".join(library.extra_compile_args) - flags += " " + " ".join(library.extra_link_args) - flags += " " + " ".join(library.extra_objects) - flags += " " + " ".join(f"-l{lib}" for lib in library.libraries) - flags += " " + " ".join(f"-L{lib}" for lib in library.library_dirs) flags += " " + " ".join(f"-D{macro}" for macro in library.define_macros) flags += " " + " ".join(f"-U{macro}" for macro in library.undef_macros) - flags += f" -o {library.name}.so" elif self.toolchain == "clang": flags = f"-I{get_path('include')} " flags += " ".join(f"-I{d}" for d in library.include_dirs) - flags += " -undefined dynamic_lookup -fPIC -shared" + flags += " -fPIC" flags += " " + " ".join(library.extra_compile_args) - flags += " " + " ".join(library.extra_link_args) - flags += " " + " ".join(library.extra_objects) - flags += " " + " ".join(f"-l{lib}" for lib in library.libraries) - flags += " " + " ".join(f"-L{lib}" for lib in library.library_dirs) flags += " " + " ".join(f"-D{macro}" for macro in library.define_macros) flags += " " + " ".join(f"-U{macro}" for macro in library.undef_macros) - flags += f" -o {library.name}.so" elif self.toolchain == "msvc": flags = f"/I{get_path('include')} " flags += " ".join(f"/I{d}" for d in library.include_dirs) @@ -98,7 +104,44 @@ def get_compile_flags(self, library: HatchCppLibrary) -> str: flags += " " + " ".join(library.extra_objects) 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 /LD" + flags += " /EHsc /DWIN32" + # clean + while flags.count(" "): + flags = flags.replace(" ", " ") + return flags + + def get_link_flags(self, library: HatchCppLibrary, build_type: BuildType = "release") -> str: + flags = "" + if self.toolchain == "gcc": + flags += " -shared" + flags += " " + " ".join(library.extra_link_args) + flags += " " + " ".join(library.extra_objects) + flags += " " + " ".join(f"-l{lib}" for lib in library.libraries) + flags += " " + " ".join(f"-L{lib}" for lib in library.library_dirs) + flags += f" -o {library.name}.so" + if self.platform == "darwin": + flags += " -undefined dynamic_lookup" + if "mold" in self.ld: + flags += f" -fuse-ld={self.ld}" + elif "lld" in self.ld: + flags += " -fuse-ld=lld" + elif self.toolchain == "clang": + flags += " -shared" + flags += " " + " ".join(library.extra_link_args) + flags += " " + " ".join(library.extra_objects) + flags += " " + " ".join(f"-l{lib}" for lib in library.libraries) + flags += " " + " ".join(f"-L{lib}" for lib in library.library_dirs) + flags += f" -o {library.name}.so" + if self.platform == "darwin": + flags += " -undefined dynamic_lookup" + if "mold" in self.ld: + flags += f" -fuse-ld={self.ld}" + elif "lld" in self.ld: + flags += " -fuse-ld=lld" + elif self.toolchain == "msvc": + 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" @@ -111,13 +154,9 @@ def get_compile_flags(self, library: HatchCppLibrary) -> str: flags = flags.replace(" ", " ") return flags - def get_link_flags(self, library: HatchCppLibrary) -> str: - # TODO - flags = "" - return flags - class HatchCppBuildPlan(BaseModel): + build_type: BuildType = "release" libraries: List[HatchCppLibrary] = Field(default_factory=list) platform: HatchCppPlatform = Field(default_factory=HatchCppPlatform.default) commands: List[str] = Field(default_factory=list) @@ -125,8 +164,11 @@ class HatchCppBuildPlan(BaseModel): def generate(self): self.commands = [] for library in self.libraries: - flags = self.platform.get_compile_flags(library) - self.commands.append(f"{self.platform.cc} {' '.join(library.sources)} {flags}") + compile_flags = self.platform.get_compile_flags(library, self.build_type) + link_flags = self.platform.get_link_flags(library, self.build_type) + self.commands.append( + f"{self.platform.cc if library.language == 'c' else self.platform.cxx} {' '.join(library.sources)} {compile_flags} {link_flags}" + ) return self.commands def execute(self): @@ -148,9 +190,3 @@ class HatchCppBuildConfig(BaseModel): verbose: Optional[bool] = Field(default=False) libraries: List[HatchCppLibrary] = Field(default_factory=list) platform: Optional[HatchCppPlatform] = Field(default_factory=HatchCppPlatform.default) - - # build_function: str | None = None - # build_kwargs: t.Mapping[str, str] = field(default_factory=dict) - # editable_build_kwargs: t.Mapping[str, str] = field(default_factory=dict) - # ensured_targets: list[str] = field(default_factory=list) - # skip_if_exists: list[str] = field(default_factory=list) diff --git a/hatch_cpp/tests/test_projects.py b/hatch_cpp/tests/test_projects.py index 4d5a39c..d05755a 100644 --- a/hatch_cpp/tests/test_projects.py +++ b/hatch_cpp/tests/test_projects.py @@ -1,7 +1,7 @@ from os import listdir from pathlib import Path from shutil import rmtree -from subprocess import check_output +from subprocess import check_call from sys import path, platform @@ -9,7 +9,7 @@ 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) - check_output( + check_call( [ "hatchling", "build", @@ -30,7 +30,7 @@ def test_basic(self): 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_output( + check_call( [ "hatchling", "build", diff --git a/pyproject.toml b/pyproject.toml index 452e4af..0218232 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -94,7 +94,7 @@ exclude_also = [ "@(abc\\.)?abstractmethod", ] ignore_errors = true -fail_under = 75 +fail_under = 70 [tool.hatch.build] artifacts = [] 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 4/5] 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", ] From 4efa073d7b93b5a0a528bc8332ac66329feacb1c Mon Sep 17 00:00:00 2001 From: Tim Paine <3105306+timkpaine@users.noreply.github.com> Date: Fri, 10 Jan 2025 15:41:24 -0500 Subject: [PATCH 5/5] =?UTF-8?q?Bump=20version:=200.1.1=20=E2=86=92=200.1.2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- hatch_cpp/__init__.py | 2 +- pyproject.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/hatch_cpp/__init__.py b/hatch_cpp/__init__.py index 88f1932..4007071 100644 --- a/hatch_cpp/__init__.py +++ b/hatch_cpp/__init__.py @@ -1,4 +1,4 @@ -__version__ = "0.1.1" +__version__ = "0.1.2" from .hooks import hatch_register_build_hook from .plugin import HatchCppBuildHook diff --git a/pyproject.toml b/pyproject.toml index 174e84e..34a0153 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ authors = [{name = "the hatch-cpp authors", email = "t.paine154@gmail.com"}] description = "Hatch plugin for C++ builds" readme = "README.md" license = { text = "Apache-2.0" } -version = "0.1.1" +version = "0.1.2" requires-python = ">=3.9" keywords = [ "hatch", @@ -62,7 +62,7 @@ Repository = "https://github.com/python-project-templates/hatch-cpp" Homepage = "https://github.com/python-project-templates/hatch-cpp" [tool.bumpversion] -current_version = "0.1.1" +current_version = "0.1.2" commit = true tag = false