|
12 | 12 | # See the License for the specific language governing permissions and |
13 | 13 | # limitations under the License. |
14 | 14 |
|
| 15 | +"""Custom build hook for platform-specific wheel tagging. |
| 16 | +
|
| 17 | +This hook generates py3-none-{platform} wheels because the native FFI libraries |
| 18 | +(.so/.dylib/.dll) don't use the Python C API - they're loaded via ctypes at |
| 19 | +runtime. This makes them compatible with any Python 3.x version. |
| 20 | +
|
| 21 | +Why not use sysconfig.get_platform()? |
| 22 | + - On macOS, it returns the Python interpreter's compile-time deployment target, |
| 23 | + not the MACOSX_DEPLOYMENT_TARGET from the environment that cibuildwheel sets. |
| 24 | +
|
| 25 | +Why not let hatchling infer the tag? |
| 26 | + - hatchling doesn't recognize bundled .so/.dylib/.dll as platform-specific |
| 27 | + unless we explicitly set pure_python=False and provide the tag. |
| 28 | +""" |
| 29 | + |
15 | 30 | import os |
16 | 31 | import platform |
17 | 32 | import sys |
|
21 | 36 |
|
22 | 37 | class CustomBuildHook(BuildHookInterface): |
23 | 38 | def initialize(self, version, build_data): |
24 | | - """Force platform-specific wheel with py3-none tag. |
25 | | -
|
26 | | - The native libraries (.so, .dylib, .dll) are not Python C extensions - |
27 | | - they're standalone FFI libraries loaded at runtime. This means they |
28 | | - don't depend on a specific CPython ABI, so we use py3-none to indicate |
29 | | - compatibility with any Python 3.x version while keeping the platform tag. |
30 | | - """ |
31 | 39 | build_data["pure_python"] = False |
32 | 40 | build_data["infer_tag"] = False |
| 41 | + build_data["tag"] = f"py3-none-{self._get_platform_tag()}" |
33 | 42 |
|
| 43 | + def _get_platform_tag(self): |
| 44 | + """Get the wheel platform tag for the current/target platform.""" |
34 | 45 | if sys.platform == "darwin": |
35 | | - plat_tag = self._get_macos_platform_tag() |
| 46 | + return self._get_macos_tag() |
| 47 | + elif sys.platform == "linux": |
| 48 | + # Return linux tag; cibuildwheel's auditwheel converts to manylinux |
| 49 | + return f"linux_{platform.machine()}" |
| 50 | + elif sys.platform == "win32": |
| 51 | + return f"win_{self._normalize_arch(platform.machine())}" |
36 | 52 | else: |
37 | | - from packaging.tags import sys_tags |
| 53 | + return f"{platform.system().lower()}_{platform.machine()}" |
38 | 54 |
|
39 | | - tag = next( |
40 | | - t |
41 | | - for t in sys_tags() |
42 | | - if "manylinux" not in t.platform and "musllinux" not in t.platform |
43 | | - ) |
44 | | - plat_tag = tag.platform |
| 55 | + def _get_macos_tag(self): |
| 56 | + """Build macOS platform tag respecting cross-compilation settings. |
45 | 57 |
|
46 | | - build_data["tag"] = f"py3-none-{plat_tag}" |
47 | | - |
48 | | - def _get_macos_platform_tag(self): |
49 | | - """Build macOS platform tag from MACOSX_DEPLOYMENT_TARGET env var.""" |
50 | | - deployment_target = os.environ.get("MACOSX_DEPLOYMENT_TARGET") |
51 | | - if not deployment_target: |
52 | | - # Fall back to current macOS version |
53 | | - deployment_target = platform.mac_ver()[0] |
54 | | - # Use only major.minor |
55 | | - parts = deployment_target.split(".") |
56 | | - deployment_target = f"{parts[0]}.{parts[1] if len(parts) > 1 else '0'}" |
57 | | - |
58 | | - # Convert version to wheel tag format (e.g., "11.0" -> "11_0") |
59 | | - version_tag = deployment_target.replace(".", "_") |
60 | | - |
61 | | - # Get target architecture from ARCHFLAGS (set by cibuildwheel for cross-compilation) |
62 | | - # or fall back to host machine architecture |
63 | | - arch = self._get_macos_target_arch() |
| 58 | + cibuildwheel sets MACOSX_DEPLOYMENT_TARGET and ARCHFLAGS when building. |
| 59 | + We must use these rather than the host machine's values. |
| 60 | + """ |
| 61 | + target = os.environ.get("MACOSX_DEPLOYMENT_TARGET") |
| 62 | + if not target: |
| 63 | + # Fall back to current macOS version (for local dev builds) |
| 64 | + target = platform.mac_ver()[0] |
| 65 | + parts = target.split(".") |
| 66 | + target = f"{parts[0]}.{parts[1] if len(parts) > 1 else '0'}" |
64 | 67 |
|
| 68 | + version_tag = target.replace(".", "_") |
| 69 | + arch = self._get_target_arch() |
65 | 70 | return f"macosx_{version_tag}_{arch}" |
66 | 71 |
|
67 | | - def _get_macos_target_arch(self): |
68 | | - """Detect target architecture for macOS builds. |
| 72 | + def _get_target_arch(self): |
| 73 | + """Detect target architecture, respecting ARCHFLAGS for cross-compilation. |
69 | 74 |
|
70 | | - Cibuildwheel sets ARCHFLAGS for cross-compilation (e.g., "-arch x86_64"). |
71 | | - Falls back to host machine architecture if not set. |
| 75 | + cibuildwheel sets ARCHFLAGS="-arch arm64" or "-arch x86_64" when |
| 76 | + cross-compiling on macOS. |
72 | 77 | """ |
73 | 78 | archflags = os.environ.get("ARCHFLAGS", "") |
74 | 79 | if "-arch arm64" in archflags: |
75 | 80 | return "arm64" |
76 | | - elif "-arch x86_64" in archflags: |
| 81 | + if "-arch x86_64" in archflags: |
77 | 82 | return "x86_64" |
| 83 | + return self._normalize_arch(platform.machine()) |
78 | 84 |
|
79 | | - # Fall back to host architecture |
80 | | - machine = platform.machine() |
81 | | - if machine == "x86_64": |
82 | | - return "x86_64" |
83 | | - elif machine == "arm64": |
84 | | - return "arm64" |
85 | | - return machine |
| 85 | + def _normalize_arch(self, arch): |
| 86 | + """Normalize architecture names to wheel tag format.""" |
| 87 | + return { |
| 88 | + "AMD64": "amd64", |
| 89 | + "x86_64": "x86_64", |
| 90 | + "arm64": "arm64", |
| 91 | + "aarch64": "aarch64", |
| 92 | + }.get(arch, arch.lower()) |
0 commit comments