|
1 | 1 | #!/usr/bin/env python3 |
2 | | -"""Ensure cibuildwheel's repaired-wheel directory contains a wheel on Windows. |
| 2 | +""" |
| 3 | +Windows wheel repair helper for cibuildwheel. |
| 4 | +
|
| 5 | +Responsibilities: |
| 6 | +1. Locate the MSVC LLVM OpenMP DLL (libomp140.x86_64.dll) via vswhere or |
| 7 | + well-known fallback paths on GitHub-hosted runners. |
| 8 | +2. Run `delvewheel repair --add-path <dir>` to bundle it into the wheel. |
| 9 | +3. If delvewheel does not produce a repaired wheel (DLL not found, or the |
| 10 | + wheel has no external dependencies), copy the built wheel as-is so that |
| 11 | + cibuildwheel always finds something in dest_dir. |
3 | 12 |
|
4 | | -delvewheel is invoked with ``|| true`` in the CIBW repair command so that a |
5 | | -missing OpenMP DLL does not hard-fail the build. This script is the safety |
6 | | -net: if delvewheel emitted a repaired wheel we confirm it; if it didn't we |
7 | | -copy the built wheel as-is so cibuildwheel finds *something* in dest_dir. |
| 13 | +Usage (called from CIBW_REPAIR_WHEEL_COMMAND_WINDOWS): |
| 14 | + python repair_windows_wheel.py <dest_dir> <wheel> |
8 | 15 | """ |
9 | 16 | from __future__ import annotations |
10 | 17 |
|
| 18 | +import os |
11 | 19 | import shutil |
| 20 | +import subprocess |
12 | 21 | import sys |
13 | 22 | from pathlib import Path |
14 | 23 |
|
15 | 24 |
|
| 25 | +# --------------------------------------------------------------------------- |
| 26 | +# DLL discovery |
| 27 | +# --------------------------------------------------------------------------- |
| 28 | + |
| 29 | +def _find_vswhere() -> Path | None: |
| 30 | + """Return the path to vswhere.exe, or None if not found.""" |
| 31 | + candidates = [] |
| 32 | + |
| 33 | + # The canonical env var on Windows is "ProgramFiles(x86)"; os.environ gives |
| 34 | + # it under that exact key (parentheses and all). |
| 35 | + pf86 = os.environ.get("ProgramFiles(x86)") or os.environ.get("PROGRAMFILES(X86)") |
| 36 | + if pf86: |
| 37 | + candidates.append(Path(pf86) / "Microsoft Visual Studio" / "Installer" / "vswhere.exe") |
| 38 | + |
| 39 | + # Some shells expose a sanitised name instead. |
| 40 | + pf86b = os.environ.get("PROGRAMFILES_X86") |
| 41 | + if pf86b: |
| 42 | + candidates.append(Path(pf86b) / "Microsoft Visual Studio" / "Installer" / "vswhere.exe") |
| 43 | + |
| 44 | + # Hard-coded last resort (GitHub-hosted Windows runners, VS2022 Enterprise). |
| 45 | + candidates.append(Path("C:/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe")) |
| 46 | + |
| 47 | + for p in candidates: |
| 48 | + if p.is_file(): |
| 49 | + return p |
| 50 | + return None |
| 51 | + |
| 52 | + |
| 53 | +def _find_omp_dll_via_vswhere(vswhere: Path) -> Path | None: |
| 54 | + """Ask vswhere for the LLVM OpenMP DLL bundled with MSVC tools.""" |
| 55 | + try: |
| 56 | + result = subprocess.run( |
| 57 | + [ |
| 58 | + str(vswhere), |
| 59 | + "-latest", "-products", "*", |
| 60 | + "-requires", "Microsoft.VisualStudio.Component.VC.Tools.x86.x64", |
| 61 | + # vswhere -find uses Windows path separators and glob patterns. |
| 62 | + "-find", r"VC\Tools\Llvm\x64\bin\libomp140.x86_64.dll", |
| 63 | + ], |
| 64 | + capture_output=True, |
| 65 | + text=True, |
| 66 | + timeout=30, |
| 67 | + ) |
| 68 | + except Exception as exc: |
| 69 | + print(f"[repair] vswhere failed: {exc}", file=sys.stderr) |
| 70 | + return None |
| 71 | + |
| 72 | + for line in result.stdout.splitlines(): |
| 73 | + p = Path(line.strip()) |
| 74 | + if p.is_file(): |
| 75 | + print(f"[repair] Found DLL via vswhere: {p}") |
| 76 | + return p |
| 77 | + return None |
| 78 | + |
| 79 | + |
| 80 | +def _find_omp_dll_fallback() -> Path | None: |
| 81 | + """Walk well-known hard-coded paths on GitHub-hosted runners.""" |
| 82 | + candidates = [ |
| 83 | + "C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Tools/Llvm/x64/bin/libomp140.x86_64.dll", |
| 84 | + "C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/Llvm/x64/bin/libomp140.x86_64.dll", |
| 85 | + "C:/Program Files/Microsoft Visual Studio/2022/Professional/VC/Tools/Llvm/x64/bin/libomp140.x86_64.dll", |
| 86 | + "C:/Program Files/LLVM/bin/libomp140.x86_64.dll", |
| 87 | + "C:/Program Files/LLVM/bin/libomp.dll", |
| 88 | + ] |
| 89 | + for c in candidates: |
| 90 | + p = Path(c) |
| 91 | + if p.is_file(): |
| 92 | + print(f"[repair] Found DLL via fallback path: {p}") |
| 93 | + return p |
| 94 | + return None |
| 95 | + |
| 96 | + |
| 97 | +def find_omp_dll() -> Path | None: |
| 98 | + vswhere = _find_vswhere() |
| 99 | + if vswhere: |
| 100 | + print(f"[repair] Using vswhere: {vswhere}") |
| 101 | + dll = _find_omp_dll_via_vswhere(vswhere) |
| 102 | + if dll: |
| 103 | + return dll |
| 104 | + print("[repair] vswhere did not find the DLL; trying fallback paths...") |
| 105 | + else: |
| 106 | + print("[repair] vswhere.exe not found; trying fallback paths...") |
| 107 | + return _find_omp_dll_fallback() |
| 108 | + |
| 109 | + |
| 110 | +# --------------------------------------------------------------------------- |
| 111 | +# Wheel repair |
| 112 | +# --------------------------------------------------------------------------- |
| 113 | + |
| 114 | +def run_delvewheel(dest: Path, wheel: Path, omp_bin: Path | None) -> bool: |
| 115 | + """Run delvewheel; return True if it produced at least one wheel.""" |
| 116 | + cmd = ["delvewheel", "repair", "-w", str(dest)] |
| 117 | + if omp_bin: |
| 118 | + cmd += ["--add-path", str(omp_bin)] |
| 119 | + cmd.append(str(wheel)) |
| 120 | + |
| 121 | + print(f"[repair] Running: {' '.join(cmd)}") |
| 122 | + result = subprocess.run(cmd) |
| 123 | + if result.returncode != 0: |
| 124 | + print(f"[repair] delvewheel exited with code {result.returncode} (continuing...)") |
| 125 | + |
| 126 | + return bool(sorted(dest.glob("*.whl"))) |
| 127 | + |
| 128 | + |
16 | 129 | def main() -> int: |
17 | 130 | if len(sys.argv) != 3: |
18 | | - print( |
19 | | - "Usage: repair_windows_wheel.py <dest_dir> <wheel>", |
20 | | - file=sys.stderr, |
21 | | - ) |
| 131 | + print("Usage: repair_windows_wheel.py <dest_dir> <wheel>", file=sys.stderr) |
22 | 132 | return 2 |
23 | 133 |
|
24 | 134 | dest = Path(sys.argv[1]).resolve() |
25 | 135 | wheel = Path(sys.argv[2]).resolve() |
26 | 136 | dest.mkdir(parents=True, exist_ok=True) |
27 | 137 |
|
28 | 138 | if not wheel.is_file(): |
29 | | - print(f"Source wheel not found: {wheel}", file=sys.stderr) |
| 139 | + print(f"[repair] Source wheel not found: {wheel}", file=sys.stderr) |
30 | 140 | return 1 |
31 | 141 |
|
32 | | - # Check whether delvewheel already placed a repaired wheel in dest. |
33 | | - wheels = sorted(dest.glob("*.whl")) |
34 | | - if wheels: |
35 | | - print(f"Repaired wheel (from delvewheel): {wheels[0]}") |
| 142 | + # --- Step 1: try to find the OpenMP DLL --- |
| 143 | + omp_dll = find_omp_dll() |
| 144 | + omp_bin = omp_dll.parent if omp_dll else None |
| 145 | + if omp_bin: |
| 146 | + print(f"[repair] Bundling OpenMP from: {omp_bin}") |
| 147 | + else: |
| 148 | + print("[repair] WARNING: OpenMP DLL not found; wheel will be built without it") |
| 149 | + |
| 150 | + # --- Step 2: run delvewheel --- |
| 151 | + if run_delvewheel(dest, wheel, omp_bin): |
| 152 | + repaired = sorted(dest.glob("*.whl"))[0] |
| 153 | + print(f"[repair] Repaired wheel (delvewheel): {repaired}") |
36 | 154 | return 0 |
37 | 155 |
|
38 | | - # delvewheel did not emit a wheel (OpenMP DLL not found, or repair was |
39 | | - # skipped). Copy the built wheel as-is so cibuildwheel can continue. |
40 | | - print("delvewheel did not emit a wheel; copying built wheel as-is.") |
| 156 | + # --- Step 3: safety net — copy built wheel as-is --- |
| 157 | + print("[repair] delvewheel did not produce a wheel; copying built wheel as-is.") |
41 | 158 | target = dest / wheel.name |
42 | 159 | shutil.copy2(wheel, target) |
43 | | - |
44 | | - # Verify the copy succeeded — don't re-glob, just stat the known path. |
45 | 160 | if not target.is_file(): |
46 | | - print( |
47 | | - f"Failed to copy wheel to {target} " |
48 | | - f"(source={wheel}, dest_exists={dest.is_dir()})", |
49 | | - file=sys.stderr, |
50 | | - ) |
| 161 | + print(f"[repair] ERROR: failed to copy {wheel} -> {target}", file=sys.stderr) |
51 | 162 | return 1 |
52 | 163 |
|
53 | | - print(f"Repaired wheel (copied as-is): {target}") |
| 164 | + print(f"[repair] Repaired wheel (copied as-is): {target}") |
54 | 165 | return 0 |
55 | 166 |
|
56 | 167 |
|
|
0 commit comments