hatch-cpp 0.5.2__py3-none-any.whl → 0.6.0__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.
Files changed (27) hide show
  1. hatch_cpp/__init__.py +1 -1
  2. hatch_cpp/config.py +194 -5
  3. hatch_cpp/plugin.py +12 -2
  4. hatch_cpp/tests/test_hatch_build.py +3 -2
  5. hatch_cpp/tests/test_hook_registration.py +2 -1
  6. hatch_cpp/tests/test_project_c_abi_library/cpp/demo.c +5 -0
  7. hatch_cpp/tests/test_project_c_abi_library/cpp/extension.c +11 -0
  8. hatch_cpp/tests/test_project_c_abi_library/cpp/include/demo/demo.h +22 -0
  9. hatch_cpp/tests/test_project_c_abi_library/project/__init__.py +5 -0
  10. hatch_cpp/tests/test_project_c_abi_library/pyproject.toml +57 -0
  11. hatch_cpp/tests/test_project_cmake_c_abi/CMakeLists.txt +28 -0
  12. hatch_cpp/tests/test_project_cmake_c_abi/cpp/demo.c +5 -0
  13. hatch_cpp/tests/test_project_cmake_c_abi/cpp/extension.c +11 -0
  14. hatch_cpp/tests/test_project_cmake_c_abi/cpp/include/demo/demo.h +22 -0
  15. hatch_cpp/tests/test_project_cmake_c_abi/project/__init__.py +5 -0
  16. hatch_cpp/tests/test_project_cmake_c_abi/pyproject.toml +50 -0
  17. hatch_cpp/tests/test_project_pyodide/pyodide_project/extension.pyi +1 -0
  18. hatch_cpp/tests/test_projects.py +3 -2
  19. hatch_cpp/tests/test_validation.py +216 -0
  20. hatch_cpp/toolchains/cmake.py +2 -2
  21. hatch_cpp/toolchains/common.py +92 -41
  22. hatch_cpp/toolchains/vcpkg.py +2 -2
  23. {hatch_cpp-0.5.2.dist-info → hatch_cpp-0.6.0.dist-info}/METADATA +71 -3
  24. {hatch_cpp-0.5.2.dist-info → hatch_cpp-0.6.0.dist-info}/RECORD +27 -14
  25. {hatch_cpp-0.5.2.dist-info → hatch_cpp-0.6.0.dist-info}/WHEEL +1 -1
  26. {hatch_cpp-0.5.2.dist-info → hatch_cpp-0.6.0.dist-info}/entry_points.txt +0 -0
  27. {hatch_cpp-0.5.2.dist-info → hatch_cpp-0.6.0.dist-info}/licenses/LICENSE +0 -0
hatch_cpp/__init__.py CHANGED
@@ -1,4 +1,4 @@
1
- __version__ = "0.5.2"
1
+ __version__ = "0.6.0"
2
2
 
3
3
  from .config import *
4
4
  from .hooks import *
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 BuildType, HatchCppCmakeConfiguration, HatchCppLibrary, HatchCppPlatform, HatchCppVcpkgConfiguration, Toolchain
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 | None = Field(default_factory=HatchCppPlatform.default)
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,15 +172,27 @@ 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 = []
@@ -108,6 +205,7 @@ class HatchCppBuildPlan(HatchCppBuildConfig):
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.libraries:
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.libraries),
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
- import project.extension
45
+ extension = import_module("project.extension")
45
46
 
46
- assert project.extension.hello() == "A string"
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,5 @@
1
+ #include "demo/demo.h"
2
+
3
+ int demo_abi_version(void) { return DEMO_ABI_VERSION; }
4
+
5
+ int demo_answer(void) { return 42; }
@@ -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,5 @@
1
+ import os
2
+ import sys
3
+
4
+ if sys.platform == "win32":
5
+ os.add_dll_directory(os.path.join(os.path.dirname(__file__), "lib"))
@@ -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,5 @@
1
+ #include "demo/demo.h"
2
+
3
+ int demo_abi_version(void) { return DEMO_ABI_VERSION; }
4
+
5
+ int demo_answer(void) { return 42; }
@@ -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,5 @@
1
+ import os
2
+ import sys
3
+
4
+ if sys.platform == "win32":
5
+ os.add_dll_directory(os.path.join(os.path.dirname(__file__), "lib"))
@@ -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: ...
@@ -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
@@ -60,6 +61,6 @@ class TestProject:
60
61
  # import
61
62
  here = Path(__file__).parent / project
62
63
  path.insert(0, str(here))
63
- import project.extension
64
+ extension = import_module("project.extension")
64
65
 
65
- assert project.extension.hello() == "A string"
66
+ assert extension.hello() == "A string"
@@ -0,0 +1,216 @@
1
+ from json import loads
2
+ from os import environ
3
+ from pathlib import Path
4
+ from shutil import copy2, copytree
5
+ from subprocess import check_call, run
6
+ from sys import executable, platform
7
+
8
+ import pytest
9
+ from pydantic import ValidationError
10
+
11
+ from hatch_cpp import HatchCppBuildPlan
12
+ from hatch_cpp.config import _artifact_role
13
+
14
+ FIXTURE = Path(__file__).parent / "test_project_c_abi_library"
15
+ LIBRARY = {
16
+ "name": "project/lib/libdemo",
17
+ "sources": ["cpp/demo.c"],
18
+ "language": "c",
19
+ "binding": "generic",
20
+ "include-dirs": ["cpp/include"],
21
+ }
22
+
23
+
24
+ def _plan(**config):
25
+ return HatchCppBuildPlan(name="demo", **config)
26
+
27
+
28
+ def test_c_abi_library_is_validated_linked_and_recorded(tmp_path):
29
+ project = copytree(FIXTURE, tmp_path / "build_root")
30
+ check_call(["hatch-build", "--hooks-only"], cwd=project)
31
+
32
+ suffix = _library_suffix()
33
+ manifest = loads((project / "project/lib/hatch-cpp-artifacts.json").read_text())
34
+ roles = {record["destination"]: record["role"] for record in manifest["artifacts"]}
35
+ assert roles[f"project/lib/libdemo.{suffix}"] == "library"
36
+ assert [role for role in roles.values() if role == "python-extension"] == ["python-extension"]
37
+
38
+ # The extension finds libdemo through a package-relative rpath, so the package still imports after it moves.
39
+ installed = copytree(project / "project", tmp_path / "site" / "project")
40
+ check_call([executable, "-c", "import project.extension as e; assert e.answer() == 42"], cwd=installed.parent)
41
+
42
+
43
+ def _library_suffix():
44
+ return {"win32": "dll", "darwin": "dylib"}.get(platform, "so")
45
+
46
+
47
+ @pytest.mark.skipif(platform == "win32", reason="the external prefix is found through the loader path, which Windows does not have")
48
+ def test_external_mode_links_a_prebuilt_library(tmp_path):
49
+ bundled = copytree(FIXTURE, tmp_path / "bundled")
50
+ check_call(["hatch-build", "--hooks-only"], cwd=bundled)
51
+ prefix = tmp_path / "prefix"
52
+ (prefix / "lib").mkdir(parents=True)
53
+ copy2(bundled / f"project/lib/libdemo.{_library_suffix()}", prefix / "lib")
54
+ copytree(bundled / "cpp/include", prefix / "include")
55
+
56
+ external = copytree(FIXTURE, tmp_path / "external")
57
+ env = {**environ, "HATCH_CPP_LIBRARY_SOURCE": "external", "HATCH_CPP_LIBRARY_ROOT": str(prefix)}
58
+ check_call(["hatch-build", "--hooks-only"], cwd=external, env=env)
59
+
60
+ assert not (external / f"project/lib/libdemo.{_library_suffix()}").exists()
61
+ manifest = loads((external / "project/lib/hatch-cpp-artifacts.json").read_text())
62
+ assert [record["role"] for record in manifest["artifacts"]] == ["python-extension"]
63
+ loader_path = "DYLD_LIBRARY_PATH" if platform == "darwin" else "LD_LIBRARY_PATH"
64
+ check_call(
65
+ [executable, "-c", "import project.extension as e; assert e.answer() == 42"],
66
+ cwd=external,
67
+ env={**environ, loader_path: str(prefix / "lib")},
68
+ )
69
+
70
+
71
+ def test_direct_mode_compiles_the_library_into_the_extension(tmp_path):
72
+ project = copytree(FIXTURE, tmp_path / "direct")
73
+ check_call(["hatch-build", "--hooks-only"], cwd=project, env={**environ, "HATCH_CPP_LIBRARY_SOURCE": "direct"})
74
+
75
+ assert not (project / f"project/lib/libdemo.{_library_suffix()}").exists()
76
+ check_call([executable, "-c", "import project.extension as e; assert e.answer() == 42"], cwd=project)
77
+
78
+
79
+ def test_static_mode_is_rejected(monkeypatch):
80
+ monkeypatch.setenv("HATCH_CPP_LIBRARY_SOURCE", "static")
81
+ extension = {"name": "project/extension", "sources": ["ext.c"], "depends": ["project/lib/libdemo"]}
82
+ with pytest.raises(ValueError, match="'static' is not supported"):
83
+ _plan(libraries=[LIBRARY, extension]).generate()
84
+
85
+
86
+ def test_python_package_is_rejected():
87
+ with pytest.raises(ValidationError, match="python-package, which hatch-cpp does not support yet"):
88
+ _plan(libraries=[{**LIBRARY, "python-package": "other"}])
89
+
90
+
91
+ CMAKE_FIXTURE = Path(__file__).parent / "test_project_cmake_c_abi"
92
+
93
+
94
+ def test_cmake_library_is_validated_by_name(tmp_path):
95
+ project = copytree(CMAKE_FIXTURE, tmp_path / "cmake")
96
+ check_call(["hatch-build", "--hooks-only"], cwd=project)
97
+
98
+ manifest = loads((project / "project/lib/hatch-cpp-artifacts.json").read_text())
99
+ roles = sorted(record["role"] for record in manifest["artifacts"])
100
+ assert roles == ["library", "python-extension"]
101
+ installed = copytree(project / "project", tmp_path / "site" / "project")
102
+ check_call([executable, "-c", "import project.extension as e; assert e.answer() == 42"], cwd=installed.parent)
103
+
104
+
105
+ def test_cmake_library_validation_failure_stops_the_build(tmp_path):
106
+ project = copytree(CMAKE_FIXTURE, tmp_path / "cmake")
107
+ pyproject = project / "pyproject.toml"
108
+ pyproject.write_text(pyproject.read_text().replace("DEMO_ABI_VERSION 1", "DEMO_ABI_VERSION 2"))
109
+ completed = run(["hatch-build", "--hooks-only"], check=False, cwd=project, capture_output=True, text=True)
110
+ assert completed.returncode != 0
111
+ assert "Artifact 'demo' expected ABI string was not found in headers: DEMO_ABI_VERSION 2" in completed.stderr + completed.stdout
112
+
113
+
114
+ def test_validation_fails_on_unexported_header_function(tmp_path, monkeypatch):
115
+ monkeypatch.setattr("hatch_native.validation.exported_symbols", lambda path: {"demo_answer"})
116
+ library = {**LIBRARY, "expected-headers": ["include/demo/demo.h"], "header-symbol-prefix": "demo_"}
117
+ (tmp_path / "include/demo").mkdir(parents=True)
118
+ (tmp_path / "include/demo/demo.h").write_text("int demo_answer(void);\nint demo_abi_version(void);\n")
119
+ plan = _plan(libraries=[library])
120
+ output = tmp_path / plan.libraries[0].get_qualified_name(plan.platform.platform)
121
+ output.parent.mkdir(parents=True)
122
+ output.write_bytes(b"")
123
+
124
+ with pytest.raises(RuntimeError, match="expected symbol was not exported: demo_abi_version"):
125
+ plan.validate_artifacts(tmp_path, [])
126
+
127
+
128
+ def test_validation_fails_on_missing_abi_string(tmp_path):
129
+ library = {**LIBRARY, "expected-headers": ["demo.h"], "expected-abi-strings": ["DEMO_ABI_VERSION 2"]}
130
+ (tmp_path / "demo.h").write_text("#define DEMO_ABI_VERSION 1\n")
131
+ plan = _plan(libraries=[library])
132
+ output = tmp_path / plan.libraries[0].get_qualified_name(plan.platform.platform)
133
+ output.parent.mkdir(parents=True)
134
+ output.write_bytes(b"")
135
+
136
+ with pytest.raises(RuntimeError, match="ABI string was not found"):
137
+ plan.validate_artifacts(tmp_path, [])
138
+
139
+
140
+ def test_libraries_without_validation_fields_are_not_targets():
141
+ assert _plan(libraries=[LIBRARY]).validation_targets([]) == []
142
+
143
+
144
+ def test_artifact_targets_resolve_by_path_library_and_packaged_name():
145
+ plan = _plan(
146
+ libraries=[LIBRARY],
147
+ artifacts=[
148
+ {"name": "explicit", "path": "somewhere/libexplicit.so"},
149
+ {"name": "project/lib/libdemo", "expected-symbols": ["demo_answer"]},
150
+ {"name": "installed", "expected-symbols": ["installed_answer"]},
151
+ ],
152
+ )
153
+ installed = {"win32": "installed.dll", "darwin": "libinstalled.dylib"}.get(plan.platform.platform, "libinstalled.so")
154
+ paths = [path for _, _, path in plan.validation_targets([f"project/lib/{installed}", "project/extension.so"])]
155
+ assert paths == ["somewhere/libexplicit.so", plan.libraries[0].get_qualified_name(plan.platform.platform), f"project/lib/{installed}"]
156
+
157
+
158
+ def test_artifact_without_unique_packaged_match_needs_a_path():
159
+ plan = _plan(artifacts=[{"name": "missing"}])
160
+ with pytest.raises(FileNotFoundError, match="Artifact 'missing' needs a path"):
161
+ plan.validation_targets([])
162
+
163
+
164
+ def test_depends_must_name_an_earlier_generic_library():
165
+ with pytest.raises(ValidationError, match="must be a generic library declared before it"):
166
+ _plan(libraries=[{"name": "project/extension", "sources": ["ext.c"], "depends": ["project/lib/libdemo"]}, LIBRARY])
167
+
168
+
169
+ def test_generic_library_gets_layout_neutral_identity():
170
+ plan = _plan(libraries=[LIBRARY])
171
+ library = plan.libraries[0]
172
+ plan.platform.toolchain = "clang"
173
+ plan.platform.platform = "darwin"
174
+ assert "-Wl,-install_name,@rpath/libdemo.dylib" in plan.platform.get_link_flags(library)
175
+ plan.platform.platform = "linux"
176
+ assert "-Wl,-soname,libdemo.so" in plan.platform.get_link_flags(library)
177
+ library.extra_link_args = ["-Wl,-soname,libdemo.so.1"]
178
+ assert "-Wl,-soname,libdemo.so " not in plan.platform.get_link_flags(library)
179
+
180
+
181
+ def test_depends_links_output_and_sets_rpath():
182
+ extension = {"name": "project/extension", "sources": ["ext.c"], "depends": ["project/lib/libdemo"]}
183
+ plan = _plan(libraries=[LIBRARY, extension])
184
+ producer, consumer = plan.libraries
185
+ plan.platform.toolchain = "gcc"
186
+ plan.platform.platform = "linux"
187
+ flags = plan.platform.get_link_flags(consumer, dependencies=[producer])
188
+ assert "project/lib/libdemo.so -Wl,-rpath,\\$ORIGIN/lib" in flags
189
+ plan.platform.toolchain = "msvc"
190
+ plan.platform.platform = "win32"
191
+ assert "project/lib/libdemo.lib" in plan.platform.get_link_flags(consumer, dependencies=[producer]).replace("\\", "/")
192
+ plan.platform.toolchain = "clang"
193
+ plan.platform.platform = "emscripten"
194
+ with pytest.raises(ValueError, match="not supported for Emscripten"):
195
+ plan.platform.get_link_flags(consumer, dependencies=[producer])
196
+
197
+
198
+ def test_artifact_manifest_requires_destination():
199
+ with pytest.raises(ValidationError, match="artifact-manifest requires artifact-manifest-destination"):
200
+ _plan(**{"artifact-manifest": True})
201
+
202
+
203
+ @pytest.mark.parametrize(
204
+ "path,target_platform,role",
205
+ [
206
+ ("project/extension.cpython-312-x86_64-linux-gnu.so", "linux", "python-extension"),
207
+ ("project/extension.abi3.so", "darwin", "python-extension"),
208
+ ("project/extension.so", "linux", "python-extension"),
209
+ ("project/extension.pyd", "win32", "python-extension"),
210
+ ("project/lib/libdemo.so", "linux", "library"),
211
+ ("project/lib/libdemo.dylib", "darwin", "library"),
212
+ ("project/lib/demo.dll", "win32", "library"),
213
+ ],
214
+ )
215
+ def test_artifact_role(path, target_platform, role):
216
+ assert _artifact_role(path, target_platform) == role
@@ -3,7 +3,6 @@ from __future__ import annotations
3
3
  from os import environ
4
4
  from pathlib import Path
5
5
  from sys import version_info
6
- from typing import Any
7
6
 
8
7
  from pydantic import BaseModel, Field
9
8
 
@@ -33,7 +32,8 @@ class HatchCppCmakeConfiguration(BaseModel):
33
32
 
34
33
  include_flags: dict[str, str | int | float | bool] | None = Field(default=None)
35
34
 
36
- def generate(self, config) -> dict[str, Any]:
35
+ def generate(self, config) -> list[str]:
36
+ assert self.root is not None
37
37
  commands = []
38
38
 
39
39
  # Derive prefix
@@ -1,6 +1,7 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from os import environ
4
+ from os.path import relpath
4
5
  from pathlib import Path
5
6
  from re import match
6
7
  from shutil import which
@@ -8,12 +9,14 @@ from sys import base_exec_prefix, exec_prefix, executable, platform as sys_platf
8
9
  from sysconfig import get_config_var, get_path
9
10
  from typing import Any, Literal
10
11
 
12
+ from hatch_native import ExternalLibraryConfig, LibraryValidationConfig
11
13
  from pydantic import AliasChoices, BaseModel, Field, field_validator, model_validator
12
14
 
13
15
  __all__ = (
14
16
  "Binding",
15
17
  "BuildType",
16
18
  "CompilerToolchain",
19
+ "HatchCppArtifact",
17
20
  "HatchCppLibrary",
18
21
  "HatchCppPlatform",
19
22
  "Language",
@@ -38,7 +41,7 @@ PlatformDefaults = {
38
41
  }
39
42
 
40
43
 
41
- class HatchCppLibrary(BaseModel, validate_assignment=True):
44
+ class HatchCppLibrary(LibraryValidationConfig, ExternalLibraryConfig, validate_assignment=True):
42
45
  """A C++ library."""
43
46
 
44
47
  name: str
@@ -48,50 +51,56 @@ class HatchCppLibrary(BaseModel, validate_assignment=True):
48
51
  binding: Binding = "cpython"
49
52
  std: str | None = None
50
53
 
51
- include_dirs: list[str] = Field(default_factory=list, alias=AliasChoices("include_dirs", "include-dirs"))
52
- include_dirs_linux: list[str] = Field(default_factory=list, alias=AliasChoices("include_dirs_linux", "include-dirs-linux"))
53
- include_dirs_darwin: list[str] = Field(default_factory=list, alias=AliasChoices("include_dirs_darwin", "include-dirs-darwin"))
54
- include_dirs_win32: list[str] = Field(default_factory=list, alias=AliasChoices("include_dirs_win32", "include-dirs-win32"))
54
+ include_dirs: list[str] = Field(default_factory=list, validation_alias=AliasChoices("include_dirs", "include-dirs"))
55
+ include_dirs_linux: list[str] = Field(default_factory=list, validation_alias=AliasChoices("include_dirs_linux", "include-dirs-linux"))
56
+ include_dirs_darwin: list[str] = Field(default_factory=list, validation_alias=AliasChoices("include_dirs_darwin", "include-dirs-darwin"))
57
+ include_dirs_win32: list[str] = Field(default_factory=list, validation_alias=AliasChoices("include_dirs_win32", "include-dirs-win32"))
55
58
 
56
- library_dirs: list[str] = Field(default_factory=list, alias=AliasChoices("library_dirs", "library-dirs"))
57
- library_dirs_linux: list[str] = Field(default_factory=list, alias=AliasChoices("library_dirs_linux", "library-dirs-linux"))
58
- library_dirs_darwin: list[str] = Field(default_factory=list, alias=AliasChoices("library_dirs_darwin", "library-dirs-darwin"))
59
- library_dirs_win32: list[str] = Field(default_factory=list, alias=AliasChoices("library_dirs_win32", "library-dirs-win32"))
59
+ library_dirs: list[str] = Field(default_factory=list, validation_alias=AliasChoices("library_dirs", "library-dirs"))
60
+ library_dirs_linux: list[str] = Field(default_factory=list, validation_alias=AliasChoices("library_dirs_linux", "library-dirs-linux"))
61
+ library_dirs_darwin: list[str] = Field(default_factory=list, validation_alias=AliasChoices("library_dirs_darwin", "library-dirs-darwin"))
62
+ library_dirs_win32: list[str] = Field(default_factory=list, validation_alias=AliasChoices("library_dirs_win32", "library-dirs-win32"))
60
63
 
61
64
  libraries: list[str] = Field(default_factory=list)
62
- libraries_linux: list[str] = Field(default_factory=list, alias=AliasChoices("libraries_linux", "libraries-linux"))
63
- libraries_darwin: list[str] = Field(default_factory=list, alias=AliasChoices("libraries_darwin", "libraries-darwin"))
64
- libraries_win32: list[str] = Field(default_factory=list, alias=AliasChoices("libraries_win32", "libraries-win32"))
65
-
66
- extra_compile_args: list[str] = Field(default_factory=list, alias=AliasChoices("extra_compile_args", "extra-compile-args"))
67
- extra_compile_args_linux: list[str] = Field(default_factory=list, alias=AliasChoices("extra_compile_args_linux", "extra-compile-args-linux"))
68
- extra_compile_args_darwin: list[str] = Field(default_factory=list, alias=AliasChoices("extra_compile_args_darwin", "extra-compile-args-darwin"))
69
- extra_compile_args_win32: list[str] = Field(default_factory=list, alias=AliasChoices("extra_compile_args_win32", "extra-compile-args-win32"))
70
-
71
- extra_link_args: list[str] = Field(default_factory=list, alias=AliasChoices("extra_link_args", "extra-link-args"))
72
- extra_link_args_linux: list[str] = Field(default_factory=list, alias=AliasChoices("extra_link_args_linux", "extra-link-args-linux"))
73
- extra_link_args_darwin: list[str] = Field(default_factory=list, alias=AliasChoices("extra_link_args_darwin", "extra-link-args-darwin"))
74
- extra_link_args_win32: list[str] = Field(default_factory=list, alias=AliasChoices("extra_link_args_win32", "extra-link-args-win32"))
75
-
76
- extra_objects: list[str] = Field(default_factory=list, alias=AliasChoices("extra_objects", "extra-objects"))
77
- extra_objects_linux: list[str] = Field(default_factory=list, alias=AliasChoices("extra_objects_linux", "extra-objects-linux"))
78
- extra_objects_darwin: list[str] = Field(default_factory=list, alias=AliasChoices("extra_objects_darwin", "extra-objects-darwin"))
79
- extra_objects_win32: list[str] = Field(default_factory=list, alias=AliasChoices("extra_objects_win32", "extra-objects-win32"))
80
-
81
- define_macros: list[str] = Field(default_factory=list, alias=AliasChoices("define_macros", "define-macros"))
82
- define_macros_linux: list[str] = Field(default_factory=list, alias=AliasChoices("define_macros_linux", "define-macros-linux"))
83
- define_macros_darwin: list[str] = Field(default_factory=list, alias=AliasChoices("define_macros_darwin", "define-macros-darwin"))
84
- define_macros_win32: list[str] = Field(default_factory=list, alias=AliasChoices("define_macros_win32", "define-macros-win32"))
85
-
86
- undef_macros: list[str] = Field(default_factory=list, alias=AliasChoices("undef_macros", "undef-macros"))
87
- undef_macros_linux: list[str] = Field(default_factory=list, alias=AliasChoices("undef_macros_linux", "undef-macros-linux"))
88
- undef_macros_darwin: list[str] = Field(default_factory=list, alias=AliasChoices("undef_macros_darwin", "undef-macros-darwin"))
89
- undef_macros_win32: list[str] = Field(default_factory=list, alias=AliasChoices("undef_macros_win32", "undef-macros-win32"))
90
-
91
- export_symbols: list[str] = Field(default_factory=list, alias=AliasChoices("export_symbols", "export-symbols"))
65
+ libraries_linux: list[str] = Field(default_factory=list, validation_alias=AliasChoices("libraries_linux", "libraries-linux"))
66
+ libraries_darwin: list[str] = Field(default_factory=list, validation_alias=AliasChoices("libraries_darwin", "libraries-darwin"))
67
+ libraries_win32: list[str] = Field(default_factory=list, validation_alias=AliasChoices("libraries_win32", "libraries-win32"))
68
+
69
+ extra_compile_args: list[str] = Field(default_factory=list, validation_alias=AliasChoices("extra_compile_args", "extra-compile-args"))
70
+ extra_compile_args_linux: list[str] = Field(
71
+ default_factory=list, validation_alias=AliasChoices("extra_compile_args_linux", "extra-compile-args-linux")
72
+ )
73
+ extra_compile_args_darwin: list[str] = Field(
74
+ default_factory=list, validation_alias=AliasChoices("extra_compile_args_darwin", "extra-compile-args-darwin")
75
+ )
76
+ extra_compile_args_win32: list[str] = Field(
77
+ default_factory=list, validation_alias=AliasChoices("extra_compile_args_win32", "extra-compile-args-win32")
78
+ )
79
+
80
+ extra_link_args: list[str] = Field(default_factory=list, validation_alias=AliasChoices("extra_link_args", "extra-link-args"))
81
+ extra_link_args_linux: list[str] = Field(default_factory=list, validation_alias=AliasChoices("extra_link_args_linux", "extra-link-args-linux"))
82
+ extra_link_args_darwin: list[str] = Field(default_factory=list, validation_alias=AliasChoices("extra_link_args_darwin", "extra-link-args-darwin"))
83
+ extra_link_args_win32: list[str] = Field(default_factory=list, validation_alias=AliasChoices("extra_link_args_win32", "extra-link-args-win32"))
84
+
85
+ extra_objects: list[str] = Field(default_factory=list, validation_alias=AliasChoices("extra_objects", "extra-objects"))
86
+ extra_objects_linux: list[str] = Field(default_factory=list, validation_alias=AliasChoices("extra_objects_linux", "extra-objects-linux"))
87
+ extra_objects_darwin: list[str] = Field(default_factory=list, validation_alias=AliasChoices("extra_objects_darwin", "extra-objects-darwin"))
88
+ extra_objects_win32: list[str] = Field(default_factory=list, validation_alias=AliasChoices("extra_objects_win32", "extra-objects-win32"))
89
+
90
+ define_macros: list[str] = Field(default_factory=list, validation_alias=AliasChoices("define_macros", "define-macros"))
91
+ define_macros_linux: list[str] = Field(default_factory=list, validation_alias=AliasChoices("define_macros_linux", "define-macros-linux"))
92
+ define_macros_darwin: list[str] = Field(default_factory=list, validation_alias=AliasChoices("define_macros_darwin", "define-macros-darwin"))
93
+ define_macros_win32: list[str] = Field(default_factory=list, validation_alias=AliasChoices("define_macros_win32", "define-macros-win32"))
94
+
95
+ undef_macros: list[str] = Field(default_factory=list, validation_alias=AliasChoices("undef_macros", "undef-macros"))
96
+ undef_macros_linux: list[str] = Field(default_factory=list, validation_alias=AliasChoices("undef_macros_linux", "undef-macros-linux"))
97
+ undef_macros_darwin: list[str] = Field(default_factory=list, validation_alias=AliasChoices("undef_macros_darwin", "undef-macros-darwin"))
98
+ undef_macros_win32: list[str] = Field(default_factory=list, validation_alias=AliasChoices("undef_macros_win32", "undef-macros-win32"))
99
+
100
+ export_symbols: list[str] = Field(default_factory=list, validation_alias=AliasChoices("export_symbols", "export-symbols"))
92
101
  depends: list[str] = Field(default_factory=list)
93
102
 
94
- py_limited_api: str | None = Field(default="", alias=AliasChoices("py_limited_api", "py-limited-api"))
103
+ py_limited_api: str | None = Field(default="", validation_alias=AliasChoices("py_limited_api", "py-limited-api"))
95
104
 
96
105
  @field_validator("py_limited_api", mode="before")
97
106
  @classmethod
@@ -215,6 +224,13 @@ class HatchCppLibrary(BaseModel, validate_assignment=True):
215
224
  return macros
216
225
 
217
226
 
227
+ class HatchCppArtifact(LibraryValidationConfig):
228
+ """A built or installed library to validate, named by a `libraries` entry, a path, or a CMake-installed library name."""
229
+
230
+ name: str
231
+ path: str | None = Field(default=None, description="Library path relative to the project root. Inferred from `name` when unset.")
232
+
233
+
218
234
  def _normalize_rpath(value: str, platform: Platform) -> str:
219
235
  r"""Translate and escape rpath values for the target platform.
220
236
 
@@ -363,9 +379,10 @@ class HatchCppPlatform(BaseModel):
363
379
  flags = flags.replace(" ", " ")
364
380
  return flags
365
381
 
366
- def get_link_flags(self, library: HatchCppLibrary, build_type: BuildType = "release") -> str:
382
+ def get_link_flags(self, library: HatchCppLibrary, build_type: BuildType = "release", dependencies: list[HatchCppLibrary] | None = None) -> str:
367
383
  flags = ""
368
384
  effective_link_args = library.get_effective_link_args(self.platform)
385
+ effective_link_args.extend(self._library_identity_args(library, effective_link_args))
369
386
  effective_extra_objects = library.get_effective_extra_objects(self.platform)
370
387
  effective_libraries = library.get_effective_libraries(self.platform)
371
388
  effective_library_dirs = library.get_effective_library_dirs(self.platform)
@@ -377,6 +394,7 @@ class HatchCppPlatform(BaseModel):
377
394
  flags += " -shared"
378
395
  flags += " " + " ".join(effective_link_args)
379
396
  flags += " " + " ".join(effective_extra_objects)
397
+ flags += " " + " ".join(self._dependency_link_args(library, dependencies or []))
380
398
  flags += " " + " ".join(f"-l{lib}" for lib in effective_libraries)
381
399
  flags += " " + " ".join(f"-L{lib}" for lib in effective_library_dirs)
382
400
  flags += f" -o {library.get_qualified_name(self.platform)}"
@@ -407,8 +425,41 @@ class HatchCppPlatform(BaseModel):
407
425
  flags += f" /LIBPATH:{libs_path!s}"
408
426
  break
409
427
  flags += " " + " ".join(f"{lib}.lib" for lib in effective_libraries)
428
+ flags += " " + " ".join(self._dependency_link_args(library, dependencies or []))
410
429
  flags += " " + " ".join(f"/LIBPATH:{lib}" for lib in effective_library_dirs)
411
430
  # clean
412
431
  while flags.count(" "):
413
432
  flags = flags.replace(" ", " ")
414
433
  return flags
434
+
435
+ def _library_identity_args(self, library: HatchCppLibrary, link_args: list[str]) -> list[str]:
436
+ """Give a generic library a layout-neutral SONAME or @rpath install name unless the project sets one."""
437
+ if library.binding != "generic" or self.toolchain == "msvc":
438
+ return []
439
+ filename = Path(library.get_qualified_name(self.platform)).name
440
+ existing = " ".join(link_args)
441
+ if self.platform == "linux" and "soname" not in existing:
442
+ return [f"-Wl,-soname,{filename}"]
443
+ if self.platform == "darwin" and "install_name" not in existing:
444
+ return [f"-Wl,-install_name,@rpath/{filename}"]
445
+ return []
446
+
447
+ def _dependency_link_args(self, library: HatchCppLibrary, dependencies: list[HatchCppLibrary]) -> list[str]:
448
+ """Link `library` against the outputs of its `depends` entries and add an rpath to each output directory."""
449
+ if dependencies and self.platform == "emscripten":
450
+ raise ValueError(f"Library '{library.name}' sets depends, which is not supported for Emscripten builds.")
451
+ output_dir = Path(library.get_qualified_name(self.platform)).parent
452
+ args: list[str] = []
453
+ rpaths: list[str] = []
454
+ for dependency in dependencies:
455
+ dependency_path = Path(dependency.get_qualified_name(self.platform))
456
+ if self.platform == "win32":
457
+ args.append(str(dependency_path.with_suffix(".lib")))
458
+ continue
459
+ args.append(dependency_path.as_posix())
460
+ relative = Path(relpath(dependency_path.parent, output_dir)).as_posix()
461
+ origin = "@loader_path" if self.platform == "darwin" else "$ORIGIN"
462
+ rpath = _normalize_rpath(origin if relative == "." else f"{origin}/{relative}", self.platform)
463
+ if rpath not in rpaths:
464
+ rpaths.append(rpath)
465
+ return args + [f"-Wl,-rpath,{rpath}" for rpath in rpaths]
@@ -31,7 +31,7 @@ VcpkgTriplet = Literal[
31
31
  "arm64-windows-static-md",
32
32
  "wasm32-emscripten",
33
33
  ]
34
- VcpkgPlatformDefaults = {
34
+ VcpkgPlatformDefaults: dict[tuple[str, str], VcpkgTriplet] = {
35
35
  ("linux", "x86_64"): "x64-linux",
36
36
  ("linux", "aarch64"): "arm64-linux",
37
37
  ("darwin", "x86_64"): "x64-osx",
@@ -65,7 +65,7 @@ def _read_vcpkg_ref_from_gitmodules(vcpkg_root: Path) -> str | None:
65
65
 
66
66
  class HatchCppVcpkgConfiguration(BaseModel):
67
67
  vcpkg: str | None = Field(default="vcpkg.json")
68
- vcpkg_root: Path | None = Field(default=Path("vcpkg"))
68
+ vcpkg_root: Path = Field(default=Path("vcpkg"))
69
69
  vcpkg_repo: str | None = Field(default="https://github.com/microsoft/vcpkg.git")
70
70
  vcpkg_triplet: VcpkgTriplet | None = Field(default=None)
71
71
  vcpkg_ref: str | None = Field(
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: hatch-cpp
3
- Version: 0.5.2
3
+ Version: 0.6.0
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,7 +20,8 @@ Classifier: Programming Language :: Python :: Implementation :: CPython
20
20
  Classifier: Programming Language :: Python :: Implementation :: PyPy
21
21
  Requires-Python: >=3.10
22
22
  Requires-Dist: hatch-build<0.6,>=0.5
23
- Requires-Dist: hatchling!=1.32.1
23
+ Requires-Dist: hatch-native<0.2,>=0.1.0
24
+ Requires-Dist: hatchling
24
25
  Requires-Dist: pkn
25
26
  Requires-Dist: pydantic
26
27
  Provides-Extra: develop
@@ -29,8 +30,9 @@ Requires-Dist: bump-my-version; extra == 'develop'
29
30
  Requires-Dist: check-dist; extra == 'develop'
30
31
  Requires-Dist: codespell; extra == 'develop'
31
32
  Requires-Dist: hatch-build>=0.3.2; extra == 'develop'
32
- Requires-Dist: hatchling!=1.32.1; extra == 'develop'
33
+ Requires-Dist: hatchling; extra == 'develop'
33
34
  Requires-Dist: mdformat; extra == 'develop'
35
+ Requires-Dist: mdformat-myst; extra == 'develop'
34
36
  Requires-Dist: mdformat-tables>=1; extra == 'develop'
35
37
  Requires-Dist: nanobind; extra == 'develop'
36
38
  Requires-Dist: pybind11; extra == 'develop'
@@ -129,6 +131,72 @@ cmake_env_args = {} # env-specific cmake args to pass
129
131
  include_flags = {} # include flags to pass -D
130
132
  ```
131
133
 
134
+ ### Linking an extension against a library
135
+
136
+ A `generic` library can ship next to the extension that links it, in the layout pyarrow uses. List the library first and name it in the extension's `depends`:
137
+
138
+ ```toml
139
+ [[tool.hatch.build.hooks.hatch-cpp.libraries]]
140
+ name = "project/lib/libdemo"
141
+ sources = ["cpp/demo.c"]
142
+ language = "c"
143
+ binding = "generic"
144
+ include-dirs = ["cpp/include"]
145
+ define-macros = ["DEMO_BUILD"]
146
+
147
+ [[tool.hatch.build.hooks.hatch-cpp.libraries]]
148
+ name = "project/extension"
149
+ sources = ["cpp/extension.c"]
150
+ language = "c"
151
+ include-dirs = ["cpp/include"]
152
+ depends = ["project/lib/libdemo"]
153
+ ```
154
+
155
+ The library builds first. On Linux it gets `-soname libdemo.so` and on macOS the install name `@rpath/libdemo.dylib`, unless `extra-link-args` already set one. The extension links the built file and gets an rpath to its directory (`$ORIGIN/lib`, `@loader_path/lib`), so the wheel works wherever it is installed. On Windows the extension links the import library `project/lib/libdemo.lib`, which is packaged next to the DLL. Windows has no rpath, so the package `__init__.py` must call `os.add_dll_directory` on `lib/` before importing the extension, and the library's header must mark its functions `__declspec(dllexport)` when building it and `__declspec(dllimport)` otherwise. `depends` is not supported for Emscripten builds.
156
+
157
+ `library-source` (or `HATCH_CPP_LIBRARY_SOURCE`, which wins) picks how libraries named in `depends` are provided:
158
+
159
+ - `bundled` (default): built and shipped in the wheel as above.
160
+ - `external`: not built. Each one is found under `library-root` (or `HATCH_CPP_LIBRARY_ROOT`) with `lib/` and `include/`, through `pkg-config = "demo"`, or under `$PREFIX`/`$CONDA_PREFIX` (`Library/` on Windows), by its name without the `lib` prefix. The extension links it with `-L`/`-l` and gets no rpath, so the packager supplies the runtime path; this is the mode a conda recipe uses. Validation fields on the entry check the external library.
161
+ - `direct`: not built. Its sources, include directories, and macros are compiled into each entry that depends on it, and no library ships. Use this for Pyodide.
162
+
163
+ `static` and `python-package` are not supported yet.
164
+
165
+ Wheel repair tools need to leave the bundled library in place: `auditwheel repair --exclude 'libdemo.so'`, `delocate-wheel --exclude libdemo`, `delvewheel repair --exclude libdemo.dll`.
166
+
167
+ ### C ABI validation
168
+
169
+ A library that other code links against can be checked after it is built. Validation reads the export table from the binary, so it also works for cross builds and Pyodide. The field names match [hatch-rs](https://github.com/python-project-templates/hatch-rs) and come from [hatch-native](https://github.com/python-project-templates/hatch-native).
170
+
171
+ Set the fields on a `libraries` entry:
172
+
173
+ ```toml
174
+ [[tool.hatch.build.hooks.hatch-cpp.libraries]]
175
+ name = "project/lib/libdemo"
176
+ sources = ["cpp/demo.c"]
177
+ language = "c"
178
+ binding = "generic"
179
+ include-dirs = ["cpp/include"]
180
+ expected-headers = ["cpp/include/demo/demo.h"]
181
+ header-symbol-prefix = "demo_" # every demo_* function in the headers must be exported
182
+ expected-abi-strings = ["DEMO_ABI_VERSION 1"]
183
+ expected-symbols = [] # extra symbols to require
184
+ validate = false # also load the library with ctypes
185
+ ```
186
+
187
+ Or list `artifacts` to validate files that `libraries` does not name, such as the output of a CMake install. Each entry takes the same fields plus `name` and an optional `path` relative to the project root. Without `path`, `name` matches a `libraries` entry, or else the one packaged file called `lib<name>.so`, `lib<name>.dylib`, or `<name>.dll`:
188
+
189
+ ```toml
190
+ [[tool.hatch.build.hooks.hatch-cpp.artifacts]]
191
+ name = "demo"
192
+ expected-headers = ["project/include/demo/demo.h"]
193
+ header-symbol-prefix = "demo_"
194
+ ```
195
+
196
+ `validation-commands` run after the checks, with `{name}`, `{path}`, and `{header}` substituted and `HATCH_CPP_ARTIFACT_NAME` and `HATCH_CPP_ARTIFACT_PATH` in the environment.
197
+
198
+ To record what was packaged, set `artifact-manifest = true` and `artifact-manifest-destination = "project/lib/hatch-cpp-artifacts.json"`. The JSON lists each packaged binary with its role (`python-extension` or `library`), platform, and profile, and is added to the wheel.
199
+
132
200
  ### CLI
133
201
 
134
202
  `hatch-cpp` is integrated with [`hatch-build`](https://github.com/python-project-templates/hatch-build) to allow easy configuration of options via command line:
@@ -1,18 +1,24 @@
1
- hatch_cpp/__init__.py,sha256=UxfEw36fxdjilyMAx80Fby60eY8HCALAzne3jFbazPc,114
2
- hatch_cpp/config.py,sha256=YY0kqhKxIPOIwiuSb6Q0u1EtbKTScw2eIs5zWYz5FBA,5188
1
+ hatch_cpp/__init__.py,sha256=yiHjMzh81f0uVdV6S_N_MGn4jDP1emy4IxL0Zu_fg0k,114
2
+ hatch_cpp/config.py,sha256=_zgE48JIAPrb5P1NawncDDogw2crEMH-95VoBKxtitg,15677
3
3
  hatch_cpp/hooks.py,sha256=OfcAgx6gVL0ukvpyln5OTiwhqxObJ0b4FKr850fYP3I,178
4
- hatch_cpp/plugin.py,sha256=yaIzmENsGuJPtBvwcJJwxxKduRJumU7zbMyDTSf2ICw,4641
4
+ hatch_cpp/plugin.py,sha256=9ZGb5XiTlHOL967tw5G0EQspcgiO8orakRJVV0lL9tk,5279
5
5
  hatch_cpp/utils.py,sha256=dxLCyoMLC7C0trJ8mlvwA5-JzVNt2JI9JSGYcHTHBY4,275
6
- hatch_cpp/tests/test_hatch_build.py,sha256=q2IKTFi3Pq99UsOyL84V-C9VtFUs3ZcA9UlA8lpRW54,1553
7
- hatch_cpp/tests/test_hook_registration.py,sha256=RQ-EqFpgqs4gkSGLEBB5sNIhVSdcAHutiH51xDhT3z4,621
6
+ hatch_cpp/tests/test_hatch_build.py,sha256=IU9E4hoQSj7STQ_3W6V_4ijo2B8p4ekP11eHoRXOsH8,1603
7
+ hatch_cpp/tests/test_hook_registration.py,sha256=CZFvwvehbNUtvNu88hvfVhaUk-0v5TZherDNxy9r8X0,759
8
8
  hatch_cpp/tests/test_platform_specific.py,sha256=SRKAojh6PkwKZnEj8SjvfFa_Dy8NSKw4R82ol8O1TZs,20126
9
- hatch_cpp/tests/test_projects.py,sha256=WAV8OAV-4Xp4o91S1nlj9ptmpexlXEIXnfSozt0mhCI,2123
9
+ hatch_cpp/tests/test_projects.py,sha256=X2XVwSI_yyle-oUcl4Fr3UyKmoe1OvXbVRd379ymyi8,2173
10
10
  hatch_cpp/tests/test_structs.py,sha256=BXpLW7pDdA3apKdFaDxVK_7pKPGi_7y0G58bm-Itz54,14782
11
+ hatch_cpp/tests/test_validation.py,sha256=CF4YAjoVBN6MTl_4EEU1h0CMN0K7qddEjhr_du8mBRY,10050
11
12
  hatch_cpp/tests/test_vcpkg_ref.py,sha256=f6VLFQZeD3aWVbAVB2Ds3VAE1kSphgbEnkfzHqxa3Uo,12116
12
13
  hatch_cpp/tests/test_project_basic/pyproject.toml,sha256=eqM1UVpNmJWDfsuO18ZG_VOV9I4tAWgsM5Dhf49X8Nc,694
13
14
  hatch_cpp/tests/test_project_basic/cpp/project/basic.cpp,sha256=gQ2nmdLqIdgaqxSKvkN_vbp6Iv_pAoVIHETXPRnALb0,117
14
15
  hatch_cpp/tests/test_project_basic/cpp/project/basic.hpp,sha256=SO5GhPj8k3RzWrfH37lFSDc8w1Vf3yqTUhxmr1hnoko,422
15
16
  hatch_cpp/tests/test_project_basic/project/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
+ hatch_cpp/tests/test_project_c_abi_library/pyproject.toml,sha256=Rsr4569r5NyH-kQxyc5tPnav1_iHU0xdSZwiXDF-BX8,1320
18
+ hatch_cpp/tests/test_project_c_abi_library/cpp/demo.c,sha256=F9G0TCGpkdeSDHNe_p_5dV2QRoLswTkRP1LryVGOLFc,118
19
+ hatch_cpp/tests/test_project_c_abi_library/cpp/extension.c,sha256=dFLHxWzfW3mfTBfqcgjoh-b44WU5q_5Ji5jJ9WsKBVY,409
20
+ hatch_cpp/tests/test_project_c_abi_library/cpp/include/demo/demo.h,sha256=v2kWL3eWeIDejZKhvJbKD1icvdbByxf-fZ7jqNJv4KA,393
21
+ hatch_cpp/tests/test_project_c_abi_library/project/__init__.py,sha256=DyTczKyrjxl9YyiG2Tz8Z5v4z58FD3suNWQvTuySzNA,123
16
22
  hatch_cpp/tests/test_project_cmake/CMakeLists.txt,sha256=_62H73yB-hJUCj_PiOGZUTauFB25q0h1TOQ9n855A74,2605
17
23
  hatch_cpp/tests/test_project_cmake/Makefile,sha256=Vfr65pvnktWTUeuvpMWqIxDR-7V9MT4PQAyj-WHG_pI,4357
18
24
  hatch_cpp/tests/test_project_cmake/pyproject.toml,sha256=judDNJ0qYM6-ocGXtUEHQlUzyj8GsF5qoJ-jPDsIAdA,815
@@ -20,6 +26,12 @@ hatch_cpp/tests/test_project_cmake/cpp/project/basic.cpp,sha256=gQ2nmdLqIdgaqxSK
20
26
  hatch_cpp/tests/test_project_cmake/cpp/project/basic.hpp,sha256=SO5GhPj8k3RzWrfH37lFSDc8w1Vf3yqTUhxmr1hnoko,422
21
27
  hatch_cpp/tests/test_project_cmake/project/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
22
28
  hatch_cpp/tests/test_project_cmake/project/include/project/basic.hpp,sha256=SO5GhPj8k3RzWrfH37lFSDc8w1Vf3yqTUhxmr1hnoko,422
29
+ hatch_cpp/tests/test_project_cmake_c_abi/CMakeLists.txt,sha256=UfT2HhfoTSO0tWH6TqQw3yuqAgUDX1gmo3wtWXHUYbE,1132
30
+ hatch_cpp/tests/test_project_cmake_c_abi/pyproject.toml,sha256=MhyApGLyN1gR6DEpxwsbbmHjEBRsI0TGBd17Fr--iXY,1218
31
+ hatch_cpp/tests/test_project_cmake_c_abi/cpp/demo.c,sha256=F9G0TCGpkdeSDHNe_p_5dV2QRoLswTkRP1LryVGOLFc,118
32
+ hatch_cpp/tests/test_project_cmake_c_abi/cpp/extension.c,sha256=dFLHxWzfW3mfTBfqcgjoh-b44WU5q_5Ji5jJ9WsKBVY,409
33
+ hatch_cpp/tests/test_project_cmake_c_abi/cpp/include/demo/demo.h,sha256=v2kWL3eWeIDejZKhvJbKD1icvdbByxf-fZ7jqNJv4KA,393
34
+ hatch_cpp/tests/test_project_cmake_c_abi/project/__init__.py,sha256=DyTczKyrjxl9YyiG2Tz8Z5v4z58FD3suNWQvTuySzNA,123
23
35
  hatch_cpp/tests/test_project_cmake_vcpkg/CMakeLists.txt,sha256=_62H73yB-hJUCj_PiOGZUTauFB25q0h1TOQ9n855A74,2605
24
36
  hatch_cpp/tests/test_project_cmake_vcpkg/Makefile,sha256=Vfr65pvnktWTUeuvpMWqIxDR-7V9MT4PQAyj-WHG_pI,4357
25
37
  hatch_cpp/tests/test_project_cmake_vcpkg/pyproject.toml,sha256=pu9RWhm_FY7KlcdOg2nmp-obnStnPJ04jW-bxei704w,814
@@ -60,12 +72,13 @@ hatch_cpp/tests/test_project_pybind_vcpkg/project/__init__.py,sha256=47DEQpj8HBS
60
72
  hatch_cpp/tests/test_project_pyodide/pyproject.toml,sha256=buJBSEm7f9WGiwtGJZ1KK0BZbh6UQVnb8TiyPdu7a7o,521
61
73
  hatch_cpp/tests/test_project_pyodide/cpp/extension.cpp,sha256=HO-gfcq9KayA_-q43_PttFWRqBzYak-rsnoBhk3l7OM,139
62
74
  hatch_cpp/tests/test_project_pyodide/pyodide_project/__init__.py,sha256=Xmqb3gucEgPuFaNg9SEDKKJwsp05NejfW0V71_lNmQ4,53
75
+ hatch_cpp/tests/test_project_pyodide/pyodide_project/extension.pyi,sha256=CEn4ckZjJImp5DwZoraAQq8BeGowVj3ahluQnY13kEo,25
63
76
  hatch_cpp/toolchains/__init__.py,sha256=anza4tXKxauobWMOUrxX3sEO0qs7sp6VdLXhLae3vkY,64
64
- hatch_cpp/toolchains/cmake.py,sha256=9fOGXuJ4bpkKjU789GApK59WS4iZQNQK19Q_Zq_Byb4,3709
65
- hatch_cpp/toolchains/common.py,sha256=lU6o5J8Kvhp-eNlq5BKp4CQ7TVOlyKMbP4pgMwIbYog,20235
66
- hatch_cpp/toolchains/vcpkg.py,sha256=q527R-6nhjkCiTG2cMyoJFwBJGjq8crGlsCy7V5skvk,5657
67
- hatch_cpp-0.5.2.dist-info/METADATA,sha256=P84AO15SOB1jfGMm8Sr4tmla4h0RmLKGpMeG1GzMq_M,10556
68
- hatch_cpp-0.5.2.dist-info/WHEEL,sha256=zOwg4jB6zX2kU910N-cMawjivD6tO8NEWvE12je1bVk,87
69
- hatch_cpp-0.5.2.dist-info/entry_points.txt,sha256=RgXfjpD4iwomJQK5n7FnPkXzb5fOnF5v3DI5hbkgVcw,30
70
- hatch_cpp-0.5.2.dist-info/licenses/LICENSE,sha256=FWyFTpd5xXEz50QpGDtsyIv6dgR2z_dHdoab_Nq_KMw,11452
71
- hatch_cpp-0.5.2.dist-info/RECORD,,
77
+ hatch_cpp/toolchains/cmake.py,sha256=u8K7tz4AjEAFurz3JrqGiixjyxn_W79a4QfHFT-dicE,3718
78
+ hatch_cpp/toolchains/common.py,sha256=tiUyjySZh6vzNZ_SRLgqhJDR4k8vexiAkRfDSnl7RoY,23399
79
+ hatch_cpp/toolchains/vcpkg.py,sha256=4zsLhJMVoTkvIN-jsWV46AolPIRB1NrU-EAAWuDJHsE,5687
80
+ hatch_cpp-0.6.0.dist-info/METADATA,sha256=wqTTbBVlgYpkKC-sDLkMbW6q6qgjrgKNQM3TrTki9-I,14870
81
+ hatch_cpp-0.6.0.dist-info/WHEEL,sha256=2iZrJkDIkeDh0k9SikKMqFuRP-ua6mb4Ir1AsER4BEw,87
82
+ hatch_cpp-0.6.0.dist-info/entry_points.txt,sha256=RgXfjpD4iwomJQK5n7FnPkXzb5fOnF5v3DI5hbkgVcw,30
83
+ hatch_cpp-0.6.0.dist-info/licenses/LICENSE,sha256=FWyFTpd5xXEz50QpGDtsyIv6dgR2z_dHdoab_Nq_KMw,11452
84
+ hatch_cpp-0.6.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: hatchling 1.32.0
2
+ Generator: hatchling 1.33.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any