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"