Skip to content

Commit ec9cd3b

Browse files
committed
1. Fixed the issue where -fno-openmp-extensions was applied in the wrong place on macOS. 2. Fixed duplicated code caused by multi-line shell blocks in the Windows YAML configuration.
1 parent 939756e commit ec9cd3b

3 files changed

Lines changed: 152 additions & 88 deletions

File tree

.github/workflows/python-app.yml

Lines changed: 5 additions & 59 deletions
Original file line numberDiff line numberDiff line change
@@ -566,65 +566,11 @@ jobs:
566566
python -m pip install delvewheel
567567
568568
CIBW_REPAIR_WHEEL_COMMAND_WINDOWS: |
569-
set -euo pipefail
570-
echo "=== Repairing Windows wheel with delvewheel ==="
571-
echo "Wheel: {wheel}"
572-
echo "Dest: {dest_dir}"
573-
574-
LLVM_OMP_BIN=""
575-
576-
# vswhere.exe path — use Windows-style separators and the canonical env var name.
577-
VSWHERE="$PROGRAMFILES(X86)/Microsoft Visual Studio/Installer/vswhere.exe"
578-
# Git-Bash on Windows sometimes exposes PROGRAMFILES_X86 instead; fall back to it.
579-
if [ ! -f "$VSWHERE" ] && [ -n "${PROGRAMFILES_X86:-}" ]; then
580-
VSWHERE="$PROGRAMFILES_X86/Microsoft Visual Studio/Installer/vswhere.exe"
581-
fi
582-
# Last-resort hard-coded path used on GitHub-hosted runners.
583-
if [ ! -f "$VSWHERE" ]; then
584-
VSWHERE="C:/Program Files (x86)/Microsoft Visual Studio/Installer/vswhere.exe"
585-
fi
586-
587-
if [ -f "$VSWHERE" ]; then
588-
echo "Using vswhere: $VSWHERE"
589-
# -find accepts glob patterns; use backslash path separators (Windows convention).
590-
OMP_DLL="$("$VSWHERE" -latest -products '*' \
591-
-requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 \
592-
-find 'VC\Tools\Llvm\x64\bin\libomp140.x86_64.dll' 2>/dev/null | head -1 || true)"
593-
if [ -n "$OMP_DLL" ] && [ -f "$OMP_DLL" ]; then
594-
LLVM_OMP_BIN="$(dirname "$OMP_DLL")"
595-
echo "Found libomp140.x86_64.dll via vswhere: $OMP_DLL"
596-
else
597-
echo "vswhere: libomp140.x86_64.dll not found via -find; trying glob fallback..."
598-
# Broad glob fallback: walk all MSVC LLVM bin dirs for the DLL.
599-
for candidate in \
600-
"C:/Program Files/Microsoft Visual Studio/2022/Enterprise/VC/Tools/Llvm/x64/bin/libomp140.x86_64.dll" \
601-
"C:/Program Files/Microsoft Visual Studio/2022/Community/VC/Tools/Llvm/x64/bin/libomp140.x86_64.dll" \
602-
"C:/Program Files/LLVM/bin/libomp.dll" \
603-
"C:/Program Files/LLVM/bin/libomp140.x86_64.dll"; do
604-
if [ -f "$candidate" ]; then
605-
LLVM_OMP_BIN="$(dirname "$candidate")"
606-
echo "Found OpenMP DLL via fallback path: $candidate"
607-
break
608-
fi
609-
done
610-
fi
611-
else
612-
echo "WARNING: vswhere.exe not found; skipping OpenMP DLL search"
613-
fi
614-
615-
if [ -n "$LLVM_OMP_BIN" ]; then
616-
echo "Bundling OpenMP from: $LLVM_OMP_BIN"
617-
delvewheel repair --add-path "$LLVM_OMP_BIN" -w "{dest_dir}" "{wheel}" || true
618-
else
619-
echo "WARNING: MSVC LLVM OpenMP not found; running delvewheel without --add-path (OpenMP may be missing from wheel)"
620-
delvewheel repair -w "{dest_dir}" "{wheel}" || true
621-
fi
622-
623-
# repair_windows_wheel.py is the safety net: if delvewheel emitted a
624-
# repaired wheel it confirms it; otherwise it copies the built wheel
625-
# as-is so cibuildwheel always finds a wheel in dest_dir.
626-
python "$GITHUB_WORKSPACE/maintenance_scripts/repair_windows_wheel.py" "{dest_dir}" "{wheel}"
627-
569+
# All DLL discovery and delvewheel invocation is handled inside
570+
# repair_windows_wheel.py to avoid YAML multi-line shell quoting
571+
# issues (nested if/else blocks at deep indentation caused the
572+
# script to be duplicated verbatim in the executed shell command).
573+
python "{project}/maintenance_scripts/repair_windows_wheel.py" "{dest_dir}" "{wheel}"
628574
# 注意:CIBW_BEFORE_BUILD(通用)在所有平台都提供了平台专用版本时
629575
# 永远不会执行(平台专用变体会覆盖通用变体),已移除死代码。
630576

maintenance_scripts/repair_windows_wheel.py

Lines changed: 136 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,167 @@
11
#!/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.
312
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>
815
"""
916
from __future__ import annotations
1017

18+
import os
1119
import shutil
20+
import subprocess
1221
import sys
1322
from pathlib import Path
1423

1524

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+
16129
def main() -> int:
17130
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)
22132
return 2
23133

24134
dest = Path(sys.argv[1]).resolve()
25135
wheel = Path(sys.argv[2]).resolve()
26136
dest.mkdir(parents=True, exist_ok=True)
27137

28138
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)
30140
return 1
31141

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}")
36154
return 0
37155

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.")
41158
target = dest / wheel.name
42159
shutil.copy2(wheel, target)
43-
44-
# Verify the copy succeeded — don't re-glob, just stat the known path.
45160
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)
51162
return 1
52163

53-
print(f"Repaired wheel (copied as-is): {target}")
164+
print(f"[repair] Repaired wheel (copied as-is): {target}")
54165
return 0
55166

56167

setup.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -411,9 +411,16 @@ def get_compile_args_for_file(filename, *, fast_math=True):
411411
# OpenMP flags with platform-specific optimization
412412
if has_openmp_support():
413413
if sys.platform == 'darwin':
414-
# macOS: 使用libomp,分离编译和链接标志
415-
openmp_flag = ['-Xpreprocessor', '-fopenmp']
416-
print("[SETUP] macOS OpenMP flags: -Xpreprocessor -fopenmp")
414+
# macOS: use libomp with separated compile/link flags.
415+
# -fno-openmp-extensions suppresses Intel-private OpenMP symbols
416+
# (e.g. __kmpc_dispatch_deinit) that clang emits by default but
417+
# the LLVM openmp runtime built from source does not export.
418+
# Without this flag the .so loads fine against Homebrew libomp
419+
# but fails with "symbol not found in flat namespace" against any
420+
# self-built libomp (used in CI to enforce the minos deployment
421+
# target of the wheel, e.g. 10.15 or 11.0).
422+
openmp_flag = ['-Xpreprocessor', '-fopenmp', '-fno-openmp-extensions']
423+
print("[SETUP] macOS OpenMP flags: -Xpreprocessor -fopenmp -fno-openmp-extensions")
417424
else:
418425
# Linux/Other: 使用libgomp
419426
openmp_flag = ['-fopenmp']
@@ -954,4 +961,4 @@ def run_setup():
954961

955962

956963
if __name__ == "__main__":
957-
run_setup()
964+
run_setup()

0 commit comments

Comments
 (0)