-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathsetup.py
More file actions
772 lines (697 loc) · 32.1 KB
/
setup.py
File metadata and controls
772 lines (697 loc) · 32.1 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
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
#!/usr/bin/env python3
"""
Build configuration for httpmorph with C extensions and Cython bindings.
This setup.py configures:
1. C extension modules with io_uring/epoll, llhttp, and BoringSSL
2. Cython bindings for Python interface
3. Platform-specific optimizations (io_uring on Linux 5.1+)
"""
import os
import platform
import sys
from pathlib import Path
# tomllib is only available in Python 3.11+, use tomli for older versions
if sys.version_info >= (3, 11):
import tomllib
else:
import tomli as tomllib
from setuptools import Extension, setup
# Detect if we're building on Read the Docs
ON_READTHEDOCS = os.environ.get('READTHEDOCS') == 'True'
# Skip C extensions on Read the Docs (docs don't need them)
if not ON_READTHEDOCS:
from Cython.Build import cythonize
# Read version from pyproject.toml (single source of truth)
with open("pyproject.toml", "rb") as f:
pyproject = tomllib.load(f)
version_string = pyproject["project"]["version"]
version_parts = version_string.split(".")
VERSION_MAJOR = int(version_parts[0])
VERSION_MINOR = int(version_parts[1])
VERSION_PATCH = int(version_parts[2]) if len(version_parts) > 2 else 0
# Detect platform
IS_LINUX = platform.system() == "Linux"
IS_WINDOWS = platform.system() == "Windows"
IS_MACOS = platform.system() == "Darwin"
HAS_IO_URING = False
if IS_LINUX:
try:
# Check kernel version for io_uring support (Linux 5.1+)
kernel_version = platform.release().split(".")
major, minor = int(kernel_version[0]), int(kernel_version[1])
kernel_supports_io_uring = (major > 5) or (major == 5 and minor >= 1)
# Also check if liburing is available (vendor or system)
# In Docker containers, kernel version comes from host, but liburing must be in container
vendor_dir = Path("vendor")
has_vendor_liburing = (
(vendor_dir / "liburing" / "install" / "lib" / "liburing.a").exists() or
(vendor_dir / "liburing" / "src" / "liburing.a").exists()
)
# Try to find system liburing as fallback
import subprocess
has_system_liburing = False
if not has_vendor_liburing:
try:
subprocess.check_output(["pkg-config", "--exists", "liburing"], stderr=subprocess.DEVNULL)
has_system_liburing = True
except (subprocess.CalledProcessError, FileNotFoundError):
pass
HAS_IO_URING = kernel_supports_io_uring and (has_vendor_liburing or has_system_liburing)
except (ValueError, IndexError):
pass
print(f"Building for platform: {platform.system()}")
print(f"io_uring support: {HAS_IO_URING}")
# Base directories
SRC_DIR = Path("src")
CORE_DIR = SRC_DIR / "core"
TLS_DIR = SRC_DIR / "tls"
BINDINGS_DIR = SRC_DIR / "bindings"
INCLUDE_DIR = Path("include")
VENDOR_DIR = Path("vendor")
# Compiler flags (platform-specific)
if IS_WINDOWS:
# MSVC flags
EXTRA_COMPILE_ARGS = [
"/O2", # Optimization
"/W3", # Warning level 3
"/D_WIN32",
"/D_CRT_SECURE_NO_WARNINGS",
# Version information from pyproject.toml
f"/DHTTPMORPH_VERSION_MAJOR={VERSION_MAJOR}",
f"/DHTTPMORPH_VERSION_MINOR={VERSION_MINOR}",
f"/DHTTPMORPH_VERSION_PATCH={VERSION_PATCH}",
]
# Note: /std:c11 is only available in VS 2019 16.8+ (MSVC 19.28+)
# For older versions, MSVC uses C89 with C99/C11 extensions by default
# We rely on C99 features like loop variable declarations which are supported
EXTRA_LINK_ARGS = ["ws2_32.lib"] # Winsock library
else:
# GCC/Clang flags
EXTRA_COMPILE_ARGS = [
"-O1", # Lower optimization for better ASan reports
"-g", # Debug symbols
"-fsanitize=address", # AddressSanitizer
"-fno-omit-frame-pointer", # Better stack traces
"-Wall", # All warnings
"-Wextra", # Extra warnings
# Note: -std flag removed to support mixed C/C++ compilation (C11/C++11 are defaults)
# Version information from pyproject.toml
f"-DHTTPMORPH_VERSION_MAJOR={VERSION_MAJOR}",
f"-DHTTPMORPH_VERSION_MINOR={VERSION_MINOR}",
f"-DHTTPMORPH_VERSION_PATCH={VERSION_PATCH}",
]
EXTRA_LINK_ARGS = ["-fsanitize=address"]
# Add io_uring if available
if HAS_IO_URING:
EXTRA_COMPILE_ARGS.append("-DHAVE_IO_URING")
EXTRA_LINK_ARGS.append("-luring")
# Platform-specific flags
if IS_LINUX:
EXTRA_COMPILE_ARGS.extend(["-D_GNU_SOURCE"])
# Include directories
INCLUDE_DIRS = [
str(INCLUDE_DIR),
str(CORE_DIR),
str(TLS_DIR),
str(VENDOR_DIR / "llhttp" / "include"),
str(VENDOR_DIR / "boringssl" / "include"),
]
# Library directories
LIBRARY_DIRS = [
str(VENDOR_DIR / "llhttp" / "build"),
str(VENDOR_DIR / "boringssl" / "build" / "ssl"),
str(VENDOR_DIR / "boringssl" / "build" / "crypto"),
]
# Libraries to link
LIBRARIES = ["ssl", "crypto"] # BoringSSL
if HAS_IO_URING:
LIBRARIES.append("uring")
if IS_WINDOWS:
LIBRARIES.extend(["ws2_32", "advapi32", "crypt32", "user32"])
# Check if vendor dependencies exist
VENDOR_EXISTS = (VENDOR_DIR / "boringssl" / "build" / "ssl" / "libssl.a").exists()
if not VENDOR_EXISTS:
print("\n" + "=" * 70)
print("WARNING: Vendor dependencies not found!")
print("=" * 70)
print("\nPlease run: make setup")
print("\nThis will download and build:")
print(" • BoringSSL (TLS library)")
print(" • liburing (io_uring - Linux only)")
print(" • nghttp2 (HTTP/2 library)")
print("\n" + "=" * 70 + "\n")
# Get BoringSSL and nghttp2 paths based on platform
def get_library_paths():
"""Detect platform and return appropriate library paths."""
import subprocess
if IS_MACOS:
# Use vendor dependencies if available, otherwise fall back to Homebrew
vendor_dir = Path("vendor").resolve()
# Check if vendor BoringSSL exists
vendor_boringssl = vendor_dir / "boringssl"
if vendor_boringssl.exists() and (vendor_boringssl / "include").exists():
openssl_include = str(vendor_boringssl / "include")
# Check both possible locations for libssl.a
if (vendor_boringssl / "build" / "ssl").exists():
openssl_lib = str(vendor_boringssl / "build")
else:
openssl_lib = str(vendor_boringssl / "build")
print(f"Using vendor BoringSSL from: {vendor_boringssl}")
else:
# Fall back to Homebrew OpenSSL
try:
openssl_prefix = (
subprocess.check_output(
["brew", "--prefix", "openssl@3"], stderr=subprocess.DEVNULL
)
.decode()
.strip()
)
except (subprocess.CalledProcessError, FileNotFoundError):
openssl_prefix = "/opt/homebrew/opt/openssl@3"
openssl_include = f"{openssl_prefix}/include"
openssl_lib = f"{openssl_prefix}/lib"
print(f"Using Homebrew OpenSSL from: {openssl_prefix}")
# Check if vendor nghttp2 exists
vendor_nghttp2 = vendor_dir / "nghttp2" / "install"
if vendor_nghttp2.exists() and (vendor_nghttp2 / "include").exists():
nghttp2_include = str(vendor_nghttp2 / "include")
nghttp2_lib = str(vendor_nghttp2 / "lib")
print(f"Using vendor nghttp2 from: {vendor_nghttp2}")
else:
# Fall back to Homebrew nghttp2
try:
nghttp2_prefix = (
subprocess.check_output(
["brew", "--prefix", "libnghttp2"], stderr=subprocess.DEVNULL
)
.decode()
.strip()
)
except (subprocess.CalledProcessError, FileNotFoundError):
nghttp2_prefix = "/opt/homebrew/opt/libnghttp2"
nghttp2_include = f"{nghttp2_prefix}/include"
nghttp2_lib = f"{nghttp2_prefix}/lib"
print(f"Using Homebrew nghttp2 from: {nghttp2_prefix}")
return {
"openssl_include": openssl_include,
"openssl_lib": openssl_lib,
"nghttp2_include": nghttp2_include,
"nghttp2_lib": nghttp2_lib,
}
elif IS_LINUX:
# Use vendor dependencies if available, otherwise system paths
vendor_dir = Path("vendor").resolve()
# Check if vendor BoringSSL exists
vendor_boringssl = vendor_dir / "boringssl"
if vendor_boringssl.exists() and (vendor_boringssl / "include").exists():
openssl_include = str(vendor_boringssl / "include")
openssl_lib = str(vendor_boringssl / "build")
print(f"Using vendor BoringSSL from: {vendor_boringssl}")
else:
openssl_include = None
openssl_lib = None
# Check if vendor nghttp2 exists
vendor_nghttp2 = vendor_dir / "nghttp2" / "install"
if vendor_nghttp2.exists() and (vendor_nghttp2 / "include").exists():
nghttp2_include = str(vendor_nghttp2 / "include")
nghttp2_lib = str(vendor_nghttp2 / "lib")
print(f"Using vendor nghttp2 from: {vendor_nghttp2}")
else:
# Try pkg-config for nghttp2
nghttp2_include = None
nghttp2_lib = None
try:
import subprocess
include_output = (
subprocess.check_output(
["pkg-config", "--cflags-only-I", "libnghttp2"], stderr=subprocess.DEVNULL
)
.decode()
.strip()
)
if include_output:
nghttp2_include = include_output.replace("-I", "").strip()
lib_output = (
subprocess.check_output(
["pkg-config", "--libs-only-L", "libnghttp2"], stderr=subprocess.DEVNULL
)
.decode()
.strip()
)
if lib_output:
nghttp2_lib = lib_output.replace("-L", "").strip()
except (subprocess.CalledProcessError, FileNotFoundError):
pass
# Fall back to common system paths if pkg-config didn't work
if not nghttp2_include:
nghttp2_include = "/usr/include"
if not nghttp2_lib:
nghttp2_lib = "/usr/lib/x86_64-linux-gnu"
# Also check alternative paths
if not Path(nghttp2_lib).exists():
for alt_path in ["/usr/lib64", "/usr/lib"]:
if Path(alt_path).exists():
nghttp2_lib = alt_path
break
# Try pkg-config for BoringSSL/OpenSSL if vendor not found
if not openssl_include or not openssl_lib:
try:
import subprocess
include_output = (
subprocess.check_output(
["pkg-config", "--cflags-only-I", "openssl"], stderr=subprocess.DEVNULL
)
.decode()
.strip()
)
if include_output:
openssl_include = include_output.replace("-I", "").strip()
lib_output = (
subprocess.check_output(
["pkg-config", "--libs-only-L", "openssl"], stderr=subprocess.DEVNULL
)
.decode()
.strip()
)
if lib_output:
openssl_lib = lib_output.replace("-L", "").strip()
except (subprocess.CalledProcessError, FileNotFoundError):
pass
# Use system SSL library on Linux if pkg-config didn't work
if not openssl_include:
openssl_include = "/usr/include"
if not openssl_lib:
openssl_lib = "/usr/lib/x86_64-linux-gnu"
if not Path(openssl_lib).exists():
for alt_path in ["/usr/lib64", "/usr/lib"]:
if Path(alt_path).exists():
openssl_lib = alt_path
break
# Validate paths before returning
if not openssl_include or not openssl_include.strip():
openssl_include = "/usr/include"
if not openssl_lib or not openssl_lib.strip():
openssl_lib = "/usr/lib/x86_64-linux-gnu"
if not nghttp2_include or not nghttp2_include.strip():
nghttp2_include = "/usr/include"
if not nghttp2_lib or not nghttp2_lib.strip():
nghttp2_lib = "/usr/lib/x86_64-linux-gnu"
# Add liburing paths if vendor build exists
liburing_include = None
liburing_lib = None
vendor_liburing_install = vendor_dir / "liburing" / "install"
if vendor_liburing_install.exists():
liburing_include = str(vendor_liburing_install / "include")
liburing_lib = str(vendor_liburing_install / "lib")
print(f"Using vendor liburing from: {vendor_liburing_install}")
elif (vendor_dir / "liburing" / "src").exists():
# Fallback to src directory if install doesn't exist
liburing_include = str(vendor_dir / "liburing" / "src" / "include")
liburing_lib = str(vendor_dir / "liburing" / "src")
print(f"Using vendor liburing from: {vendor_dir / 'liburing'}")
return {
"openssl_include": openssl_include,
"openssl_lib": openssl_lib,
"nghttp2_include": nghttp2_include,
"nghttp2_lib": nghttp2_lib,
"liburing_include": liburing_include,
"liburing_lib": liburing_lib,
}
elif IS_WINDOWS:
# Windows - use vendor BoringSSL build, and vcpkg/MSYS2 for nghttp2
import os
vendor_dir = Path("vendor").resolve()
vendor_boringssl = vendor_dir / "boringssl"
# BoringSSL paths (always use vendor build)
# Windows builds output to build/Release/ directory
boringssl_include = str(vendor_boringssl / "include")
boringssl_lib = str(vendor_boringssl / "build" / "Release")
# Check if BoringSSL was built successfully
if not (vendor_boringssl / "build" / "Release" / "ssl.lib").exists():
print(f"WARNING: BoringSSL not found at {vendor_boringssl}")
print("Please run: make setup")
# nghttp2 paths - prefer vendor build, then vcpkg, then MSYS2
vendor_nghttp2 = vendor_dir / "nghttp2"
if (vendor_nghttp2 / "lib" / "includes").exists():
# Vendor build exists
print(f"Using vendor nghttp2 from: {vendor_nghttp2}")
nghttp2_include = str(vendor_nghttp2 / "lib" / "includes")
nghttp2_lib = str(vendor_nghttp2 / "build" / "lib" / "Release")
elif (vendor_nghttp2 / "include" / "nghttp2").exists():
# Vendor build with install structure
print(f"Using vendor nghttp2 from: {vendor_nghttp2}")
nghttp2_include = str(vendor_nghttp2 / "include")
nghttp2_lib = str(vendor_nghttp2 / "build" / "lib" / "Release")
else:
# Try vcpkg
vcpkg_root = os.environ.get("VCPKG_ROOT", "C:/vcpkg")
vcpkg_installed = Path(vcpkg_root) / "installed" / "x64-windows"
if vcpkg_installed.exists() and (vcpkg_installed / "include" / "nghttp2").exists():
print(f"Using vcpkg nghttp2 from: {vcpkg_installed}")
nghttp2_include = str(vcpkg_installed / "include")
nghttp2_lib = str(vcpkg_installed / "lib")
elif Path("/mingw64/include/nghttp2").exists():
print("Using MSYS2 nghttp2 from: /mingw64")
nghttp2_include = "/mingw64/include"
nghttp2_lib = "/mingw64/lib"
else:
# Fallback to default paths
print("WARNING: nghttp2 not found. Install via vcpkg or MSYS2")
nghttp2_include = "C:/Program Files/nghttp2/include"
nghttp2_lib = "C:/Program Files/nghttp2/lib"
# zlib paths - prefer vendor, then vcpkg
vendor_zlib = vendor_dir / "zlib"
vcpkg_root = os.environ.get("VCPKG_ROOT", "C:/vcpkg")
vcpkg_installed = Path(vcpkg_root) / "installed" / "x64-windows"
if (vendor_zlib / "build" / "Release" / "zlibstatic.lib").exists():
print(f"Using vendor zlib from: {vendor_zlib}")
# Need both source dir (for zlib.h) and build dir (for zconf.h)
zlib_include = [str(vendor_zlib), str(vendor_zlib / "build")]
zlib_lib = str(vendor_zlib / "build" / "Release")
elif vcpkg_installed.exists() and (vcpkg_installed / "lib" / "zlib.lib").exists():
print(f"Using vcpkg zlib from: {vcpkg_installed}")
zlib_include = str(vcpkg_installed / "include")
zlib_lib = str(vcpkg_installed / "lib")
else:
print("WARNING: zlib not found. Install via vcpkg: vcpkg install zlib:x64-windows")
zlib_include = None
zlib_lib = None
# brotli paths - vcpkg only (no vendor build on Windows)
if vcpkg_installed.exists() and (vcpkg_installed / "include" / "brotli").exists():
print(f"Using vcpkg brotli from: {vcpkg_installed}")
brotli_include = str(vcpkg_installed / "include")
brotli_lib = str(vcpkg_installed / "lib")
else:
print("WARNING: brotli not found. Install via vcpkg: vcpkg install brotli:x64-windows")
brotli_include = None
brotli_lib = None
return {
"openssl_include": boringssl_include,
"openssl_lib": boringssl_lib,
"nghttp2_include": nghttp2_include,
"nghttp2_lib": nghttp2_lib,
"zlib_include": zlib_include,
"zlib_lib": zlib_lib,
"brotli_include": brotli_include,
"brotli_lib": brotli_lib,
}
else:
# Other platforms - use default system paths
return {
"openssl_include": "/usr/include",
"openssl_lib": "/usr/lib",
"nghttp2_include": "/usr/include",
"nghttp2_lib": "/usr/lib",
}
if not ON_READTHEDOCS:
LIB_PATHS = get_library_paths()
# Debug output
print("\nLibrary paths detected:")
print(f" BoringSSL include: {LIB_PATHS['openssl_include']}")
print(f" BoringSSL lib: {LIB_PATHS['openssl_lib']}")
print(f" nghttp2 include: {LIB_PATHS['nghttp2_include']}")
print(f" nghttp2 lib: {LIB_PATHS['nghttp2_lib']}")
print()
# Platform-specific compile args and libraries for extensions (skip on RTD)
if not ON_READTHEDOCS:
if IS_WINDOWS:
# Use /TP to compile as C++ (required for BoringSSL compatibility on Windows)
# Define WIN32, _WINDOWS, and OPENSSL_WINDOWS for proper BoringSSL compilation
EXT_COMPILE_ARGS = [
"/O2",
"/DHAVE_NGHTTP2",
"/DNGHTTP2_STATICLIB", # Static linking for nghttp2
"/EHsc",
"/DWIN32",
"/D_WINDOWS",
"/DOPENSSL_WINDOWS",
"/D_WIN32",
# Force include windows_compat.h to define ssize_t properly for all compilation units
"/FIwindows_compat.h",
# Version information from pyproject.toml
f"/DHTTPMORPH_VERSION_MAJOR={VERSION_MAJOR}",
f"/DHTTPMORPH_VERSION_MINOR={VERSION_MINOR}",
f"/DHTTPMORPH_VERSION_PATCH={VERSION_PATCH}",
]
# BoringSSL and nghttp2 library names on Windows (without .lib extension)
# Links to: ssl.lib, crypto.lib, nghttp2.lib, zlib.lib (or zlibstatic.lib if vendor), brotlidec.lib
# Detect which zlib we're using
vendor_dir = Path("vendor").resolve()
vendor_zlib = vendor_dir / "zlib"
if (vendor_zlib / "build" / "Release" / "zlibstatic.lib").exists():
zlib_lib_name = "zlibstatic"
else:
zlib_lib_name = "zlib"
# brotlidec is needed for TLS certificate decompression
EXT_LIBRARIES = ["ssl", "crypto", "nghttp2", zlib_lib_name, "brotlidec", "brotlicommon"]
EXT_LINK_ARGS = [] # No special linker flags for Windows
else:
# Production optimized build
# Skip architecture-specific flags (-march=native, -mcpu=native) for portable wheels
# These flags optimize for the build machine's CPU but create non-portable binaries
# that fail with "Illegal instruction" on different CPUs
EXT_COMPILE_ARGS = [
# Note: -std flag removed to support mixed C/C++ compilation
"-O3",
"-ffast-math",
"-DHAVE_NGHTTP2",
# Version information from pyproject.toml
f"-DHTTPMORPH_VERSION_MAJOR={VERSION_MAJOR}",
f"-DHTTPMORPH_VERSION_MINOR={VERSION_MINOR}",
f"-DHTTPMORPH_VERSION_PATCH={VERSION_PATCH}",
]
EXT_LINK_ARGS = []
# Unix library names
if IS_MACOS or IS_LINUX:
# Use extra_objects for static linking (vendor .a files)
# This ensures we link against vendor static libs, not system dynamic libs
# Note: liburing will be linked statically via EXTRA_OBJECTS, not via -luring
# brotlidec is needed for compress_certificate extension (TLS cert decompression)
# On macOS, prefer vendor brotli (has correct deployment target for wheels)
vendor_dir = Path("vendor").resolve()
vendor_brotli_dec = vendor_dir / "brotli" / "build" / "libbrotlidec.a"
if IS_MACOS and vendor_brotli_dec.exists():
# Vendor brotli will be linked via EXTRA_OBJECTS
EXT_LIBRARIES = ["z"]
else:
# Use system brotli
EXT_LIBRARIES = ["z", "brotlidec"]
else:
# Other Unix - use library names (will find .a or .so)
EXT_LIBRARIES = ["ssl", "crypto", "nghttp2", "z", "brotlidec"]
# Define C extension modules
# Build library directories list
BORINGSSL_LIB_DIRS = [LIB_PATHS["openssl_lib"]]
# Build include and library directory lists
INCLUDE_DIRS = [
str(INCLUDE_DIR),
str(CORE_DIR),
str(CORE_DIR / "internal"), # Add internal headers directory for modular architecture
str(TLS_DIR),
str(SRC_DIR / "include"), # Add src/include for windows_compat.h
LIB_PATHS["openssl_include"],
LIB_PATHS["nghttp2_include"],
]
# Add liburing include directory if available (Linux only)
if IS_LINUX and HAS_IO_URING and LIB_PATHS.get("liburing_include"):
INCLUDE_DIRS.append(LIB_PATHS["liburing_include"])
LIBRARY_DIRS = BORINGSSL_LIB_DIRS + [LIB_PATHS["nghttp2_lib"]]
# Add brotli include/lib directories (for compress_certificate extension)
if IS_MACOS:
# Prefer vendor brotli (built with correct deployment target for wheels)
vendor_dir = Path("vendor").resolve()
vendor_brotli_include = vendor_dir / "brotli" / "c" / "include"
vendor_brotli_lib = vendor_dir / "brotli" / "build"
if vendor_brotli_include.exists() and (vendor_brotli_lib / "libbrotlidec.a").exists():
print(f"Using vendor brotli from: {vendor_dir / 'brotli'}")
INCLUDE_DIRS.append(str(vendor_brotli_include))
# Library dir not needed - we'll use EXTRA_OBJECTS for static linking
else:
# Fall back to Homebrew paths for brotli (ARM64 and Intel)
# Note: Homebrew brotli may have higher deployment target than wheel
homebrew_prefix = "/opt/homebrew" if os.path.exists("/opt/homebrew") else "/usr/local"
brotli_include = os.path.join(homebrew_prefix, "include")
brotli_lib = os.path.join(homebrew_prefix, "lib")
if os.path.exists(os.path.join(brotli_include, "brotli")):
print(f"Using Homebrew brotli from: {homebrew_prefix}")
INCLUDE_DIRS.append(brotli_include)
LIBRARY_DIRS.append(brotli_lib)
elif IS_LINUX:
# Standard Linux paths
if os.path.exists("/usr/include/brotli"):
pass # Already in default include path
elif os.path.exists("/usr/local/include/brotli"):
INCLUDE_DIRS.append("/usr/local/include")
LIBRARY_DIRS.append("/usr/local/lib")
# Add zlib paths on Windows if available
if IS_WINDOWS and LIB_PATHS.get("zlib_include"):
zlib_inc = LIB_PATHS["zlib_include"]
if isinstance(zlib_inc, list):
INCLUDE_DIRS.extend(zlib_inc)
else:
INCLUDE_DIRS.append(zlib_inc)
LIBRARY_DIRS.append(LIB_PATHS["zlib_lib"])
# Add brotli paths on Windows if available
if IS_WINDOWS and LIB_PATHS.get("brotli_include"):
INCLUDE_DIRS.append(LIB_PATHS["brotli_include"])
LIBRARY_DIRS.append(LIB_PATHS["brotli_lib"])
# On macOS and Linux, explicitly link against vendor static libraries
EXTRA_OBJECTS = []
EXTRA_LINK_ARGS_LIBS = []
if IS_MACOS or IS_LINUX:
vendor_dir = Path("vendor").resolve()
static_libs = []
# BoringSSL static libraries (check both possible locations)
boringssl_build = vendor_dir / "boringssl" / "build"
if (boringssl_build / "ssl" / "libssl.a").exists():
static_libs.append(str(boringssl_build / "ssl" / "libssl.a"))
static_libs.append(str(boringssl_build / "crypto" / "libcrypto.a"))
elif (boringssl_build / "libssl.a").exists():
static_libs.append(str(boringssl_build / "libssl.a"))
static_libs.append(str(boringssl_build / "libcrypto.a"))
# nghttp2 static library
nghttp2_lib = vendor_dir / "nghttp2" / "install" / "lib" / "libnghttp2.a"
if nghttp2_lib.exists():
static_libs.append(str(nghttp2_lib))
# liburing static library (Linux only)
if IS_LINUX and HAS_IO_URING:
# Check multiple possible locations for liburing
uring_paths = [
vendor_dir / "liburing" / "install" / "lib" / "liburing.a",
vendor_dir / "liburing" / "src" / "liburing.a",
]
for uring_path in uring_paths:
if uring_path.exists():
static_libs.append(str(uring_path))
break
# Brotli static libraries (macOS vendor build for correct deployment target)
if IS_MACOS:
brotli_build = vendor_dir / "brotli" / "build"
brotli_dec = brotli_build / "libbrotlidec.a"
brotli_common = brotli_build / "libbrotlicommon.a"
if brotli_dec.exists() and brotli_common.exists():
static_libs.append(str(brotli_dec))
static_libs.append(str(brotli_common))
if static_libs:
print("\nUsing static libraries:")
for lib in static_libs:
print(f" {lib}")
# On Linux, use --no-as-needed to ensure all symbols are included
# and force C++ linkage for BoringSSL (which has C++ code)
if IS_LINUX:
# Add static libs directly to link args with --no-as-needed
# This ensures all symbols from the static libraries are included
EXTRA_LINK_ARGS_LIBS = ["-Wl,--no-as-needed"] + static_libs + ["-lstdc++", "-lpthread"]
else:
# On macOS, use extra_objects
EXTRA_OBJECTS = static_libs
extensions = [
# Main httpmorph C extension
Extension(
"httpmorph._httpmorph",
sources=[
str(BINDINGS_DIR / "_httpmorph.pyx"),
# Modular C source files (refactored from httpmorph.c)
str(CORE_DIR / "util.c"),
str(CORE_DIR / "url.c"),
str(CORE_DIR / "network.c"),
str(CORE_DIR / "proxy.c"),
str(CORE_DIR / "tls.c"),
str(CORE_DIR / "boringssl_wrapper.cc"), # C++ wrapper for BoringSSL C++ functions
str(CORE_DIR / "compression.c"),
str(CORE_DIR / "cookies.c"),
str(CORE_DIR / "request.c"),
str(CORE_DIR / "response.c"),
str(CORE_DIR / "client.c"),
str(CORE_DIR / "session.c"),
str(CORE_DIR / "http1.c"),
str(CORE_DIR / "http2_logic.c"),
str(CORE_DIR / "http2_session_manager.c"),
str(CORE_DIR / "core.c"),
# Supporting modules
str(CORE_DIR / "connection_pool.c"),
str(CORE_DIR / "buffer_pool.c"),
str(CORE_DIR / "request_builder.c"),
str(CORE_DIR / "string_intern.c"),
str(CORE_DIR / "io_engine.c"),
str(CORE_DIR / "iocp_dispatcher.c"), # Windows IOCP dispatcher
str(CORE_DIR / "async_request.c"),
str(CORE_DIR / "async_request_manager.c"),
str(TLS_DIR / "browser_profiles.c"),
],
include_dirs=INCLUDE_DIRS,
library_dirs=LIBRARY_DIRS,
libraries=EXT_LIBRARIES,
extra_compile_args=EXT_COMPILE_ARGS,
extra_link_args=EXT_LINK_ARGS + EXTRA_LINK_ARGS_LIBS,
extra_objects=EXTRA_OBJECTS, # Static libraries on macOS
language="c++" if (IS_WINDOWS or IS_LINUX) else "c", # Use C++ on Windows/Linux for BoringSSL
),
# HTTP/2 client extension
Extension(
"httpmorph._http2",
sources=[
str(BINDINGS_DIR / "_http2.pyx"),
str(CORE_DIR / "http2_client.c"),
],
include_dirs=INCLUDE_DIRS, # Use same include dirs as main extension
library_dirs=LIBRARY_DIRS,
libraries=EXT_LIBRARIES,
extra_compile_args=EXT_COMPILE_ARGS,
extra_link_args=EXT_LINK_ARGS + EXTRA_LINK_ARGS_LIBS,
extra_objects=EXTRA_OBJECTS, # Static libraries on macOS
language="c++" if (IS_WINDOWS or IS_LINUX) else "c", # Use C++ on Windows/Linux for BoringSSL
),
# Async I/O extension (new!)
Extension(
"httpmorph._async",
sources=[
str(BINDINGS_DIR / "_async.pyx"),
# Core async I/O modules (already compiled in main extension, but needed here too)
str(CORE_DIR / "io_engine.c"),
str(CORE_DIR / "iocp_dispatcher.c"), # Windows IOCP dispatcher
str(CORE_DIR / "async_request.c"),
str(CORE_DIR / "async_request_manager.c"),
# Dependencies needed by async modules
str(CORE_DIR / "util.c"),
str(CORE_DIR / "url.c"),
str(CORE_DIR / "network.c"),
str(CORE_DIR / "proxy.c"), # Proxy support for async
str(CORE_DIR / "tls.c"),
str(CORE_DIR / "boringssl_wrapper.cc"), # C++ wrapper for BoringSSL C++ functions
str(CORE_DIR / "request.c"),
str(CORE_DIR / "response.c"),
str(CORE_DIR / "buffer_pool.c"),
str(CORE_DIR / "string_intern.c"),
str(TLS_DIR / "browser_profiles.c"),
],
include_dirs=INCLUDE_DIRS,
library_dirs=LIBRARY_DIRS,
libraries=EXT_LIBRARIES,
extra_compile_args=EXT_COMPILE_ARGS,
extra_link_args=EXT_LINK_ARGS + EXTRA_LINK_ARGS_LIBS,
extra_objects=EXTRA_OBJECTS, # Static libraries on macOS/Linux
language="c++" if (IS_WINDOWS or IS_LINUX) else "c", # Use C++ on Windows/Linux for BoringSSL
),
]
# Cythonize extensions (skip on Read the Docs)
if ON_READTHEDOCS:
# On Read the Docs, skip C extensions (docs don't need them)
print("Skipping C extension build on Read the Docs")
ext_modules = []
else:
ext_modules = cythonize(
extensions,
compiler_directives={
"language_level": "3",
"embedsignature": True,
"boundscheck": False, # Disable bounds checking for speed
"wraparound": False, # Disable negative indexing for speed
"cdivision": True, # Use C division semantics
"initializedcheck": False, # Disable initialization checks for speed
},
annotate=True, # Generate HTML annotation files
)
if __name__ == "__main__":
setup(ext_modules=ext_modules)