Skip to content

Commit 51bd29c

Browse files
committed
Fixed path errors when building windows wheel.
1 parent 56cfd42 commit 51bd29c

2 files changed

Lines changed: 82 additions & 33 deletions

File tree

maintenance_scripts/repair_windows_wheel.py

Lines changed: 80 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
Responsibilities:
66
1. Locate the MSVC LLVM OpenMP DLL (libomp140.x86_64.dll) via vswhere or
77
well-known fallback paths on GitHub-hosted runners.
8-
2. Run `delvewheel repair --add-path <dir>` to bundle it into the wheel.
8+
2. Run `python -m delvewheel repair --add-path <dir>` to bundle it into the wheel.
99
3. If delvewheel does not produce a repaired wheel (DLL not found, or the
1010
wheel has no external dependencies), copy the built wheel as-is so that
1111
cibuildwheel always finds something in dest_dir.
@@ -21,6 +21,30 @@
2121
import sys
2222
from pathlib import Path
2323

24+
_MSVC_OMP_DLL_NAMES = (
25+
"libomp140.x86_64.dll",
26+
"libomp140.dll",
27+
)
28+
29+
30+
# ---------------------------------------------------------------------------
31+
# delvewheel invocation
32+
# ---------------------------------------------------------------------------
33+
34+
def _delvewheel_cmd(*args: str) -> list[str]:
35+
"""Invoke delvewheel via the active Python (Scripts/ may be off PATH)."""
36+
return [sys.executable, "-m", "delvewheel", *args]
37+
38+
39+
def _run_delvewheel(args: list[str]) -> subprocess.CompletedProcess | None:
40+
cmd = _delvewheel_cmd(*args)
41+
print(f"[repair] Running: {' '.join(cmd)}")
42+
try:
43+
return subprocess.run(cmd, check=False)
44+
except FileNotFoundError as exc:
45+
print(f"[repair] delvewheel launch failed: {exc}", file=sys.stderr)
46+
return None
47+
2448

2549
# ---------------------------------------------------------------------------
2650
# DLL discovery
@@ -30,18 +54,14 @@ def _find_vswhere() -> Path | None:
3054
"""Return the path to vswhere.exe, or None if not found."""
3155
candidates = []
3256

33-
# The canonical env var on Windows is "ProgramFiles(x86)"; os.environ gives
34-
# it under that exact key (parentheses and all).
3557
pf86 = os.environ.get("ProgramFiles(x86)") or os.environ.get("PROGRAMFILES(X86)")
3658
if pf86:
3759
candidates.append(Path(pf86) / "Microsoft Visual Studio" / "Installer" / "vswhere.exe")
3860

39-
# Some shells expose a sanitised name instead.
4061
pf86b = os.environ.get("PROGRAMFILES_X86")
4162
if pf86b:
4263
candidates.append(Path(pf86b) / "Microsoft Visual Studio" / "Installer" / "vswhere.exe")
4364

44-
# Hard-coded last resort (GitHub-hosted Windows runners, VS2022 Enterprise).
4565
candidates.append(Path("C:/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe"))
4666

4767
for p in candidates:
@@ -51,40 +71,68 @@ def _find_vswhere() -> Path | None:
5171

5272

5373
def _find_omp_dll_via_vswhere(vswhere: Path) -> Path | None:
54-
"""Ask vswhere for the LLVM OpenMP DLL bundled with MSVC tools."""
74+
"""Locate MSVC libomp140*.dll under the active Visual Studio install."""
75+
for pattern in (
76+
r"VC\Tools\Llvm\x64\bin\libomp140.x86_64.dll",
77+
r"VC\Tools\Llvm\x64\bin\libomp140.dll",
78+
):
79+
try:
80+
result = subprocess.run(
81+
[
82+
str(vswhere),
83+
"-latest",
84+
"-products",
85+
"*",
86+
"-requires",
87+
"Microsoft.VisualStudio.Component.VC.Tools.x86.x64",
88+
"-find",
89+
pattern,
90+
],
91+
capture_output=True,
92+
text=True,
93+
timeout=30,
94+
)
95+
except Exception as exc:
96+
print(f"[repair] vswhere failed for {pattern}: {exc}", file=sys.stderr)
97+
continue
98+
99+
for line in result.stdout.splitlines():
100+
p = Path(line.strip())
101+
if p.is_file() and p.name.lower() in _MSVC_OMP_DLL_NAMES:
102+
print(f"[repair] Found DLL via vswhere: {p}")
103+
return p
104+
55105
try:
56106
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-
],
107+
[str(vswhere), "-latest", "-products", "*", "-property", "installationPath"],
64108
capture_output=True,
65109
text=True,
66110
timeout=30,
111+
check=False,
67112
)
113+
install_root = Path(result.stdout.strip())
68114
except Exception as exc:
69-
print(f"[repair] vswhere failed: {exc}", file=sys.stderr)
115+
print(f"[repair] vswhere installationPath lookup failed: {exc}", file=sys.stderr)
70116
return None
71117

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
118+
if install_root.is_dir():
119+
for name in _MSVC_OMP_DLL_NAMES:
120+
matches = sorted(install_root.glob(f"VC/Tools/Llvm/**/bin/{name}"))
121+
if matches:
122+
print(f"[repair] Found DLL under VS install root: {matches[0]}")
123+
return matches[0]
124+
77125
return None
78126

79127

80128
def _find_omp_dll_fallback() -> Path | None:
81-
"""Walk well-known hard-coded paths on GitHub-hosted runners."""
129+
"""Walk well-known MSVC LLVM OpenMP paths on GitHub-hosted runners."""
82130
candidates = [
83131
"C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Tools/Llvm/x64/bin/libomp140.x86_64.dll",
132+
"C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Tools/Llvm/x64/bin/libomp140.dll",
84133
"C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/Llvm/x64/bin/libomp140.x86_64.dll",
85134
"C:/Program Files/Microsoft Visual Studio/2022/Professional/VC/Tools/Llvm/x64/bin/libomp140.x86_64.dll",
86135
"C:/Program Files/LLVM/bin/libomp140.x86_64.dll",
87-
"C:/Program Files/LLVM/bin/libomp.dll",
88136
]
89137
for c in candidates:
90138
p = Path(c)
@@ -101,7 +149,7 @@ def find_omp_dll() -> Path | None:
101149
dll = _find_omp_dll_via_vswhere(vswhere)
102150
if dll:
103151
return dll
104-
print("[repair] vswhere did not find the DLL; trying fallback paths...")
152+
print("[repair] vswhere did not find libomp140*.dll; trying fallback paths...")
105153
else:
106154
print("[repair] vswhere.exe not found; trying fallback paths...")
107155
return _find_omp_dll_fallback()
@@ -113,14 +161,13 @@ def find_omp_dll() -> Path | None:
113161

114162
def run_delvewheel(dest: Path, wheel: Path, omp_bin: Path | None) -> bool:
115163
"""Run delvewheel; return True if it produced at least one wheel."""
116-
cmd = ["delvewheel", "repair", "-w", str(dest)]
164+
args = ["repair", "-w", str(dest)]
117165
if omp_bin:
118-
cmd += ["--add-path", str(omp_bin)]
119-
cmd.append(str(wheel))
166+
args += ["--add-path", str(omp_bin)]
167+
args.append(str(wheel))
120168

121-
print(f"[repair] Running: {' '.join(cmd)}")
122-
result = subprocess.run(cmd)
123-
if result.returncode != 0:
169+
result = _run_delvewheel(args)
170+
if result is not None and result.returncode != 0:
124171
print(f"[repair] delvewheel exited with code {result.returncode} (continuing...)")
125172

126173
return bool(sorted(dest.glob("*.whl")))
@@ -139,21 +186,18 @@ def main() -> int:
139186
print(f"[repair] Source wheel not found: {wheel}", file=sys.stderr)
140187
return 1
141188

142-
# --- Step 1: try to find the OpenMP DLL ---
143189
omp_dll = find_omp_dll()
144190
omp_bin = omp_dll.parent if omp_dll else None
145191
if omp_bin:
146192
print(f"[repair] Bundling OpenMP from: {omp_bin}")
147193
else:
148-
print("[repair] WARNING: OpenMP DLL not found; wheel will be built without it")
194+
print("[repair] WARNING: libomp140*.dll not found; repair may miss OpenMP runtime")
149195

150-
# --- Step 2: run delvewheel ---
151196
if run_delvewheel(dest, wheel, omp_bin):
152197
repaired = sorted(dest.glob("*.whl"))[0]
153198
print(f"[repair] Repaired wheel (delvewheel): {repaired}")
154199
return _finalize_repaired_wheel(repaired)
155200

156-
# --- Step 3: safety net — copy built wheel as-is ---
157201
print("[repair] delvewheel did not emit a wheel; copying built wheel as-is.")
158202
target = dest / wheel.name
159203
shutil.copy2(wheel, target)
@@ -187,7 +231,10 @@ def _verify_bundled_openmp(wheel_path: Path) -> None:
187231

188232
def _finalize_repaired_wheel(wheel_path: Path) -> int:
189233
print("=== delvewheel show repaired wheel ===")
190-
subprocess.run(["delvewheel", "show", str(wheel_path)], check=True)
234+
result = _run_delvewheel(["show", str(wheel_path)])
235+
if result is None or result.returncode != 0:
236+
print("[repair] ERROR: delvewheel show failed", file=sys.stderr)
237+
return 1
191238
_verify_bundled_openmp(wheel_path)
192239
return 0
193240

tests/test_build_release_boundaries.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ def test_macos_wheel_repair_fails_instead_of_copying_unrepaired_wheel():
3232
assert 'CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: python maintenance_scripts/repair_windows_wheel.py' in source
3333
repair_script = (repo_root / "maintenance_scripts" / "repair_windows_wheel.py").read_text()
3434
assert "delvewheel did not emit a wheel; copying built wheel as-is" in repair_script
35+
assert '"-m", "delvewheel"' in repair_script or "'-m', 'delvewheel'" in repair_script
36+
assert "libomp.dll" not in repair_script or "libomp140" in repair_script
3537
assert "bundled OpenMP DLLs" in repair_script
3638
assert (repo_root / "maintenance_scripts" / "repair_windows_wheel.py").is_file()
3739
assert "exit 1" in source

0 commit comments

Comments
 (0)