forked from crossbario/autobahn-python
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhatch_build.py
More file actions
410 lines (347 loc) · 15.9 KB
/
Copy pathhatch_build.py
File metadata and controls
410 lines (347 loc) · 15.9 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
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
"""
Hatchling custom build hook for CFFI extension modules and flatc compiler.
This builds:
1. The NVX (Native Vector Extensions) for WebSocket frame masking and UTF-8 validation
2. The FlatBuffers compiler (flatc) from deps/flatbuffers
Note: the FlatBuffers binary schemas (``reflection.bfbs`` and the WAMP
``wamp.bfbs``) are NOT generated here. They are committed to the source tree
and shipped as-is, so that building the package never requires *running*
flatc. This is what makes cross-compilation (e.g. Buildroot/Yocto/aarch64
from the PyPI sdist) work: a flatc built for the target architecture cannot
execute on the build host to emit the schemas. Regenerate the committed
artifacts offline with ``just build-fbs`` / ``just generate-reflection``.
See: https://hatch.pypa.io/latest/plugins/build-hook/custom/
"""
import importlib.util
import os
import shutil
import subprocess
import sys
import sysconfig
from pathlib import Path
from hatchling.builders.hooks.plugin.interface import BuildHookInterface
class CFfiBuildHook(BuildHookInterface):
"""Build hook for compiling CFFI extension modules and flatc compiler."""
PLUGIN_NAME = "cffi"
def initialize(self, version, build_data):
"""
Called before each build.
For wheel builds, compile the CFFI modules and flatc.
For sdist builds, just ensure source files are included.
"""
# Always capture flatbuffers git version (for both wheel and sdist)
self._update_flatbuffers_git_version()
if self.target_name != "wheel":
# Only compile for wheel builds, sdist just includes source
return
built_nvx = False
built_flatc = False
# NVX (Native Vector Extensions) is an OPTIONAL accelerator: autobahn
# ships pure-Python fallbacks for both the XOR masker and the UTF-8
# validator (see autobahn.websocket.xormasker), and AUTOBAHN_USE_NVX=0
# is an explicitly supported configuration that yields a legitimate
# pure-Python (py3-none-any) wheel.
nvx_requested = os.environ.get("AUTOBAHN_USE_NVX", "1") not in ("0", "false")
if nvx_requested:
# Build CFFI modules (NVX)
built_nvx = self._build_cffi_modules(build_data)
else:
print("AUTOBAHN_USE_NVX is disabled, skipping CFFI build")
# When NVX was requested but no extension was produced, the CFFI compile
# failed silently (_build_cffi_modules swallows compile errors and just
# returns False). Refuse to degrade a platform wheel into a structurally
# valid but unintended pure-Python (py3-none-any) wheel: fail the build
# hard so that a transient native-compile crash (e.g. a gcc SIGSEGV
# under QEMU ARM64 emulation) aborts with a non-zero exit and is retried
# by CI, instead of being uploaded as a degraded artifact. See #1856.
if nvx_requested and not built_nvx:
raise RuntimeError(
"NVX CFFI extension was requested (AUTOBAHN_USE_NVX) but was not "
"built - refusing to emit a pure-Python (py3-none-any) autobahn "
"wheel. See the build log above for the underlying compile "
"failure. Set AUTOBAHN_USE_NVX=0 to intentionally build a "
"pure-Python wheel."
)
# Build and bundle the flatc compiler (developer convenience). The
# binary FlatBuffers schemas (reflection.bfbs, wamp.bfbs) are NOT
# generated here: they are committed to the source tree and shipped
# as-is, so a package build never needs to *run* flatc (required for
# cross-compilation - see module docstring). flatc is best-effort and
# does NOT gate the wheel tag.
built_flatc = self._build_flatc(build_data)
# If we built any extensions, mark this as a platform-specific wheel.
if built_nvx or built_flatc:
build_data["infer_tag"] = True
build_data["pure_python"] = False
def _get_ext_suffix(self):
"""Get the extension suffix for the current Python interpreter.
E.g., '.cpython-311-x86_64-linux-gnu.so' or '.pypy311-pp73-x86_64-linux-gnu.so'
"""
return sysconfig.get_config_var("EXT_SUFFIX") or ".so"
def _build_cffi_modules(self, build_data):
"""Compile the CFFI extension modules using direct file execution.
Returns True if any extensions were successfully built.
"""
src_path = Path(self.root) / "src"
nvx_dir = src_path / "autobahn" / "nvx"
built_any = False
# Get the extension suffix for current Python to filter artifacts
ext_suffix = self._get_ext_suffix()
print(f"Building for Python with extension suffix: {ext_suffix}")
# CFFI module files to build
cffi_modules = [
("_utf8validator.py", "ffi"),
("_xormasker.py", "ffi"),
]
for module_file, ffi_name in cffi_modules:
module_path = nvx_dir / module_file
print(f"Building CFFI module: {module_path}")
try:
# Load the module directly from file (like CFFI's setuptools integration)
# This avoids triggering package-level imports
spec = importlib.util.spec_from_file_location(
f"_cffi_build_{module_file}", module_path
)
module = importlib.util.module_from_spec(spec)
# We need to set up sys.path so the module can find _compile_args.py
old_path = sys.path.copy()
sys.path.insert(0, str(nvx_dir))
sys.path.insert(0, str(src_path))
try:
spec.loader.exec_module(module)
ffi = getattr(module, ffi_name)
# Compile the CFFI module
# The compiled .so/.pyd goes to the current directory by default
# We want it in the nvx_dir
old_cwd = os.getcwd()
os.chdir(nvx_dir)
try:
ffi.compile(verbose=True)
finally:
os.chdir(old_cwd)
finally:
sys.path = old_path
# Find the compiled artifact matching CURRENT Python and add to build_data
# Only include .so files that match the current interpreter's extension suffix
#
# IMPORTANT: The .so files must be placed at the WHEEL ROOT (not in autobahn/nvx/)
# because CFFI creates top-level modules (e.g., "_nvx_utf8validator")
# and the Python code does `import _nvx_utf8validator` (top-level import).
for artifact in nvx_dir.glob("_nvx_*" + ext_suffix):
src_file = str(artifact)
# Place at wheel root for top-level import
dest_path = artifact.name
build_data["force_include"][src_file] = dest_path
print(
f" -> Added artifact: {artifact.name} -> {dest_path} (wheel root)"
)
built_any = True
except Exception as e:
print(f"Warning: Could not build {module_file}: {e}")
import traceback
traceback.print_exc()
return built_any
def _build_flatc(self, build_data):
"""Build the FlatBuffers compiler (flatc) from deps/flatbuffers.
Returns True if flatc was successfully built.
"""
print("\n" + "=" * 70)
print("Building FlatBuffers compiler (flatc)")
print("=" * 70)
flatbuffers_dir = Path(self.root) / "deps" / "flatbuffers"
build_dir = flatbuffers_dir / "build"
flatc_bin_dir = Path(self.root) / "src" / "autobahn" / "_flatc" / "bin"
# Determine executable name based on platform
exe_name = "flatc.exe" if os.name == "nt" else "flatc"
# Check if cmake is available
cmake_path = shutil.which("cmake")
if not cmake_path:
print("WARNING: cmake not found, skipping flatc build")
print(" -> Install cmake to enable flatc bundling")
return False
# Check if flatbuffers source exists
if not flatbuffers_dir.exists():
print(f"WARNING: {flatbuffers_dir} not found")
print(" -> Initialize git submodule: git submodule update --init")
return False
# Clean and create build directory (remove any cached cmake config)
if build_dir.exists():
shutil.rmtree(build_dir)
build_dir.mkdir(parents=True, exist_ok=True)
# Step 1: Configure with cmake
print(" -> Configuring with cmake...")
cmake_args = [
cmake_path,
"..",
"-DCMAKE_BUILD_TYPE=Release",
"-DFLATBUFFERS_BUILD_TESTS=OFF",
"-DFLATBUFFERS_BUILD_FLATLIB=OFF",
"-DFLATBUFFERS_BUILD_FLATHASH=OFF",
"-DFLATBUFFERS_BUILD_GRPCTEST=OFF",
"-DFLATBUFFERS_BUILD_SHAREDLIB=OFF",
]
# ====================================================================
# Note on manylinux compatibility:
# ====================================================================
# For manylinux-compatible Linux wheels, flatc must be built inside
# official PyPA manylinux containers (e.g., manylinux_2_28_x86_64).
# These containers have toolchains pre-configured for the correct
# glibc and ISA requirements. No special compiler flags needed.
#
# The wheels-docker.yml and wheels-arm64.yml workflows handle Linux
# builds using these containers. This hatch_build.py works correctly
# in those environments without any ISA-specific flags.
#
# macOS and Windows builds use native GitHub runners (wheels.yml).
# ====================================================================
result = subprocess.run(
cmake_args,
cwd=build_dir,
capture_output=True,
text=True,
)
if result.returncode != 0:
print(f"ERROR: cmake configure failed:\n{result.stderr}")
return False
# Step 2: Build flatc
print(" -> Building flatc...")
build_args = [
cmake_path,
"--build",
".",
"--config",
"Release",
"--target",
"flatc",
]
result = subprocess.run(
build_args,
cwd=build_dir,
capture_output=True,
text=True,
)
if result.returncode != 0:
print(f"ERROR: cmake build failed:\n{result.stderr}")
return False
# Step 3: Find and copy the built flatc
# flatc might be in different locations depending on platform/generator
possible_locations = [
build_dir / exe_name,
build_dir / "Release" / exe_name, # Windows/MSVC
build_dir / "Debug" / exe_name,
]
flatc_src = None
for loc in possible_locations:
if loc.exists():
flatc_src = loc
break
if not flatc_src:
print(f"ERROR: Built flatc not found in {build_dir}")
for loc in possible_locations:
print(f" Checked: {loc}")
return False
# Copy flatc to package bin directory
flatc_bin_dir.mkdir(parents=True, exist_ok=True)
flatc_dest = flatc_bin_dir / exe_name
shutil.copy2(flatc_src, flatc_dest)
# Make executable on Unix
if os.name != "nt":
flatc_dest.chmod(0o755)
print(f" -> Built flatc: {flatc_dest}")
# Verify ISA level on Linux (check for x86_64_v2 instructions)
if sys.platform.startswith("linux"):
print(" -> Verifying ISA level...")
readelf_result = subprocess.run(
["readelf", "-A", str(flatc_dest)],
capture_output=True,
text=True,
)
if readelf_result.returncode == 0:
# Look for ISA info in output
for line in readelf_result.stdout.splitlines():
if "ISA" in line or "x86" in line.lower():
print(f" {line.strip()}")
# Also check file command for architecture info
file_result = subprocess.run(
["file", str(flatc_dest)],
capture_output=True,
text=True,
)
if file_result.returncode == 0:
print(f" {file_result.stdout.strip()}")
# Add flatc to wheel
src_file = str(flatc_dest)
dest_path = f"autobahn/_flatc/bin/{exe_name}"
build_data["force_include"][src_file] = dest_path
print(f" -> Added to wheel: {dest_path}")
return True
def _update_flatbuffers_git_version(self):
"""
Capture the git describe version of deps/flatbuffers submodule.
This writes the version to flatbuffers/_git_version.py so that
autobahn.flatbuffers.version() returns the exact git version at runtime.
"""
print("=" * 70)
print("Capturing FlatBuffers git version from deps/flatbuffers")
print("=" * 70)
flatbuffers_dir = Path(self.root) / "deps" / "flatbuffers"
git_version_file = (
Path(self.root) / "src" / "autobahn" / "flatbuffers" / "_git_version.py"
)
# Default version if git is not available or submodule not initialized
git_version = "unknown"
if flatbuffers_dir.exists() and (flatbuffers_dir / ".git").exists():
try:
result = subprocess.run(
["git", "describe", "--tags", "--always"],
cwd=flatbuffers_dir,
capture_output=True,
text=True,
timeout=10,
)
if result.returncode == 0:
git_version = result.stdout.strip()
print(f" -> Git version: {git_version}")
else:
print(f" -> git describe failed: {result.stderr}")
except FileNotFoundError:
print(" -> git command not found, using existing version")
# Keep existing version in file if git not available
return
except subprocess.TimeoutExpired:
print(" -> git describe timed out, using existing version")
return
except Exception as e:
print(f" -> Error getting git version: {e}")
return
else:
print(" -> deps/flatbuffers not found or not a git repo")
print(f" -> Using existing version in {git_version_file.name}")
return
# Write the version file
content = """\
# Copyright 2014 Google Inc. All rights reserved.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# Git version from deps/flatbuffers submodule.
# This file is regenerated at build time by hatch_build.py.
# The version is captured via `git describe --tags` in the submodule.
#
# Format: "v25.9.23" (tagged release) or "v25.9.23-2-g95053e6a" (post-tag)
#
# If building from sdist without git, this will retain the version
# from when the sdist was created.
__git_version__ = "{version}"
""".format(version=git_version)
git_version_file.write_text(content)
print(f" -> Updated {git_version_file.name}")