55Responsibilities:
661. 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.
993. 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.
2121import sys
2222from 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
5373def _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
80128def _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
114162def 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
188232def _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
0 commit comments