-
Notifications
You must be signed in to change notification settings - Fork 38
/
build_cmake.py
225 lines (171 loc) · 7.6 KB
/
build_cmake.py
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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
from __future__ import annotations
import shutil
import sys
from pathlib import Path
from typing import TYPE_CHECKING, ClassVar
import setuptools
import setuptools.errors
from packaging.version import Version
from ..builder.builder import Builder, get_archs
from ..builder.macos import normalize_macos_version
from ..cmake import CMake, CMaker
from ..settings.skbuild_read_settings import SettingsReader
if TYPE_CHECKING:
from setuptools.dist import Distribution
from .._compat.typing import Literal
__all__ = ["BuildCMake", "finalize_distribution_options", "cmake_source_dir"]
def __dir__() -> list[str]:
return __all__
def _validate_settings() -> None:
settings = SettingsReader.from_file("pyproject.toml", {}).settings
assert (
not settings.wheel.expand_macos_universal_tags
), "wheel.expand_macos_universal_tags is not supported in setuptools mode"
assert (
settings.logging.level == "WARNING"
), "Logging is not adjustable in setuptools mode yet"
assert not settings.wheel.py_api, "wheel.py_api is not supported in setuptools mode, use bdist_wheel options instead"
class BuildCMake(setuptools.Command):
source_dir: str | None = None
cmake_args: list[str] | str | None = None
build_lib: str | None
build_temp: str | None
debug: bool | None
editable_mode: bool
parallel: int | None
plat_name: str | None
user_options: ClassVar[list[tuple[str, str, str]]] = [
("build-lib=", "b", "directory for compiled extension modules"),
("build-temp=", "t", "directory for temporary files (build by-products)"),
("plat-name=", "p", "platform name to cross-compile for, if supported "),
("debug", "g", "compile/link with debugging information"),
("parallel=", "j", "number of parallel build jobs"),
("source-dir=", "j", "directory with CMakeLists.txt"),
("cmake-args=", "a", "extra arguments for CMake"),
]
def initialize_options(self) -> None:
self.build_lib = None
self.build_temp = None
self.debug = None
self.editable_mode = False
self.parallel = None
self.plat_name = None
self.source_dir = None
self.cmake_args = None
def finalize_options(self) -> None:
self.set_undefined_options(
"build_ext",
("build_lib", "build_lib"),
("build_temp", "build_temp"),
("debug", "debug"),
("parallel", "parallel"),
("plat_name", "plat_name"),
)
if isinstance(self.cmake_args, str):
self.cmake_args = [
b.strip() for a in self.cmake_args.split() for b in a.split(";")
]
def run(self) -> None:
assert self.build_lib is not None
assert self.build_temp is not None
assert self.plat_name is not None
_validate_settings()
build_tmp_folder = Path(self.build_temp)
build_temp = build_tmp_folder / "_skbuild" # TODO: include python platform
dist = self.distribution
dist_source_dir = getattr(self.distribution, "cmake_source_dir", None)
source_dir = self.source_dir if dist_source_dir is None else dist_source_dir
assert source_dir is not None, "This should not be reachable"
configure_args = self.cmake_args or []
assert isinstance(configure_args, list)
dist_cmake_args = getattr(self.distribution, "cmake_args", None)
configure_args.extend(dist_cmake_args or [])
bdist_wheel = dist.get_command_obj("bdist_wheel")
assert bdist_wheel is not None
limited_api = bdist_wheel.py_limited_api # type: ignore[attr-defined]
# TODO: this is a hack due to moving temporary paths for isolation
if build_temp.exists():
shutil.rmtree(build_temp)
settings = SettingsReader.from_file("pyproject.toml", {}).settings
cmake = CMake.default_search(minimum_version=settings.cmake.minimum_version)
config = CMaker(
cmake,
source_dir=Path(source_dir),
build_dir=build_temp,
build_type=settings.cmake.build_type,
)
builder = Builder(
settings=settings,
config=config,
)
# Setuptools requires this be specified if there's a mismatch.
if sys.platform.startswith("darwin"):
arm_only = get_archs(builder.config.env) == ["arm64"]
orig_macos_str = self.plat_name.rsplit("-", 1)[0].split("-", 1)[1]
orig_macos = normalize_macos_version(orig_macos_str, arm=arm_only)
config.env.setdefault("MACOSX_DEPLOYMENT_TARGET", str(orig_macos))
builder.config.build_type = "Debug" if self.debug else settings.cmake.build_type
builder.configure(
name=dist.get_name(),
version=Version(dist.get_version()),
defines={},
limited_abi=limited_api,
configure_args=configure_args,
)
# Set CMAKE_BUILD_PARALLEL_LEVEL to control the parallel build level
# across all generators.
build_args = []
# self.parallel is a way to set parallel jobs by hand using -j in the
# build_ext call, not supported by pip or PyPA-build.
if "CMAKE_BUILD_PARALLEL_LEVEL" not in builder.config.env and self.parallel:
build_args.append(f"-j{self.parallel}")
builder.build(build_args=build_args)
builder.install(Path(self.build_lib))
# def "get_source_file+ys"(self) -> list[str]:
# return ["CMakeLists.txt"]
# def get_outputs(self) -> list[str]:
# return []
# def get_output_mapping(self) -> dict[str, str]:
# return {}
def _has_cmake(dist: Distribution) -> bool:
build_cmake = dist.get_command_obj("build_cmake")
assert isinstance(build_cmake, BuildCMake)
return (
build_cmake.source_dir is not None
or getattr(dist, "cmake_source_dir", None) is not None
)
def _prepare_extension_detection(dist: Distribution) -> None:
# Setuptools needs to know that it has extensions modules
orig_has_ext_modules = dist.has_ext_modules
dist.has_ext_modules = lambda: orig_has_ext_modules() or _has_cmake(dist) # type: ignore[method-assign]
# Hack for stdlib distutils
if not setuptools.distutils.__package__.startswith("setuptools"): # type: ignore[attr-defined]
class EvilList(list): # type: ignore[type-arg]
def __len__(self) -> int:
return super().__len__() or int(_has_cmake(dist))
dist.ext_modules = getattr(dist, "ext_modules", []) or EvilList()
def _prepare_build_cmake_command(dist: Distribution) -> None:
# Prepare new build_cmake command and make sure build calls it
build = dist.get_command_class("build")
assert build is not None
if "build_cmake" not in {x for x, _ in build.sub_commands}:
build.sub_commands.append(
("build_cmake", lambda cmd: _has_cmake(cmd.distribution)) # type: ignore[arg-type]
)
def cmake_args(
_dist: Distribution, attr: Literal["cmake_args"], value: list[str]
) -> None:
assert attr == "cmake_args"
if not isinstance(value, list):
msg = "cmake_args must be a list"
raise setuptools.errors.SetupError(msg)
def cmake_source_dir(
_dist: Distribution, attr: Literal["cmake_source_dir"], value: str
) -> None:
assert attr == "cmake_source_dir"
if not Path(value).is_dir():
msg = "cmake_source_dir must be an existing directory"
raise setuptools.errors.SetupError(msg)
def finalize_distribution_options(dist: Distribution) -> None:
_prepare_extension_detection(dist)
_prepare_build_cmake_command(dist)