Mercurial > hgrepos > Python2 > PyMuPDF
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}' |
