hatch-cpp 0.1.1__tar.gz → 0.1.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. {hatch_cpp-0.1.1 → hatch_cpp-0.1.2}/PKG-INFO +26 -1
  2. {hatch_cpp-0.1.1 → hatch_cpp-0.1.2}/README.md +22 -0
  3. hatch_cpp-0.1.2/hatch_cpp/__init__.py +5 -0
  4. hatch_cpp-0.1.2/hatch_cpp/plugin.py +67 -0
  5. hatch_cpp-0.1.2/hatch_cpp/structs.py +218 -0
  6. {hatch_cpp-0.1.1/hatch_cpp/tests/test_project_basic/cpp/basic-project → hatch_cpp-0.1.2/hatch_cpp/tests/test_project_basic/cpp/project}/basic.cpp +1 -1
  7. hatch_cpp-0.1.2/hatch_cpp/tests/test_project_basic/pyproject.toml +35 -0
  8. hatch_cpp-0.1.2/hatch_cpp/tests/test_project_nanobind/cpp/project/basic.cpp +2 -0
  9. hatch_cpp-0.1.2/hatch_cpp/tests/test_project_nanobind/cpp/project/basic.hpp +7 -0
  10. hatch_cpp-0.1.2/hatch_cpp/tests/test_project_nanobind/pyproject.toml +35 -0
  11. hatch_cpp-0.1.2/hatch_cpp/tests/test_project_override_classes/cpp/project/basic.cpp +5 -0
  12. hatch_cpp-0.1.2/hatch_cpp/tests/test_project_override_classes/cpp/project/basic.hpp +17 -0
  13. hatch_cpp-0.1.2/hatch_cpp/tests/test_project_override_classes/pyproject.toml +37 -0
  14. hatch_cpp-0.1.2/hatch_cpp/tests/test_project_pybind/cpp/project/basic.cpp +6 -0
  15. hatch_cpp-0.1.2/hatch_cpp/tests/test_project_pybind/cpp/project/basic.hpp +9 -0
  16. hatch_cpp-0.1.2/hatch_cpp/tests/test_project_pybind/project/__init__.py +0 -0
  17. hatch_cpp-0.1.2/hatch_cpp/tests/test_project_pybind/pyproject.toml +35 -0
  18. hatch_cpp-0.1.2/hatch_cpp/tests/test_projects.py +41 -0
  19. hatch_cpp-0.1.2/hatch_cpp/toolchains/__init__.py +0 -0
  20. hatch_cpp-0.1.2/hatch_cpp/toolchains/cmake.py +0 -0
  21. {hatch_cpp-0.1.1 → hatch_cpp-0.1.2}/hatch_cpp/utils.py +12 -0
  22. {hatch_cpp-0.1.1 → hatch_cpp-0.1.2}/pyproject.toml +8 -5
  23. hatch_cpp-0.1.1/hatch_cpp/__init__.py +0 -1
  24. hatch_cpp-0.1.1/hatch_cpp/__main__.py +0 -4
  25. hatch_cpp-0.1.1/hatch_cpp/plugin.py +0 -89
  26. hatch_cpp-0.1.1/hatch_cpp/structs.py +0 -159
  27. hatch_cpp-0.1.1/hatch_cpp/tests/test_project_basic/pyproject.toml +0 -63
  28. hatch_cpp-0.1.1/hatch_cpp/tests/test_project_basic.py +0 -28
  29. {hatch_cpp-0.1.1 → hatch_cpp-0.1.2}/.gitignore +0 -0
  30. {hatch_cpp-0.1.1 → hatch_cpp-0.1.2}/LICENSE +0 -0
  31. {hatch_cpp-0.1.1 → hatch_cpp-0.1.2}/hatch_cpp/hooks.py +0 -0
  32. {hatch_cpp-0.1.1 → hatch_cpp-0.1.2}/hatch_cpp/tests/test_all.py +0 -0
  33. {hatch_cpp-0.1.1/hatch_cpp/tests/test_project_basic/cpp/basic-project → hatch_cpp-0.1.2/hatch_cpp/tests/test_project_basic/cpp/project}/basic.hpp +0 -0
  34. {hatch_cpp-0.1.1/hatch_cpp/tests/test_project_basic/basic_project → hatch_cpp-0.1.2/hatch_cpp/tests/test_project_basic/project}/__init__.py +0 -0
  35. {hatch_cpp-0.1.1/hatch_cpp/toolchains → hatch_cpp-0.1.2/hatch_cpp/tests/test_project_nanobind/project}/__init__.py +0 -0
  36. /hatch_cpp-0.1.1/hatch_cpp/toolchains/cmake.py → /hatch_cpp-0.1.2/hatch_cpp/tests/test_project_override_classes/project/__init__.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: hatch-cpp
3
- Version: 0.1.1
3
+ Version: 0.1.2
4
4
  Summary: Hatch plugin for C++ builds
5
5
  Project-URL: Repository, https://github.com/python-project-templates/hatch-cpp
6
6
  Project-URL: Homepage, https://github.com/python-project-templates/hatch-cpp
@@ -20,10 +20,13 @@ Classifier: Programming Language :: Python :: Implementation :: CPython
20
20
  Classifier: Programming Language :: Python :: Implementation :: PyPy
21
21
  Requires-Python: >=3.9
22
22
  Requires-Dist: hatchling>=1.20
23
+ Requires-Dist: pydantic
23
24
  Provides-Extra: develop
24
25
  Requires-Dist: build; extra == 'develop'
25
26
  Requires-Dist: bump-my-version; extra == 'develop'
26
27
  Requires-Dist: check-manifest; extra == 'develop'
28
+ Requires-Dist: nanobind; extra == 'develop'
29
+ Requires-Dist: pybind11; extra == 'develop'
27
30
  Requires-Dist: pytest; extra == 'develop'
28
31
  Requires-Dist: pytest-cov; extra == 'develop'
29
32
  Requires-Dist: ruff<0.9,>=0.3; extra == 'develop'
@@ -42,5 +45,27 @@ Hatch plugin for C++ builds
42
45
 
43
46
  ## Overview
44
47
 
48
+ A simple, extensible C++ build plugin for [hatch](https://hatch.pypa.io/latest/).
49
+
50
+ ```toml
51
+ [tool.hatch.build.hooks.hatch-cpp]
52
+ libraries = [
53
+ {name = "project/extension", sources = ["cpp/project/basic.cpp"], include-dirs = ["cpp"]}
54
+ ]
55
+ ```
56
+
57
+ For more complete systems, see:
58
+ - [scikit-build-core](https://github.com/scikit-build/scikit-build-core)
59
+ - [setuptools](https://setuptools.pypa.io/en/latest/userguide/ext_modules.html)
60
+
61
+ ## Environment Variables
62
+ | Name | Default | Description |
63
+ |:-----|:--------|:------------|
64
+ |`CC`| | |
65
+ |`CXX`| | |
66
+ |`LD`| | |
67
+ |`HATCH_CPP_PLATFORM`| | |
68
+ |`HATCH_CPP_DISABLE_CCACHE`| | |
69
+
45
70
  > [!NOTE]
46
71
  > 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).
@@ -9,5 +9,27 @@ Hatch plugin for C++ builds
9
9
 
10
10
  ## Overview
11
11
 
12
+ A simple, extensible C++ build plugin for [hatch](https://hatch.pypa.io/latest/).
13
+
14
+ ```toml
15
+ [tool.hatch.build.hooks.hatch-cpp]
16
+ libraries = [
17
+ {name = "project/extension", sources = ["cpp/project/basic.cpp"], include-dirs = ["cpp"]}
18
+ ]
19
+ ```
20
+
21
+ For more complete systems, see:
22
+ - [scikit-build-core](https://github.com/scikit-build/scikit-build-core)
23
+ - [setuptools](https://setuptools.pypa.io/en/latest/userguide/ext_modules.html)
24
+
25
+ ## Environment Variables
26
+ | Name | Default | Description |
27
+ |:-----|:--------|:------------|
28
+ |`CC`| | |
29
+ |`CXX`| | |
30
+ |`LD`| | |
31
+ |`HATCH_CPP_PLATFORM`| | |
32
+ |`HATCH_CPP_DISABLE_CCACHE`| | |
33
+
12
34
  > [!NOTE]
13
35
  > 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).
@@ -0,0 +1,5 @@
1
+ __version__ = "0.1.2"
2
+
3
+ from .hooks import hatch_register_build_hook
4
+ from .plugin import HatchCppBuildHook
5
+ from .structs import *
@@ -0,0 +1,67 @@
1
+ from __future__ import annotations
2
+
3
+ import logging
4
+ import os
5
+ import typing as t
6
+
7
+ from hatchling.builders.hooks.plugin.interface import BuildHookInterface
8
+
9
+ from .structs import HatchCppBuildConfig, HatchCppBuildPlan
10
+ from .utils import import_string
11
+
12
+ __all__ = ("HatchCppBuildHook",)
13
+
14
+
15
+ class HatchCppBuildHook(BuildHookInterface[HatchCppBuildConfig]):
16
+ """The hatch-cpp build hook."""
17
+
18
+ PLUGIN_NAME = "hatch-cpp"
19
+ _logger = logging.getLogger(__name__)
20
+
21
+ def initialize(self, version: str, _: dict[str, t.Any]) -> None:
22
+ """Initialize the plugin."""
23
+ # Log some basic information
24
+ self._logger.info("Initializing hatch-cpp plugin version %s", version)
25
+ self._logger.info("Running hatch-cpp")
26
+
27
+ # Only run if creating wheel
28
+ # TODO: Add support for specify sdist-plan
29
+ if self.target_name != "wheel":
30
+ self._logger.info("ignoring target name %s", self.target_name)
31
+ return
32
+
33
+ # Skip if SKIP_HATCH_CPP is set
34
+ # TODO: Support CLI once https://github.com/pypa/hatch/pull/1743
35
+ if os.getenv("SKIP_HATCH_CPP"):
36
+ self._logger.info("Skipping the build hook since SKIP_HATCH_CPP was set")
37
+ return
38
+
39
+ # Get build config class or use default
40
+ build_config_class = import_string(self.config["build-config-class"]) if "build-config-class" in self.config else HatchCppBuildConfig
41
+
42
+ # Instantiate build config
43
+ config = build_config_class(**self.config)
44
+
45
+ # Grab libraries and platform
46
+ libraries = config.libraries
47
+ platform = config.platform
48
+
49
+ # Get build plan class or use default
50
+ build_plan_class = import_string(self.config["build-plan-class"]) if "build-plan-class" in self.config else HatchCppBuildPlan
51
+
52
+ # Instantiate builder
53
+ build_plan = build_plan_class(libraries=libraries, platform=platform)
54
+
55
+ # Generate commands
56
+ build_plan.generate()
57
+
58
+ # Log commands if in verbose mode
59
+ if config.verbose:
60
+ for command in build_plan.commands:
61
+ self._logger.warning(command)
62
+
63
+ # Execute build plan
64
+ build_plan.execute()
65
+
66
+ # Perform any cleanup actions
67
+ build_plan.cleanup()
@@ -0,0 +1,218 @@
1
+ from __future__ import annotations
2
+
3
+ from os import environ, system
4
+ from pathlib import Path
5
+ from shutil import which
6
+ from sys import executable, platform as sys_platform
7
+ from sysconfig import get_path
8
+ from typing import List, Literal, Optional
9
+
10
+ from pydantic import BaseModel, Field
11
+
12
+ __all__ = (
13
+ "HatchCppBuildConfig",
14
+ "HatchCppLibrary",
15
+ "HatchCppPlatform",
16
+ "HatchCppBuildPlan",
17
+ )
18
+
19
+ BuildType = Literal["debug", "release"]
20
+ CompilerToolchain = Literal["gcc", "clang", "msvc"]
21
+ Language = Literal["c", "c++"]
22
+ Binding = Literal["cpython", "pybind11", "nanobind"]
23
+ Platform = Literal["linux", "darwin", "win32"]
24
+ PlatformDefaults = {
25
+ "linux": {"CC": "gcc", "CXX": "g++", "LD": "ld"},
26
+ "darwin": {"CC": "clang", "CXX": "clang++", "LD": "ld"},
27
+ "win32": {"CC": "cl", "CXX": "cl", "LD": "link"},
28
+ }
29
+
30
+
31
+ class HatchCppLibrary(BaseModel):
32
+ """A C++ library."""
33
+
34
+ name: str
35
+ sources: List[str]
36
+ language: Language = "c++"
37
+
38
+ binding: Binding = "cpython"
39
+ std: Optional[str] = None
40
+
41
+ include_dirs: List[str] = Field(default_factory=list, alias="include-dirs")
42
+ library_dirs: List[str] = Field(default_factory=list, alias="library-dirs")
43
+ libraries: List[str] = Field(default_factory=list)
44
+
45
+ extra_compile_args: List[str] = Field(default_factory=list, alias="extra-compile-args")
46
+ extra_link_args: List[str] = Field(default_factory=list, alias="extra-link-args")
47
+ extra_objects: List[str] = Field(default_factory=list, alias="extra-objects")
48
+
49
+ define_macros: List[str] = Field(default_factory=list, alias="define-macros")
50
+ undef_macros: List[str] = Field(default_factory=list, alias="undef-macros")
51
+
52
+ export_symbols: List[str] = Field(default_factory=list, alias="export-symbols")
53
+ depends: List[str] = Field(default_factory=list)
54
+
55
+
56
+ class HatchCppPlatform(BaseModel):
57
+ cc: str
58
+ cxx: str
59
+ ld: str
60
+ platform: Platform
61
+ toolchain: CompilerToolchain
62
+
63
+ @staticmethod
64
+ def default() -> HatchCppPlatform:
65
+ platform = environ.get("HATCH_CPP_PLATFORM", sys_platform)
66
+ CC = environ.get("CC", PlatformDefaults[platform]["CC"])
67
+ CXX = environ.get("CXX", PlatformDefaults[platform]["CXX"])
68
+ LD = environ.get("LD", PlatformDefaults[platform]["LD"])
69
+ if "gcc" in CC and "g++" in CXX:
70
+ toolchain = "gcc"
71
+ elif "clang" in CC and "clang++" in CXX:
72
+ toolchain = "clang"
73
+ elif "cl" in CC and "cl" in CXX:
74
+ toolchain = "msvc"
75
+ else:
76
+ raise Exception(f"Unrecognized toolchain: {CC}, {CXX}")
77
+
78
+ # Customizations
79
+ if which("ccache") and not environ.get("HATCH_CPP_DISABLE_CCACHE"):
80
+ CC = f"ccache {CC}"
81
+ CXX = f"ccache {CXX}"
82
+
83
+ # https://github.com/rui314/mold/issues/647
84
+ # if which("ld.mold"):
85
+ # LD = which("ld.mold")
86
+ # elif which("ld.lld"):
87
+ # LD = which("ld.lld")
88
+ return HatchCppPlatform(cc=CC, cxx=CXX, ld=LD, platform=platform, toolchain=toolchain)
89
+
90
+ def get_compile_flags(self, library: HatchCppLibrary, build_type: BuildType = "release") -> str:
91
+ flags = ""
92
+
93
+ # Python.h
94
+ library.include_dirs.append(get_path("include"))
95
+
96
+ if library.binding == "pybind11":
97
+ import pybind11
98
+
99
+ library.include_dirs.append(pybind11.get_include())
100
+ if not library.std:
101
+ library.std = "c++11"
102
+ elif library.binding == "nanobind":
103
+ import nanobind
104
+
105
+ library.include_dirs.append(nanobind.include_dir())
106
+ if not library.std:
107
+ library.std = "c++17"
108
+ library.sources.append(str(Path(nanobind.include_dir()).parent / "src" / "nb_combined.cpp"))
109
+ library.include_dirs.append(str((Path(nanobind.include_dir()).parent / "ext" / "robin_map" / "include")))
110
+
111
+ if self.toolchain == "gcc":
112
+ flags += " " + " ".join(f"-I{d}" for d in library.include_dirs)
113
+ flags += " -fPIC"
114
+ flags += " " + " ".join(library.extra_compile_args)
115
+ flags += " " + " ".join(f"-D{macro}" for macro in library.define_macros)
116
+ flags += " " + " ".join(f"-U{macro}" for macro in library.undef_macros)
117
+ if library.std:
118
+ flags += f" -std={library.std}"
119
+ elif self.toolchain == "clang":
120
+ flags += " ".join(f"-I{d}" for d in library.include_dirs)
121
+ flags += " -fPIC"
122
+ flags += " " + " ".join(library.extra_compile_args)
123
+ flags += " " + " ".join(f"-D{macro}" for macro in library.define_macros)
124
+ flags += " " + " ".join(f"-U{macro}" for macro in library.undef_macros)
125
+ if library.std:
126
+ flags += f" -std={library.std}"
127
+ elif self.toolchain == "msvc":
128
+ flags += " ".join(f"/I{d}" for d in library.include_dirs)
129
+ flags += " " + " ".join(library.extra_compile_args)
130
+ flags += " " + " ".join(library.extra_link_args)
131
+ flags += " " + " ".join(library.extra_objects)
132
+ flags += " " + " ".join(f"/D{macro}" for macro in library.define_macros)
133
+ flags += " " + " ".join(f"/U{macro}" for macro in library.undef_macros)
134
+ flags += " /EHsc /DWIN32"
135
+ if library.std:
136
+ flags += f" /std:{library.std}"
137
+ # clean
138
+ while flags.count(" "):
139
+ flags = flags.replace(" ", " ")
140
+ return flags
141
+
142
+ def get_link_flags(self, library: HatchCppLibrary, build_type: BuildType = "release") -> str:
143
+ flags = ""
144
+ if self.toolchain == "gcc":
145
+ flags += " -shared"
146
+ flags += " " + " ".join(library.extra_link_args)
147
+ flags += " " + " ".join(library.extra_objects)
148
+ flags += " " + " ".join(f"-l{lib}" for lib in library.libraries)
149
+ flags += " " + " ".join(f"-L{lib}" for lib in library.library_dirs)
150
+ flags += f" -o {library.name}.so"
151
+ if self.platform == "darwin":
152
+ flags += " -undefined dynamic_lookup"
153
+ if "mold" in self.ld:
154
+ flags += f" -fuse-ld={self.ld}"
155
+ elif "lld" in self.ld:
156
+ flags += " -fuse-ld=lld"
157
+ elif self.toolchain == "clang":
158
+ flags += " -shared"
159
+ flags += " " + " ".join(library.extra_link_args)
160
+ flags += " " + " ".join(library.extra_objects)
161
+ flags += " " + " ".join(f"-l{lib}" for lib in library.libraries)
162
+ flags += " " + " ".join(f"-L{lib}" for lib in library.library_dirs)
163
+ flags += f" -o {library.name}.so"
164
+ if self.platform == "darwin":
165
+ flags += " -undefined dynamic_lookup"
166
+ if "mold" in self.ld:
167
+ flags += f" -fuse-ld={self.ld}"
168
+ elif "lld" in self.ld:
169
+ flags += " -fuse-ld=lld"
170
+ elif self.toolchain == "msvc":
171
+ flags += " " + " ".join(library.extra_link_args)
172
+ flags += " " + " ".join(library.extra_objects)
173
+ flags += " /LD"
174
+ flags += f" /Fe:{library.name}.pyd"
175
+ flags += " /link /DLL"
176
+ if (Path(executable).parent / "libs").exists():
177
+ flags += f" /LIBPATH:{str(Path(executable).parent / 'libs')}"
178
+ flags += " " + " ".join(f"{lib}.lib" for lib in library.libraries)
179
+ flags += " " + " ".join(f"/LIBPATH:{lib}" for lib in library.library_dirs)
180
+ # clean
181
+ while flags.count(" "):
182
+ flags = flags.replace(" ", " ")
183
+ return flags
184
+
185
+
186
+ class HatchCppBuildPlan(BaseModel):
187
+ build_type: BuildType = "release"
188
+ libraries: List[HatchCppLibrary] = Field(default_factory=list)
189
+ platform: HatchCppPlatform = Field(default_factory=HatchCppPlatform.default)
190
+ commands: List[str] = Field(default_factory=list)
191
+
192
+ def generate(self):
193
+ self.commands = []
194
+ for library in self.libraries:
195
+ compile_flags = self.platform.get_compile_flags(library, self.build_type)
196
+ link_flags = self.platform.get_link_flags(library, self.build_type)
197
+ self.commands.append(
198
+ f"{self.platform.cc if library.language == 'c' else self.platform.cxx} {' '.join(library.sources)} {compile_flags} {link_flags}"
199
+ )
200
+ return self.commands
201
+
202
+ def execute(self):
203
+ for command in self.commands:
204
+ system(command)
205
+ return self.commands
206
+
207
+ def cleanup(self):
208
+ if self.platform.platform == "win32":
209
+ for temp_obj in Path(".").glob("*.obj"):
210
+ temp_obj.unlink()
211
+
212
+
213
+ class HatchCppBuildConfig(BaseModel):
214
+ """Build config values for Hatch C++ Builder."""
215
+
216
+ verbose: Optional[bool] = Field(default=False)
217
+ libraries: List[HatchCppLibrary] = Field(default_factory=list)
218
+ platform: Optional[HatchCppPlatform] = Field(default_factory=HatchCppPlatform.default)
@@ -1,4 +1,4 @@
1
- #include "basic-project/basic.hpp"
1
+ #include "project/basic.hpp"
2
2
 
3
3
  PyObject* hello(PyObject*, PyObject*) {
4
4
  return PyUnicode_FromString("A string");
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.20"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "hatch-cpp-test-project-basic"
7
+ description = "Basic test project for hatch-cpp"
8
+ version = "0.1.0"
9
+ requires-python = ">=3.9"
10
+ dependencies = [
11
+ "hatchling>=1.20",
12
+ "hatch-cpp",
13
+ ]
14
+
15
+ [tool.hatch.build]
16
+ artifacts = [
17
+ "project/*.dll",
18
+ "project/*.dylib",
19
+ "project/*.so",
20
+ ]
21
+
22
+ [tool.hatch.build.sources]
23
+ src = "/"
24
+
25
+ [tool.hatch.build.targets.sdist]
26
+ packages = ["project"]
27
+
28
+ [tool.hatch.build.targets.wheel]
29
+ packages = ["project"]
30
+
31
+ [tool.hatch.build.hooks.hatch-cpp]
32
+ verbose = true
33
+ libraries = [
34
+ {name = "project/extension", sources = ["cpp/project/basic.cpp"], include-dirs = ["cpp"]}
35
+ ]
@@ -0,0 +1,2 @@
1
+ #include "project/basic.hpp"
2
+
@@ -0,0 +1,7 @@
1
+ #pragma once
2
+ #include <nanobind/nanobind.h>
3
+ #include <nanobind/stl/string.h>
4
+
5
+ NB_MODULE(extension, m) {
6
+ m.def("hello", []() { return "A string"; });
7
+ }
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.20"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "hatch-cpp-test-project-basic"
7
+ description = "Basic test project for hatch-cpp"
8
+ version = "0.1.0"
9
+ requires-python = ">=3.9"
10
+ dependencies = [
11
+ "hatchling>=1.20",
12
+ "hatch-cpp",
13
+ ]
14
+
15
+ [tool.hatch.build]
16
+ artifacts = [
17
+ "project/*.dll",
18
+ "project/*.dylib",
19
+ "project/*.so",
20
+ ]
21
+
22
+ [tool.hatch.build.sources]
23
+ src = "/"
24
+
25
+ [tool.hatch.build.targets.sdist]
26
+ packages = ["project"]
27
+
28
+ [tool.hatch.build.targets.wheel]
29
+ packages = ["project"]
30
+
31
+ [tool.hatch.build.hooks.hatch-cpp]
32
+ verbose = true
33
+ libraries = [
34
+ {name = "project/extension", sources = ["cpp/project/basic.cpp"], include-dirs = ["cpp"], binding = "nanobind"},
35
+ ]
@@ -0,0 +1,5 @@
1
+ #include "project/basic.hpp"
2
+
3
+ PyObject* hello(PyObject*, PyObject*) {
4
+ return PyUnicode_FromString("A string");
5
+ }
@@ -0,0 +1,17 @@
1
+ #pragma once
2
+ #include "Python.h"
3
+
4
+ PyObject* hello(PyObject*, PyObject*);
5
+
6
+ static PyMethodDef extension_methods[] = {
7
+ {"hello", (PyCFunction)hello, METH_NOARGS},
8
+ {nullptr, nullptr, 0, nullptr}
9
+ };
10
+
11
+ static PyModuleDef extension_module = {
12
+ PyModuleDef_HEAD_INIT, "extension", "extension", -1, extension_methods};
13
+
14
+ PyMODINIT_FUNC PyInit_extension(void) {
15
+ Py_Initialize();
16
+ return PyModule_Create(&extension_module);
17
+ }
@@ -0,0 +1,37 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.20"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "hatch-cpp-test-project-basic"
7
+ description = "Basic test project for hatch-cpp"
8
+ version = "0.1.0"
9
+ requires-python = ">=3.9"
10
+ dependencies = [
11
+ "hatchling>=1.20",
12
+ "hatch-cpp",
13
+ ]
14
+
15
+ [tool.hatch.build]
16
+ artifacts = [
17
+ "project/*.dll",
18
+ "project/*.dylib",
19
+ "project/*.so",
20
+ ]
21
+
22
+ [tool.hatch.build.sources]
23
+ src = "/"
24
+
25
+ [tool.hatch.build.targets.sdist]
26
+ packages = ["project"]
27
+
28
+ [tool.hatch.build.targets.wheel]
29
+ packages = ["project"]
30
+
31
+ [tool.hatch.build.hooks.hatch-cpp]
32
+ build-config-class = "hatch_cpp.HatchCppBuildConfig"
33
+ build-plan-class = "hatch_cpp.HatchCppBuildPlan"
34
+ verbose = true
35
+ libraries = [
36
+ {name = "project/extension", sources = ["cpp/project/basic.cpp"], include-dirs = ["cpp"]}
37
+ ]
@@ -0,0 +1,6 @@
1
+ #include "project/basic.hpp"
2
+
3
+ std::string hello() {
4
+ return "A string";
5
+ }
6
+
@@ -0,0 +1,9 @@
1
+ #pragma once
2
+ #include <pybind11/pybind11.h>
3
+ #include <string>
4
+
5
+ std::string hello();
6
+
7
+ PYBIND11_MODULE(extension, m) {
8
+ m.def("hello", &hello);
9
+ }
@@ -0,0 +1,35 @@
1
+ [build-system]
2
+ requires = ["hatchling>=1.20"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "hatch-cpp-test-project-basic"
7
+ description = "Basic test project for hatch-cpp"
8
+ version = "0.1.0"
9
+ requires-python = ">=3.9"
10
+ dependencies = [
11
+ "hatchling>=1.20",
12
+ "hatch-cpp",
13
+ ]
14
+
15
+ [tool.hatch.build]
16
+ artifacts = [
17
+ "project/*.dll",
18
+ "project/*.dylib",
19
+ "project/*.so",
20
+ ]
21
+
22
+ [tool.hatch.build.sources]
23
+ src = "/"
24
+
25
+ [tool.hatch.build.targets.sdist]
26
+ packages = ["project"]
27
+
28
+ [tool.hatch.build.targets.wheel]
29
+ packages = ["project"]
30
+
31
+ [tool.hatch.build.hooks.hatch-cpp]
32
+ verbose = true
33
+ libraries = [
34
+ {name = "project/extension", sources = ["cpp/project/basic.cpp"], include-dirs = ["cpp"], binding="pybind11"},
35
+ ]
@@ -0,0 +1,41 @@
1
+ from os import listdir
2
+ from pathlib import Path
3
+ from shutil import rmtree
4
+ from subprocess import check_call
5
+ from sys import modules, path, platform
6
+
7
+ import pytest
8
+
9
+
10
+ class TestProject:
11
+ @pytest.mark.parametrize("project", ["test_project_basic", "test_project_override_classes", "test_project_pybind", "test_project_nanobind"])
12
+ def test_basic(self, project):
13
+ # cleanup
14
+ rmtree(f"hatch_cpp/tests/{project}/project/extension.so", ignore_errors=True)
15
+ rmtree(f"hatch_cpp/tests/{project}/project/extension.pyd", ignore_errors=True)
16
+ modules.pop("project", None)
17
+ modules.pop("project.extension", None)
18
+
19
+ # compile
20
+ check_call(
21
+ [
22
+ "hatchling",
23
+ "build",
24
+ "--hooks-only",
25
+ ],
26
+ cwd=f"hatch_cpp/tests/{project}",
27
+ )
28
+
29
+ # assert built
30
+
31
+ if platform == "win32":
32
+ assert "extension.pyd" in listdir(f"hatch_cpp/tests/{project}/project")
33
+ else:
34
+ assert "extension.so" in listdir(f"hatch_cpp/tests/{project}/project")
35
+
36
+ # import
37
+ here = Path(__file__).parent / project
38
+ path.insert(0, str(here))
39
+ import project.extension
40
+
41
+ assert project.extension.hello() == "A string"
File without changes
File without changes
@@ -1,5 +1,17 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from functools import lru_cache
4
+
5
+ from pydantic import ImportString, TypeAdapter
6
+
7
+ _import_string_adapter = TypeAdapter(ImportString)
8
+
9
+
10
+ @lru_cache(maxsize=None)
11
+ def import_string(input_string: str):
12
+ return _import_string_adapter.validate_python(input_string)
13
+
14
+
3
15
  # import multiprocessing
4
16
  # import os
5
17
  # import os.path
@@ -8,7 +8,7 @@ authors = [{name = "the hatch-cpp authors", email = "t.paine154@gmail.com"}]
8
8
  description = "Hatch plugin for C++ builds"
9
9
  readme = "README.md"
10
10
  license = { text = "Apache-2.0" }
11
- version = "0.1.1"
11
+ version = "0.1.2"
12
12
  requires-python = ">=3.9"
13
13
  keywords = [
14
14
  "hatch",
@@ -33,6 +33,7 @@ classifiers = [
33
33
 
34
34
  dependencies = [
35
35
  "hatchling>=1.20",
36
+ "pydantic",
36
37
  ]
37
38
 
38
39
  [project.optional-dependencies]
@@ -44,6 +45,8 @@ develop = [
44
45
  "twine",
45
46
  "wheel",
46
47
  # test
48
+ "nanobind",
49
+ "pybind11",
47
50
  "pytest",
48
51
  "pytest-cov",
49
52
  ]
@@ -51,15 +54,15 @@ develop = [
51
54
  [project.entry-points.hatch]
52
55
  cpp = "hatch_cpp.hooks"
53
56
 
54
- [project.scripts]
55
- hatch-cpp = "hatch_cpp.cli:main"
57
+ # [project.scripts]
58
+ # hatch-cpp = "hatch_cpp.cli:main"
56
59
 
57
60
  [project.urls]
58
61
  Repository = "https://github.com/python-project-templates/hatch-cpp"
59
62
  Homepage = "https://github.com/python-project-templates/hatch-cpp"
60
63
 
61
64
  [tool.bumpversion]
62
- current_version = "0.1.1"
65
+ current_version = "0.1.2"
63
66
  commit = true
64
67
  tag = false
65
68
 
@@ -93,7 +96,7 @@ exclude_also = [
93
96
  "@(abc\\.)?abstractmethod",
94
97
  ]
95
98
  ignore_errors = true
96
- fail_under = 75
99
+ fail_under = 70
97
100
 
98
101
  [tool.hatch.build]
99
102
  artifacts = []
@@ -1 +0,0 @@
1
- __version__ = "0.1.1"
@@ -1,4 +0,0 @@
1
- from .cli import main
2
-
3
- if __name__ == "__main__":
4
- main()
@@ -1,89 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import logging
4
- import os
5
- import typing as t
6
- from dataclasses import fields
7
-
8
- from hatchling.builders.hooks.plugin.interface import BuildHookInterface
9
-
10
- from .structs import HatchCppBuildConfig, HatchCppBuildPlan, HatchCppLibrary, HatchCppPlatform
11
-
12
- __all__ = ("HatchCppBuildHook",)
13
-
14
-
15
- class HatchCppBuildHook(BuildHookInterface[HatchCppBuildConfig]):
16
- """The hatch-cpp build hook."""
17
-
18
- PLUGIN_NAME = "hatch-cpp"
19
- _logger = logging.getLogger(__name__)
20
-
21
- def initialize(self, version: str, _: dict[str, t.Any]) -> None:
22
- """Initialize the plugin."""
23
- self._logger.info("Running hatch-cpp")
24
-
25
- if self.target_name != "wheel":
26
- self._logger.info("ignoring target name %s", self.target_name)
27
- return
28
-
29
- if os.getenv("SKIP_HATCH_CPP"):
30
- self._logger.info("Skipping the build hook since SKIP_HATCH_CPP was set")
31
- return
32
-
33
- kwargs = {k.replace("-", "_"): v if not isinstance(v, bool) else str(v) for k, v in self.config.items()}
34
- available_fields = [f.name for f in fields(HatchCppBuildConfig)]
35
- for key in list(kwargs):
36
- if key not in available_fields:
37
- del kwargs[key]
38
- config = HatchCppBuildConfig(**kwargs)
39
-
40
- library_kwargs = [
41
- {k.replace("-", "_"): v if not isinstance(v, bool) else str(v) for k, v in library_kwargs.items()} for library_kwargs in config.libraries
42
- ]
43
- libraries = [HatchCppLibrary(**library_kwargs) for library_kwargs in library_kwargs]
44
- platform = HatchCppPlatform.default()
45
- if config.toolchain == "raw":
46
- build_plan = HatchCppBuildPlan(libraries=libraries, platform=platform)
47
- build_plan.generate()
48
- if config.verbose:
49
- for command in build_plan.commands:
50
- self._logger.info(command)
51
- build_plan.execute()
52
- build_plan.cleanup()
53
-
54
- # build_kwargs = config.build_kwargs
55
- # if version == "editable":
56
- # build_kwargs = config.editable_build_kwargs or build_kwargs
57
-
58
- # should_skip_build = False
59
- # if not config.build_function:
60
- # log.warning("No build function found")
61
- # should_skip_build = True
62
-
63
- # elif config.skip_if_exists and version == "standard":
64
- # should_skip_build = should_skip(config.skip_if_exists)
65
- # if should_skip_build:
66
- # log.info("Skip-if-exists file(s) found")
67
-
68
- # # Get build function and call it with normalized parameter names.
69
- # if not should_skip_build and config.build_function:
70
- # build_func = get_build_func(config.build_function)
71
- # build_kwargs = normalize_kwargs(build_kwargs)
72
- # log.info("Building with %s", config.build_function)
73
- # log.info("With kwargs: %s", build_kwargs)
74
- # try:
75
- # build_func(self.target_name, version, **build_kwargs)
76
- # except Exception as e:
77
- # if version == "editable" and config.optional_editable_build.lower() == "true":
78
- # warnings.warn(f"Encountered build error:\n{e}", stacklevel=2)
79
- # else:
80
- # raise e
81
- # else:
82
- # log.info("Skipping build")
83
-
84
- # # Ensure targets in distributable dists.
85
- # if version == "standard":
86
- # ensure_targets(config.ensured_targets)
87
-
88
- self._logger.info("Finished running hatch-cpp")
89
- return
@@ -1,159 +0,0 @@
1
- from __future__ import annotations
2
-
3
- from dataclasses import dataclass, field
4
- from os import environ, system
5
- from pathlib import Path
6
- from sys import executable, platform as sys_platform
7
- from sysconfig import get_path
8
- from typing import Literal
9
-
10
- from hatchling.builders.config import BuilderConfig
11
-
12
- __all__ = (
13
- "HatchCppBuildConfig",
14
- "HatchCppLibrary",
15
- "HatchCppPlatform",
16
- "HatchCppBuildPlan",
17
- )
18
-
19
- Platform = Literal["linux", "darwin", "win32"]
20
- CompilerToolchain = Literal["gcc", "clang", "msvc"]
21
- PlatformDefaults = {
22
- "linux": {"CC": "gcc", "CXX": "g++"},
23
- "darwin": {"CC": "clang", "CXX": "clang++"},
24
- "win32": {"CC": "cl", "CXX": "cl"},
25
- }
26
-
27
-
28
- @dataclass
29
- class HatchCppBuildConfig(BuilderConfig):
30
- """Build config values for Hatch C++ Builder."""
31
-
32
- toolchain: str | None = field(default="raw")
33
- libraries: list[dict[str, str]] = field(default_factory=list)
34
- verbose: bool | None = field(default=False)
35
- # build_function: str | None = None
36
- # build_kwargs: t.Mapping[str, str] = field(default_factory=dict)
37
- # editable_build_kwargs: t.Mapping[str, str] = field(default_factory=dict)
38
- # ensured_targets: list[str] = field(default_factory=list)
39
- # skip_if_exists: list[str] = field(default_factory=list)
40
-
41
-
42
- @dataclass
43
- class HatchCppLibrary(object):
44
- """A C++ library."""
45
-
46
- name: str
47
- sources: list[str]
48
-
49
- include_dirs: list[str] = field(default_factory=list)
50
- library_dirs: list[str] = field(default_factory=list)
51
- libraries: list[str] = field(default_factory=list)
52
- extra_compile_args: list[str] = field(default_factory=list)
53
- extra_link_args: list[str] = field(default_factory=list)
54
- extra_objects: list[str] = field(default_factory=list)
55
- define_macros: list[str] = field(default_factory=list)
56
- undef_macros: list[str] = field(default_factory=list)
57
-
58
- export_symbols: list[str] = field(default_factory=list)
59
- depends: list[str] = field(default_factory=list)
60
-
61
-
62
- @dataclass
63
- class HatchCppPlatform(object):
64
- cc: str
65
- cxx: str
66
- platform: Platform
67
- toolchain: CompilerToolchain
68
-
69
- @staticmethod
70
- def default() -> HatchCppPlatform:
71
- platform = environ.get("HATCH_CPP_PLATFORM", sys_platform)
72
- CC = environ.get("CC", PlatformDefaults[platform]["CC"])
73
- CXX = environ.get("CXX", PlatformDefaults[platform]["CXX"])
74
- if "gcc" in CC and "g++" in CXX:
75
- toolchain = "gcc"
76
- elif "clang" in CC and "clang++" in CXX:
77
- toolchain = "clang"
78
- elif "cl" in CC and "cl" in CXX:
79
- toolchain = "msvc"
80
- else:
81
- raise Exception(f"Unrecognized toolchain: {CC}, {CXX}")
82
- return HatchCppPlatform(cc=CC, cxx=CXX, platform=platform, toolchain=toolchain)
83
-
84
- def get_compile_flags(self, library: HatchCppLibrary) -> str:
85
- flags = ""
86
- if self.toolchain == "gcc":
87
- flags = f"-I{get_path('include')}"
88
- flags += " " + " ".join(f"-I{d}" for d in library.include_dirs)
89
- flags += " -fPIC -shared"
90
- flags += " " + " ".join(library.extra_compile_args)
91
- flags += " " + " ".join(library.extra_link_args)
92
- flags += " " + " ".join(library.extra_objects)
93
- flags += " " + " ".join(f"-l{lib}" for lib in library.libraries)
94
- flags += " " + " ".join(f"-L{lib}" for lib in library.library_dirs)
95
- flags += " " + " ".join(f"-D{macro}" for macro in library.define_macros)
96
- flags += " " + " ".join(f"-U{macro}" for macro in library.undef_macros)
97
- flags += f" -o {library.name}.so"
98
- elif self.toolchain == "clang":
99
- flags = f"-I{get_path('include')} "
100
- flags += " ".join(f"-I{d}" for d in library.include_dirs)
101
- flags += " -undefined dynamic_lookup -fPIC -shared"
102
- flags += " " + " ".join(library.extra_compile_args)
103
- flags += " " + " ".join(library.extra_link_args)
104
- flags += " " + " ".join(library.extra_objects)
105
- flags += " " + " ".join(f"-l{lib}" for lib in library.libraries)
106
- flags += " " + " ".join(f"-L{lib}" for lib in library.library_dirs)
107
- flags += " " + " ".join(f"-D{macro}" for macro in library.define_macros)
108
- flags += " " + " ".join(f"-U{macro}" for macro in library.undef_macros)
109
- flags += f" -o {library.name}.so"
110
- elif self.toolchain == "msvc":
111
- flags = f"/I{get_path('include')} "
112
- flags += " ".join(f"/I{d}" for d in library.include_dirs)
113
- flags += " " + " ".join(library.extra_compile_args)
114
- flags += " " + " ".join(library.extra_link_args)
115
- flags += " " + " ".join(library.extra_objects)
116
- flags += " " + " ".join(f"/D{macro}" for macro in library.define_macros)
117
- flags += " " + " ".join(f"/U{macro}" for macro in library.undef_macros)
118
- flags += " /EHsc /DWIN32 /LD"
119
- flags += f" /Fo:{library.name}.obj"
120
- flags += f" /Fe:{library.name}.pyd"
121
- flags += " /link /DLL"
122
- if (Path(executable).parent / "libs").exists():
123
- flags += f" /LIBPATH:{str(Path(executable).parent / 'libs')}"
124
- flags += " " + " ".join(f"{lib}.lib" for lib in library.libraries)
125
- flags += " " + " ".join(f"/LIBPATH:{lib}" for lib in library.library_dirs)
126
- # clean
127
- while flags.count(" "):
128
- flags = flags.replace(" ", " ")
129
- return flags
130
-
131
- def get_link_flags(self, library: HatchCppLibrary) -> str:
132
- flags = ""
133
- return flags
134
-
135
-
136
- @dataclass
137
- class HatchCppBuildPlan(object):
138
- libraries: list[HatchCppLibrary] = field(default_factory=list)
139
- platform: HatchCppPlatform = field(default_factory=HatchCppPlatform.default)
140
- commands: list[str] = field(default_factory=list)
141
-
142
- def generate(self):
143
- self.commands = []
144
- for library in self.libraries:
145
- flags = self.platform.get_compile_flags(library)
146
- self.commands.append(f"{self.platform.cc} {' '.join(library.sources)} {flags}")
147
- return self.commands
148
-
149
- def execute(self):
150
- for command in self.commands:
151
- system(command)
152
- return self.commands
153
-
154
- def cleanup(self):
155
- if self.platform.platform == "win32":
156
- for library in self.libraries:
157
- temp_obj = Path(f"{library.name}.obj")
158
- if temp_obj.exists():
159
- temp_obj.unlink()
@@ -1,63 +0,0 @@
1
- [build-system]
2
- requires = ["hatchling>=1.20"]
3
- build-backend = "hatchling.build"
4
-
5
- [project]
6
- name = "hatch-cpp-test-project-basic"
7
- description = "Basic test project for hatch-cpp"
8
- version = "0.1.0"
9
- requires-python = ">=3.9"
10
- dependencies = [
11
- "hatchling>=1.20",
12
- "hatch-cpp",
13
- ]
14
-
15
- [tool.hatch.build]
16
- artifacts = [
17
- "basic_project/*.dll",
18
- "basic_project/*.dylib",
19
- "basic_project/*.so",
20
- ]
21
-
22
- [tool.hatch.build.sources]
23
- src = "/"
24
-
25
- [tool.hatch.build.targets.sdist]
26
- packages = ["basic_project"]
27
-
28
- [tool.hatch.build.targets.wheel]
29
- packages = ["basic_project"]
30
-
31
- [tool.hatch.build.hooks.hatch-cpp]
32
- verbose = true
33
- libraries = [
34
- {name = "basic_project/extension", sources = ["cpp/basic-project/basic.cpp"], include-dirs = ["cpp"]}
35
- ]
36
-
37
- # build-function = "hatch_cpp.cpp_builder"
38
-
39
- # [tool.hatch.build.hooks.defaults]
40
- # build-type = "release"
41
-
42
- # [tool.hatch.build.hooks.env-vars]
43
- # TODO: these will all be available via
44
- # CLI after https://github.com/pypa/hatch/pull/1743
45
- # e.g. --hatch-cpp-build-type=debug
46
- # build-type = "BUILD_TYPE"
47
- # ccache = "USE_CCACHE"
48
- # manylinux = "MANYLINUX"
49
- # vcpkg = "USE_VCPKG"
50
-
51
- # [tool.hatch.build.hooks.cmake]
52
-
53
- # [tool.hatch.build.hooks.vcpkg]
54
- # triplets = {linux="x64-linux", macos="x64-osx", windows="x64-windows-static-md"}
55
- # clone = true
56
- # update = true
57
-
58
- # [tool.hatch.build.hooks.hatch-cpp.build-kwargs]
59
- # path = "cpp"
60
-
61
- [tool.pytest.ini_options]
62
- asyncio_mode = "strict"
63
- testpaths = "basic_project/tests"
@@ -1,28 +0,0 @@
1
- from os import listdir
2
- from pathlib import Path
3
- from shutil import rmtree
4
- from subprocess import check_output
5
- from sys import path, platform
6
-
7
-
8
- class TestProject:
9
- def test_basic(self):
10
- rmtree("hatch_cpp/tests/test_project_basic/basic_project/extension.so", ignore_errors=True)
11
- rmtree("hatch_cpp/tests/test_project_basic/basic_project/extension.pyd", ignore_errors=True)
12
- check_output(
13
- [
14
- "hatchling",
15
- "build",
16
- "--hooks-only",
17
- ],
18
- cwd="hatch_cpp/tests/test_project_basic",
19
- )
20
- if platform == "win32":
21
- assert "extension.pyd" in listdir("hatch_cpp/tests/test_project_basic/basic_project")
22
- else:
23
- assert "extension.so" in listdir("hatch_cpp/tests/test_project_basic/basic_project")
24
- here = Path(__file__).parent / "test_project_basic"
25
- path.insert(0, str(here))
26
- import basic_project.extension
27
-
28
- assert basic_project.extension.hello() == "A string"
File without changes
File without changes
File without changes