-
Notifications
You must be signed in to change notification settings - Fork 105
Expand file tree
/
Copy pathsetup.py
More file actions
363 lines (289 loc) · 12.5 KB
/
setup.py
File metadata and controls
363 lines (289 loc) · 12.5 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
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
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
#!/usr/bin/env python3
"""Build configuration for the libfyaml Python extension."""
import os
import re
import struct
import shlex
import shutil
import subprocess
import sys
from pathlib import Path
from typing import Dict, List, Optional, Tuple
from setuptools import Extension, setup
from setuptools.command.build_ext import build_ext
THIS_DIR = Path(__file__).resolve().parent
def resolve_repo_root() -> Path:
env_root = os.environ.get("LIBFYAML_REPO_ROOT")
candidates = []
if env_root:
candidates.append(Path(env_root))
candidates.append(THIS_DIR)
candidates.append(THIS_DIR.parent)
for candidate in candidates:
resolved = candidate.resolve()
if (resolved / "CMakeLists.txt").exists() and (resolved / "include" / "libfyaml.h").exists():
return resolved
return candidates[0].resolve() if candidates else THIS_DIR.parent.resolve()
REPO_ROOT = resolve_repo_root()
def _pep440_from_libfyaml_version(raw_version: str) -> str:
"""Translate libfyaml release versions to PEP 440."""
match = re.fullmatch(r"(\d+\.\d+\.\d+)(?:-(alpha|beta|rc)(\d+))?", raw_version.strip())
if not match:
raise RuntimeError(f"Unsupported libfyaml release version format: {raw_version!r}")
version = match.group(1)
if match.group(2):
version += {"alpha": "a", "beta": "b", "rc": "rc"}[match.group(2)] + match.group(3)
return version
def _read_pkg_info_version() -> Optional[str]:
"""Read the version from sdist metadata when repo version files are absent."""
for candidate in (THIS_DIR / "PKG-INFO", THIS_DIR / "libfyaml.egg-info" / "PKG-INFO"):
if not candidate.exists():
continue
for line in candidate.read_text().splitlines():
if line.startswith("Version: "):
return line.split(": ", 1)[1].strip()
return None
def resolve_package_version() -> str:
"""Resolve the Python package version from the core libfyaml release version."""
version_file = REPO_ROOT / ".tarball-version"
if version_file.exists():
return _pep440_from_libfyaml_version(version_file.read_text().strip())
git_version_gen = REPO_ROOT / "build-aux" / "git-version-gen"
if git_version_gen.exists():
result = subprocess.run(
[str(git_version_gen), str(version_file)],
capture_output=True,
text=True,
check=True,
)
return _pep440_from_libfyaml_version(result.stdout.strip())
pkg_info_version = _read_pkg_info_version()
if pkg_info_version:
return pkg_info_version
raise RuntimeError("Could not determine libfyaml package version")
def run_command(args: List[str], cwd: Optional[Path] = None) -> None:
"""Run a command and fail loudly on error."""
subprocess.run(args, cwd=cwd, check=True)
def get_pkg_config(package: str, option: str) -> List[str]:
"""Return pkg-config output split into arguments."""
try:
result = subprocess.run(
["pkg-config", option, package],
capture_output=True,
text=True,
check=True,
)
except (subprocess.CalledProcessError, FileNotFoundError):
return []
return result.stdout.strip().split()
def have_repo_sources() -> bool:
"""Check whether the full libfyaml source tree is available."""
return (REPO_ROOT / "CMakeLists.txt").exists() and (
REPO_ROOT / "include" / "libfyaml.h"
).exists()
def generic_platform_supported() -> bool:
"""Return True when libfyaml generics are supported on this target."""
return struct.calcsize("P") == 8 and sys.byteorder == "little"
def base_compile_args(compiler_type: Optional[str]) -> List[str]:
if compiler_type == "msvc":
return ["/W3", "/wd4100", "/Zc:preprocessor", "/clang:-fno-ms-compatibility"]
args = ["-Wall", "-Wextra", "-Wno-unused-parameter"]
if sys.platform != "win32":
args.insert(0, "-std=gnu2x")
return args
def base_link_args() -> Tuple[List[str], List[str]]:
extra_link_args: List[str] = []
libraries: List[str] = []
if sys.platform.startswith("linux"):
extra_link_args.append("-pthread")
libraries.append("m")
return extra_link_args, libraries
def windows_compiler_supported(compiler_type: Optional[str], compiler) -> bool:
if sys.platform != "win32":
return True
candidates = []
for env_name in ("CC", "CXX"):
value = os.environ.get(env_name)
if value:
candidates.extend(shlex.split(value))
for attr in ("compiler", "compiler_so", "linker_so"):
value = getattr(compiler, attr, None)
if isinstance(value, (list, tuple)):
candidates.extend(str(item) for item in value)
elif value:
candidates.append(str(value))
if compiler_type == "unix":
return True
normalized = " ".join(candidates).lower()
return "clang" in normalized
class CustomBuildExt(build_ext):
"""Build the extension against a bundled static libfyaml when possible."""
def build_extension(self, ext: Extension) -> None:
if not generic_platform_supported():
raise RuntimeError(
"libfyaml generics are currently supported only on 64-bit "
"little-endian targets"
)
if sys.platform == "win32":
self._build_windows_extension_with_cmake(ext)
return
compiler_type = getattr(self.compiler, "compiler_type", None)
if not windows_compiler_supported(compiler_type, self.compiler):
raise RuntimeError(
"Windows Python bindings require a Clang-family compiler "
"(clang or clang-cl)."
)
build_info = self._resolve_libfyaml_build(compiler_type)
ext.include_dirs = build_info["include_dirs"]
ext.library_dirs = build_info["library_dirs"]
ext.runtime_library_dirs = build_info["runtime_library_dirs"]
ext.libraries = build_info["libraries"]
ext.extra_objects = build_info["extra_objects"]
ext.extra_compile_args = build_info["extra_compile_args"]
ext.extra_link_args = build_info["extra_link_args"]
super().build_extension(ext)
def _build_windows_extension_with_cmake(self, ext: Extension) -> None:
build_dir = Path(self.build_temp) / "libfyaml-python"
if build_dir.exists():
shutil.rmtree(build_dir)
cmake_args = [
"cmake",
"-S",
str(REPO_ROOT),
"-B",
str(build_dir),
"-DBUILD_SHARED_LIBS=OFF",
"-DBUILD_TESTING=OFF",
"-DENABLE_NETWORK=OFF",
"-DENABLE_PYTHON_BINDINGS=ON",
"-DENABLE_REFLECTION=OFF",
"-DENABLE_LIBCLANG=OFF",
"-DENABLE_PORTABLE_TARGET=ON",
"-DCMAKE_BUILD_TYPE=Release",
f"-DPython3_EXECUTABLE={sys.executable}",
]
if "CMAKE_GENERATOR" not in os.environ and shutil.which("ninja"):
cmake_args.extend(["-G", "Ninja"])
extra_cmake_args = os.environ.get("LIBFYAML_CMAKE_ARGS")
if extra_cmake_args:
cmake_args.extend(shlex.split(extra_cmake_args))
run_command(cmake_args)
run_command(
["cmake", "--build", str(build_dir), "--config", "Release", "--target", "_libfyaml"]
)
ext_dir = build_dir / "python-libfyaml" / "libfyaml"
built_extensions = sorted(ext_dir.glob("_libfyaml*.abi3.pyd")) or \
sorted(ext_dir.glob("_libfyaml*.pyd"))
if not built_extensions:
raise RuntimeError("CMake did not produce a Windows _libfyaml extension")
destination = Path(self.get_ext_fullpath(ext.name))
destination.parent.mkdir(parents=True, exist_ok=True)
shutil.copy2(built_extensions[0], destination)
def _resolve_libfyaml_build(self, compiler_type: Optional[str]) -> Dict[str, List[str]]:
if os.environ.get("LIBFYAML_USE_SYSTEM") == "1":
return self._system_build_info(compiler_type)
if have_repo_sources():
return self._bundled_build_info(compiler_type)
return self._system_build_info(compiler_type)
def _bundled_build_info(self, compiler_type: Optional[str]) -> Dict[str, List[str]]:
build_dir = Path(self.build_temp) / "libfyaml-bundled"
if build_dir.exists():
shutil.rmtree(build_dir)
cmake_args = [
"cmake",
"-S",
str(REPO_ROOT),
"-B",
str(build_dir),
"-DBUILD_SHARED_LIBS=OFF",
"-DCMAKE_POSITION_INDEPENDENT_CODE=ON",
"-DBUILD_TESTING=OFF",
"-DENABLE_NETWORK=OFF",
"-DENABLE_PYTHON_BINDINGS=OFF",
"-DENABLE_REFLECTION=OFF",
"-DENABLE_LIBCLANG=OFF",
"-DENABLE_PORTABLE_TARGET=ON",
"-DCMAKE_BUILD_TYPE=Release",
]
if "CMAKE_GENERATOR" not in os.environ and shutil.which("ninja"):
cmake_args.extend(["-G", "Ninja"])
extra_cmake_args = os.environ.get("LIBFYAML_CMAKE_ARGS")
if extra_cmake_args:
cmake_args.extend(shlex.split(extra_cmake_args))
run_command(cmake_args)
run_command(
["cmake", "--build", str(build_dir), "--config", "Release", "--target", "fyaml_static"]
)
static_library = build_dir / (
"fyaml_static.lib" if sys.platform == "win32" else "libfyaml_static.a"
)
if not static_library.exists():
raise RuntimeError(f"Bundled static library was not produced: {static_library}")
extra_link_args, libraries = base_link_args()
return {
"include_dirs": [str(REPO_ROOT / "include"), str(build_dir)],
"library_dirs": [],
"runtime_library_dirs": [],
"libraries": libraries,
"extra_objects": [str(static_library)],
"extra_compile_args": base_compile_args(compiler_type),
"extra_link_args": extra_link_args,
}
def _system_build_info(self, compiler_type: Optional[str]) -> Dict[str, List[str]]:
include_dirs: List[str] = []
library_dirs: List[str] = []
runtime_library_dirs: List[str] = []
libraries = ["fyaml"]
cflags = get_pkg_config("libfyaml", "--cflags-only-I")
ldflags = get_pkg_config("libfyaml", "--libs-only-L")
libs = get_pkg_config("libfyaml", "--libs-only-l")
include_env = os.environ.get("LIBFYAML_INCLUDE_DIR")
library_env = os.environ.get("LIBFYAML_LIBRARY_DIR")
libraries_env = os.environ.get("LIBFYAML_LIBRARIES")
if cflags or ldflags or libs:
include_dirs = [flag[2:] for flag in cflags]
library_dirs = [flag[2:] for flag in ldflags]
libraries = [flag[2:] for flag in libs] or libraries
elif include_env or library_env or libraries_env:
if include_env:
include_dirs = [path for path in include_env.split(os.pathsep) if path]
if library_env:
library_dirs = [path for path in library_env.split(os.pathsep) if path]
if libraries_env:
libraries = [lib for lib in libraries_env.split(os.pathsep) if lib]
else:
if sys.platform == "win32":
raise RuntimeError(
"System libfyaml lookup on Windows requires pkg-config or "
"LIBFYAML_INCLUDE_DIR/LIBFYAML_LIBRARY_DIR."
)
include_dirs = ["/usr/local/include", "/usr/include"]
library_dirs = ["/usr/local/lib", "/usr/lib"]
extra_link_args, extra_libraries = base_link_args()
for library in extra_libraries:
if library not in libraries:
libraries.append(library)
return {
"include_dirs": include_dirs,
"library_dirs": library_dirs,
"runtime_library_dirs": runtime_library_dirs,
"libraries": libraries,
"extra_objects": [],
"extra_compile_args": base_compile_args(compiler_type),
"extra_link_args": extra_link_args,
}
setup(
version=resolve_package_version(),
ext_modules=[
Extension(
"libfyaml._libfyaml",
sources=["libfyaml/_libfyaml.c"],
define_macros=[("Py_LIMITED_API", "0x030A0000")],
py_limited_api=True,
),
],
packages=["libfyaml"],
package_data={"libfyaml": ["*.pyi"]},
cmdclass={"build_ext": CustomBuildExt},
options={"bdist_wheel": {"py_limited_api": "cp310"}},
)