comparison mupdf-source/scripts/wrap/state.py @ 2:b50eed0cc0ef upstream

ADD: MuPDF v1.26.7: the MuPDF source as downloaded by a default build of PyMuPDF 1.26.4. The directory name has changed: no version number in the expanded directory now.
author Franz Glasner <fzglas.hg@dom66.de>
date Mon, 15 Sep 2025 11:43:07 +0200
parents
children b5f06508363a
comparison
equal deleted inserted replaced
1:1d09e1dec1d9 2:b50eed0cc0ef
1 '''
2 Misc state.
3 '''
4
5 import glob
6 import os
7 import platform
8 import re
9 import sys
10
11 import jlib
12
13 from . import parse
14
15 try:
16 import clang.cindex
17 except Exception as e:
18 jlib.log('Warning: failed to import clang.cindex: {e=}\n'
19 f'We need Clang Python to build MuPDF python.\n'
20 f'Install with `pip install libclang` (typically inside a Python venv),\n'
21 f'or (OpenBSD only) `pkg_add py3-llvm.`\n'
22 )
23 clang = None
24
25 omit_fns = [
26 'fz_open_file_w',
27 'fz_colorspace_name_process_colorants', # Not implemented in mupdf.so?
28 'fz_clone_context_internal', # Not implemented in mupdf?
29 'fz_assert_lock_held', # Is a macro if NDEBUG defined.
30 'fz_assert_lock_not_held', # Is a macro if NDEBUG defined.
31 'fz_lock_debug_lock', # Is a macro if NDEBUG defined.
32 'fz_lock_debug_unlock', # Is a macro if NDEBUG defined.
33 'fz_argv_from_wargv', # Only defined on Windows. Breaks our out-param wrapper code.
34
35 # Only defined on Windows, so breaks building Windows wheels from
36 # sdist, because the C++ source in sdist (usually generated on Unix)
37 # does not contain these functions, but SWIG-generated code will try to
38 # call them.
39 'fz_utf8_from_wchar',
40 'fz_wchar_from_utf8',
41 'fz_fopen_utf8',
42 'fz_remove_utf8',
43 'fz_argv_from_wargv',
44 'fz_free_argv',
45 'fz_stdods',
46 ]
47
48 omit_methods = []
49
50
51 def get_name_canonical( type_):
52 '''
53 Wrap Clang's clang.cindex.Type.get_canonical() to avoid returning anonymous
54 struct that clang spells as 'struct (unnamed at ...)'.
55 '''
56 if type_.spelling in ('size_t', 'int64_t'):
57 #jlib.log( 'Not canonicalising {self.spelling=}')
58 return type_
59 ret = type_.get_canonical()
60 if 'struct (unnamed' in ret.spelling:
61 jlib.log( 'Not canonicalising {type_.spelling=}')
62 ret = type_
63 return ret
64
65
66 class State:
67 def __init__( self):
68 self.os_name = platform.system()
69 self.windows = (self.os_name == 'Windows' or self.os_name.startswith('CYGWIN'))
70 self.cygwin = self.os_name.startswith('CYGWIN')
71 self.openbsd = self.os_name == 'OpenBSD'
72 self.linux = self.os_name == 'Linux'
73 self.macos = self.os_name == 'Darwin'
74 self.pyodide = os.environ.get('OS') == 'pyodide'
75 self.have_done_build_0 = False
76
77 # Maps from <tu> to dict of fnname: cursor.
78 self.functions_cache = dict()
79
80 # Maps from <tu> to dict of dataname: cursor.
81 self.global_data = dict()
82
83 self.enums = dict()
84 self.structs = dict()
85
86 # Code should show extra information if state_.show_details(name)
87 # returns true.
88 #
89 self.show_details = lambda name: False
90
91 def functions_cache_populate( self, tu):
92 if tu in self.functions_cache:
93 return
94 fns = dict()
95 global_data = dict()
96 enums = dict()
97 structs = dict()
98
99 for cursor in parse.get_children(tu.cursor):
100 verbose = state_.show_details( cursor.spelling)
101 if verbose:
102 jlib.log('Looking at {cursor.spelling=} {cursor.kind=} {cursor.location=}')
103 if cursor.kind==clang.cindex.CursorKind.ENUM_DECL:
104 #jlib.log('ENUM_DECL: {cursor.spelling=}')
105 enum_values = list()
106 for cursor2 in cursor.get_children():
107 #jlib.log(' {cursor2.spelling=}')
108 name = cursor2.spelling
109 enum_values.append(name)
110 enums[ get_name_canonical( cursor.type).spelling] = enum_values
111 if cursor.kind==clang.cindex.CursorKind.TYPEDEF_DECL:
112 name = cursor.spelling
113 if name.startswith( ( 'fz_', 'pdf_')):
114 structs[ name] = cursor
115 if cursor.kind == clang.cindex.CursorKind.FUNCTION_DECL:
116 fnname = cursor.spelling
117 if self.show_details( fnname):
118 jlib.log( 'Looking at {fnname=}')
119 if fnname in omit_fns:
120 jlib.log1('{fnname=} is in omit_fns')
121 else:
122 fns[ fnname] = cursor
123 if (cursor.kind == clang.cindex.CursorKind.VAR_DECL
124 and cursor.linkage == clang.cindex.LinkageKind.EXTERNAL
125 ):
126 global_data[ cursor.spelling] = cursor
127
128 self.functions_cache[ tu] = fns
129 self.global_data[ tu] = global_data
130 self.enums[ tu] = enums
131 self.structs[ tu] = structs
132 jlib.log1('Have populated fns and global_data. {len(enums)=} {len(self.structs)} {len(fns)=}')
133
134 def find_functions_starting_with( self, tu, name_prefix, method):
135 '''
136 Yields (name, cursor) for all functions in <tu> whose names start with
137 <name_prefix>.
138
139 method:
140 If true, we omit names that are in omit_methods
141 '''
142 self.functions_cache_populate( tu)
143 fn_to_cursor = self.functions_cache[ tu]
144 for fnname, cursor in fn_to_cursor.items():
145 verbose = state_.show_details( fnname)
146 if method and fnname in omit_methods:
147 if verbose:
148 jlib.log('{fnname=} is in {omit_methods=}')
149 continue
150 if not fnname.startswith( name_prefix):
151 if 0 and verbose:
152 jlib.log('{fnname=} does not start with {name_prefix=}')
153 continue
154 if verbose:
155 jlib.log('{name_prefix=} yielding {fnname=}')
156 yield fnname, cursor
157
158 def find_global_data_starting_with( self, tu, prefix):
159 for name, cursor in self.global_data[tu].items():
160 if name.startswith( prefix):
161 yield name, cursor
162
163 def find_function( self, tu, fnname, method):
164 '''
165 Returns cursor for function called <fnname> in <tu>, or None if not found.
166 '''
167 assert ' ' not in fnname, f'fnname={fnname}'
168 if method and fnname in omit_methods:
169 assert 0, f'method={method} fnname={fnname} omit_methods={omit_methods}'
170 self.functions_cache_populate( tu)
171 return self.functions_cache[ tu].get( fnname)
172
173
174
175 state_ = State()
176
177
178 def abspath(path):
179 '''
180 Like os.path.absath() but converts backslashes to forward slashes; this
181 simplifies things on Windows - allows us to use '/' as directory separator
182 when constructing paths, which is simpler than using os.sep everywhere.
183 '''
184 ret = os.path.abspath(path)
185 ret = ret.replace('\\', '/')
186 return ret
187
188
189 class Cpu:
190 '''
191 For Windows only. Paths and names that depend on cpu.
192
193 Members:
194 .bits
195 .
196 .windows_subdir
197 '' or 'x64/', e.g. platform/win32/x64/Release.
198 .windows_name
199 'x86' or 'x64'.
200 .windows_config
201 'x64' or 'Win32', e.g. /Build Release|x64
202 .windows_suffix
203 '64' or '', e.g. mupdfcpp64.dll
204 '''
205 def __init__(self, name=None):
206 if name is None:
207 name = cpu_name()
208 self.name = name
209 if name == 'x32':
210 self.bits = 32
211 self.windows_subdir = ''
212 self.windows_name = 'x86'
213 self.windows_config = 'Win32'
214 self.windows_suffix = ''
215 elif name == 'x64':
216 self.bits = 64
217 self.windows_subdir = 'x64/'
218 self.windows_name = 'x64'
219 self.windows_config = 'x64'
220 self.windows_suffix = '64'
221 else:
222 assert 0, f'Unrecognised cpu name: {name}'
223
224 def __str__(self):
225 return self.name
226 def __repr__(self):
227 return f'Cpu:{self.name}'
228
229 def python_version():
230 '''
231 Returns two-digit version number of Python as a string, e.g. '3.9'.
232 '''
233 ret = '.'.join(platform.python_version().split('.')[:2])
234 #jlib.log(f'returning ret={ret!r}')
235 return ret
236
237 def cpu_name():
238 '''
239 Returns 'x32' or 'x64' depending on Python build.
240 '''
241 ret = f'x{32 if sys.maxsize == 2**31 - 1 else 64}'
242 #jlib.log(f'returning ret={ret!r}')
243 return ret
244
245 def cmd_run_multiple(commands, prefix=None):
246 '''
247 Windows-only.
248
249 Runs multiple commands joined by &&, using cmd.exe if we are running under
250 Cygwin. We cope with commands that already contain double-quote characters.
251 '''
252 if state_.cygwin:
253 command = 'cmd.exe /V /C @ ' + ' "&&" '.join(commands)
254 else:
255 command = ' && '.join(commands)
256 jlib.system(command, verbose=1, out='log', prefix=prefix)
257
258
259 class BuildDirs:
260 '''
261 Locations of various generated files.
262 '''
263 def __init__( self):
264
265 # Assume we are in mupdf/scripts/.
266 #jlib.log( f'platform.platform(): {platform.platform()}')
267 file_ = abspath( __file__)
268 assert file_.endswith( f'/scripts/wrap/state.py'), \
269 'Unexpected __file__=%s file_=%s' % (__file__, file_)
270 dir_mupdf = abspath( f'{file_}/../../../')
271 assert not dir_mupdf.endswith( '/')
272
273 # Directories used with --build.
274 self.dir_mupdf = dir_mupdf
275
276 # Directory used with --ref.
277 self.ref_dir = abspath( f'{self.dir_mupdf}/mupdfwrap_ref')
278 assert not self.ref_dir.endswith( '/')
279
280 self.set_dir_so( f'{self.dir_mupdf}/build/shared-release')
281
282 def set_dir_so( self, dir_so):
283 '''
284 Sets self.dir_so and also updates self.cpp_flags etc. Special case
285 `dir_so='-'` sets to None.
286 '''
287 if dir_so == '-':
288 self.dir_so = None
289 self.cpp_flags = None
290 return
291
292 dir_so = abspath( dir_so)
293 self.dir_so = dir_so
294
295 if state_.windows:
296 # debug builds have:
297 # /Od
298 # /D _DEBUG
299 # /RTC1
300 # /MDd
301 #
302 if 0: pass # lgtm [py/unreachable-statement]
303 elif '-release' in dir_so:
304 self.cpp_flags = '/O2 /DNDEBUG'
305 elif '-debug' in dir_so:
306 # `/MDd` forces use of debug runtime and (i think via
307 # it setting `/D _DEBUG`) debug versions of things like
308 # `std::string` (incompatible with release builds). We also set
309 # `/Od` (no optimisation) and `/RTC1` (extra runtime checks)
310 # because these seem to be conventionally set in VS.
311 #
312 self.cpp_flags = '/MDd /Od /RTC1'
313 elif '-memento' in dir_so:
314 self.cpp_flags = '/MDd /Od /RTC1 /DMEMENTO'
315 else:
316 self.cpp_flags = None
317 jlib.log( 'Warning: unrecognised {dir_so=}, so cannot determine cpp_flags')
318 else:
319 if 0: pass # lgtm [py/unreachable-statement]
320 elif '-debug' in dir_so: self.cpp_flags = '-g'
321 elif '-release' in dir_so: self.cpp_flags = '-O2 -DNDEBUG'
322 elif '-memento' in dir_so: self.cpp_flags = '-g -DMEMENTO'
323 else:
324 self.cpp_flags = None
325 jlib.log( 'Warning: unrecognised {dir_so=}, so cannot determine cpp_flags')
326
327 # Set self.cpu and self.python_version.
328 if state_.windows:
329 # Infer cpu and python version from self.dir_so. And append current
330 # cpu and python version if not already present.
331 m = re.search( '-(x[0-9]+)-py([0-9.]+)$', self.dir_so)
332 if not m:
333 suffix = f'-{Cpu(cpu_name())}-py{python_version()}'
334 jlib.log('Adding suffix to {self.dir_so=}: {suffix!r}')
335 self.dir_so += suffix
336 m = re.search( '-(x[0-9]+)-py([0-9.]+)$', self.dir_so)
337 assert m
338 #log(f'self.dir_so={self.dir_so} {os.path.basename(self.dir_so)} m={m}')
339 assert m, f'Failed to parse dir_so={self.dir_so!r} - should be *-x32|x64-pyA.B'
340 self.cpu = Cpu( m.group(1))
341 self.python_version = m.group(2)
342 #jlib.log('{self.cpu=} {self.python_version=} {dir_so=}')
343 else:
344 # Use Python we are running under.
345 self.cpu = Cpu(cpu_name())
346 self.python_version = python_version()
347
348 # Set Py_LIMITED_API if it occurs in dir_so.
349 self.Py_LIMITED_API = None
350 flags = os.path.basename(self.dir_so).split('-')
351 for flag in flags:
352 if flag in ('Py_LIMITED_API', 'PLA'):
353 self.Py_LIMITED_API = '0x03080000'
354 elif flag.startswith('Py_LIMITED_API='): # 2024-11-15: fixme: obsolete
355 self.Py_LIMITED_API = flag[len('Py_LIMITED_API='):]
356 elif flag.startswith('Py_LIMITED_API_'):
357 self.Py_LIMITED_API = flag[len('Py_LIMITED_API_'):]
358 elif flag.startswith('PLA_'):
359 self.Py_LIMITED_API = flag[len('PLA_'):]
360 jlib.log(f'{self.Py_LIMITED_API=}')
361
362 # Set swig .i and .cpp paths, including Py_LIMITED_API so that
363 # different values of Py_LIMITED_API can be tested without rebuilding
364 # unnecessarily.
365 Py_LIMITED_API_infix = f'-Py_LIMITED_API_{self.Py_LIMITED_API}' if self.Py_LIMITED_API else ''
366 self.mupdfcpp_swig_i = lambda language: f'{self.dir_mupdf}/platform/{language}/mupdfcpp_swig{Py_LIMITED_API_infix}.i'
367 self.mupdfcpp_swig_cpp = lambda language: self.mupdfcpp_swig_i(language) + '.cpp'
368
369 def windows_build_type(self):
370 '''
371 Returns `Release` or `Debug`.
372 '''
373 dir_so_flags = os.path.basename( self.dir_so).split( '-')
374 if 'debug' in dir_so_flags:
375 return 'Debug'
376 elif 'release' in dir_so_flags:
377 return 'Release'
378 else:
379 assert 0, f'Expecting "-release-" or "-debug-" in build_dirs.dir_so={self.dir_so}'