1111from shlex import join as shell_join
1212from shutil import copy2
1313from subprocess import run
14- from sys import platform as sys_platform
14+ from sys import platform as sys_platform , version_info
1515from tempfile import TemporaryDirectory
1616from typing import Any , List , Literal , Optional
1717
18+ from packaging .tags import cpython_tags , mac_platforms
1819from pydantic import BaseModel , ConfigDict , Field , PrivateAttr , field_validator
1920
2021__all__ = (
2930 "python_extension_name" ,
3031 "resolve_target_triple" ,
3132 "shared_library_name" ,
33+ "wheel_tag" ,
3234)
3335
3436ArtifactKind = Literal ["python-extension" , "shared-library" , "command" , "header" ]
@@ -74,14 +76,115 @@ class CopiedArtifact:
7476 distribution_path : str
7577
7678
79+ WINDOWS_TARGETS = {
80+ "x86_64" : "x86_64-pc-windows-msvc" ,
81+ "i686" : "i686-pc-windows-msvc" ,
82+ "aarch64" : "aarch64-pc-windows-msvc" ,
83+ }
84+
85+ DARWIN_TARGETS = {
86+ "x86_64" : "x86_64-apple-darwin" ,
87+ "aarch64" : "aarch64-apple-darwin" ,
88+ }
89+
90+ LINUX_GNU_TARGETS = {
91+ "x86_64" : "x86_64-unknown-linux-gnu" ,
92+ "i686" : "i686-unknown-linux-gnu" ,
93+ "aarch64" : "aarch64-unknown-linux-gnu" ,
94+ "armv7" : "armv7-unknown-linux-gnueabihf" ,
95+ "ppc64le" : "powerpc64le-unknown-linux-gnu" ,
96+ "s390x" : "s390x-unknown-linux-gnu" ,
97+ "riscv64" : "riscv64gc-unknown-linux-gnu" ,
98+ }
99+
100+ LINUX_MUSL_TARGETS = {
101+ "x86_64" : "x86_64-unknown-linux-musl" ,
102+ "i686" : "i686-unknown-linux-musl" ,
103+ "aarch64" : "aarch64-unknown-linux-musl" ,
104+ "armv7" : "armv7-unknown-linux-musleabihf" ,
105+ }
106+
107+ WHEEL_ARCHES = {
108+ "x86_64" : "x86_64" ,
109+ "i686" : "i686" ,
110+ "aarch64" : "aarch64" ,
111+ "armv7" : "armv7l" ,
112+ "ppc64le" : "ppc64le" ,
113+ "s390x" : "s390x" ,
114+ "riscv64" : "riscv64" ,
115+ }
116+
117+
118+ def _normalize_machine (machine : str ) -> str :
119+ normalized = machine .lower ().replace ("-" , "_" )
120+ aliases = {
121+ "amd64" : "x86_64" ,
122+ "x64" : "x86_64" ,
123+ "x86" : "i686" ,
124+ "i386" : "i686" ,
125+ "arm64" : "aarch64" ,
126+ "armv7l" : "armv7" ,
127+ "powerpc64le" : "ppc64le" ,
128+ "riscv64gc" : "riscv64" ,
129+ }
130+ return aliases .get (normalized , normalized )
131+
132+
133+ def _target_machine (target : str ) -> str :
134+ return _normalize_machine (target .split ("-" , 1 )[0 ])
135+
136+
137+ def _normalize_platform (platform : str ) -> str :
138+ normalized = platform .lower ()
139+ if normalized .startswith ("win" ):
140+ return "win32"
141+ if normalized .startswith ("macosx" ) or normalized == "darwin" :
142+ return "darwin"
143+ if normalized .startswith (("linux" , "manylinux" , "musllinux" )):
144+ return "linux"
145+ return normalized
146+
147+
148+ def _linux_targets_for_platform (platform : str ) -> dict [str , str ]:
149+ if platform .lower ().startswith ("musllinux" ):
150+ return LINUX_MUSL_TARGETS
151+ return LINUX_GNU_TARGETS
152+
153+
154+ def _unsupported_machine (platform : str , machine : str , supported : dict [str , str ]) -> ValueError :
155+ supported_machines = ", " .join (sorted (supported ))
156+ return ValueError (f"Unsupported machine type: { machine } for { platform } platform. Supported machines: { supported_machines } ." )
157+
158+
159+ def _explicit_target (target : str , * , platform : str , machine : str ) -> ResolvedTarget :
160+ if "manylinux" in target or "musllinux" in target :
161+ raise ValueError (
162+ "Rust target triples use linux-gnu or linux-musl, not manylinux or musllinux wheel platform tags. "
163+ "Use a Rust target such as x86_64-unknown-linux-gnu or x86_64-unknown-linux-musl."
164+ )
165+ if "universal2" in target :
166+ raise ValueError (
167+ "macOS universal2 is a wheel strategy, not a Rust target triple. Build x86_64-apple-darwin and "
168+ "aarch64-apple-darwin artifacts separately, then combine them with a project-specific step if needed."
169+ )
170+
171+ if target .endswith ("-pc-windows-msvc" ):
172+ return ResolvedTarget (platform = "win32" , machine = _target_machine (target ), triple = target )
173+ if target .endswith ("-apple-darwin" ):
174+ return ResolvedTarget (platform = "darwin" , machine = _target_machine (target ), triple = target )
175+ if "-linux-" in target :
176+ return ResolvedTarget (platform = "linux" , machine = _target_machine (target ), triple = target )
177+ return ResolvedTarget (platform = platform , machine = machine , triple = target )
178+
179+
77180def resolve_target_triple (target : Optional [str ] = None , * , platform : Optional [str ] = None , machine : Optional [str ] = None ) -> str :
78181 """Resolve a Rust target triple from explicit config or host platform details."""
79182 return _resolve_target (target , platform = platform , machine = machine ).triple
80183
81184
82185def shared_library_name (library : str , * , platform : Optional [str ] = None ) -> str :
83186 """Render a platform-specific standalone shared-library filename."""
84- platform = platform or environ .get ("HATCH_RUST_PLATFORM" , sys_platform )
187+ platform = _normalize_platform ( platform or environ .get ("HATCH_RUST_PLATFORM" , sys_platform ) )
85188 if platform == "win32" :
86189 return f"{ library } .dll"
87190 if platform == "darwin" :
@@ -93,7 +196,7 @@ def shared_library_name(library: str, *, platform: Optional[str] = None) -> str:
93196
94197def python_extension_name (source_stem : str , * , abi3 : bool = False , platform : Optional [str ] = None ) -> str :
95198 """Render the Python extension filename for a Cargo cdylib artifact stem."""
96- platform = platform or environ .get ("HATCH_RUST_PLATFORM" , sys_platform )
199+ platform = _normalize_platform ( platform or environ .get ("HATCH_RUST_PLATFORM" , sys_platform ) )
97200 module_name = source_stem .removeprefix ("lib" )
98201 if platform == "win32" :
99202 return f"{ module_name } .pyd"
@@ -103,52 +206,97 @@ def python_extension_name(source_stem: str, *, abi3: bool = False, platform: Opt
103206
104207
105208def _resolve_target (target : Optional [str ] = None , * , platform : Optional [str ] = None , machine : Optional [str ] = None ) -> ResolvedTarget :
106- platform = platform or environ .get ("HATCH_RUST_PLATFORM" , sys_platform )
107- machine = machine or environ .get ("HATCH_RUST_MACHINE" , platform_machine ())
209+ raw_platform = platform or environ .get ("HATCH_RUST_PLATFORM" , sys_platform )
210+ platform = _normalize_platform (raw_platform )
211+ machine = _normalize_machine (machine or environ .get ("HATCH_RUST_MACHINE" , platform_machine ()))
108212
109213 if target :
110- if target .endswith ("-pc-windows-msvc" ):
111- platform = "win32"
112- machine = target .split ("-" , 1 )[0 ]
113- elif target .endswith ("-apple-darwin" ):
114- platform = "darwin"
115- machine = target .split ("-" , 1 )[0 ]
116- elif "-unknown-linux-" in target :
117- platform = "linux"
118- machine = target .split ("-" , 1 )[0 ]
119- return ResolvedTarget (platform = platform , machine = machine , triple = target )
214+ return _explicit_target (target , platform = platform , machine = machine )
120215
121216 if platform == "win32" :
122- if machine in ("x86_64" , "AMD64" ):
123- triple = "x86_64-pc-windows-msvc"
124- elif machine == "i686" :
125- triple = "i686-pc-windows-msvc"
126- elif machine in ("arm64" , "aarch64" ):
127- triple = "aarch64-pc-windows-msvc"
128- else :
129- raise ValueError (f"Unsupported machine type: { machine } for Windows platform" )
217+ try :
218+ triple = WINDOWS_TARGETS [machine ]
219+ except KeyError as error :
220+ raise _unsupported_machine ("Windows" , machine , WINDOWS_TARGETS ) from error
130221 elif platform == "darwin" :
131- if machine == "x86_64" :
132- triple = "x86_64-apple-darwin"
133- elif machine in ("arm64" , "aarch64" ):
134- triple = "aarch64-apple-darwin"
135- else :
136- raise ValueError (f"Unsupported machine type: { machine } for macOS platform" )
222+ if machine == "universal2" :
223+ raise ValueError (
224+ "macOS universal2 wheels require separate concrete Rust targets, x86_64-apple-darwin and aarch64-apple-darwin, "
225+ "plus a project-specific combine step."
226+ )
227+ try :
228+ triple = DARWIN_TARGETS [machine ]
229+ except KeyError as error :
230+ raise _unsupported_machine ("macOS" , machine , DARWIN_TARGETS ) from error
137231 elif platform == "linux" :
138- if machine == "x86_64" :
139- triple = "x86_64-unknown-linux-gnu"
140- elif machine == "i686" :
141- triple = "i686-unknown-linux-gnu"
142- elif machine in ("arm64" , "aarch64" ):
143- triple = "aarch64-unknown-linux-gnu"
144- else :
145- raise ValueError (f"Unsupported machine type: { machine } for Linux platform" )
232+ linux_targets = _linux_targets_for_platform (raw_platform )
233+ try :
234+ triple = linux_targets [machine ]
235+ except KeyError as error :
236+ raise _unsupported_machine ("Linux" , machine , linux_targets ) from error
146237 else :
147238 raise ValueError (f"Unsupported platform: { platform } " )
148239
149240 return ResolvedTarget (platform = platform , machine = machine , triple = triple )
150241
151242
243+ def _linux_wheel_platform (resolved_target : ResolvedTarget , platform_tag : Optional [str ]) -> str :
244+ if platform_tag :
245+ return platform_tag
246+
247+ auditwheel_platform = environ .get ("AUDITWHEEL_PLAT" )
248+ if auditwheel_platform :
249+ return auditwheel_platform
250+
251+ arch = WHEEL_ARCHES .get (resolved_target .machine )
252+ if arch is None :
253+ raise _unsupported_machine ("Linux wheel" , resolved_target .machine , WHEEL_ARCHES )
254+ if "musl" in resolved_target .triple :
255+ return f"musllinux_1_2_{ arch } "
256+ return f"linux_{ arch } "
257+
258+
259+ def _wheel_platform (resolved_target : ResolvedTarget , platform_tag : Optional [str ]) -> str :
260+ if resolved_target .platform == "win32" :
261+ windows_platforms = {
262+ "x86_64" : "win_amd64" ,
263+ "i686" : "win32" ,
264+ "aarch64" : "win_arm64" ,
265+ }
266+ try :
267+ return windows_platforms [resolved_target .machine ]
268+ except KeyError as error :
269+ raise _unsupported_machine ("Windows wheel" , resolved_target .machine , windows_platforms ) from error
270+ if resolved_target .platform == "darwin" :
271+ if platform_tag :
272+ return platform_tag
273+ darwin_arches = {"x86_64" : "x86_64" , "aarch64" : "arm64" }
274+ try :
275+ return next (mac_platforms ((11 , 0 ), darwin_arches [resolved_target .machine ]))
276+ except KeyError as error :
277+ raise _unsupported_machine ("macOS wheel" , resolved_target .machine , darwin_arches ) from error
278+ if resolved_target .platform == "linux" :
279+ return _linux_wheel_platform (resolved_target , platform_tag )
280+ raise ValueError (f"Unsupported platform for wheel tag: { resolved_target .platform } " )
281+
282+
283+ def wheel_tag (
284+ * ,
285+ abi3 : bool = False ,
286+ target : Optional [str ] = None ,
287+ platform : Optional [str ] = None ,
288+ machine : Optional [str ] = None ,
289+ resolved_target : Optional [ResolvedTarget ] = None ,
290+ platform_tag : Optional [str ] = None ,
291+ python_version : Optional [tuple [int , int ]] = None ,
292+ ) -> str :
293+ """Render a wheel tag for the resolved Rust target using packaging.tags."""
294+ resolved = resolved_target or _resolve_target (target , platform = platform , machine = machine )
295+ version = python_version or (version_info .major , version_info .minor )
296+ abis = ["abi3" ] if abi3 else None
297+ return str (next (cpython_tags (python_version = version , abis = abis , platforms = [_wheel_platform (resolved , platform_tag )])))
298+
299+
152300def _artifact_patterns (platform : str ) -> tuple [str , ...]:
153301 if platform == "win32" :
154302 return ("*.dll" , "*.pyd" )
@@ -429,6 +577,11 @@ class HatchRustBuildConfig(BaseModel):
429577 alias = "artifact-manifest-destination" ,
430578 description = "Wheel-relative destination template for the artifact metadata manifest." ,
431579 )
580+ wheel_platform_tag : Optional [str ] = Field (
581+ default = None ,
582+ alias = "wheel-platform-tag" ,
583+ description = "Override the wheel platform tag, such as manylinux_2_28_x86_64 or musllinux_1_2_x86_64." ,
584+ )
432585
433586 abi3 : bool = Field (
434587 default = False ,
@@ -501,6 +654,10 @@ def copied_artifacts(self) -> List[CopiedArtifact]:
501654 def shared_data (self ) -> dict [str , str ]:
502655 return dict (self ._shared_data )
503656
657+ @property
658+ def resolved_target (self ) -> Optional [ResolvedTarget ]:
659+ return self ._resolved_target
660+
504661 def _configured_artifacts (self ) -> list [RustArtifactConfig ]:
505662 if self .artifacts :
506663 return list (self .artifacts )
0 commit comments