File: state.py

package info (click to toggle)
mupdf 1.27.0%2Bds1-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 29,224 kB
  • sloc: ansic: 335,320; python: 20,906; java: 7,520; javascript: 2,213; makefile: 1,152; xml: 675; cpp: 639; sh: 513; cs: 307; awk: 10; sed: 7; lisp: 3
file content (387 lines) | stat: -rw-r--r-- 14,053 bytes parent folder | download
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
'''
Misc state.
'''

import glob
import os
import platform
import re
import sys

import jlib

from . import parse

try:
    import clang.cindex
except Exception as e:
    if '--venv' not in sys.argv:
        jlib.log('Warning: failed to import clang.cindex: {e=}\n'
                f'We need Clang Python to build MuPDF python.\n'
                f'Install with `pip install libclang` (typically inside a Python venv),\n'
                f'or (OpenBSD only) `pkg_add py3-llvm.`\n'
                )
    clang = None

omit_fns = [
        'fz_open_file_w',
        'fz_colorspace_name_process_colorants', # Not implemented in mupdf.so?
        'fz_clone_context_internal',            # Not implemented in mupdf?
        'fz_assert_lock_held',      # Is a macro if NDEBUG defined.
        'fz_assert_lock_not_held',  # Is a macro if NDEBUG defined.
        'fz_lock_debug_lock',       # Is a macro if NDEBUG defined.
        'fz_lock_debug_unlock',     # Is a macro if NDEBUG defined.
        'fz_argv_from_wargv',       # Only defined on Windows. Breaks our out-param wrapper code.

        # Only defined on Windows, so breaks building Windows wheels from
        # sdist, because the C++ source in sdist (usually generated on Unix)
        # does not contain these functions, but SWIG-generated code will try to
        # call them.
        'fz_utf8_from_wchar',
        'fz_wchar_from_utf8',
        'fz_fopen_utf8',
        'fz_remove_utf8',
        'fz_argv_from_wargv',
        'fz_free_argv',
        'fz_stdods',
        ]

omit_methods = []


def get_name_canonical( type_):
    '''
    Wrap Clang's clang.cindex.Type.get_canonical() to avoid returning anonymous
    struct that clang spells as 'struct (unnamed at ...)'.
    '''
    if type_.spelling in ('size_t', 'int64_t'):
        #jlib.log( 'Not canonicalising {self.spelling=}')
        return type_
    ret = type_.get_canonical()
    if 'struct (unnamed' in ret.spelling:
        jlib.log( 'Not canonicalising {type_.spelling=}')
        ret = type_
    return ret


class State:
    def __init__( self):
        self.os_name = platform.system()
        self.windows = (self.os_name == 'Windows' or self.os_name.startswith('CYGWIN'))
        self.cygwin = self.os_name.startswith('CYGWIN')
        self.openbsd = self.os_name == 'OpenBSD'
        self.linux = self.os_name == 'Linux'
        self.macos = self.os_name == 'Darwin'
        self.pyodide = os.environ.get('OS') == 'pyodide'
        self.have_done_build_0 = False

        # Maps from <tu> to dict of fnname: cursor.
        self.functions_cache = dict()

        # Maps from <tu> to dict of dataname: cursor.
        self.global_data = dict()

        self.enums = dict()
        self.structs = dict()

        # Code should show extra information if state_.show_details(name)
        # returns true.
        #
        self.show_details = lambda name: False

    def functions_cache_populate( self, tu):
        if tu in self.functions_cache:
            return
        fns = dict()
        global_data = dict()
        enums = dict()
        structs = dict()

        for cursor in parse.get_children(tu.cursor):
            verbose = state_.show_details( cursor.spelling)
            if verbose:
                jlib.log('Looking at {cursor.spelling=} {cursor.kind=} {cursor.location=}')
            if cursor.kind==clang.cindex.CursorKind.ENUM_DECL:
                #jlib.log('ENUM_DECL: {cursor.spelling=}')
                enum_values = list()
                for cursor2 in cursor.get_children():
                    #jlib.log('    {cursor2.spelling=}')
                    name = cursor2.spelling
                    enum_values.append(name)
                enums[ get_name_canonical( cursor.type).spelling] = enum_values
            if cursor.kind==clang.cindex.CursorKind.TYPEDEF_DECL:
                name = cursor.spelling
                if name.startswith( ( 'fz_', 'pdf_')):
                    structs[ name] = cursor
            if cursor.kind == clang.cindex.CursorKind.FUNCTION_DECL:
                fnname = cursor.spelling
                if self.show_details( fnname):
                    jlib.log( 'Looking at {fnname=}')
                if fnname in omit_fns:
                    jlib.log1('{fnname=} is in omit_fns')
                else:
                    fns[ fnname] = cursor
            if (cursor.kind == clang.cindex.CursorKind.VAR_DECL
                    and cursor.linkage == clang.cindex.LinkageKind.EXTERNAL
                    ):
                global_data[ cursor.spelling] = cursor

        self.functions_cache[ tu] = fns
        self.global_data[ tu] = global_data
        self.enums[ tu] = enums
        self.structs[ tu] = structs
        jlib.log1('Have populated fns and global_data. {len(enums)=} {len(self.structs)} {len(fns)=}')

    def find_functions_starting_with( self, tu, name_prefix, method):
        '''
        Yields (name, cursor) for all functions in <tu> whose names start with
        <name_prefix>.

        method:
            If true, we omit names that are in omit_methods
        '''
        self.functions_cache_populate( tu)
        fn_to_cursor = self.functions_cache[ tu]
        for fnname, cursor in fn_to_cursor.items():
            verbose = state_.show_details( fnname)
            if method and fnname in omit_methods:
                if verbose:
                    jlib.log('{fnname=} is in {omit_methods=}')
                continue
            if not fnname.startswith( name_prefix):
                if 0 and verbose:
                    jlib.log('{fnname=} does not start with {name_prefix=}')
                continue
            if verbose:
                jlib.log('{name_prefix=} yielding {fnname=}')
            yield fnname, cursor

    def find_global_data_starting_with( self, tu, prefix):
        for name, cursor in self.global_data[tu].items():
            if name.startswith( prefix):
                yield name, cursor

    def find_function( self, tu, fnname, method):
        '''
        Returns cursor for function called <fnname> in <tu>, or None if not found.
        '''
        assert ' ' not in fnname, f'fnname={fnname}'
        if method and fnname in omit_methods:
            assert 0, f'method={method} fnname={fnname} omit_methods={omit_methods}'
        self.functions_cache_populate( tu)
        return self.functions_cache[ tu].get( fnname)



state_ = State()


def abspath(path):
    '''
    Like os.path.absath() but converts backslashes to forward slashes; this
    simplifies things on Windows - allows us to use '/' as directory separator
    when constructing paths, which is simpler than using os.sep everywhere.
    '''
    ret = os.path.abspath(path)
    ret = ret.replace('\\', '/')
    return ret


class Cpu:
    '''
    For Windows only. Paths and names that depend on cpu.

    Members:
        .bits
            .
        .name:
            'x32' or 'x64'.
        .windows_subdir
            '' or 'x64/', e.g. platform/win32/x64/Release.
        .windows_name
            'x86' or 'x64'.
        .windows_config
            'x64' or 'Win32', e.g. /Build Release|x64
        .windows_suffix
            '64' or '', e.g. mupdfcpp64.dll
    '''
    def __init__(self, name=None):
        if name is None:
            name = cpu_name()
        self.name = name
        if name == 'x32':
            self.bits = 32
            self.windows_subdir = ''
            self.windows_name = 'x86'
            self.windows_config = 'Win32'
            self.windows_suffix = ''
        elif name == 'x64':
            self.bits = 64
            self.windows_subdir = 'x64/'
            self.windows_name = 'x64'
            self.windows_config = 'x64'
            self.windows_suffix = '64'
        else:
            assert 0, f'Unrecognised cpu name: {name}'

    def __str__(self):
        return self.name
    def __repr__(self):
        return f'Cpu:{self.name}'

def python_version():
    '''
    Returns two-digit version number of Python as a string, e.g. '3.9'.
    '''
    ret = '.'.join(platform.python_version().split('.')[:2])
    #jlib.log(f'returning ret={ret!r}')
    return ret

def cpu_name():
    '''
    Returns 'x32' or 'x64' depending on Python build.
    '''
    ret = f'x{32 if sys.maxsize == 2**31 - 1 else 64}'
    #jlib.log(f'returning ret={ret!r}')
    return ret

def cmd_run_multiple(commands, prefix=None):
    '''
    Windows-only.

    Runs multiple commands joined by &&, using cmd.exe if we are running under
    Cygwin. We cope with commands that already contain double-quote characters.
    '''
    if state_.cygwin:
        command = 'cmd.exe /V /C @ ' + ' "&&" '.join(commands)
    else:
        command = ' && '.join(commands)
    jlib.system(command, verbose=1, out='log', prefix=prefix)


class BuildDirs:
    '''
    Locations of various generated files.
    '''
    def __init__( self):

        # Assume we are in mupdf/scripts/.
        #jlib.log( f'platform.platform(): {platform.platform()}')
        file_ = abspath( __file__)
        assert file_.endswith( f'/scripts/wrap/state.py'), \
                'Unexpected __file__=%s file_=%s' % (__file__, file_)
        dir_mupdf = abspath( f'{file_}/../../../')
        assert not dir_mupdf.endswith( '/')

        # Directories used with --build.
        self.dir_mupdf = dir_mupdf

        # Directory used with --ref.
        self.ref_dir = abspath( f'{self.dir_mupdf}/mupdfwrap_ref')
        assert not self.ref_dir.endswith( '/')

        self.set_dir_so( f'{self.dir_mupdf}/build/shared-release')

    def set_dir_so( self, dir_so):
        '''
        Sets self.dir_so and also updates self.cpp_flags etc. Special case
        `dir_so='-'` sets to None.
        '''
        if dir_so == '-':
            self.dir_so = None
            self.cpp_flags = None
            return

        dir_so = abspath( dir_so)
        self.dir_so = dir_so

        if state_.windows:
            # debug builds have:
            # /Od
            # /D _DEBUG
            # /RTC1
            # /MDd
            #
            if 0: pass  # lgtm [py/unreachable-statement]
            elif '-release' in dir_so:
                self.cpp_flags = '/O2 /DNDEBUG'
            elif '-debug' in dir_so:
                # `/MDd` forces use of debug runtime and (i think via
                # it setting `/D _DEBUG`) debug versions of things like
                # `std::string` (incompatible with release builds). We also set
                # `/Od` (no optimisation) and `/RTC1` (extra runtime checks)
                # because these seem to be conventionally set in VS.
                #
                self.cpp_flags = '/MDd /Od /RTC1 /D _DEBUG'
            elif '-memento' in dir_so:
                self.cpp_flags = '/MDd /Od /RTC1 /D _DEBUG /DMEMENTO'
            else:
                self.cpp_flags = None
                jlib.log( 'Warning: unrecognised {dir_so=}, so cannot determine cpp_flags')
        else:
            if 0: pass  # lgtm [py/unreachable-statement]
            elif '-debug' in dir_so:    self.cpp_flags = '-g'
            elif '-release' in dir_so:  self.cpp_flags = '-O2 -DNDEBUG'
            elif '-memento' in dir_so:  self.cpp_flags = '-g -DMEMENTO'
            else:
                self.cpp_flags = None
                jlib.log( 'Warning: unrecognised {dir_so=}, so cannot determine cpp_flags')

        # Set self.cpu and self.python_version.
        if state_.windows:
            # Infer cpu and python version from self.dir_so. And append current
            # cpu and python version if not already present.
            flags = self.dir_so.split('-')
            self.cpu = None
            self.python_version = None
            for flag in flags:
                if flag in ('x32', 'x64'):
                    self.cpu = Cpu(flag)
                if flag.startswith('py'):
                    self.python_version = flag[2:]
            if not self.cpu:
                self.cpu = Cpu(cpu_name())
                self.dir_so += f'-{self.cpu.name}'
            if not self.python_version:
                self.python_version = python_version()
                self.dir_so += f'-py{self.python_version}'
            #jlib.log('{self.cpu=} {self.python_version=} {dir_so=}')
        else:
            # Use Python we are running under.
            self.cpu = Cpu(cpu_name())
            self.python_version = python_version()

        # Set Py_LIMITED_API if it occurs in dir_so.
        self.Py_LIMITED_API = None
        flags = os.path.basename(self.dir_so).split('-')
        for flag in flags:
            if flag in ('Py_LIMITED_API', 'PLA'):
                self.Py_LIMITED_API = '0x03080000'
            elif flag.startswith('Py_LIMITED_API='):    # 2024-11-15: fixme: obsolete
                self.Py_LIMITED_API = flag[len('Py_LIMITED_API='):]
            elif flag.startswith('Py_LIMITED_API_'):
                self.Py_LIMITED_API = flag[len('Py_LIMITED_API_'):]
            elif flag.startswith('PLA_'):
                self.Py_LIMITED_API = flag[len('PLA_'):]
        jlib.log(f'{self.Py_LIMITED_API=}')

        # Set swig .i and .cpp paths, including Py_LIMITED_API so that
        # different values of Py_LIMITED_API can be tested without rebuilding
        # unnecessarily.
        Py_LIMITED_API_infix = f'-Py_LIMITED_API_{self.Py_LIMITED_API}' if self.Py_LIMITED_API else ''
        self.mupdfcpp_swig_i    = lambda language: f'{self.dir_mupdf}/platform/{language}/mupdfcpp_swig{Py_LIMITED_API_infix}.i'
        self.mupdfcpp_swig_cpp  = lambda language: self.mupdfcpp_swig_i(language) + '.cpp'

    def windows_build_type(self):
        '''
        Returns `Release` or `Debug`.
        '''
        dir_so_flags = os.path.basename( self.dir_so).split( '-')
        if 'debug' in dir_so_flags:
            return 'Debug'
        elif 'release' in dir_so_flags:
            return 'Release'
        elif 'memento' in dir_so_flags:
            return 'Memento'
        else:
            assert 0, f'Expecting "-release-" or "-debug-" in build_dirs.dir_so={self.dir_so}'