-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathsetup.py
More file actions
141 lines (118 loc) · 4.82 KB
/
setup.py
File metadata and controls
141 lines (118 loc) · 4.82 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
"""
setup.py — build-logic only; all project metadata lives in pyproject.toml.
Correct invocations
-------------------
Build a wheel (recommended):
python -m build # needs: pip install build
Install directly from source:
pip install .
Developer in-place build:
pip install -e . --no-build-isolation
Key design decisions
--------------------
* CMakeBuild overrides build_extension(), NOT run().
run() is skipped by setuptools >= 68 in PEP-517 isolated builds.
build_extension() is guaranteed to execute once per Extension object on
every setuptools version >= 42.
* get_ext_fullpath(ext.name) returns the exact .so path pip expects.
CMAKE_LIBRARY_OUTPUT_DIRECTORY is set to its parent so CMake deposits
the .so in exactly that location — no manual copy needed.
* -DPython3_EXECUTABLE pins CMake to the same interpreter running this
script, which is essential inside venvs and conda environments.
"""
from __future__ import annotations
import os
import subprocess
import sys
from pathlib import Path
from setuptools import Extension, setup
from setuptools.command.build_ext import build_ext
def _find_cmake() -> str:
"""Prefer the 'cmake' pip package if present, else fall back to system cmake."""
try:
import cmake as _pkg
candidate = Path(_pkg.CMAKE_BIN_DIR) / "cmake"
if candidate.exists():
return str(candidate)
except ImportError:
pass
return "cmake"
def _cpu_count() -> int:
try:
return os.cpu_count() or 4
except Exception:
return 4
class CMakeBuild(build_ext):
def build_extension(self, ext: Extension) -> None:
# Exact path pip expects the .so at, e.g.:
# build/lib.linux-x86_64-cpython-312/equationsolver.cpython-312-...so
ext_fullpath = Path(self.get_ext_fullpath(ext.name)).resolve()
ext_dir = ext_fullpath.parent
ext_dir.mkdir(parents=True, exist_ok=True)
# Per-extension cmake scratch directory (safe for parallel builds)
build_temp = Path(self.build_temp).resolve() / ext.name
build_temp.mkdir(parents=True, exist_ok=True)
source_dir = Path(__file__).parent.resolve()
cmake_exe = _find_cmake()
cmake_args = [
# Place the .so exactly where setuptools expects it
f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY={ext_dir}",
# Pins CMake's FindPython3 to the same interpreter running this
# script — critical inside venvs and conda environments.
f"-DPython3_EXECUTABLE={sys.executable}",
"-DCMAKE_BUILD_TYPE=Release",
]
if sys.platform == "win32":
cmake_args += [
f"-DCMAKE_RUNTIME_OUTPUT_DIRECTORY={ext_dir}",
f"-DCMAKE_RUNTIME_OUTPUT_DIRECTORY_RELEASE={ext_dir}",
f"-DCMAKE_LIBRARY_OUTPUT_DIRECTORY_RELEASE={ext_dir}",
]
build_args = [
"--target", "equationsolver",
"--config", "Release",
f"-j{_cpu_count()}",
]
# Pass the interpreter path as an env var so excel_writer.cpp's
# getPythonExecutable() can find it without needing Python.h.
build_env = os.environ.copy()
build_env["PYTHONEXECUTABLE"] = sys.executable
try:
subprocess.check_call(
[cmake_exe, str(source_dir)] + cmake_args,
cwd=str(build_temp),
env=build_env,
)
subprocess.check_call(
[cmake_exe, "--build", "."] + build_args,
cwd=str(build_temp),
env=build_env,
)
except subprocess.CalledProcessError as exc:
raise RuntimeError(
f"\nCMake build failed (exit {exc.returncode}).\n\n"
"Required system packages:\n"
" Ubuntu/Debian: sudo apt install cmake g++ libeigen3-dev\n"
" Fedora/RHEL: sudo dnf install cmake gcc-c++ eigen3-devel\n"
" macOS (Homebrew): brew install cmake eigen\n"
) from exc
# Verify the .so was produced; copy to ext_fullpath if names differ
produced = (
list(ext_dir.glob("equationsolver*.so")) +
list(ext_dir.glob("equationsolver*.pyd"))
)
if not produced:
raise RuntimeError(
f"CMake succeeded but no equationsolver .so/.pyd found in {ext_dir}"
)
actual = produced[0].resolve()
if actual != ext_fullpath:
import shutil
shutil.copy2(str(actual), str(ext_fullpath))
setup(
# All metadata (name, version, description, dependencies, …) comes from
# pyproject.toml [project]. Only the extension build machinery lives here.
ext_modules=[Extension("equationsolver", sources=[])],
cmdclass={"build_ext": CMakeBuild},
zip_safe=False,
)