main
py 142 lines 5.02 KB
Raw
1 #!/usr/bin/env python3
2 # SPDX-License-Identifier: Apache-2.0
3 # SPDX-FileCopyrightText: Copyright contributors to the vLLM project
4 """Detect the manylinux platform tag for a wheel and rename it in place.
5
6 vLLM's build images produce wheels with the generic ``linux_<arch>`` platform
7 tag, which installers like ``pip`` won't accept off PyPI/our index. We need to
8 rewrite the platform tag to the appropriate ``manylinux_<major>_<minor>_<arch>``
9 before uploading.
10
11 Historically the tag was hard-coded per build (``manylinux_2_31`` for the
12 Ubuntu 20.04-based image, ``manylinux_2_35`` for the Ubuntu 22.04-based
13 images). That is brittle: bumping the base image silently produces wheels
14 labelled with the wrong glibc requirement. This script asks ``auditwheel``
15 to derive the tag from the symbol versions actually referenced by the
16 binaries inside the wheel, so the label tracks reality.
17
18 We can't simply call ``auditwheel repair`` -- it tries to graft external
19 shared libraries into the wheel and fails on vLLM's CUDA/cuBLAS dependencies.
20 Instead we use ``auditwheel.wheel_abi.analyze_wheel_abi`` directly, which is
21 the same call that powers ``auditwheel show``, and read off
22 ``winfo.sym_policy.name``.
23
24 Usage:
25 detect-manylinux-tag.py <wheel_path>
26
27 The wheel is renamed in place; the new path is printed on stdout. All
28 diagnostics go to stderr so callers can capture stdout safely.
29 """
30
31 from __future__ import annotations
32
33 import argparse
34 import sys
35 from pathlib import Path
36
37 from auditwheel.error import (
38 AuditwheelError,
39 NonPlatformWheelError,
40 WheelToolsError,
41 )
42 from auditwheel.wheel_abi import analyze_wheel_abi
43 from auditwheel.wheeltools import get_wheel_architecture, get_wheel_libc
44
45
46 def detect_platform_tag(wheel_path: Path) -> str:
47 """Return the most precise platform tag the wheel is consistent with.
48
49 Mirrors ``auditwheel show`` but returns ``sym_policy`` rather than
50 ``overall_policy``: we only care about the glibc symbol versions used,
51 not about other policy axes (ISA extensions, blacklist, etc.) that
52 ``overall_policy`` folds in.
53 """
54 fn = wheel_path.name
55
56 try:
57 arch = get_wheel_architecture(fn)
58 except (WheelToolsError, NonPlatformWheelError):
59 # Architecture isn't deducible from the filename; let auditwheel
60 # infer it from the ELF binaries inside the wheel.
61 arch = None
62
63 try:
64 libc = get_wheel_libc(fn)
65 except WheelToolsError:
66 # An unrepaired wheel uses ``linux_<arch>``, which doesn't encode
67 # libc. Let auditwheel infer it from the ELF binaries.
68 libc = None
69
70 winfo = analyze_wheel_abi(
71 libc,
72 arch,
73 wheel_path,
74 frozenset(),
75 disable_isa_ext_check=False,
76 allow_graft=False,
77 )
78 return winfo.sym_policy.name
79
80
81 def rename_wheel(wheel_path: Path, new_platform_tag: str) -> Path:
82 """Rename the wheel in place, replacing only its platform tag."""
83 # Wheel filename per PEP 427:
84 # {distribution}-{version}(-{build})?-{python}-{abi}-{platform}.whl
85 # The platform tag is always the last ``-``-separated token before
86 # ``.whl``. Compound tags like ``manylinux_2_31_x86_64`` use ``_`` as the
87 # internal separator, so ``-``-splitting is unambiguous.
88 parts = wheel_path.stem.split("-")
89 if len(parts) < 5:
90 raise ValueError(f"Unrecognised wheel filename: {wheel_path.name}")
91 parts[-1] = new_platform_tag
92 new_path = wheel_path.with_name("-".join(parts) + ".whl")
93 if new_path != wheel_path:
94 wheel_path.rename(new_path)
95 return new_path
96
97
98 def main() -> int:
99 parser = argparse.ArgumentParser(
100 description="Detect a wheel's manylinux platform tag with "
101 "auditwheel and rename the wheel in place."
102 )
103 parser.add_argument(
104 "wheel",
105 type=Path,
106 help="Path to the wheel to inspect and rename.",
107 )
108 args = parser.parse_args()
109
110 wheel_path: Path = args.wheel
111 if not wheel_path.is_file():
112 print(f"error: {wheel_path} is not a file", file=sys.stderr)
113 return 1
114
115 # Catch the things that ``analyze_wheel_abi`` and ``rename_wheel`` can
116 # raise: any subclass of ``AuditwheelError`` (pure-Python wheels,
117 # invalid libc, malformed wheels), filesystem errors, or our own
118 # ``ValueError`` for an unrecognised wheel filename. Print a single
119 # ``ERROR_TYPE: message`` line to stderr instead of a Python
120 # traceback, which is much friendlier in CI logs.
121 try:
122 new_tag = detect_platform_tag(wheel_path)
123 print(f"detected platform tag: {new_tag}", file=sys.stderr)
124 new_path = rename_wheel(wheel_path, new_tag)
125 except (AuditwheelError, ValueError, OSError) as e:
126 print(
127 f"error: failed to retag {wheel_path.name}: {type(e).__name__}: {e}",
128 file=sys.stderr,
129 )
130 return 2
131
132 if new_path != wheel_path:
133 print(f"renamed {wheel_path.name} -> {new_path.name}", file=sys.stderr)
134 else:
135 print(f"wheel already tagged {new_tag}", file=sys.stderr)
136
137 print(new_path)
138 return 0
139
140
141 if __name__ == "__main__":
142 sys.exit(main())