11from __future__ import annotations
22
3+ import sys
4+ import sysconfig
35from ctypes import CDLL
46from dataclasses import dataclass
57from 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+
130168def _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) -
344387def 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 ,
0 commit comments