Skip to content

Commit 710091a

Browse files
authored
Merge pull request #23 from python-project-templates/tkp/lim
Add support for limited abi, fixes #18
2 parents 2d21bf2 + 2568897 commit 710091a

File tree

12 files changed

+153
-38
lines changed

12 files changed

+153
-38
lines changed

hatch_cpp/plugin.py

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -32,19 +32,6 @@ def initialize(self, version: str, build_data: dict[str, t.Any]) -> None:
3232
self._logger.info("ignoring target name %s", self.target_name)
3333
return
3434

35-
build_data["pure_python"] = False
36-
machine = sysplatform.machine()
37-
version_major = sys.version_info.major
38-
version_minor = sys.version_info.minor
39-
# TODO abi3
40-
if "darwin" in sys.platform:
41-
os_name = "macosx_11_0"
42-
elif "linux" in sys.platform:
43-
os_name = "linux"
44-
else:
45-
os_name = "win"
46-
build_data["tag"] = f"cp{version_major}{version_minor}-cp{version_major}{version_minor}-{os_name}_{machine}"
47-
4835
# Skip if SKIP_HATCH_CPP is set
4936
# TODO: Support CLI once https://github.com/pypa/hatch/pull/1743
5037
if os.getenv("SKIP_HATCH_CPP"):
@@ -85,3 +72,20 @@ def initialize(self, version: str, build_data: dict[str, t.Any]) -> None:
8572
for library in libraries:
8673
name = library.get_qualified_name(build_plan.platform.platform)
8774
build_data["force_include"][name] = name
75+
76+
if libraries:
77+
build_data["pure_python"] = False
78+
machine = sysplatform.machine()
79+
version_major = sys.version_info.major
80+
version_minor = sys.version_info.minor
81+
# TODO abi3
82+
if "darwin" in sys.platform:
83+
os_name = "macosx_11_0"
84+
elif "linux" in sys.platform:
85+
os_name = "linux"
86+
else:
87+
os_name = "win"
88+
if all([lib.py_limited_api for lib in libraries]):
89+
build_data["tag"] = f"cp{version_major}{version_minor}-abi3-{os_name}_{machine}"
90+
else:
91+
build_data["tag"] = f"cp{version_major}{version_minor}-cp{version_major}{version_minor}-{os_name}_{machine}"

hatch_cpp/structs.py

Lines changed: 41 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,13 @@
22

33
from os import environ, system
44
from pathlib import Path
5+
from re import match
56
from shutil import which
67
from sys import executable, platform as sys_platform
78
from sysconfig import get_path
8-
from typing import List, Literal, Optional
9+
from typing import Any, List, Literal, Optional
910

10-
from pydantic import BaseModel, Field
11+
from pydantic import AliasChoices, BaseModel, Field, field_validator, model_validator
1112

1213
__all__ = (
1314
"HatchCppBuildConfig",
@@ -28,7 +29,7 @@
2829
}
2930

3031

31-
class HatchCppLibrary(BaseModel):
32+
class HatchCppLibrary(BaseModel, validate_assignment=True):
3233
"""A C++ library."""
3334

3435
name: str
@@ -38,29 +39,47 @@ class HatchCppLibrary(BaseModel):
3839
binding: Binding = "cpython"
3940
std: Optional[str] = None
4041

41-
include_dirs: List[str] = Field(default_factory=list, alias="include-dirs")
42-
library_dirs: List[str] = Field(default_factory=list, alias="library-dirs")
42+
include_dirs: List[str] = Field(default_factory=list, alias=AliasChoices("include_dirs", "include-dirs"))
43+
library_dirs: List[str] = Field(default_factory=list, alias=AliasChoices("library_dirs", "library-dirs"))
4344
libraries: List[str] = Field(default_factory=list)
4445

45-
extra_compile_args: List[str] = Field(default_factory=list, alias="extra-compile-args")
46-
extra_link_args: List[str] = Field(default_factory=list, alias="extra-link-args")
47-
extra_objects: List[str] = Field(default_factory=list, alias="extra-objects")
46+
extra_compile_args: List[str] = Field(default_factory=list, alias=AliasChoices("extra_compile_args", "extra-compile-args"))
47+
extra_link_args: List[str] = Field(default_factory=list, alias=AliasChoices("extra_link_args", "extra-link-args"))
48+
extra_objects: List[str] = Field(default_factory=list, alias=AliasChoices("extra_objects", "extra-objects"))
4849

49-
define_macros: List[str] = Field(default_factory=list, alias="define-macros")
50-
undef_macros: List[str] = Field(default_factory=list, alias="undef-macros")
50+
define_macros: List[str] = Field(default_factory=list, alias=AliasChoices("define_macros", "define-macros"))
51+
undef_macros: List[str] = Field(default_factory=list, alias=AliasChoices("undef_macros", "undef-macros"))
5152

52-
export_symbols: List[str] = Field(default_factory=list, alias="export-symbols")
53+
export_symbols: List[str] = Field(default_factory=list, alias=AliasChoices("export_symbols", "export-symbols"))
5354
depends: List[str] = Field(default_factory=list)
5455

56+
py_limited_api: Optional[str] = Field(default="", alias=AliasChoices("py_limited_api", "py-limited-api"))
57+
58+
@field_validator("py_limited_api", mode="before")
59+
@classmethod
60+
def check_py_limited_api(cls, value: Any) -> Any:
61+
if value:
62+
if not match(r"cp3\d", value):
63+
raise ValueError("py-limited-api must be in the form of cp3X")
64+
return value
65+
5566
def get_qualified_name(self, platform):
5667
if platform == "win32":
5768
suffix = "dll" if self.binding == "none" else "pyd"
58-
elif platform == "darwin" and self.binding == "none":
59-
suffix = "dylib"
69+
elif platform == "darwin":
70+
suffix = "dylib" if self.binding == "none" else "so"
6071
else:
6172
suffix = "so"
73+
if self.py_limited_api and platform != "win32":
74+
return f"{self.name}.abi3.{suffix}"
6275
return f"{self.name}.{suffix}"
6376

77+
@model_validator(mode="after")
78+
def check_binding_and_py_limited_api(self):
79+
if self.binding == "pybind11" and self.py_limited_api:
80+
raise ValueError("pybind11 does not support Py_LIMITED_API")
81+
return self
82+
6483

6584
class HatchCppPlatform(BaseModel):
6685
cc: str
@@ -117,6 +136,12 @@ def get_compile_flags(self, library: HatchCppLibrary, build_type: BuildType = "r
117136
library.sources.append(str(Path(nanobind.include_dir()).parent / "src" / "nb_combined.cpp"))
118137
library.include_dirs.append(str((Path(nanobind.include_dir()).parent / "ext" / "robin_map" / "include")))
119138

139+
if library.py_limited_api:
140+
if library.binding == "pybind11":
141+
raise ValueError("pybind11 does not support Py_LIMITED_API")
142+
library.define_macros.append(f"Py_LIMITED_API=0x0{library.py_limited_api[2]}0{hex(int(library.py_limited_api[3:]))[2:]}00f0")
143+
144+
# Toolchain-specific flags
120145
if self.toolchain == "gcc":
121146
flags += " " + " ".join(f"-I{d}" for d in library.include_dirs)
122147
flags += " -fPIC"
@@ -156,7 +181,7 @@ def get_link_flags(self, library: HatchCppLibrary, build_type: BuildType = "rele
156181
flags += " " + " ".join(library.extra_objects)
157182
flags += " " + " ".join(f"-l{lib}" for lib in library.libraries)
158183
flags += " " + " ".join(f"-L{lib}" for lib in library.library_dirs)
159-
flags += f" -o {library.name}.so"
184+
flags += f" -o {library.get_qualified_name(self.platform)}"
160185
if self.platform == "darwin":
161186
flags += " -undefined dynamic_lookup"
162187
if "mold" in self.ld:
@@ -169,7 +194,7 @@ def get_link_flags(self, library: HatchCppLibrary, build_type: BuildType = "rele
169194
flags += " " + " ".join(library.extra_objects)
170195
flags += " " + " ".join(f"-l{lib}" for lib in library.libraries)
171196
flags += " " + " ".join(f"-L{lib}" for lib in library.library_dirs)
172-
flags += f" -o {library.name}.so"
197+
flags += f" -o {library.get_qualified_name(self.platform)}"
173198
if self.platform == "darwin":
174199
flags += " -undefined dynamic_lookup"
175200
if "mold" in self.ld:
@@ -180,7 +205,7 @@ def get_link_flags(self, library: HatchCppLibrary, build_type: BuildType = "rele
180205
flags += " " + " ".join(library.extra_link_args)
181206
flags += " " + " ".join(library.extra_objects)
182207
flags += " /LD"
183-
flags += f" /Fe:{library.name}.pyd"
208+
flags += f" /Fe:{library.get_qualified_name(self.platform)}"
184209
flags += " /link /DLL"
185210
if (Path(executable).parent / "libs").exists():
186211
flags += f" /LIBPATH:{str(Path(executable).parent / 'libs')}"

hatch_cpp/tests/test_all.py

Lines changed: 0 additions & 2 deletions
This file was deleted.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
#include "project/basic.hpp"
2+
3+
PyObject* hello(PyObject*, PyObject*) {
4+
return PyUnicode_FromString("A string");
5+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
#pragma once
2+
#include "Python.h"
3+
4+
PyObject* hello(PyObject*, PyObject*);
5+
6+
static PyMethodDef extension_methods[] = {
7+
{"hello", (PyCFunction)hello, METH_NOARGS},
8+
{nullptr, nullptr, 0, nullptr}
9+
};
10+
11+
static PyModuleDef extension_module = {
12+
PyModuleDef_HEAD_INIT, "extension", "extension", -1, extension_methods};
13+
14+
PyMODINIT_FUNC PyInit_extension(void) {
15+
Py_Initialize();
16+
return PyModule_Create(&extension_module);
17+
}

hatch_cpp/tests/test_project_limited_api/project/__init__.py

Whitespace-only changes.
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
[build-system]
2+
requires = ["hatchling>=1.20"]
3+
build-backend = "hatchling.build"
4+
5+
[project]
6+
name = "hatch-cpp-test-project-limtied-api"
7+
description = "Basic test project for hatch-cpp"
8+
version = "0.1.0"
9+
requires-python = ">=3.9"
10+
dependencies = [
11+
"hatchling>=1.20",
12+
"hatch-cpp",
13+
]
14+
15+
[tool.hatch.build]
16+
artifacts = [
17+
"project/*.dll",
18+
"project/*.dylib",
19+
"project/*.so",
20+
]
21+
22+
[tool.hatch.build.sources]
23+
src = "/"
24+
25+
[tool.hatch.build.targets.sdist]
26+
packages = ["project"]
27+
28+
[tool.hatch.build.targets.wheel]
29+
packages = ["project"]
30+
31+
[tool.hatch.build.hooks.hatch-cpp]
32+
verbose = true
33+
libraries = [
34+
{name = "project/extension", sources = ["cpp/project/basic.cpp"], include-dirs = ["cpp"], py-limited-api = "cp39"},
35+
]

hatch_cpp/tests/test_project_nanobind/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ requires = ["hatchling>=1.20"]
33
build-backend = "hatchling.build"
44

55
[project]
6-
name = "hatch-cpp-test-project-basic"
6+
name = "hatch-cpp-test-project-nanobind"
77
description = "Basic test project for hatch-cpp"
88
version = "0.1.0"
99
requires-python = ">=3.9"

hatch_cpp/tests/test_project_override_classes/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ requires = ["hatchling>=1.20"]
33
build-backend = "hatchling.build"
44

55
[project]
6-
name = "hatch-cpp-test-project-basic"
6+
name = "hatch-cpp-test-project-override-classes"
77
description = "Basic test project for hatch-cpp"
88
version = "0.1.0"
99
requires-python = ">=3.9"

hatch_cpp/tests/test_project_pybind/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ requires = ["hatchling>=1.20"]
33
build-backend = "hatchling.build"
44

55
[project]
6-
name = "hatch-cpp-test-project-basic"
6+
name = "hatch-cpp-test-project-pybind"
77
description = "Basic test project for hatch-cpp"
88
version = "0.1.0"
99
requires-python = ">=3.9"

hatch_cpp/tests/test_projects.py

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88

99

1010
class TestProject:
11-
@pytest.mark.parametrize("project", ["test_project_basic", "test_project_override_classes", "test_project_pybind", "test_project_nanobind"])
11+
@pytest.mark.parametrize(
12+
"project", ["test_project_basic", "test_project_override_classes", "test_project_pybind", "test_project_nanobind", "test_project_limited_api"]
13+
)
1214
def test_basic(self, project):
1315
# cleanup
1416
rmtree(f"hatch_cpp/tests/{project}/project/extension.so", ignore_errors=True)
@@ -28,10 +30,13 @@ def test_basic(self, project):
2830

2931
# assert built
3032

31-
if platform == "win32":
32-
assert "extension.pyd" in listdir(f"hatch_cpp/tests/{project}/project")
33+
if project == "test_project_limited_api" and platform != "win32":
34+
assert "extension.abi3.so" in listdir(f"hatch_cpp/tests/{project}/project")
3335
else:
34-
assert "extension.so" in listdir(f"hatch_cpp/tests/{project}/project")
36+
if platform == "win32":
37+
assert "extension.pyd" in listdir(f"hatch_cpp/tests/{project}/project")
38+
else:
39+
assert "extension.so" in listdir(f"hatch_cpp/tests/{project}/project")
3540

3641
# import
3742
here = Path(__file__).parent / project

hatch_cpp/tests/test_structs.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
import pytest
2+
from pydantic import ValidationError
3+
4+
from hatch_cpp.structs import HatchCppLibrary, HatchCppPlatform
5+
6+
7+
class TestStructs:
8+
def test_validate_py_limited_api(self):
9+
with pytest.raises(ValidationError):
10+
library = HatchCppLibrary(
11+
name="test",
12+
sources=["test.cpp"],
13+
py_limited_api="42",
14+
)
15+
library = HatchCppLibrary(
16+
name="test",
17+
sources=["test.cpp"],
18+
py_limited_api="cp39",
19+
)
20+
assert library.py_limited_api == "cp39"
21+
platform = HatchCppPlatform.default()
22+
flags = platform.get_compile_flags(library)
23+
assert "-DPy_LIMITED_API=0x030900f0" in flags or "/DPy_LIMITED_API=0x030900f0" in flags
24+
25+
with pytest.raises(ValidationError):
26+
library.binding = "pybind11"

0 commit comments

Comments
 (0)