state.py 14 KB

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