@@ -280,16 +280,15 @@ def python_extension_name(
280280 platform = _normalize_platform (platform or environ .get ("HATCH_RUST_PLATFORM" , sys_platform ))
281281 module_name = source_stem .removeprefix ("lib" )
282282 major , minor = python_version or (version_info .major , version_info .minor )
283+ is_freethreaded = _resolve_free_threaded (free_threaded , python_version = (major , minor )) if abi3 else False
284+ if abi3 and is_freethreaded and (major , minor ) < (3 , 15 ):
285+ raise ValueError (f"ABI3 free-threaded builds require Python 3.15 or newer, got { major } .{ minor } ." )
283286 if platform == "win32" :
284287 return f"{ module_name } .pyd"
285288 if platform == "emscripten" :
286289 return f"{ module_name } .cpython-{ major } { minor } -wasm32-emscripten.so"
287290 if abi3 :
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- )
291+ return f"{ module_name } .abi3t.so" if is_freethreaded else f"{ module_name } .abi3.so"
293292 return f"{ module_name } .so"
294293
295294
@@ -399,13 +398,12 @@ def wheel_tag(
399398 resolved = resolved_target or _resolve_target (target , platform = platform , machine = machine )
400399 version = python_version or (version_info .major , version_info .minor )
401400 is_freethreaded = _resolve_free_threaded (free_threaded , python_version = version )
401+ if abi3 and is_freethreaded and version < (3 , 15 ):
402+ raise ValueError (f"ABI3 free-threaded builds require Python 3.15 or newer, got { version [0 ]} .{ version [1 ]} ." )
402403 if resolved .platform == "emscripten" :
403404 abis = [f"cp{ version [0 ]} { version [1 ]} " ]
404405 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" ]
406+ abis = ["abi3.abi3t" ] if is_freethreaded else ["abi3" ]
409407 elif is_freethreaded :
410408 abis = [f"cp{ version [0 ]} { version [1 ]} t" ]
411409 else :
@@ -831,7 +829,7 @@ def _is_python_extension_artifact(self, artifact: RustArtifactConfig) -> bool:
831829 return artifact .destination is not None and "{python_extension_name}" in artifact .destination
832830
833831 def _is_executable_artifact (self , artifact : RustArtifactConfig ) -> bool :
834- return artifact .cargo_target_kind in ("bin" , "example" )
832+ return artifact .cargo_target_kind in ("bin" , "example" , "test" , "bench" )
835833
836834 def _artifact_role (self , artifact : RustArtifactConfig ) -> str :
837835 if self ._is_generated_artifact (artifact ):
@@ -1116,7 +1114,7 @@ def _find_exact_artifact(self, target_path: Path, expected_name: str, *, search_
11161114 deps_dir = target_path / "deps"
11171115 if deps_dir .is_dir () and (search_deps or not candidates ):
11181116 for candidate in deps_dir .glob (f"*{ expected_path .suffix } " ):
1119- if candidate .is_file () and _cargo_artifact_stem (candidate ) == expected_path .stem :
1117+ if candidate .is_file () and candidate . suffix == expected_path . suffix and _cargo_artifact_stem (candidate ) == expected_path .stem :
11201118 candidates .append (candidate )
11211119
11221120 if not candidates :
@@ -1138,6 +1136,7 @@ def _format_destination(
11381136 source : Path ,
11391137 shared_library : str = "" ,
11401138 import_library : str = "" ,
1139+ executable : str = "" ,
11411140 ) -> Path :
11421141 artifact = planned_artifact .artifact
11431142 extension_stem = artifact .python_extension_name or _cargo_artifact_stem (source )
@@ -1155,7 +1154,7 @@ def _format_destination(
11551154 "shared_library" : shared_library ,
11561155 "import_library" : import_library ,
11571156 "python_extension_name" : python_extension ,
1158- "executable" : source .name ,
1157+ "executable" : executable or source .name ,
11591158 }
11601159 try :
11611160 rendered = template .format (** values )
@@ -1423,7 +1422,12 @@ def _copy_executable(self, planned_artifact: PlannedArtifact, *, build_root: Pat
14231422 executable = executable_name (name , platform = planned_artifact .resolved_target .platform )
14241423 source = self ._find_exact_artifact (target_path , executable , search_deps = artifact .search_deps , artifact = artifact )
14251424 template = artifact .destination or self ._default_executable_destination (artifact )
1426- distribution_path = self ._format_destination (template , planned_artifact = planned_artifact , source = source )
1425+ distribution_path = self ._format_destination (
1426+ template ,
1427+ planned_artifact = planned_artifact ,
1428+ source = source ,
1429+ executable = executable ,
1430+ )
14271431 return self ._place_artifact (
14281432 source ,
14291433 distribution_path ,
0 commit comments