Skip to content

Commit d30fe01

Browse files
committed
Tried to fix the error that can't find libomp path when building windows wheel.
1 parent 51bd29c commit d30fe01

5 files changed

Lines changed: 174 additions & 7 deletions

File tree

.github/workflows/python-app.yml

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -370,6 +370,39 @@ jobs:
370370
where.exe cl
371371
where.exe link
372372
373+
- name: Locate MSVC LLVM OpenMP runtime (Windows)
374+
if: runner.os == 'Windows'
375+
shell: pwsh
376+
run: |
377+
$vswhere = "${env:ProgramFiles(x86)}\Microsoft Visual Studio\Installer\vswhere.exe"
378+
$installPath = & $vswhere -latest -products * -property installationPath
379+
if (-not $installPath) {
380+
throw "vswhere did not return a Visual Studio installation path"
381+
}
382+
383+
$patterns = @(
384+
"$installPath\VC\Redist\MSVC\*\x64\Microsoft.VC*.OpenMP.LLVM\libomp140.x86_64.dll",
385+
"$installPath\VC\Redist\MSVC\*\debug_nonredist\x64\Microsoft.VC*.OpenMP.LLVM\libomp140.x86_64.dll",
386+
"$installPath\VC\Tools\Llvm\x64\bin\libomp140.x86_64.dll",
387+
"$installPath\VC\Tools\Llvm\x64\bin\libomp140.dll"
388+
)
389+
390+
$dll = $null
391+
foreach ($pattern in $patterns) {
392+
$match = Get-ChildItem -Path $pattern -ErrorAction SilentlyContinue | Select-Object -First 1
393+
if ($match) {
394+
$dll = $match
395+
break
396+
}
397+
}
398+
399+
if (-not $dll) {
400+
throw "libomp140*.dll not found under Visual Studio install: $installPath"
401+
}
402+
403+
Write-Host "Found OpenMP runtime: $($dll.FullName)"
404+
"LIBOMP_DLL_DIR=$($dll.DirectoryName)" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
405+
373406
# Install dependencies with fallback strategy
374407
- name: Install dependencies
375408
run: |
@@ -421,7 +454,7 @@ jobs:
421454

422455
# 显式将 runner 级别的环境变量传入 cibuildwheel 的隔离 venv
423456
# 避免 $LIBOMP_PATH 因隐式继承失败而导致 delocate 找不到 libomp
424-
CIBW_PASS_ENV: "LIBOMP_PATH SEQUENZO_LIBOMP_PREFIX SEQUENZO_ENABLE_OPENMP MACOSX_DEPLOYMENT_TARGET GITHUB_WORKSPACE"
457+
CIBW_PASS_ENV: "LIBOMP_PATH LIBOMP_DLL_DIR SEQUENZO_LIBOMP_PREFIX SEQUENZO_ENABLE_OPENMP MACOSX_DEPLOYMENT_TARGET GITHUB_WORKSPACE"
425458

426459
# macOS: Set OpenMP environment with proper library paths and rpath for bundling
427460
# REPAIR_LIBRARY_PATH 是 libomp 路径的副本,专用于在 repair 阶段
@@ -551,6 +584,9 @@ jobs:
551584
552585
# Windows: setup MSVC environment
553586
CIBW_BEFORE_BUILD_WINDOWS: |
587+
echo "Installing wheel repair dependencies"
588+
python -m pip install --upgrade pip
589+
python -m pip install delvewheel
554590
echo "Setting up MSVC environment"
555591
call "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\VC\Auxiliary\Build\vcvars64.bat"
556592
for /f "usebackq tokens=*" %%i in (`"%ProgramFiles(x86)%\Microsoft Visual Studio\Installer\vswhere.exe" -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -find VC\Tools\MSVC\**\bin\Hostx64\x64`) do set "VCTOOLS_BIN=%%i"
@@ -562,8 +598,6 @@ jobs:
562598
where cl
563599
where link
564600
echo "Using minimal build dependencies"
565-
python -m pip install --upgrade pip
566-
python -m pip install delvewheel
567601
568602
CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: python maintenance_scripts/repair_windows_wheel.py "{dest_dir}" "{wheel}"
569603

maintenance_scripts/repair_windows_wheel.py

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
"""
1616
from __future__ import annotations
1717

18+
import importlib.util
1819
import os
1920
import shutil
2021
import subprocess
@@ -31,6 +32,21 @@
3132
# delvewheel invocation
3233
# ---------------------------------------------------------------------------
3334

35+
def _ensure_delvewheel() -> None:
36+
"""Install delvewheel into the active cibuildwheel venv when missing."""
37+
if importlib.util.find_spec("delvewheel") is not None:
38+
return
39+
40+
print("[repair] delvewheel not found in build venv; installing...")
41+
result = subprocess.run(
42+
[sys.executable, "-m", "pip", "install", "delvewheel"],
43+
check=False,
44+
)
45+
if result.returncode != 0 or importlib.util.find_spec("delvewheel") is None:
46+
print("[repair] ERROR: failed to install delvewheel", file=sys.stderr)
47+
raise SystemExit(1)
48+
49+
3450
def _delvewheel_cmd(*args: str) -> list[str]:
3551
"""Invoke delvewheel via the active Python (Scripts/ may be off PATH)."""
3652
return [sys.executable, "-m", "delvewheel", *args]
@@ -122,6 +138,61 @@ def _find_omp_dll_via_vswhere(vswhere: Path) -> Path | None:
122138
print(f"[repair] Found DLL under VS install root: {matches[0]}")
123139
return matches[0]
124140

141+
redist_match = _find_omp_dll_via_redist(install_root)
142+
if redist_match:
143+
return redist_match
144+
145+
return None
146+
147+
148+
def _find_omp_dll_via_redist(install_root: Path | None = None) -> Path | None:
149+
"""Search VC Redist trees where MSVC ships libomp140 for /openmp:llvm."""
150+
roots: list[Path] = []
151+
if install_root is not None and install_root.is_dir():
152+
roots.append(install_root)
153+
roots.extend(
154+
[
155+
Path("C:/Program Files/Microsoft Visual Studio/2022/Enterprise"),
156+
Path("C:/Program Files/Microsoft Visual Studio/2022/Community"),
157+
Path("C:/Program Files/Microsoft Visual Studio/2022/Professional"),
158+
]
159+
)
160+
161+
seen: set[Path] = set()
162+
for root in roots:
163+
try:
164+
resolved = root.resolve()
165+
except OSError:
166+
continue
167+
if resolved in seen or not resolved.is_dir():
168+
continue
169+
seen.add(resolved)
170+
171+
for name in _MSVC_OMP_DLL_NAMES:
172+
for pattern in (
173+
f"VC/Redist/MSVC/*/x64/Microsoft.VC*.OpenMP.LLVM/{name}",
174+
f"VC/Redist/MSVC/*/debug_nonredist/x64/Microsoft.VC*.OpenMP.LLVM/{name}",
175+
):
176+
matches = sorted(resolved.glob(pattern))
177+
if matches:
178+
print(f"[repair] Found DLL via VS redist: {matches[0]}")
179+
return matches[0]
180+
181+
return None
182+
183+
184+
def _find_omp_dll_from_env() -> Path | None:
185+
"""Use LIBOMP_DLL_DIR when CI has already resolved the OpenMP runtime."""
186+
env_dir = os.environ.get("LIBOMP_DLL_DIR", "").strip()
187+
if not env_dir:
188+
return None
189+
190+
directory = Path(env_dir)
191+
for name in _MSVC_OMP_DLL_NAMES:
192+
candidate = directory / name
193+
if candidate.is_file():
194+
print(f"[repair] Found DLL via LIBOMP_DLL_DIR: {candidate}")
195+
return candidate
125196
return None
126197

127198

@@ -143,6 +214,10 @@ def _find_omp_dll_fallback() -> Path | None:
143214

144215

145216
def find_omp_dll() -> Path | None:
217+
dll = _find_omp_dll_from_env()
218+
if dll:
219+
return dll
220+
146221
vswhere = _find_vswhere()
147222
if vswhere:
148223
print(f"[repair] Using vswhere: {vswhere}")
@@ -152,6 +227,11 @@ def find_omp_dll() -> Path | None:
152227
print("[repair] vswhere did not find libomp140*.dll; trying fallback paths...")
153228
else:
154229
print("[repair] vswhere.exe not found; trying fallback paths...")
230+
231+
dll = _find_omp_dll_via_redist()
232+
if dll:
233+
return dll
234+
155235
return _find_omp_dll_fallback()
156236

157237

@@ -178,6 +258,8 @@ def main() -> int:
178258
print("Usage: repair_windows_wheel.py <dest_dir> <wheel>", file=sys.stderr)
179259
return 2
180260

261+
_ensure_delvewheel()
262+
181263
dest = Path(sys.argv[1]).resolve()
182264
wheel = Path(sys.argv[2]).resolve()
183265
dest.mkdir(parents=True, exist_ok=True)
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import importlib.util
2+
import sys
3+
from pathlib import Path
4+
5+
6+
def _load_repair_module():
7+
repo_root = Path(__file__).resolve().parents[2]
8+
module_path = repo_root / "maintenance_scripts" / "repair_windows_wheel.py"
9+
spec = importlib.util.spec_from_file_location("repair_windows_wheel", module_path)
10+
module = importlib.util.module_from_spec(spec)
11+
assert spec.loader is not None
12+
sys.modules[spec.name] = module
13+
spec.loader.exec_module(module)
14+
return module
15+
16+
17+
def test_find_omp_dll_prefers_libomp_dll_dir(monkeypatch, tmp_path):
18+
repair = _load_repair_module()
19+
dll_dir = tmp_path / "openmp"
20+
dll_dir.mkdir()
21+
dll = dll_dir / "libomp140.x86_64.dll"
22+
dll.write_bytes(b"fake")
23+
24+
monkeypatch.setenv("LIBOMP_DLL_DIR", str(dll_dir))
25+
assert repair.find_omp_dll() == dll
26+
27+
28+
def test_find_omp_dll_via_redist_glob(tmp_path):
29+
repair = _load_repair_module()
30+
redist_dir = (
31+
tmp_path
32+
/ "VC"
33+
/ "Redist"
34+
/ "MSVC"
35+
/ "14.44.35207"
36+
/ "x64"
37+
/ "Microsoft.VC143.OpenMP.LLVM"
38+
)
39+
redist_dir.mkdir(parents=True)
40+
dll = redist_dir / "libomp140.x86_64.dll"
41+
dll.write_bytes(b"fake")
42+
43+
assert repair._find_omp_dll_via_redist(tmp_path) == dll

tests/openmp/test_windows_wheel_workflow.py

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,14 @@ def test_windows_cibuildwheel_smoke_test_uses_cmd_safe_python():
1414

1515
def test_windows_wheel_repair_verifies_bundled_openmp_dlls():
1616
workflow = WORKFLOW.read_text(encoding="utf-8")
17-
18-
assert "delvewheel show" in workflow
19-
assert "sequenzo.libs" in workflow
20-
assert "libomp140" in workflow
17+
repair_script = (
18+
WORKFLOW.parents[2] / "maintenance_scripts" / "repair_windows_wheel.py"
19+
).read_text(encoding="utf-8")
20+
21+
assert "delvewheel show" in repair_script
22+
assert "LIBOMP_DLL_DIR" in workflow
23+
assert "sequenzo.libs" in repair_script
24+
assert "libomp140" in repair_script
2125

2226

2327
def test_windows_conda_smoke_installs_repaired_wheel():

tests/test_build_release_boundaries.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,13 @@ def test_macos_wheel_repair_fails_instead_of_copying_unrepaired_wheel():
3030
assert "build_macos_ci_libomp.sh" in source
3131
assert "repair_windows_wheel.py" in source
3232
assert 'CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: python maintenance_scripts/repair_windows_wheel.py' in source
33+
assert "LIBOMP_DLL_DIR" in source
34+
assert "Locate MSVC LLVM OpenMP runtime (Windows)" in source
3335
repair_script = (repo_root / "maintenance_scripts" / "repair_windows_wheel.py").read_text()
3436
assert "delvewheel did not emit a wheel; copying built wheel as-is" in repair_script
3537
assert '"-m", "delvewheel"' in repair_script or "'-m', 'delvewheel'" in repair_script
38+
assert "_ensure_delvewheel" in repair_script
39+
assert "LIBOMP_DLL_DIR" in repair_script
3640
assert "libomp.dll" not in repair_script or "libomp140" in repair_script
3741
assert "bundled OpenMP DLLs" in repair_script
3842
assert (repo_root / "maintenance_scripts" / "repair_windows_wheel.py").is_file()

0 commit comments

Comments
 (0)