Skip to content

Commit 2a2bbbf

Browse files
authored
Merge pull request #83 from python-project-templates/fix/pr-81-abi3t
Support PEP 803 abi3t free-threaded Stable ABI
2 parents f63bf62 + eafbfc6 commit 2a2bbbf

10 files changed

Lines changed: 195 additions & 138 deletions

File tree

‎hatch_rs/plugin.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,7 @@ def initialize(self, version: str, build_data: dict[str, Any]) -> None:
9090
build_data["pure_python"] = False
9191
build_data["tag"] = wheel_tag(
9292
abi3=config.abi3,
93+
free_threaded=config.free_threaded,
9394
resolved_target=build_plan.resolved_target,
9495
platform_tag=config.wheel_platform_tag,
9596
)

‎hatch_rs/structs.py‎

Lines changed: 69 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
from __future__ import annotations
22

3+
import sys
4+
import sysconfig
35
from ctypes import CDLL
46
from dataclasses import dataclass
57
from glob import glob
@@ -127,6 +129,42 @@ def _env_truthy(value: str | None) -> bool:
127129
return value.strip().lower() not in ("", "0", "false", "no", "off")
128130

129131

132+
def _resolve_free_threaded(
133+
free_threaded: bool | None = None,
134+
*,
135+
python_version: tuple[int, int] | None = None,
136+
) -> bool:
137+
"""Return True when targeting or executing under free-threaded CPython."""
138+
if free_threaded is not None:
139+
if free_threaded and python_version is not None and python_version < (3, 13):
140+
raise ValueError(f"Free-threaded builds require Python 3.13 or newer, got {python_version[0]}.{python_version[1]}.")
141+
return free_threaded
142+
143+
if python_version is not None and python_version < (3, 13):
144+
return False
145+
146+
env_val = environ.get("HATCH_RUST_FREE_THREADED")
147+
if env_val is not None:
148+
return _env_truthy(env_val)
149+
150+
if python_version is not None and python_version != (version_info.major, version_info.minor):
151+
return False
152+
153+
if sysconfig.get_config_var("Py_GIL_DISABLED") == 1:
154+
return True
155+
156+
if "t" in getattr(sys, "abiflags", ""):
157+
return True
158+
159+
if hasattr(sys, "abi_info") and sys.abi_info.free_threaded:
160+
return True
161+
162+
if hasattr(sys, "_is_gil_enabled"):
163+
return not sys._is_gil_enabled()
164+
165+
return False
166+
167+
130168
def _is_musl_linux() -> bool:
131169
"""Return True if running in a musl Linux environment or container."""
132170
auditwheel = environ.get("AUDITWHEEL_PLAT", "")
@@ -234,19 +272,24 @@ def python_extension_name(
234272
source_stem: str,
235273
*,
236274
abi3: bool = False,
275+
free_threaded: bool | None = None,
237276
platform: str | None = None,
238277
python_version: tuple[int, int] | None = None,
239278
) -> str:
240279
"""Render the Python extension filename for a Cargo cdylib artifact stem."""
241280
platform = _normalize_platform(platform or environ.get("HATCH_RUST_PLATFORM", sys_platform))
242281
module_name = source_stem.removeprefix("lib")
282+
major, minor = python_version or (version_info.major, version_info.minor)
243283
if platform == "win32":
244284
return f"{module_name}.pyd"
245285
if platform == "emscripten":
246-
major, minor = python_version or (version_info.major, version_info.minor)
247286
return f"{module_name}.cpython-{major}{minor}-wasm32-emscripten.so"
248287
if abi3:
249-
return f"{module_name}.abi3.so"
288+
return (
289+
f"{module_name}.abi3t.so"
290+
if _resolve_free_threaded(free_threaded, python_version=(major, minor)) and (major, minor) >= (3, 15)
291+
else f"{module_name}.abi3.so"
292+
)
250293
return f"{module_name}.so"
251294

252295

@@ -344,6 +387,7 @@ def _wheel_platform(resolved_target: ResolvedTarget, platform_tag: str | None) -
344387
def wheel_tag(
345388
*,
346389
abi3: bool = False,
390+
free_threaded: bool | None = None,
347391
target: str | None = None,
348392
platform: str | None = None,
349393
machine: str | None = None,
@@ -354,7 +398,18 @@ def wheel_tag(
354398
"""Render a wheel tag for the resolved Rust target using packaging.tags."""
355399
resolved = resolved_target or _resolve_target(target, platform=platform, machine=machine)
356400
version = python_version or (version_info.major, version_info.minor)
357-
abis = [f"cp{version[0]}{version[1]}"] if resolved.platform == "emscripten" else (["abi3"] if abi3 else None)
401+
is_freethreaded = _resolve_free_threaded(free_threaded, python_version=version)
402+
if resolved.platform == "emscripten":
403+
abis = [f"cp{version[0]}{version[1]}"]
404+
elif abi3:
405+
if is_freethreaded:
406+
abis = ["abi3t"] if version >= (3, 15) else [f"cp{version[0]}{version[1]}t"]
407+
else:
408+
abis = ["abi3"]
409+
elif is_freethreaded:
410+
abis = [f"cp{version[0]}{version[1]}t"]
411+
else:
412+
abis = None
358413
return str(next(cpython_tags(python_version=version, abis=abis, platforms=[_wheel_platform(resolved, platform_tag)])))
359414

360415

@@ -669,6 +724,11 @@ class HatchRustBuildConfig(BaseModel):
669724
default=False,
670725
description="If True, build the extension with Python's ABI3 compatibility.",
671726
)
727+
free_threaded: bool | None = Field(
728+
default=None,
729+
alias="free-threaded",
730+
description="If True, build for free-threaded CPython. If None, auto-detected from environment.",
731+
)
672732

673733
target: str | None = Field(
674734
default=None,
@@ -1081,7 +1141,12 @@ def _format_destination(
10811141
) -> Path:
10821142
artifact = planned_artifact.artifact
10831143
extension_stem = artifact.python_extension_name or _cargo_artifact_stem(source)
1084-
python_extension = python_extension_name(extension_stem, abi3=self.abi3, platform=planned_artifact.resolved_target.platform)
1144+
python_extension = python_extension_name(
1145+
extension_stem,
1146+
abi3=self.abi3,
1147+
free_threaded=self.free_threaded,
1148+
platform=planned_artifact.resolved_target.platform,
1149+
)
10851150
values = {
10861151
"module": self.module,
10871152
"target": planned_artifact.resolved_target.triple,

‎hatch_rs/tests/test_project_basic/Cargo.lock‎

Lines changed: 30 additions & 62 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎hatch_rs/tests/test_project_basic/Cargo.toml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ path = "src/lib.rs"
1010
crate-type = ["cdylib"]
1111

1212
[dependencies]
13-
pyo3 = { version = "0.25", features = ["abi3-py39", "extension-module", "multiple-pymethods"] }
13+
pyo3 = { version = "0.29.2", features = ["abi3-py39", "extension-module", "multiple-pymethods"] }
1414

1515
[profile.release]
1616
panic = 'abort'

0 commit comments

Comments
 (0)