diff --git a/README.md b/README.md index d47e19c..1f26618 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 = "project/extension", sources = ["cpp/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/__init__.py b/hatch_cpp/__init__.py index 485f44a..4007071 100644 --- a/hatch_cpp/__init__.py +++ b/hatch_cpp/__init__.py @@ -1 +1,5 @@ -__version__ = "0.1.1" +__version__ = "0.1.2" + +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 0de620f..469ee39 100644 --- a/hatch_cpp/plugin.py +++ b/hatch_cpp/plugin.py @@ -3,11 +3,11 @@ 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 +from .utils import import_string __all__ = ("HatchCppBuildHook",) @@ -20,70 +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 - 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) - - 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() - 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() - - # 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 + # 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 + + # 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.warning(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 cc9c99c..26f846d 100644 --- a/hatch_cpp/structs.py +++ b/hatch_cpp/structs.py @@ -1,13 +1,13 @@ from __future__ import annotations -from dataclasses import dataclass, field 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 Literal +from typing import List, Literal, Optional -from hatchling.builders.config import BuilderConfig +from pydantic import BaseModel, Field __all__ = ( "HatchCppBuildConfig", @@ -16,53 +16,47 @@ "HatchCppBuildPlan", ) -Platform = Literal["linux", "darwin", "win32"] +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++"}, - "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"}, } -@dataclass -class HatchCppBuildConfig(BuilderConfig): - """Build config values for Hatch C++ Builder.""" +class HatchCppLibrary(BaseModel): + """A C++ library.""" - 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) + name: str + sources: List[str] + language: Language = "c++" + binding: Binding = "cpython" + std: Optional[str] = None -@dataclass -class HatchCppLibrary(object): - """A C++ library.""" + 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) - name: str - sources: list[str] + 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") - 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) + 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 + ld: str platform: Platform toolchain: CompilerToolchain @@ -71,6 +65,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: @@ -79,44 +74,103 @@ 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 = "" + + # 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 -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" + 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 += " -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" + 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) 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 += f" /Fo:{library.name}.obj" + flags += " /EHsc /DWIN32" + if library.std: + flags += f" /std:{library.std}" + # 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" /Fe:{library.name}.pyd" flags += " /link /DLL" if (Path(executable).parent / "libs").exists(): @@ -128,22 +182,21 @@ def get_compile_flags(self, library: HatchCppLibrary) -> str: flags = flags.replace(" ", " ") return flags - def get_link_flags(self, library: HatchCppLibrary) -> str: - flags = "" - 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): + 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) 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): @@ -153,7 +206,13 @@ 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): + """Build config values for Hatch C++ Builder.""" + + verbose: Optional[bool] = Field(default=False) + libraries: List[HatchCppLibrary] = Field(default_factory=list) + platform: Optional[HatchCppPlatform] = Field(default_factory=HatchCppPlatform.default) diff --git a/hatch_cpp/tests/test_project_basic.py b/hatch_cpp/tests/test_project_basic.py deleted file mode 100644 index 03de9f9..0000000 --- a/hatch_cpp/tests/test_project_basic.py +++ /dev/null @@ -1,28 +0,0 @@ -from os import listdir -from pathlib import Path -from shutil import rmtree -from subprocess import check_output -from sys import path, platform - - -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( - [ - "hatchling", - "build", - "--hooks-only", - ], - cwd="hatch_cpp/tests/test_project_basic", - ) - 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" 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_nanobind/project/__init__.py b/hatch_cpp/tests/test_project_nanobind/project/__init__.py new file mode 100644 index 0000000..e69de29 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/project/basic.cpp b/hatch_cpp/tests/test_project_override_classes/cpp/project/basic.cpp new file mode 100644 index 0000000..db4432a --- /dev/null +++ b/hatch_cpp/tests/test_project_override_classes/cpp/project/basic.cpp @@ -0,0 +1,5 @@ +#include "project/basic.hpp" + +PyObject* hello(PyObject*, PyObject*) { + return PyUnicode_FromString("A string"); +} diff --git a/hatch_cpp/tests/test_project_override_classes/cpp/project/basic.hpp b/hatch_cpp/tests/test_project_override_classes/cpp/project/basic.hpp new file mode 100644 index 0000000..65cb62e --- /dev/null +++ b/hatch_cpp/tests/test_project_override_classes/cpp/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/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 new file mode 100644 index 0000000..57fd83e --- /dev/null +++ b/hatch_cpp/tests/test_project_override_classes/pyproject.toml @@ -0,0 +1,37 @@ +[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] +build-config-class = "hatch_cpp.HatchCppBuildConfig" +build-plan-class = "hatch_cpp.HatchCppBuildPlan" +verbose = true +libraries = [ + {name = "project/extension", sources = ["cpp/project/basic.cpp"], include-dirs = ["cpp"]} +] 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 new file mode 100644 index 0000000..34e5bf7 --- /dev/null +++ b/hatch_cpp/tests/test_projects.py @@ -0,0 +1,41 @@ +from os import listdir +from pathlib import Path +from shutil import rmtree +from subprocess import check_call +from sys import modules, path, platform + +import pytest + + +class TestProject: + @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=f"hatch_cpp/tests/{project}", + ) + + # assert built + + if platform == "win32": + assert "extension.pyd" in listdir(f"hatch_cpp/tests/{project}/project") + else: + assert "extension.so" in listdir(f"hatch_cpp/tests/{project}/project") + + # import + here = Path(__file__).parent / project + path.insert(0, str(here)) + import project.extension + + assert 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 451b580..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", @@ -33,6 +33,7 @@ classifiers = [ dependencies = [ "hatchling>=1.20", + "pydantic", ] [project.optional-dependencies] @@ -44,6 +45,8 @@ develop = [ "twine", "wheel", # test + "nanobind", + "pybind11", "pytest", "pytest-cov", ] @@ -51,15 +54,15 @@ 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" 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 @@ -93,7 +96,7 @@ exclude_also = [ "@(abc\\.)?abstractmethod", ] ignore_errors = true -fail_under = 75 +fail_under = 70 [tool.hatch.build] artifacts = []