-
Notifications
You must be signed in to change notification settings - Fork 9
Expand file tree
/
Copy pathbuild_lmdb.py
More file actions
278 lines (233 loc) · 9.96 KB
/
Copy pathbuild_lmdb.py
File metadata and controls
278 lines (233 loc) · 9.96 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
#!/usr/bin/env python
"""
Build script for vendored LMDB with CFFI.
This script:
1. Copies LMDB C sources from lmdb-upstream/ submodule to build directory
2. Applies patches from lmdb-patches/
3. Generates zlmdb/lmdb/_config.py for CFFI compilation
4. Configures CFFI to build the LMDB extension
Based on py-lmdb setup.py patch application logic.
"""
import os
import sys
import shutil
import subprocess
import platform
def apply_patches():
"""Copy LMDB sources and apply patches."""
print("=" * 70)
print("Building vendored LMDB with CFFI")
print("=" * 70)
# Source and destination paths
lmdb_src = os.path.join('lmdb-upstream', 'libraries', 'liblmdb')
build_dir = os.path.join('build', 'lmdb-src')
patches_dir = 'lmdb-patches'
if not os.path.exists(lmdb_src):
print(f"ERROR: LMDB source not found at {lmdb_src}")
print("Did you initialize git submodules?")
print("Run: git submodule update --init --recursive")
sys.exit(1)
# Create build directory
os.makedirs('build', exist_ok=True)
# Clean old build directory
if os.path.exists(build_dir):
print(f"Removing old build directory: {build_dir}")
shutil.rmtree(build_dir)
# Copy LMDB sources to build directory
print(f"Copying LMDB sources from {lmdb_src} to {build_dir}")
shutil.copytree(lmdb_src, build_dir)
# Apply patches
#
# Canonical hardening patch set, vendored from py-lmdb (jnwatson/py-lmdb,
# lib/py-lmdb/*.patch) and authored against upstream LMDB 0.9.35. Order is
# significant: each patch's line numbers reflect the state after all
# preceding patches have been applied (mirrors py-lmdb setup.py).
#
# Besides env-copy-txn, this set adds the CVE-2019-1622x fixes and the
# additional validate-*/guard-* corruption-rejection hardening that
# upstream LMDB does not carry. See zlmdb issue #111 / #113.
#
# Note: the previous its-10346.patch was dropped — that fix is now
# incorporated upstream in LMDB 0.9.35.
patch_files = [
'env-copy-txn.patch',
'cursor-next-prev-uninitialized.patch',
'cve-2019-16224-validate-db-flags.patch',
'cve-2019-16225-reject-dirty-pages.patch',
'cve-2019-16226-validate-node-del-size.patch',
'cve-2019-16227-guard-xcursor-null.patch',
'cve-2019-16228-validate-psize.patch',
'validate-page-bounds.patch',
'validate-node-read-size.patch',
'validate-subpage-bounds.patch',
'validate-xcursor-nodedsz.patch',
'validate-leaf2-keysize.patch',
'guard-xcursor-null-d3d4.patch',
'validate-nodedsz-page-split.patch',
'validate-node-shrink-delta.patch',
'validate-overflow-pages.patch',
'validate-nodedsz-cursor-put.patch',
'validate-md-depth.patch',
'validate-md-root.patch',
'win32-sparse-file.patch',
]
for patch_file in patch_files:
patch_path = os.path.join(patches_dir, patch_file)
if not os.path.exists(patch_path):
print(f"WARNING: Patch file not found: {patch_path}")
continue
print(f"Applying patch: {patch_file}")
# The patches are from py-lmdb which has structure:
# libraries/liblmdb/...
# We need to apply with -p3 to strip those path components
if sys.platform.startswith('win'):
try:
import patch_ng as patch
# Use absolute paths for patch-ng
abs_patch_path = os.path.abspath(patch_path)
abs_build_dir = os.path.abspath(build_dir)
# Debug info
print(f" Patch file: {abs_patch_path}")
print(f" Build directory: {abs_build_dir}")
print(f" Build directory exists: {os.path.exists(abs_build_dir)}")
if os.path.exists(abs_build_dir):
print(f" Files in build dir: {os.listdir(abs_build_dir)[:5]}")
patchset = patch.fromfile(abs_patch_path)
if not patchset:
print(f"ERROR: Failed to parse patch file {patch_file}")
sys.exit(1)
print(f" Parsed {len(patchset.items)} patch items")
# Debug: show what we're trying to patch
for item in patchset.items:
target = item.target if isinstance(item.target, str) else item.target.decode('utf-8', errors='replace')
print(f" Patch item target (raw): {target}")
# List files in build directory to verify they exist
print(f" Files in {abs_build_dir}:")
for f in os.listdir(abs_build_dir)[:10]:
print(f" - {f}")
# Apply patch - try strip=3 first, fallback to auto-detect
# The patches have paths like: a/libraries/liblmdb/lmdb.h
# We need to strip 3 levels: a/, libraries/, liblmdb/
# to get: lmdb.h (which is directly in build/lmdb-src/)
print(" Attempting to apply with strip=3...")
success = False
# Try strip levels 0-4 until one works
for strip_level in [3, 2, 4, 1, 0]:
print(f" Trying strip={strip_level}...")
try:
# patch-ng.PatchSet.apply(strip=N, root=path)
result = patchset.apply(strip_level, abs_build_dir)
if result:
print(f" [OK] Successfully applied with strip={strip_level}")
success = True
break
else:
print(f" [FAIL] strip={strip_level} did not work")
except Exception as e:
print(f" [ERROR] strip={strip_level} raised: {e}")
continue
if not success:
# None of the strip levels worked
print(f" [FAIL] Failed to apply patch {patch_file} with any strip level")
print(f"ERROR: Failed to apply patch {patch_file}")
sys.exit(1)
except ImportError:
print("ERROR: patch-ng module required on Windows")
print("Install with: pip install patch-ng")
sys.exit(1)
except Exception as e:
print(f"ERROR: Exception while applying patch {patch_file}: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
else:
# Unix: use patch command
cmd = ['patch', '-N', '-p3', '-d', build_dir, '-i',
os.path.abspath(patch_path)]
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode != 0:
# patch returns non-zero if already applied, check output
if 'Reversed (or previously applied) patch detected' in result.stdout:
print(" (already applied)")
else:
print(f"ERROR: Failed to apply patch {patch_file}")
print(result.stdout)
print(result.stderr)
sys.exit(1)
else:
print(" [OK] Applied successfully")
return build_dir
def generate_config(build_dir):
"""Generate src/zlmdb/_lmdb_vendor/_config.py with build configuration."""
print("\nGenerating src/zlmdb/_lmdb_vendor/_config.py")
# Source files to compile
extra_sources = [
os.path.join(build_dir, 'mdb.c'),
os.path.join(build_dir, 'midl.c'),
]
# Include directories
extra_include_dirs = [
build_dir,
'lmdb-patches', # for preload.h
]
# Compile args
extra_compile_args = [
'-UNDEBUG', # Disable NDEBUG
'-DHAVE_PATCHED_LMDB=1', # Mark as patched version
]
# Disable warnings unless maintainer mode
if not os.getenv('LMDB_MAINTAINER'):
extra_compile_args.append('-w')
# Windows-specific configuration
libraries = []
if sys.platform.startswith('win'):
# Add Windows-specific includes for MSVC compatibility
extra_include_dirs.append('lmdb-patches')
libraries.append('Advapi32')
# MSVC version detection
p = sys.version.find('MSC v.')
msvc_ver = int(sys.version[p + 6: p + 10]) if p != -1 else None
if msvc_ver and not msvc_ver >= 1600:
# Visual Studio <= 2010 needs stdint.h emulation
print(" (adding stdint.h emulation for old MSVC)")
# Write configuration
config = {
'extra_compile_args': extra_compile_args,
'extra_sources': extra_sources,
'extra_library_dirs': [],
'extra_include_dirs': extra_include_dirs,
'libraries': libraries,
}
config_file = os.path.join('src', 'zlmdb', '_lmdb_vendor', '_config.py')
print(f" Writing to {config_file}")
with open(config_file, 'w') as f:
f.write('# Auto-generated by build_lmdb.py\n')
f.write('# DO NOT EDIT - regenerated on each build\n\n')
f.write('CONFIG = {\n')
for key, value in config.items():
f.write(f' {key!r}: {value!r},\n')
f.write('}\n')
print(" [OK] Configuration written")
return config
def main():
"""Main build entry point."""
print("\n" + "=" * 70)
print("zlmdb LMDB build script")
print("=" * 70)
print(f"Python: {sys.version}")
print(f"Platform: {platform.platform()}")
print(f"Working directory: {os.getcwd()}")
print()
# Step 1: Apply patches
build_dir = apply_patches()
# Step 2: Generate config
config = generate_config(build_dir)
print("\n" + "=" * 70)
print("LMDB build preparation complete")
print("=" * 70)
print("\nCFFI will now compile the patched LMDB sources.")
print("This happens automatically during 'pip install' or 'python -m build'")
print()
return 0
if __name__ == '__main__':
sys.exit(main())