hatch-cpp 0.5.2__py3-none-any.whl → 0.6.1__py3-none-any.whl
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.
- hatch_cpp/__init__.py +1 -1
- hatch_cpp/config.py +196 -7
- hatch_cpp/plugin.py +12 -2
- hatch_cpp/tests/test_hatch_build.py +3 -2
- hatch_cpp/tests/test_hook_registration.py +2 -1
- hatch_cpp/tests/test_project_c_abi_library/cpp/demo.c +5 -0
- hatch_cpp/tests/test_project_c_abi_library/cpp/extension.c +11 -0
- hatch_cpp/tests/test_project_c_abi_library/cpp/include/demo/demo.h +22 -0
- hatch_cpp/tests/test_project_c_abi_library/project/__init__.py +5 -0
- hatch_cpp/tests/test_project_c_abi_library/pyproject.toml +57 -0
- hatch_cpp/tests/test_project_cmake_c_abi/CMakeLists.txt +28 -0
- hatch_cpp/tests/test_project_cmake_c_abi/cpp/demo.c +5 -0
- hatch_cpp/tests/test_project_cmake_c_abi/cpp/extension.c +11 -0
- hatch_cpp/tests/test_project_cmake_c_abi/cpp/include/demo/demo.h +22 -0
- hatch_cpp/tests/test_project_cmake_c_abi/project/__init__.py +5 -0
- hatch_cpp/tests/test_project_cmake_c_abi/pyproject.toml +50 -0
- hatch_cpp/tests/test_project_pyodide/pyodide_project/extension.pyi +1 -0
- hatch_cpp/tests/test_projects.py +9 -3
- hatch_cpp/tests/test_validation.py +216 -0
- hatch_cpp/tests/test_vcpkg_ref.py +10 -0
- hatch_cpp/toolchains/cmake.py +5 -18
- hatch_cpp/toolchains/common.py +92 -41
- hatch_cpp/toolchains/vcpkg.py +4 -4
- {hatch_cpp-0.5.2.dist-info → hatch_cpp-0.6.1.dist-info}/METADATA +71 -3
- {hatch_cpp-0.5.2.dist-info → hatch_cpp-0.6.1.dist-info}/RECORD +28 -15
- {hatch_cpp-0.5.2.dist-info → hatch_cpp-0.6.1.dist-info}/WHEEL +1 -1
- {hatch_cpp-0.5.2.dist-info → hatch_cpp-0.6.1.dist-info}/entry_points.txt +0 -0
- {hatch_cpp-0.5.2.dist-info → hatch_cpp-0.6.1.dist-info}/licenses/LICENSE +0 -0
hatch_cpp/__init__.py
CHANGED
hatch_cpp/config.py
CHANGED
|
@@ -1,12 +1,33 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
from importlib.machinery import EXTENSION_SUFFIXES
|
|
3
4
|
from os import environ, system as system_call
|
|
4
5
|
from pathlib import Path
|
|
5
6
|
|
|
7
|
+
from hatch_native import (
|
|
8
|
+
ExternalLibrary,
|
|
9
|
+
LibraryValidationConfig,
|
|
10
|
+
check_exports,
|
|
11
|
+
check_headers,
|
|
12
|
+
prefixed_header_symbols,
|
|
13
|
+
resolve_external_library,
|
|
14
|
+
resolve_library_source,
|
|
15
|
+
run_validation_command,
|
|
16
|
+
shared_library_name,
|
|
17
|
+
write_artifact_manifest,
|
|
18
|
+
)
|
|
6
19
|
from pkn import getSimpleLogger
|
|
7
|
-
from pydantic import BaseModel, Field, model_validator
|
|
20
|
+
from pydantic import AliasChoices, BaseModel, Field, PrivateAttr, model_validator
|
|
8
21
|
|
|
9
|
-
from .toolchains import
|
|
22
|
+
from .toolchains import (
|
|
23
|
+
BuildType,
|
|
24
|
+
HatchCppArtifact,
|
|
25
|
+
HatchCppCmakeConfiguration,
|
|
26
|
+
HatchCppLibrary,
|
|
27
|
+
HatchCppPlatform,
|
|
28
|
+
HatchCppVcpkgConfiguration,
|
|
29
|
+
Toolchain,
|
|
30
|
+
)
|
|
10
31
|
|
|
11
32
|
__all__ = (
|
|
12
33
|
"HatchCppBuildConfig",
|
|
@@ -25,8 +46,20 @@ class HatchCppBuildConfig(BaseModel):
|
|
|
25
46
|
name: str | None = Field(default=None)
|
|
26
47
|
libraries: list[HatchCppLibrary] = Field(default_factory=list)
|
|
27
48
|
cmake: HatchCppCmakeConfiguration | None = Field(default=None)
|
|
28
|
-
platform: HatchCppPlatform
|
|
49
|
+
platform: HatchCppPlatform = Field(default_factory=HatchCppPlatform.default)
|
|
29
50
|
vcpkg: HatchCppVcpkgConfiguration | None = Field(default_factory=HatchCppVcpkgConfiguration)
|
|
51
|
+
library_source: str = Field(
|
|
52
|
+
default="bundled",
|
|
53
|
+
validation_alias=AliasChoices("library-source", "library_source"),
|
|
54
|
+
description="How libraries named in depends are provided: bundled, external, or direct. Overridden by HATCH_CPP_LIBRARY_SOURCE.",
|
|
55
|
+
)
|
|
56
|
+
artifacts: list[HatchCppArtifact] = Field(default_factory=list)
|
|
57
|
+
artifact_manifest: bool = Field(default=False, validation_alias=AliasChoices("artifact-manifest", "artifact_manifest"))
|
|
58
|
+
artifact_manifest_destination: str | None = Field(
|
|
59
|
+
default=None,
|
|
60
|
+
validation_alias=AliasChoices("artifact-manifest-destination", "artifact_manifest_destination"),
|
|
61
|
+
description="Wheel-relative path of the artifact manifest JSON, such as project/lib/hatch-cpp-artifacts.json.",
|
|
62
|
+
)
|
|
30
63
|
|
|
31
64
|
@model_validator(mode="wrap")
|
|
32
65
|
@classmethod
|
|
@@ -50,6 +83,16 @@ class HatchCppBuildConfig(BaseModel):
|
|
|
50
83
|
model = handler(data)
|
|
51
84
|
if model.cmake and model.libraries:
|
|
52
85
|
raise ValueError("Must not provide libraries when using cmake toolchain.")
|
|
86
|
+
declared: dict[str, HatchCppLibrary] = {}
|
|
87
|
+
for library in model.libraries:
|
|
88
|
+
if library.python_package is not None:
|
|
89
|
+
raise ValueError(f"Library '{library.name}' sets python-package, which hatch-cpp does not support yet.")
|
|
90
|
+
for name in library.depends:
|
|
91
|
+
if name not in declared or declared[name].binding != "generic":
|
|
92
|
+
raise ValueError(f"Library '{library.name}' depends on '{name}', which must be a generic library declared before it.")
|
|
93
|
+
declared[library.name] = library
|
|
94
|
+
if model.artifact_manifest and not model.artifact_manifest_destination:
|
|
95
|
+
raise ValueError("artifact-manifest requires artifact-manifest-destination.")
|
|
53
96
|
return model
|
|
54
97
|
|
|
55
98
|
|
|
@@ -58,6 +101,48 @@ class HatchCppBuildPlan(HatchCppBuildConfig):
|
|
|
58
101
|
commands: list[str] = Field(default_factory=list)
|
|
59
102
|
|
|
60
103
|
_active_toolchains: list[Toolchain] = []
|
|
104
|
+
_external_libraries: dict[str, ExternalLibrary] = PrivateAttr(default_factory=dict)
|
|
105
|
+
_skipped_libraries: set[str] = PrivateAttr(default_factory=set)
|
|
106
|
+
|
|
107
|
+
def built_libraries(self) -> list[HatchCppLibrary]:
|
|
108
|
+
"""Libraries this build compiles; external and direct mode skip the ones other entries depend on."""
|
|
109
|
+
return [library for library in self.libraries if library.name not in self._skipped_libraries]
|
|
110
|
+
|
|
111
|
+
def _resolve_external(self, library: HatchCppLibrary) -> ExternalLibrary:
|
|
112
|
+
root = environ.get("HATCH_CPP_LIBRARY_ROOT") or (str(library.library_root) if library.library_root is not None else None)
|
|
113
|
+
name = Path(library.name).name
|
|
114
|
+
if name.startswith("lib") and len(name) > 3:
|
|
115
|
+
name = name[3:]
|
|
116
|
+
return resolve_external_library(
|
|
117
|
+
name,
|
|
118
|
+
label=library.name,
|
|
119
|
+
platform=self.platform.platform,
|
|
120
|
+
root_env_var="HATCH_CPP_LIBRARY_ROOT",
|
|
121
|
+
root=Path(root).resolve() if root else None,
|
|
122
|
+
pkg_config=library.pkg_config,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
def _consumer(self, library: HatchCppLibrary, source: str) -> tuple[HatchCppLibrary, list[HatchCppLibrary]]:
|
|
126
|
+
"""The library to compile and the libraries it links, after applying the library-source mode to its depends."""
|
|
127
|
+
dependencies = [next(other for other in self.libraries if other.name == name) for name in library.depends]
|
|
128
|
+
if not dependencies or source == "bundled":
|
|
129
|
+
return library, dependencies
|
|
130
|
+
if source == "external":
|
|
131
|
+
externals = [self._external_libraries[dependency.name] for dependency in dependencies]
|
|
132
|
+
return library.model_copy(
|
|
133
|
+
update={
|
|
134
|
+
"include_dirs": [*library.include_dirs, *(str(external.include_dir) for external in externals if external.include_dir)],
|
|
135
|
+
"library_dirs": [*library.library_dirs, *(str(external.library_dir) for external in externals)],
|
|
136
|
+
"libraries": [*library.libraries, *(external.link_name for external in externals)],
|
|
137
|
+
}
|
|
138
|
+
), []
|
|
139
|
+
return library.model_copy(
|
|
140
|
+
update={
|
|
141
|
+
"sources": [*library.sources, *(path for dependency in dependencies for path in dependency.sources)],
|
|
142
|
+
"include_dirs": [*library.include_dirs, *(path for dependency in dependencies for path in dependency.include_dirs)],
|
|
143
|
+
"define_macros": [*library.define_macros, *(macro for dependency in dependencies for macro in dependency.define_macros)],
|
|
144
|
+
}
|
|
145
|
+
), []
|
|
61
146
|
|
|
62
147
|
def generate(self):
|
|
63
148
|
self.commands = []
|
|
@@ -72,7 +157,7 @@ class HatchCppBuildPlan(HatchCppBuildConfig):
|
|
|
72
157
|
self._active_toolchains.append("vcpkg")
|
|
73
158
|
else:
|
|
74
159
|
log.warning("HATCH_CPP_VCPKG=1 set but no vcpkg configuration found; ignoring.")
|
|
75
|
-
elif vcpkg_override != "0" and self.vcpkg and Path(self.vcpkg.vcpkg).exists():
|
|
160
|
+
elif vcpkg_override != "0" and self.vcpkg and self.vcpkg.vcpkg and Path(self.vcpkg.vcpkg).exists():
|
|
76
161
|
self._active_toolchains.append("vcpkg")
|
|
77
162
|
|
|
78
163
|
if self.libraries:
|
|
@@ -87,27 +172,40 @@ class HatchCppBuildPlan(HatchCppBuildConfig):
|
|
|
87
172
|
|
|
88
173
|
# Collect toolchain commands
|
|
89
174
|
if "vcpkg" in self._active_toolchains:
|
|
175
|
+
assert self.vcpkg is not None
|
|
90
176
|
self.commands.extend(self.vcpkg.generate(self))
|
|
91
177
|
|
|
92
178
|
if "vanilla" in self._active_toolchains:
|
|
93
179
|
if "vcpkg" in self._active_toolchains:
|
|
94
180
|
log.warning("vcpkg toolchain is active; ensure that your compiler is configured to use vcpkg includes and libs.")
|
|
95
181
|
|
|
182
|
+
source = resolve_library_source(self.library_source, env_var="HATCH_CPP_LIBRARY_SOURCE")
|
|
183
|
+
if source == "static":
|
|
184
|
+
raise ValueError("library-source 'static' is not supported by hatch-cpp; use bundled, external, or direct.")
|
|
185
|
+
linked = {name for library in self.libraries for name in library.depends}
|
|
186
|
+
self._external_libraries = {}
|
|
187
|
+
self._skipped_libraries = set() if source == "bundled" else linked
|
|
96
188
|
for library_index, library in enumerate(self.libraries):
|
|
189
|
+
if library.name in self._skipped_libraries:
|
|
190
|
+
if source == "external":
|
|
191
|
+
self._external_libraries[library.name] = self._resolve_external(library)
|
|
192
|
+
continue
|
|
193
|
+
library, dependencies = self._consumer(library, source)
|
|
97
194
|
compile_flags = self.platform.get_compile_flags(library, self.build_type)
|
|
98
|
-
link_flags = self.platform.get_link_flags(library, self.build_type)
|
|
195
|
+
link_flags = self.platform.get_link_flags(library, self.build_type, dependencies)
|
|
99
196
|
compiler = self.platform.cc if library.language == "c" else self.platform.cxx
|
|
100
197
|
if self.platform.platform == "emscripten":
|
|
101
198
|
objects = []
|
|
102
199
|
for source_index, source in enumerate(library.sources):
|
|
103
|
-
obj =
|
|
104
|
-
objects.append(
|
|
200
|
+
obj = f"build/hatch-cpp/{library_index}-{source_index}-{Path(source).stem}.o"
|
|
201
|
+
objects.append(obj)
|
|
105
202
|
self.commands.append(f"{compiler} -c {source} {compile_flags} -o {obj}")
|
|
106
203
|
self.commands.append(f"{compiler} {' '.join(objects)} {link_flags}")
|
|
107
204
|
else:
|
|
108
205
|
self.commands.append(f"{compiler} {' '.join(library.sources)} {compile_flags} {link_flags}")
|
|
109
206
|
|
|
110
207
|
if "cmake" in self._active_toolchains:
|
|
208
|
+
assert self.cmake is not None
|
|
111
209
|
self.commands.extend(self.cmake.generate(self))
|
|
112
210
|
|
|
113
211
|
return self.commands
|
|
@@ -115,6 +213,9 @@ class HatchCppBuildPlan(HatchCppBuildConfig):
|
|
|
115
213
|
def execute(self):
|
|
116
214
|
if self.platform.platform == "emscripten" and "vanilla" in self._active_toolchains:
|
|
117
215
|
Path("build/hatch-cpp").mkdir(parents=True, exist_ok=True)
|
|
216
|
+
if "vanilla" in self._active_toolchains:
|
|
217
|
+
for library in self.built_libraries():
|
|
218
|
+
Path(self._library_output(library)).parent.mkdir(parents=True, exist_ok=True)
|
|
118
219
|
for command in self.commands:
|
|
119
220
|
ret = system_call(command)
|
|
120
221
|
if ret != 0:
|
|
@@ -125,3 +226,91 @@ class HatchCppBuildPlan(HatchCppBuildConfig):
|
|
|
125
226
|
if self.platform.platform == "win32":
|
|
126
227
|
for temp_obj in Path(".").glob("*.obj"):
|
|
127
228
|
temp_obj.unlink()
|
|
229
|
+
|
|
230
|
+
def _library_output(self, library: HatchCppLibrary) -> str:
|
|
231
|
+
return library.get_qualified_name(self.platform.platform)
|
|
232
|
+
|
|
233
|
+
def _artifact_path(self, artifact: HatchCppArtifact, packaged: list[str]) -> str:
|
|
234
|
+
if artifact.path:
|
|
235
|
+
return artifact.path
|
|
236
|
+
library = next((library for library in self.libraries if library.name == artifact.name), None)
|
|
237
|
+
if library is not None:
|
|
238
|
+
return self._library_output(library)
|
|
239
|
+
filename = shared_library_name(Path(artifact.name).name, platform=self.platform.platform)
|
|
240
|
+
matches = [path for path in packaged if Path(path).name == filename]
|
|
241
|
+
if len(matches) != 1:
|
|
242
|
+
found = ", ".join(matches) if matches else "none"
|
|
243
|
+
raise FileNotFoundError(f"Artifact '{artifact.name}' needs a path: expected one packaged '{filename}', found {found}.")
|
|
244
|
+
return matches[0]
|
|
245
|
+
|
|
246
|
+
def validation_targets(self, packaged: list[str]) -> list[tuple[str, LibraryValidationConfig, str]]:
|
|
247
|
+
"""Libraries with validation fields set, plus every `artifacts` entry, as (label, config, project-relative path)."""
|
|
248
|
+
targets: list[tuple[str, LibraryValidationConfig, str]] = []
|
|
249
|
+
for library in self.libraries:
|
|
250
|
+
if not _has_validation(library):
|
|
251
|
+
continue
|
|
252
|
+
if library.name in self._external_libraries:
|
|
253
|
+
targets.append((library.name, library, str(self._external_libraries[library.name].library)))
|
|
254
|
+
elif library.name not in self._skipped_libraries:
|
|
255
|
+
targets.append((library.name, library, self._library_output(library)))
|
|
256
|
+
targets.extend((artifact.name, artifact, self._artifact_path(artifact, packaged)) for artifact in self.artifacts)
|
|
257
|
+
return targets
|
|
258
|
+
|
|
259
|
+
def validate_artifacts(self, root: Path, packaged: list[str]) -> None:
|
|
260
|
+
"""Check headers, ABI strings, and exports of each validation target, then run its validation commands."""
|
|
261
|
+
for label, config, relative in self.validation_targets(packaged):
|
|
262
|
+
library = root / relative
|
|
263
|
+
if not library.is_file():
|
|
264
|
+
raise FileNotFoundError(f"Artifact '{label}' library was not built: {relative}")
|
|
265
|
+
headers = check_headers(config.expected_headers, config.expected_abi_strings, base=root, label=label)
|
|
266
|
+
expected = list(dict.fromkeys([*config.expected_symbols, *prefixed_header_symbols(headers, config.header_symbol_prefix, label=label)]))
|
|
267
|
+
check_exports(library, expected=expected, load=config.validate_artifact, label=label, display_path=relative)
|
|
268
|
+
tokens = {"name": label, "path": str(library), "header": str(headers[0]) if headers else ""}
|
|
269
|
+
for command in config.validation_commands:
|
|
270
|
+
env = {**environ, **{key: value.format(**tokens) for key, value in command.env.items()}}
|
|
271
|
+
env.update({"HATCH_CPP_ARTIFACT_NAME": label, "HATCH_CPP_ARTIFACT_PATH": str(library)})
|
|
272
|
+
cwd = root / command.working_directory if command.working_directory else root
|
|
273
|
+
run_validation_command([value.format(**tokens) for value in command.command], cwd=cwd, env=env, label=label)
|
|
274
|
+
|
|
275
|
+
def write_manifest(self, root: Path, packaged: list[str]) -> str | None:
|
|
276
|
+
"""Write the manifest of packaged binaries under the project root and return its relative path."""
|
|
277
|
+
if not self.artifact_manifest:
|
|
278
|
+
return None
|
|
279
|
+
assert self.artifact_manifest_destination is not None
|
|
280
|
+
records = [
|
|
281
|
+
{
|
|
282
|
+
"name": Path(path).name,
|
|
283
|
+
"role": _artifact_role(path, self.platform.platform),
|
|
284
|
+
"platform": self.platform.platform,
|
|
285
|
+
"profile": self.build_type,
|
|
286
|
+
"destination": Path(path).as_posix(),
|
|
287
|
+
}
|
|
288
|
+
for path in sorted(packaged)
|
|
289
|
+
]
|
|
290
|
+
write_artifact_manifest(root / self.artifact_manifest_destination, module=self.name or "", records=records)
|
|
291
|
+
return self.artifact_manifest_destination
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
def _has_validation(config: LibraryValidationConfig) -> bool:
|
|
295
|
+
return bool(
|
|
296
|
+
config.expected_symbols
|
|
297
|
+
or config.expected_headers
|
|
298
|
+
or config.expected_abi_strings
|
|
299
|
+
or config.header_symbol_prefix
|
|
300
|
+
or config.validate_artifact
|
|
301
|
+
or config.validation_commands
|
|
302
|
+
)
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def _artifact_role(path: str, platform: str) -> str:
|
|
306
|
+
"""Classify a packaged binary: a Python extension module or a plain shared library."""
|
|
307
|
+
filename = Path(path).name
|
|
308
|
+
if filename.endswith(".pyd"):
|
|
309
|
+
return "python-extension"
|
|
310
|
+
if platform != "win32" and filename.startswith("lib"):
|
|
311
|
+
return "library"
|
|
312
|
+
if any(filename.endswith(suffix) for suffix in EXTENSION_SUFFIXES if suffix != ".so") or ".abi3." in filename:
|
|
313
|
+
return "python-extension"
|
|
314
|
+
if filename.endswith(".so") and platform in ("linux", "darwin", "emscripten"):
|
|
315
|
+
return "python-extension"
|
|
316
|
+
return "library"
|
hatch_cpp/plugin.py
CHANGED
|
@@ -88,9 +88,12 @@ class HatchCppBuildHook(BuildHookInterface):
|
|
|
88
88
|
|
|
89
89
|
if build_plan.libraries:
|
|
90
90
|
# force include libraries
|
|
91
|
-
for library in build_plan.
|
|
91
|
+
for library in build_plan.built_libraries():
|
|
92
92
|
name = library.get_qualified_name(build_plan.platform.platform)
|
|
93
93
|
build_data["force_include"][name] = name
|
|
94
|
+
import_library = Path(name).with_suffix(".lib")
|
|
95
|
+
if build_plan.platform.platform == "win32" and library.binding == "generic" and import_library.is_file():
|
|
96
|
+
build_data["force_include"][import_library.as_posix()] = import_library.as_posix()
|
|
94
97
|
|
|
95
98
|
build_data["pure_python"] = False
|
|
96
99
|
machine = platform_machine()
|
|
@@ -101,9 +104,10 @@ class HatchCppBuildHook(BuildHookInterface):
|
|
|
101
104
|
machine,
|
|
102
105
|
version_major,
|
|
103
106
|
version_minor,
|
|
104
|
-
all(lib.py_limited_api for lib in build_plan.
|
|
107
|
+
all(lib.py_limited_api for lib in build_plan.built_libraries()),
|
|
105
108
|
)
|
|
106
109
|
else:
|
|
110
|
+
assert build_plan.cmake is not None
|
|
107
111
|
build_data["pure_python"] = False
|
|
108
112
|
machine = platform_machine()
|
|
109
113
|
version_major = version_info.major
|
|
@@ -119,5 +123,11 @@ class HatchCppBuildHook(BuildHookInterface):
|
|
|
119
123
|
if path.suffix in (".pyd", ".dll", ".so", ".dylib"):
|
|
120
124
|
build_data["force_include"][str(path)] = str(path)
|
|
121
125
|
|
|
126
|
+
packaged = list(build_data["force_include"])
|
|
127
|
+
build_plan.validate_artifacts(Path(self.root), packaged)
|
|
128
|
+
manifest = build_plan.write_manifest(Path(self.root), packaged)
|
|
129
|
+
if manifest is not None:
|
|
130
|
+
build_data["force_include"][manifest] = manifest
|
|
131
|
+
|
|
122
132
|
for path in build_data["force_include"]:
|
|
123
133
|
self._logger.info(f"Force include: {path}")
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
from importlib import import_module
|
|
1
2
|
from os import listdir
|
|
2
3
|
from pathlib import Path
|
|
3
4
|
from shutil import rmtree
|
|
@@ -41,6 +42,6 @@ class TestHatchBuild:
|
|
|
41
42
|
# import
|
|
42
43
|
here = Path(__file__).parent / project
|
|
43
44
|
path.insert(0, str(here))
|
|
44
|
-
|
|
45
|
+
extension = import_module("project.extension")
|
|
45
46
|
|
|
46
|
-
assert
|
|
47
|
+
assert extension.hello() == "A string"
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
|
|
2
|
+
from hatchling.metadata.core import ProjectMetadata
|
|
2
3
|
|
|
3
4
|
from hatch_cpp.hooks import hatch_register_build_hook
|
|
4
5
|
|
|
@@ -10,7 +11,7 @@ def test_registered_build_hook_accepts_hatchling_configuration(tmp_path):
|
|
|
10
11
|
root=str(tmp_path),
|
|
11
12
|
config=config,
|
|
12
13
|
build_config=None,
|
|
13
|
-
metadata=None,
|
|
14
|
+
metadata=ProjectMetadata(str(tmp_path), None, {"project": {"name": "project", "version": "0.1.0"}}),
|
|
14
15
|
directory=str(tmp_path),
|
|
15
16
|
target_name="wheel",
|
|
16
17
|
)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#include <Python.h>
|
|
2
|
+
|
|
3
|
+
#include "demo/demo.h"
|
|
4
|
+
|
|
5
|
+
static PyObject *answer(PyObject *self, PyObject *args) { return PyLong_FromLong(demo_answer()); }
|
|
6
|
+
|
|
7
|
+
static PyMethodDef methods[] = {{"answer", answer, METH_NOARGS, NULL}, {NULL, NULL, 0, NULL}};
|
|
8
|
+
|
|
9
|
+
static struct PyModuleDef module = {PyModuleDef_HEAD_INIT, "extension", NULL, -1, methods};
|
|
10
|
+
|
|
11
|
+
PyMODINIT_FUNC PyInit_extension(void) { return PyModule_Create(&module); }
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#define DEMO_ABI_VERSION 1
|
|
4
|
+
|
|
5
|
+
#if defined(_WIN32) && defined(DEMO_BUILD)
|
|
6
|
+
#define DEMO_API __declspec(dllexport)
|
|
7
|
+
#elif defined(_WIN32)
|
|
8
|
+
#define DEMO_API __declspec(dllimport)
|
|
9
|
+
#else
|
|
10
|
+
#define DEMO_API __attribute__((visibility("default")))
|
|
11
|
+
#endif
|
|
12
|
+
|
|
13
|
+
#ifdef __cplusplus
|
|
14
|
+
extern "C" {
|
|
15
|
+
#endif
|
|
16
|
+
|
|
17
|
+
DEMO_API int demo_abi_version(void);
|
|
18
|
+
DEMO_API int demo_answer(void);
|
|
19
|
+
|
|
20
|
+
#ifdef __cplusplus
|
|
21
|
+
}
|
|
22
|
+
#endif
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.20"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hatch-cpp-test-project-c-abi-library"
|
|
7
|
+
description = "C ABI library validated by 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/*.pyd",
|
|
18
|
+
"project/*.so",
|
|
19
|
+
"project/lib/*.lib",
|
|
20
|
+
"project/lib/*.dll",
|
|
21
|
+
"project/lib/*.dylib",
|
|
22
|
+
"project/lib/*.so",
|
|
23
|
+
"project/lib/*.json",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[tool.hatch.build.sources]
|
|
27
|
+
src = "/"
|
|
28
|
+
|
|
29
|
+
[tool.hatch.build.targets.sdist]
|
|
30
|
+
packages = ["project"]
|
|
31
|
+
|
|
32
|
+
[tool.hatch.build.targets.wheel]
|
|
33
|
+
packages = ["project"]
|
|
34
|
+
|
|
35
|
+
[tool.hatch.build.hooks.hatch-cpp]
|
|
36
|
+
verbose = true
|
|
37
|
+
artifact-manifest = true
|
|
38
|
+
artifact-manifest-destination = "project/lib/hatch-cpp-artifacts.json"
|
|
39
|
+
|
|
40
|
+
[[tool.hatch.build.hooks.hatch-cpp.libraries]]
|
|
41
|
+
name = "project/lib/libdemo"
|
|
42
|
+
sources = ["cpp/demo.c"]
|
|
43
|
+
language = "c"
|
|
44
|
+
binding = "generic"
|
|
45
|
+
include-dirs = ["cpp/include"]
|
|
46
|
+
define-macros = ["DEMO_BUILD"]
|
|
47
|
+
expected-headers = ["cpp/include/demo/demo.h"]
|
|
48
|
+
header-symbol-prefix = "demo_"
|
|
49
|
+
expected-abi-strings = ["DEMO_ABI_VERSION 1"]
|
|
50
|
+
validate = true
|
|
51
|
+
|
|
52
|
+
[[tool.hatch.build.hooks.hatch-cpp.libraries]]
|
|
53
|
+
name = "project/extension"
|
|
54
|
+
sources = ["cpp/extension.c"]
|
|
55
|
+
language = "c"
|
|
56
|
+
include-dirs = ["cpp/include"]
|
|
57
|
+
depends = ["project/lib/libdemo"]
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
cmake_minimum_required(VERSION 3.20.0)
|
|
2
|
+
project(hatch-cpp-test-project-cmake-c-abi VERSION "0.1.0" LANGUAGES C)
|
|
3
|
+
|
|
4
|
+
set(CMAKE_INSTALL_NAME_DIR "@rpath")
|
|
5
|
+
|
|
6
|
+
add_library(demo SHARED cpp/demo.c)
|
|
7
|
+
target_include_directories(demo PUBLIC "${CMAKE_SOURCE_DIR}/cpp/include")
|
|
8
|
+
target_compile_definitions(demo PRIVATE DEMO_BUILD)
|
|
9
|
+
set_target_properties(demo PROPERTIES PUBLIC_HEADER cpp/include/demo/demo.h)
|
|
10
|
+
install(TARGETS demo
|
|
11
|
+
LIBRARY DESTINATION project/lib
|
|
12
|
+
RUNTIME DESTINATION project/lib
|
|
13
|
+
ARCHIVE DESTINATION project/lib
|
|
14
|
+
PUBLIC_HEADER DESTINATION project/include/demo
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
find_package(Python ${HATCH_CPP_TEST_PROJECT_CMAKE_C_ABI_PYTHON_VERSION} EXACT REQUIRED COMPONENTS Interpreter Development.Module)
|
|
18
|
+
python_add_library(extension MODULE WITH_SOABI cpp/extension.c)
|
|
19
|
+
target_link_libraries(extension PRIVATE demo)
|
|
20
|
+
if(APPLE)
|
|
21
|
+
set_target_properties(extension PROPERTIES INSTALL_RPATH "@loader_path/lib")
|
|
22
|
+
elseif(UNIX)
|
|
23
|
+
set_target_properties(extension PROPERTIES INSTALL_RPATH "\$ORIGIN/lib")
|
|
24
|
+
endif()
|
|
25
|
+
install(TARGETS extension
|
|
26
|
+
LIBRARY DESTINATION project
|
|
27
|
+
RUNTIME DESTINATION project
|
|
28
|
+
)
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
#include <Python.h>
|
|
2
|
+
|
|
3
|
+
#include "demo/demo.h"
|
|
4
|
+
|
|
5
|
+
static PyObject *answer(PyObject *self, PyObject *args) { return PyLong_FromLong(demo_answer()); }
|
|
6
|
+
|
|
7
|
+
static PyMethodDef methods[] = {{"answer", answer, METH_NOARGS, NULL}, {NULL, NULL, 0, NULL}};
|
|
8
|
+
|
|
9
|
+
static struct PyModuleDef module = {PyModuleDef_HEAD_INIT, "extension", NULL, -1, methods};
|
|
10
|
+
|
|
11
|
+
PyMODINIT_FUNC PyInit_extension(void) { return PyModule_Create(&module); }
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
#pragma once
|
|
2
|
+
|
|
3
|
+
#define DEMO_ABI_VERSION 1
|
|
4
|
+
|
|
5
|
+
#if defined(_WIN32) && defined(DEMO_BUILD)
|
|
6
|
+
#define DEMO_API __declspec(dllexport)
|
|
7
|
+
#elif defined(_WIN32)
|
|
8
|
+
#define DEMO_API __declspec(dllimport)
|
|
9
|
+
#else
|
|
10
|
+
#define DEMO_API __attribute__((visibility("default")))
|
|
11
|
+
#endif
|
|
12
|
+
|
|
13
|
+
#ifdef __cplusplus
|
|
14
|
+
extern "C" {
|
|
15
|
+
#endif
|
|
16
|
+
|
|
17
|
+
DEMO_API int demo_abi_version(void);
|
|
18
|
+
DEMO_API int demo_answer(void);
|
|
19
|
+
|
|
20
|
+
#ifdef __cplusplus
|
|
21
|
+
}
|
|
22
|
+
#endif
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.20"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "hatch-cpp-test-project-cmake-c-abi"
|
|
7
|
+
description = "CMake-built C ABI library validated by 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/*.pyd",
|
|
18
|
+
"project/*.so",
|
|
19
|
+
"project/include/**/*.h",
|
|
20
|
+
"project/lib/*.dll",
|
|
21
|
+
"project/lib/*.dylib",
|
|
22
|
+
"project/lib/*.json",
|
|
23
|
+
"project/lib/*.lib",
|
|
24
|
+
"project/lib/*.so",
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
[tool.hatch.build.sources]
|
|
28
|
+
src = "/"
|
|
29
|
+
|
|
30
|
+
[tool.hatch.build.targets.sdist]
|
|
31
|
+
packages = ["project"]
|
|
32
|
+
|
|
33
|
+
[tool.hatch.build.targets.wheel]
|
|
34
|
+
packages = ["project"]
|
|
35
|
+
|
|
36
|
+
[tool.hatch.build.hooks.hatch-cpp]
|
|
37
|
+
verbose = true
|
|
38
|
+
artifact-manifest = true
|
|
39
|
+
artifact-manifest-destination = "project/lib/hatch-cpp-artifacts.json"
|
|
40
|
+
|
|
41
|
+
[tool.hatch.build.hooks.hatch-cpp.cmake]
|
|
42
|
+
root = "CMakeLists.txt"
|
|
43
|
+
include_flags = {"python_version" = true}
|
|
44
|
+
|
|
45
|
+
# No path: hatch-cpp finds the one packaged libdemo.so, libdemo.dylib, or demo.dll.
|
|
46
|
+
[[tool.hatch.build.hooks.hatch-cpp.artifacts]]
|
|
47
|
+
name = "demo"
|
|
48
|
+
expected-headers = ["project/include/demo/demo.h"]
|
|
49
|
+
header-symbol-prefix = "demo_"
|
|
50
|
+
expected-abi-strings = ["DEMO_ABI_VERSION 1"]
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
def answer() -> int: ...
|
hatch_cpp/tests/test_projects.py
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import contextlib
|
|
2
|
+
from importlib import import_module
|
|
2
3
|
from os import listdir, remove
|
|
3
4
|
from pathlib import Path
|
|
4
5
|
from subprocess import check_call
|
|
@@ -14,7 +15,12 @@ class TestProject:
|
|
|
14
15
|
[
|
|
15
16
|
"test_project_basic",
|
|
16
17
|
"test_project_override_classes",
|
|
17
|
-
|
|
18
|
+
pytest.param(
|
|
19
|
+
"test_project_override_toolchain",
|
|
20
|
+
marks=pytest.mark.skipif(
|
|
21
|
+
platform == "win32", reason="the fixture selects the gcc toolchain, whose flags clang rejects for MSVC targets"
|
|
22
|
+
),
|
|
23
|
+
),
|
|
18
24
|
"test_project_pybind",
|
|
19
25
|
"test_project_pybind_vcpkg",
|
|
20
26
|
"test_project_nanobind",
|
|
@@ -60,6 +66,6 @@ class TestProject:
|
|
|
60
66
|
# import
|
|
61
67
|
here = Path(__file__).parent / project
|
|
62
68
|
path.insert(0, str(here))
|
|
63
|
-
|
|
69
|
+
extension = import_module("project.extension")
|
|
64
70
|
|
|
65
|
-
assert
|
|
71
|
+
assert extension.hello() == "A string"
|