Mercurial > hgrepos > Python2 > PyMuPDF
diff mupdf-source/scripts/wrap/__main__.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 aa33339d6b8a |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/mupdf-source/scripts/wrap/__main__.py Mon Sep 15 11:43:07 2025 +0200 @@ -0,0 +1,3060 @@ +#!/usr/bin/env python3 + +r''' +Support for generating C++ and python wrappers for the mupdf API. + +Overview: + + We generate C++, Python and C# wrappers. + + +C++ wrapping: + + Namespaces: + + All generated functions and classes are in the 'mupdf' namespace. + + Wrapper classes: + + For each MuPDF C struct, we provide a wrapper class with a CamelCase + version of the struct name, e.g. the wrapper for fz_display_list is + mupdf::FzDisplayList. + + These wrapper classes generally have a member `m_internal` that is a + pointer to an instance of the underlying struct. + + Member functions: + + Member functions are provided which wrap all relevant MuPDF C + functions (those with first arg being a pointer to an instance of + the C struct). These methods have the same name as the wrapped + function. + + They generally take args that are references to wrapper classes + instead of pointers to MuPDF C structs, and similarly return + wrapper classes by value instead of returning a pointer to a MuPDF + C struct. + + Reference counting: + + Wrapper classes automatically take care of reference counting, so + user code can freely use instances of wrapper classes as required, + for example making copies and allowing instances to go out of + scope. + + Lifetime-related functions - constructors, copy constructors, + operator= and destructors - make internal calls to + `fz_keep_<structname>()` and `fz_drop_<structname>()` as required. + + Raw constructors that take a pointer to an underlying MuPDF struct + do not call `fz_keep_*()` - it is expected that any supplied MuPDF + struct is already owned. Most of the time user code will not need + to use raw constructors directly. + + Debugging reference counting: + + If environmental variable MUPDF_check_refs is "1", we do + runtime checks of the generated code's handling of structs that + have a reference count (i.e. they have a `int refs;` member). + + If the number of wrapper class instances for a particular MuPDF + struct instance is more than the `.ref` value for that struct + instance, we generate a diagnostic and call `abort()`. + + We also output reference-counting diagnostics each time a + wrapper class constructor, member function or destructor is + called. + + POD wrappers: + + For simple POD structs such as `fz_rect` which are not reference + counted, the wrapper class's `m_internal` can be an instance of + the underlying struct instead of a pointer. Some wrappers for POD + structs take this one step further and embed the struct members + directly in the wrapper class. + + Wrapper functions: + + Class-aware wrappers: + + We provide a class-aware wrapper for each MuPDF C function; these + have the same name as the MuPDF C function and are identical to + the corresponding class member function except that they take an + explicit first arg instead of the implicit C++ `this`. + + Low-level wrappers: + + We provide a low-level wrapper for each C MuPDF function; these + have a `ll_` prefix, do not take a 'fz_context* ctx' arg, and + convert any fz_try..fz_catch exceptions into C++ exceptions. + + Most calling code should use class-aware wrapper functions or + wrapper class methods in preference to these low-level wrapper + functions. + + Text representation of POD data: + + For selected POD MuPDF structs, we provide functions that give a + labelled text representation of the data, for example a `fz_rect` will + be represented like: + + (x0=90.51 y0=160.65 x1=501.39 y1=215.6) + + Text representation of a POD wrapper class: + + * An `operator<< (std::ostream&, <wrapperclass>&)` overload for the wrapper class. + * A member function `std::string to_string();` in the wrapper class. + + Text representation of a MuPDF POD C struct: + + * Function `std::string to_string( const <structname>&);`. + * Function `std::string to_string_<structname>( const <structname>&);`. + + Examples: + + MuPDF C API: + + fz_device *fz_begin_page(fz_context *ctx, fz_document_writer *wri, fz_rect mediabox); + + MuPDF C++ API: + + namespace mupdf + { + struct FzDevice + { + ... + fz_device* m_internal; + }; + + struct FzDocumentWriter + { + ... + FzDevice fz_begin_page(FzRect& mediabox); + ... + fz_document_writer* m_internal; + }; + + FzDevice fz_begin_page(const FzDocumentWriter& wri, FzRect& mediabox); + + fz_device *ll_fz_begin_page(fz_document_writer *wri, fz_rect mediabox); + } + + Environmental variables control runtime diagnostics in debug builds of + generated code: + + MUPDF_trace + If "1", generated code outputs a diagnostic each time it calls + a MuPDF function, showing the args. + + MUPDF_trace_director + If "1", generated code outputs a diagnostic when doing special + handling of MuPDF structs containing function pointers. + + MUPDF_trace_exceptions + If "1", generated code outputs diagnostics when we catch a + MuPDF setjmp/longjmp exception and convert it into a C++ + exception. + + MUPDF_check_refs + If "1", generated code checks MuPDF struct reference counts at + runtime. See below for details. + + Details: + + We use clang-python to parse the MuPDF header files, and generate C++ + headers and source code that gives wrappers for all MuPDF functions. + + We also generate C++ classes that wrap all MuPDF structs, adding in + various constructors and methods that wrap auto-detected MuPDF C + functions, plus explicitly-specified methods that wrap/use MuPDF C + functions. + + More specifically, for each wrapper class: + + Copy constructors/operator=: + + If `fz_keep_<name>()` and `fz_drop_<name>()` exist, we generate + copy constructor and `operator=()` that use these functions. + + Constructors: + + We look for all MuPDF functions called `fz_new_*()` or + `pdf_new_*()` that return a pointer to the wrapped class, and + wrap these into constructors. If any of these constructors have + duplicate prototypes, we cannot provide them as constructors so + instead we provide them as static methods. This is not possible + if the class is not copyable, in which case we include the + constructor code but commented-out and with an explanation. + + Methods: + + We look for all MuPDF functions that take the wrapped struct as + a first arg (ignoring any `fz_context*` arg), and wrap these + into auto-generated class methods. If there are duplicate + prototypes, we comment-out all but the first. + + Auto-generated methods are omitted if a custom method is + defined with the same name. + + Other: + + There are various subleties with wrapper classes for MuPDF + structs that are not copyable etc. + + Internal `fz_context*`'s: + + `mupdf::*` functions and methods generally have the same args + as the MuPDF functions that they wrap except that they don't + take any `fz_context*` parameter. When required, per-thread + `fz_context`'s are generated automatically at runtime, using + `platform/c++/implementation/internal.cpp:internal_context_get()`. + + Extra items: + + `mupdf::metadata_keys`: This is a global const vector of + strings contains the keys that are suitable for passing to + `fz_lookup_metadata()` and its wrappers. + + Output parameters: + + We provide two different ways of wrapping functions with + out-params. + + Using SWIG OUTPUT markers: + + First, in generated C++ prototypes, we use `OUTPUT` as + the name of out-params, which tells SWIG to treat them as + out-params. This works for basic out-params such as `int*`, so + SWIG will generate Python code that returns a tuple and C# code + that takes args marked with the C# keyword `out`. + + Unfortunately SWIG doesn't appear to handle out-params that + are zero terminated strings (`char**`) and cannot generically + handle binary data out-params (often indicated with `unsigned + char**`). Also, SWIG-generated C# out-params are a little + inconvenient compared to returning a C# tuple (requires C# 7 or + later). + + So we provide an additional mechanism in the generated C++. + + Out-params in a struct: + + For each function with out-params, we provide a class + containing just the out-params and a function taking just the + non-out-param args, plus a pointer to the class. This function + fills in the members of this class instead of returning + individual out-params. We then generate extra Python or C# code + that uses these special functions to get the out-params in a + class instance and return them as a tuple in both Python and + C#. + + Binary out-param data: + + Some MuPDF functions return binary data, typically with an + `unsigned char**` out-param. It is not possible to generically + handle these in Python or C# because the size of the returned + buffer is specified elsewhere (for example in a different + out-param or in the return value). So we generate custom Python + and C# code to give a convenient interface, e.g. copying the + returned data into a Python `bytes` object or a C# byte array. + + +Python wrapping: + + We generate a Python module called `mupdf` which directly wraps the C++ API, + using identical names for functions, classes and methods. + + Out-parameters: + + Functions and methods that have out-parameters are modified to return + the out-parameters directly, usually as a tuple. + + Examples: + + `fz_read_best()`: + + MuPDF C function: + + `fz_buffer *fz_read_best(fz_context *ctx, fz_stream *stm, size_t initial, int *truncated);` + + Class-aware C++ wrapper: + + `FzBuffer read_best(FzStream& stm, size_t initial, int *truncated);` + + Class-aware python wrapper: + + `def read_best(stm, initial)` + + and returns: `(buffer, truncated)`, where `buffer` is a SWIG + proxy for a `FzBuffer` instance and `truncated` is an integer. + + `pdf_parse_ind_obj()`: + + MuPDF C function: + + `pdf_obj *pdf_parse_ind_obj(fz_context *ctx, pdf_document *doc, fz_stream *f, int *num, int *gen, int64_t *stm_ofs, int *try_repair);` + + Class-aware C++ wrapper: + + `PdfObj pdf_parse_ind_obj(PdfDocument& doc, const FzStream& f, int *num, int *gen, int64_t *stm_ofs, int *try_repair);` + + Class-aware Python wrapper: + + `def pdf_parse_ind_obj(doc, f)` + + and returns: (ret, num, gen, stm_ofs, try_repair) + + Special handing if `fz_buffer` data: + + Generic data access: + + `mupdf.python_buffer_data(b: bytes)`: + Returns SWIG proxy for an `unsigned char*` that points to + `<b>`'s data. + + `mupdf.raw_to_python_bytes(data, size):` + Returns Python `bytes` instance containing copy of data + specified by `data` (a SWIG proxy for a `const unsigned char* + c`) and `size` (the length of the data). + + Wrappers for `fz_buffer_extract()`: + + These return a Python `bytes` instance containing a copy of the + buffer's data and the buffer is left empty. This is equivalent to + the underlying fz_buffer_extract() function, but it involves an + internal copy of the data. + + New function `fz_buffer_extract_copy` and new method + `FzBuffer.buffer_extract_copy()` are like `fz_buffer_extract()` + except that they don't clear the buffer. They have no direct + analogy in the C API. + + Wrappers for `fz_buffer_storage()`: + + These return `(size, data)` where `data` is a low-level + SWIG representation of the buffer's storage. One can call + `mupdf.raw_to_python_bytes(data, size)` to get a Python `bytes` + object containing a copy of this data. + + Wrappers for `fz_new_buffer_from_copied_data()`: + + These take a Python `bytes` instance. + + One can create an MuPDF buffer that contains a copy of a Python + `bytes` by using the special `mupdf.python_buffer_data()` + function. This returns a SWIG proxy for an `unsigned char*` that + points to the `bytes` instance's data: + + ``` + bs = b'qwerty' + buffer_ = mupdf.new_buffer_from_copied_data(mupdf.python_buffer_data(bs), len(bs)) + ``` + + Functions taking a `va_list` arg: + + We do not provide Python wrappers for functions such as `fz_vsnprintf()`. + + Details: + + The Python module is generated using SWIG. + + Out-parameters: + + Out-parameters are not implemented using SWIG typemaps because it's + very difficult to make things work that way. Instead we internally + create a struct containing the out-params together with C and + Python wrapper functions that use the struct to pass the out-params + back from C into Python. + + The Python function ends up returning the out parameters in the + same order as they occur in the original function's args, prefixed + by the original function's return value if it is not void. + + If a function returns void and has exactly one out-param, the + Python wrapper will return the out-param directly, not as part of a + tuple. + + +Tools required to build: + + Clang: + + Clang versions: + + We work with clang-6 or clang-7, but clang-6 appears to not be able + to cope with function args that are themselves function pointers, + so wrappers for MuPDF functions are omitted from the generated C++ + code. + + Unix: + + It seems that clang-python packages such as Debian's python-clang + and OpenBSD's py3-llvm require us to explicitly specify the + location of libclang, so we search in various locations. + + Alternatively on Linux one can (perhaps in a venv) do: + + pip install libclang + + This makes clang available directly as a Python module. + + On Windows, one must install clang-python with: + + pip install libclang + + setuptools: + Used internally. + + SWIG for Python/C# bindings: + + We work with swig-3 and swig-4. If swig-4 is used, we propagate + doxygen-style comments for structures and functions into the generated + C++ code. + + Mono for C# bindings on Unix. + + +Building Python bindings: + + Build and install the MuPDF Python bindings as module `mupdf` in a Python + virtual environment, using MuPDF's `setup.py` script: + + Linux: + > python3 -m venv pylocal + > . pylocal/bin/activate + (pylocal) > pip install pyqt5 libclang + (pylocal) > cd .../mupdf + (pylocal) > python setup.py install + + Windows: + > py -m venv pylocal + > pylocal\Scripts\activate + (pylocal) > pip install libclang pyqt5 + (pylocal) > cd ...\mupdf + (pylocal) > python setup.py install + + OpenBSD: + [It seems that pip can't install pyqt5 or libclang so instead we + install system packages and use --system-site-packages.] + + > sudo pkg_add py3-llvm py3-qt5 + > python3 -m venv --system-site-packages pylocal + > . pylocal/bin/activate + (pylocal) > cd .../mupdf + (pylocal) > python setup.py install + + Use the mupdf module: + (pylocal) > python + >>> import mupdf + >>> + + Build MuPDF Python bindings without a Python virtual environment, using + scripts/mupdfwrap.py: + + [Have not yet found a way to use clang from python on Windows without a + virtual environment, so this is Unix-only.] + + > cd .../mupdf + + Install required packages: + Debian: + > sudo apt install clang python3-clang python3-dev swig + + OpenBSD: + > pkg_add py3-llvm py3-qt5 + + Build and test: + > ./scripts/mupdfwrap.py -d build/shared-release -b all --test-python + + Use the mupdf module by setting PYTHONPATH: + > PYTHONPATH=build/shared-release python3 + >>> import mupdf + >>> + + +Building C# bindings: + + Build MuPDF C# bindings using scripts/mupdfwrap.py: + + > cd .../mupdf + + Install required packages: + Debian: + > sudo apt install clang python3-clang python3-dev mono-devel + + OpenBSD: + > sudo pkg_add py3-llvm py3-qt5 mono + + Build and test: + > ./scripts/mupdfwrap.py -d build/shared-release -b --csharp all --test-csharp + + +Windows builds: + + Required predefined macros: + + Code that will use the MuPDF DLL must be built with FZ_DLL_CLIENT + predefined. + + The MuPDF DLL itself is built with FZ_DLL predefined. + + DLLs: + + There is no separate C library, instead the C and C++ API are + both in mupdfcpp.dll, which is built by running devenv on + platform/win32/mupdf.sln. + + The Python SWIG library is called _mupdf.pyd which, + despite the name, is a standard Windows DLL, built from + platform/python/mupdfcpp_swig.i.cpp. + + DLL export of functions and data: + + On Windows, include/mupdf/fitz/export.h defines FZ_FUNCTION and FZ_DATA + to __declspec(dllexport) and/or __declspec(dllimport) depending on + whether FZ_DLL or FZ_DLL_CLIENT are defined. + + All MuPDF headers prefix declarations of public global data with + FZ_DATA. + + All generated C++ code prefixes functions with FZ_FUNCTION and data + with FZ_DATA. + + When building mupdfcpp.dll on Windows we link with the auto-generated + platform/c++/windows_mupdf.def file; this lists all C public global + data. + + For reasons that i don't yet understand, we don't seem to need to tag + C functions with FZ_FUNCTION, but this is required for C++ functions + otherwise we get unresolved symbols when building MuPDF client code. + + Building the DLLs: + + We build Windows binaries by running devenv.com directly. We search + for this using scripts/wdev.py. + + Building _mupdf.pyd is tricky because it needs to be built with a + specific Python.h and linked with a specific python.lib. This is done + by setting environmental variables MUPDF_PYTHON_INCLUDE_PATH and + MUPDF_PYTHON_LIBRARY_PATH when running devenv.com, which are referenced + by platform/win32/mupdfpyswig.vcxproj. Thus one cannot easily build + _mupdf.pyd directly from the Visual Studio GUI. + + [In the git history there is code that builds _mupdf.pyd by running the + Windows compiler and linker cl.exe and link.exe directly, which avoids + the complications of going via devenv, at the expense of needing to + know where cl.exe and link.exe are.] + +Usage: + + Args: + + -b [<args>] <actions>: + --build [<args>] <actions>: + Builds some or all of the C++ and python interfaces. + + By default we create source files in: + mupdf/platform/c++/ + mupdf/platform/python/ + + - and .so files in directory specified by --dir-so. + + We avoid unnecessary compiling or running of swig by looking at file + mtimes. We also write commands to .cmd files which allows us to force + rebuilds if commands change. + + args: + --clang-verbose + Generate extra diagnostics in action=0 when looking for + libclang.so. + -d <details> + If specified, we show extra diagnostics when wrapping + functions whose name contains <details>. Can be specified + multiple times. + --devenv <path> + Set path of devenv.com script on Windows. If not specified, + we search for a suitable Visual Studio installation. + -f + Force rebuilds. + -j <N> + Set -j arg used when action 'm' calls make (not + Windows). If <N> is 0 we use the number of CPUs + (from Python's multiprocessing.cpu_count()). + --m-target <target> + Comma-separated list of target(s) to be built by action 'm' + (Unix) or action '1' (Windows). + + On Unix, the specified target(s) are used as Make target(s) + instead of implicit `all`. For example `--m-target libs` + can be used to disable the default building of tools. + + On Windows, for each specified target, `/Project <target>` + is appended to the devenv command. So one can use + `--m-target mutool,muraster` to build mutool.exe and + muraster.exe as well as mupdfcpp64.dll. + --m-vars <text> + Text to insert near start of the action 'm' make command, + typically to set MuPDF build flags, for example: + --m-vars 'HAVE_LIBCRYPTO=no' + --regress + Checks for regressions in generated C++ code and SWIG .i + file (actions 0 and 2 below). If a generated file already + exists and its content differs from our generated content, + show diff and exit with an error. This can be used to check + for regressions when modifying this script. + --refcheck-if <text> + Set text used to determine whether to enabling + reference-checking code. For example use `--refcheck-if + '#if 1'` to always enable, `--refcheck-if '#if 0'` to + always disable. Default is '#ifndef NDEBUG'. + --trace-if <text> + Set text used to determine whether to enabling + runtime diagnostics code. For example use `--trace-if + '#if 1'` to always enable, `--refcheck-if '#if 0'` to + always disable. Default is '#ifndef NDEBUG'. + --python + --csharp + Whether to generated bindings for python or C#. Default is + --python. If specified multiple times, the last wins. + + <actions> is list of single-character actions which are processed in + order. If <actions> is 'all', it is replaced by m0123. + + m: + Builds libmupdf.so by running make in the mupdf/ + directory. Default is release build, but this can be changed + using --dir-so. + + 0: + Create C++ source for C++ interface onto the fz_* API. Uses + clang-python to parse the fz_* API. + + Generates various files including: + mupdf/platform/c++/ + implementation/ + classes.cpp + exceptions.cpp + functions.cpp + include/ + classes.h + classes2.h + exceptions.h + functions.h + + If files already contain the generated text, they are not + updated, so that mtimes are unchanged. + + Also removes any other .cpp or .h files from + mupdf/platform/c++/{implementation,include}. + + 1: + Compile and link source files created by action=0. + + Generates: + <dir-so>/libmupdfcpp.so + + This gives a C++ interface onto mupdf. + + 2: + Run SWIG on the C++ source built by action=0 to generate source + for python interface onto the C++ API. + + For example for Python this generates: + + mupdf/platform/python/mupdfcpp_swig.i + mupdf/platform/python/mupdfcpp_swig.i.cpp + mupdf/build/shared-release/mupdf.py + + Note that this requires action=0 to have been run previously. + + 3: + Compile and links the mupdfcpp_swig.i.cpp file created by + action=2. Requires libmupdf.so to be available, e.g. built by + the --libmupdf.so option. + + For example for Python this generates: + + mupdf/build/shared-release/_mupdf.so + + Along with mupdf/platform/python/mupdf.py (generated by + action=2), this implements the mupdf python module. + + .: + Ignores following actions; useful to quickly avoid unnecessary + rebuild if it is known to be unnecessary. + + --check-headers [-k] <which> + Runs cc on header files to check they #include all required headers. + + -k: + If present, we carry on after errors. + which: + If 'all', we run on all headers in .../mupdf/include. Otherwise + if <which> ends with '+', we run on all remaining headers in + .../mupdf/include starting with <which>. Otherwise the name of + header to test. + + --compare-fz_usage <directory> + Finds all fz_*() function calls in git files within <directory>, and + compares with all the fz_*() functions that are wrapped up as class + methods. + + Useful to see what functionality we are missing. + + --diff + Compares generated files with those in the mupdfwrap_ref/ directory + populated by --ref option. + + -d + --dir-so <directory> + Set build directory. + + Default is: build/shared-release + + We use different C++ compile flags depending on release or debug + builds (specifically, the definition of NDEBUG is important because + it must match what was used when libmupdf.so was built). + + If <directory> starts with `build/fpic-`, the C and C++ API are + built as `.a` archives but compiled with -fPIC so that they can be + linked into shared libraries. + + If <directory> is '-' we do not set any paths when running tests + e.g. with --test-python. This is for testing after installing into + a venv. + + Examples: + -d build/shared-debug + -d build/shared-release [default] + + On Windows one can specify the CPU and Python version; we then + use 'py -0f' to find the matching installed Python along with its + Python.h and python.lib. For example: + + -d build/shared-release-x32-py3.8 + -d build/shared-release-x64-py3.9 + + --doc <languages> + Generates documentation for the different APIs in + mupdf/docs/generated/. + + <languages> is either 'all' or a comma-separated list of API languages: + + c + Generate documentation for the C API with doxygen: + include/html/index.html + c++ + Generate documentation for the C++ API with doxygen: + platform/c++/include/html/index.html + python + Generate documentation for the Python API using pydoc3: + platform/python/mupdf.html + + Also see '--sync-docs' option for copying these generated + documentation files elsewhere. + + --make <make-command> + Override make command, e.g. `--make gmake`. + If not specified, we use $MUPDF_MAKE. If this is not set, we use + `make` (or `gmake` on OpenBSD). + + --ref + Copy generated C++ files to mupdfwrap_ref/ directory for use by --diff. + + --run-py <arg> <arg> ... + Runs command with LD_LIBRARY_PATH and PYTHONPATH set up for use with + mupdf.py. + + Exits with same code as the command. + + --swig <swig> + Sets the swig command to use. + + If this is version 4+, we use the <swig> -doxygen to copy + over doxygen-style comments into mupdf.py. Otherwise we use + '%feature("autodoc", "3");' to generate comments with type information + for args in mupdf.py. [These two don't seem to be usable at the same + time in swig-4.] + + --swig-windows-auto + Downloads swig if not present in current directory, extracts + swig.exe and sets things up to use it subsequently. + + --sync-docs <destination> + Use rsync to copy contents of docs/generated/ to remote destination. + + --sync-pretty <destination> + Use rsync to copy generated C++ and Python files to <destination>. Also + uses generates and copies .html versions of these files that use + run_prettify.js from cdn.jsdelivr.net to show embelished content. + + --test-csharp + Tests the experimental C# API. + + --test-python + Tests the python API. + + --test-python-fitz [<options>] all|iter|<script-name> + Tests fitz.py with PyMuPDF. Requires 'pkg_add py3-test' or similar. + options: + Passed to py.test-3. + -x: stop at first error. + -s: show stdout/err. + all: + Runs all tests with py.test-3 + iter: + Runs each test in turn until one fails. + <script-name>: + Runs a single test, e.g.: test_general.py + + --test-setup.py <arg> + Tests that setup.py installs a usable Python mupdf module. + + * Creates a Python virtual environment. + * Activates the Python environment. + * Runs setup.py install. + * Builds C, C++ and Python librariess in build/shared-release. + * Copies build/shared-release/*.so into virtual environment. + * Runs scripts/mupdfwrap_test.py. + * Imports mupdf and checks basic functionality. + * Deactivates the Python environment. + + --venv + If specified, should be the first arg in the command line. + + Re-runs mupdfwrap.py in a Python venv containing libclang + and swig, passing remaining args. + + --vs-upgrade 0 | 1 + If 1, we use a copy of the Windows build file tree + `platform/win32/` called `platform/win32-vs-upgrade`, modifying the + copied files with `devenv.com /upgrade`. + + For example this allows use with Visual Studio 2022 if it doesn't + have the v142 tools installed. + + --windows-cmd ... + Runs mupdfwrap.py via cmd.exe, passing remaining args. Useful to + get from cygwin to native Windows. + + E.g.: + --windows-cmd --venv --swig-windows-auto -b all + + Examples: + + ./scripts/mupdfwrap.py -b all -t + Build all (release build) and test. + + ./scripts/mupdfwrap.py -d build/shared-debug -b all -t + Build all (debug build) and test. + + ./scripts/mupdfwrap.py -b 0 --compare-fz_usage platform/gl + Compare generated class methods with functions called by platform/gl + code. + + python3 -m cProfile -s cumulative ./scripts/mupdfwrap.py --venv -b 0 + Profile generation of C++ source code. + + ./scripts/mupdfwrap.py --venv -b all -t + Build and test on Windows. + + +''' + +import glob +import multiprocessing +import os +import pickle +import platform +import re +import shlex +import shutil +import sys +import sysconfig +import tempfile +import textwrap + +if platform.system() == 'Windows': + ''' + shlex.quote() is broken. + ''' + def quote(text): + if ' ' in text: + if '"' not in text: + return f'"{text}"' + if "'" not in text: + return f"'{text}'" + assert 0, f'Cannot handle quotes in {text=}' + return text + shlex.quote = quote + +try: + import resource +except ModuleNotFoundError: + # Not available on Windows. + resource = None + +import jlib +import pipcl +import wdev + +from . import classes +from . import cpp +from . import csharp +from . import make_cppyy +from . import parse +from . import state +from . import swig + +clang = state.clang + + +# We use f-strings, so need python-3.6+. +assert sys.version_info[0] == 3 and sys.version_info[1] >= 6, ( + 'We require python-3.6+') + + +def compare_fz_usage( + tu, + directory, + fn_usage, + ): + ''' + Looks for fz_ items in git files within <directory> and compares to what + functions we have wrapped in <fn_usage>. + ''' + + filenames = jlib.system( f'cd {directory}; git ls-files .', out='return') + + class FzItem: + def __init__( self, type_, uses_structs=None): + self.type_ = type_ + if self.type_ == 'function': + self.uses_structs = uses_structs + + # Set fz_items to map name to info about function/struct. + # + fz_items = dict() + for cursor in parse.get_members(tu.cursor): + name = cursor.spelling + if not name.startswith( ('fz_', 'pdf_')): + continue + uses_structs = False + if (1 + and name.startswith( ('fz_', 'pdf_')) + and cursor.kind == clang.cindex.CursorKind.FUNCTION_DECL + and ( + cursor.linkage == clang.cindex.LinkageKind.EXTERNAL + or + cursor.is_definition() # Picks up static inline functions. + ) + ): + def uses_struct( type_): + ''' + Returns true if <type_> is a fz struct or pointer to fz struct. + ''' + if type_.kind == clang.cindex.TypeKind.POINTER: + type_ = type_.get_pointee() + type_ = parse.get_name_canonical( type_) + if type_.spelling.startswith( 'struct fz_'): + return True + # Set uses_structs to true if fn returns a fz struct or any + # argument is a fz struct. + if uses_struct( cursor.result_type): + uses_structs = True + else: + for arg in parse.get_args( tu, cursor): + if uses_struct( arg.cursor.type): + uses_structs = True + break + if uses_structs: + pass + #log( 'adding function {name=} {uses_structs=}') + fz_items[ name] = FzItem( 'function', uses_structs) + + directory_names = dict() + for filename in filenames.split( '\n'): + if not filename: + continue + path = os.path.join( directory, filename) + jlib.log( '{filename!r=} {path=}') + with open( path, 'r', encoding='utf-8', errors='replace') as f: + text = f.read() + for m in re.finditer( '(fz_[a-z0-9_]+)', text): + + name = m.group(1) + info = fz_items.get( name) + if info: + if (0 + or (info.type_ == 'function' and info.uses_structs) + or (info.type_ == 'fz-struct') + ): + directory_names.setdefault( name, 0) + directory_names[ name] += 1 + + name_max_len = 0 + for name, n in sorted( directory_names.items()): + name_max_len = max( name_max_len, len( name)) + + n_missing = 0 + fnnames = sorted( fn_usage.keys()) + for fnname in fnnames: + classes_n, cursor = fn_usage[ fnname] + directory_n = directory_names.get( name, 0) + if classes_n==0 and directory_n: + n_missing += 1 + jlib.log( ' {fnname:40} {classes_n=} {directory_n=}') + + jlib.log( '{n_missing}') + + +g_have_done_build_0 = False + + +def _test_get_m_command(): + ''' + Tests _get_m_command(). + ''' + def test( dir_so, expected_command): + build_dirs = state.BuildDirs() + build_dirs.dir_so = dir_so + command, actual_build_dir = _get_m_command( build_dirs) + assert command == expected_command, f'\nExpected: {expected_command}\nBut: {command}' + + mupdf_root = os.path.abspath( f'{__file__}/../../../') + infix = 'CXX=c++ ' if state.state_.openbsd else '' + + test( + 'shared-release', + f'cd {mupdf_root} && {infix}gmake HAVE_GLUT=no HAVE_PTHREAD=yes verbose=yes shared=yes build=release build_prefix=shared-', + ) + test( + 'mupdfpy-amd64-shared-release', + f'cd {mupdf_root} && {infix}gmake HAVE_GLUT=no HAVE_PTHREAD=yes verbose=yes shared=yes build=release build_prefix=mupdfpy-amd64-shared-', + ) + test( + 'mupdfpy-amd64-fpic-release', + f'cd {mupdf_root} && CFLAGS="-fPIC" {infix}gmake HAVE_GLUT=no HAVE_PTHREAD=yes verbose=yes build=release build_prefix=mupdfpy-amd64-fpic-', + ) + jlib.log( '_get_m_command() ok') + + +def get_so_version( build_dirs): + ''' + Returns `.<minor>.<patch>` from include/mupdf/fitz/version.h. + + Returns '' on macos. + ''' + if state.state_.macos or state.state_.pyodide: + return '' + if os.environ.get('USE_SONAME') == 'no': + return '' + d = dict() + def get_v( name): + path = f'{build_dirs.dir_mupdf}/include/mupdf/fitz/version.h' + with open( path) as f: + for line in f: + m = re.match(f'^#define {name} (.+)\n$', line) + if m: + return m.group(1) + assert 0, f'Cannot find #define of {name=} in {path=}.' + major = get_v('FZ_VERSION_MAJOR') + minor = get_v('FZ_VERSION_MINOR') + patch = get_v('FZ_VERSION_PATCH') + return f'.{minor}.{patch}' + + +def _get_m_command( build_dirs, j=None, make=None, m_target=None, m_vars=None): + ''' + Generates a `make` command for building with `build_dirs.dir_mupdf`. + + Returns `(command, actual_build_dir, suffix)`. + ''' + assert not state.state_.windows, 'Cannot do "-b m" on Windows; C library is integrated into C++ library built by "-b 01"' + #jlib.log( '{build_dirs.dir_mupdf=}') + if not make: + make = os.environ.get('MUPDF_MAKE') + if make: + jlib.log('Overriding from $MUPDF_MAKE: {make=}.') + if not make: + if state.state_.openbsd: + # Need to run gmake, not make. Also for some + # reason gmake on OpenBSD sets CC to clang, but + # CXX to g++, so need to force CXX=c++ too. + # + make = 'CXX=c++ gmake' + jlib.log('OpenBSD, using: {make=}.') + if not make: + make = 'make' + + if j is not None: + if j == 0: + j = multiprocessing.cpu_count() + jlib.log('Setting -j to multiprocessing.cpu_count()={j}') + make += f' -j {j}' + flags = os.path.basename( build_dirs.dir_so).split('-') + make_env = '' + make_args = ' HAVE_GLUT=no HAVE_PTHREAD=yes verbose=yes barcode=yes' + if m_vars: + make_args += f' {m_vars}' + suffix = None + for i, flag in enumerate( flags): + if flag in ('x32', 'x64') or re.match('py[0-9]', flag): + # setup.py puts cpu and python version + # elements into the build directory name + # when creating wheels; we need to ignore + # them. + jlib.log('Ignoring {flag=}') + else: + if 0: pass # lgtm [py/unreachable-statement] + elif flag == 'debug': + make_args += ' build=debug' + elif flag == 'release': + make_args += ' build=release' + elif flag == 'memento': + make_args += ' build=memento' + elif flag == 'shared': + make_args += ' shared=yes' + suffix = '.so' + elif flag == 'tesseract': + make_args += ' HAVE_LEPTONICA=yes HAVE_TESSERACT=yes' + elif flag == 'bsymbolic': + make_env += ' XLIB_LDFLAGS=-Wl,-Bsymbolic' + elif flag in ('Py_LIMITED_API', 'PLA'): + pass + elif flag.startswith('Py_LIMITED_API='): # fixme: obsolete. + pass + elif flag.startswith('Py_LIMITED_API_'): + pass + elif flag.startswith('PLA_'): + pass + else: + jlib.log(f'Ignoring unrecognised flag {flag!r} in {flags!r} in {build_dirs.dir_so!r}') + make_args += f' OUT=build/{os.path.basename(build_dirs.dir_so)}' + if m_target: + for t in m_target.split(','): + make_args += f' {t}' + else: + make_args += f' libs libmupdf-threads' + command = f'cd {build_dirs.dir_mupdf} &&' + if make_env: + command += make_env + command += f' {make}{make_args}' + + return command, build_dirs.dir_so, suffix + +_windows_vs_upgrade_cache = dict() +def _windows_vs_upgrade( vs_upgrade, build_dirs, devenv): + ''' + If `vs_upgrade` is true, creates new + {build_dirs.dir_mupdf}/platform/win32-vs-upgrade/ tree with upgraded .sln + and .vcxproj files. Returns 'win32-vs-upgrade'. + + Otherwise returns 'win32'. + ''' + if not vs_upgrade: + return 'win32' + key = (build_dirs, devenv) + infix = _windows_vs_upgrade_cache.get(key) + if infix is None: + infix = 'win32-vs-upgrade' + prefix1 = f'{build_dirs.dir_mupdf}/platform/win32/' + prefix2 = f'{build_dirs.dir_mupdf}/platform/{infix}/' + for dirpath, dirnames, filenames in os.walk( prefix1): + for filename in filenames: + if os.path.splitext( filename)[ 1] in ( + '.sln', + '.vcxproj', + '.props', + '.targets', + '.xml', + '.c', + ): + path1 = f'{dirpath}/{filename}' + assert path1.startswith(prefix1) + path2 = prefix2 + path1[ len(prefix1):] + os.makedirs( os.path.dirname(path2), exist_ok=True) + jlib.log('Calling shutil.copy2 {path1=} {path2=}') + shutil.copy2(path1, path2) + for path in glob.glob( f'{prefix2}*.sln'): + jlib.system(f'"{devenv}" {path} /upgrade', verbose=1) + _windows_vs_upgrade_cache[ key] = infix + jlib.log('returning {infix=}') + return infix + + +def macos_patch( library, *sublibraries): + ''' + Patches `library` so that all references to items in `sublibraries` are + changed to `@rpath/<leafname>`. + + library: + Path of shared library. + sublibraries: + List of paths of shared libraries; these have typically been + specified with `-l` when `library` was created. + ''' + if not state.state_.macos: + return + jlib.log( f'macos_patch(): library={library} sublibraries={sublibraries}') + # Find what shared libraries are used by `library`. + jlib.system( f'otool -L {library}', out='log') + command = 'install_name_tool' + names = [] + for sublibrary in sublibraries: + name = jlib.system( f'otool -D {sublibrary}', out='return').strip() + name = name.split('\n') + assert len(name) == 2 and name[0] == f'{sublibrary}:', f'{name=}' + name = name[1] + # strip trailing so_name. + leaf = os.path.basename(name) + m = re.match('^(.+[.]((so)|(dylib)))[0-9.]*$', leaf) + assert m + jlib.log(f'Changing {leaf=} to {m.group(1)}') + leaf = m.group(1) + command += f' -change {name} @rpath/{leaf}' + command += f' {library}' + jlib.system( command, out='log') + jlib.system( f'otool -L {library}', out='log') + + +def build_0( + build_dirs, + header_git, + check_regress, + clang_info_verbose, + refcheck_if, + trace_if, + cpp_files, + h_files, + ): + ''' + Handles `-b 0` - generate C++ bindings source. + ''' + # Generate C++ code that wraps the fz_* API. + + if state.state_.have_done_build_0: + # This -b 0 stage modifies global data, for example adding + # begin() and end() methods to extras[], so must not be run + # more than once. + jlib.log( 'Skipping second -b 0') + return + + jlib.log( 'Generating C++ source code ...') + + # On 32-bit Windows, libclang doesn't work. So we attempt to run 64-bit `-b + # 0` to generate C++ code. + jlib.log1( '{state.state_.windows=} {build_dirs.cpu.bits=}') + if state.state_.windows and build_dirs.cpu.bits == 32: + try: + jlib.log( 'Windows 32-bit: trying dummy call of clang.cindex.Index.create()') + state.clang.cindex.Index.create() + except Exception as e: + py = f'py -{state.python_version()}' + jlib.log( 'libclang not available on win32; attempting to run separate 64-bit invocation of {sys.argv[0]} with `-b 0`.') + # We use --venv-force-reinstall to workaround a problem where `pip + # install libclang` seems to fail to install in the new 64-bit venv + # if we are in a 'parent' venv created by pip itself. Maybe venv's + # created by pip are somehow more sticky than plain venv's? + # + jlib.system( f'{py} {sys.argv[0]} --venv-force-reinstall -b 0') + return + + namespace = 'mupdf' + generated = cpp.Generated() + + cpp.cpp_source( + build_dirs.dir_mupdf, + namespace, + f'{build_dirs.dir_mupdf}/platform/c++', + header_git, + generated, + check_regress, + clang_info_verbose, + refcheck_if, + trace_if, + 'debug' in build_dirs.dir_so, + ) + + generated.save(f'{build_dirs.dir_mupdf}/platform/c++') + + def check_lists_equal(name, expected, actual): + expected.sort() + actual.sort() + if expected != actual: + text = f'Generated {name} filenames differ from expected:\n' + text += f' expected {len(expected)}:\n' + for i in expected: + text += f' {i}\n' + text += f' generated {len(actual)}:\n' + for i in actual: + text += f' {i}\n' + raise Exception(text) + check_lists_equal('C++ source', cpp_files, generated.cpp_files) + check_lists_equal('C++ headers', h_files, generated.h_files) + + for dir_ in ( + f'{build_dirs.dir_mupdf}/platform/c++/implementation/', + f'{build_dirs.dir_mupdf}/platform/c++/include/', '.h', + ): + for path in jlib.fs_paths( dir_): + path = path.replace('\\', '/') + _, ext = os.path.splitext( path) + if ext not in ('.h', '.cpp'): + continue + if path in h_files + cpp_files: + continue + jlib.log( 'Removing unknown C++ file: {path}') + os.remove( path) + + jlib.log( 'Wrapper classes that are containers: {generated.container_classnames=}') + + # Output info about fz_*() functions that we don't make use + # of in class methods. + # + # This is superseded by automatically finding functions to wrap. + # + if 0: # lgtm [py/unreachable-statement] + jlib.log( 'functions that take struct args and are not used exactly once in methods:') + num = 0 + for name in sorted( fn_usage.keys()): + n, cursor = fn_usage[ name] + if n == 1: + continue + if not fn_has_struct_args( tu, cursor): + continue + jlib.log( ' {n} {cursor.displayname} -> {cursor.result_type.spelling}') + num += 1 + jlib.log( 'number of functions that we should maybe add wrappers for: {num}') + + +def link_l_flags(sos): + ld_origin = None + if state.state_.pyodide: + # Don't add '-Wl,-rpath*' etc if building for Pyodide. + ld_origin = False + ret = jlib.link_l_flags( sos, ld_origin) + r = os.environ.get('LDFLAGS') + if r: + ret += f' {r}' + return ret + + +def build_so_windows( + build_dirs, + path_cpp, + path_so, + path_lib, + *, + defines=(), + includes=(), + libs=(), + libpaths=(), + debug=False, + export=None, + force_rebuild=False, + ): + ''' + Compiles and links <path_cpp> into DLL <path_so> and .lib <path_lib>. + ''' + if isinstance(defines, str): defines = defines, + if isinstance(includes, str): includes = includes, + if isinstance(libs, str): libs = libs, + if isinstance(libpaths, str): libpaths = libpaths, + vs = wdev.WindowsVS() + path_cpp_rel = os.path.relpath(path_cpp) + path_o = f'{path_cpp}.o' + # Compile. + command = textwrap.dedent(f''' + "{vs.vcvars}"&&"{vs.cl}" + /D "UNICODE" + /D "_UNICODE" + /D "_WINDLL" + /EHsc + /Fo"{path_o}" + /GS # Buffer security check. + /O2 + /Tp"{path_cpp_rel}" + /W3 # Warning level, IDE default. + /Zi # Debug Information Format + /bigobj + /c # Compile without linking. + /diagnostics:caret + /nologo + /permissive- + {'' if debug else '/D "NDEBUG"'} + {'/MDd' if debug else '/MD'} # Multithread DLL run-time library + ''') + if sys.maxsize != 2**31 - 1: + command += f' /D "WIN64"\n' + for define in defines: + command += f' /D "{define}"\n' + for include in includes: + command += f' /I"{include}"\n' + infiles = [path_cpp] + list(includes) + jlib.build( + infiles, + path_o, + command, + force_rebuild, + ) + # Link + command = textwrap.dedent(f''' + "{vs.vcvars}"&&"{vs.link}" + /DLL # Builds a DLL. + /IMPLIB:"{path_lib}" # Name of generated .lib. + /OUT:"{path_so}" # Name of generated .dll. + {'/DEBUG' if debug else ''} + {path_o} + ''') + for lib in libs: + command += f' "{lib}"\n' + for libpath in libpaths: + command += f' /LIBPATH:"{libpath}"\n' + if export: + command += f' /EXPORT:{export}' + infiles = [path_o] + list(libs) + jlib.build( + infiles, + path_so, + command, + force_rebuild, + ) + + +def build( build_dirs, swig_command, args, vs_upgrade, make_command): + ''' + Handles -b ... + ''' + cpp_files = [ + f'{build_dirs.dir_mupdf}/platform/c++/implementation/classes.cpp', + f'{build_dirs.dir_mupdf}/platform/c++/implementation/classes2.cpp', + f'{build_dirs.dir_mupdf}/platform/c++/implementation/exceptions.cpp', + f'{build_dirs.dir_mupdf}/platform/c++/implementation/functions.cpp', + f'{build_dirs.dir_mupdf}/platform/c++/implementation/internal.cpp', + f'{build_dirs.dir_mupdf}/platform/c++/implementation/extra.cpp', + ] + h_files = [ + f'{build_dirs.dir_mupdf}/platform/c++/include/mupdf/classes.h', + f'{build_dirs.dir_mupdf}/platform/c++/include/mupdf/classes2.h', + f'{build_dirs.dir_mupdf}/platform/c++/include/mupdf/exceptions.h', + f'{build_dirs.dir_mupdf}/platform/c++/include/mupdf/functions.h', + f'{build_dirs.dir_mupdf}/platform/c++/include/mupdf/internal.h', + f'{build_dirs.dir_mupdf}/platform/c++/include/mupdf/extra.h', + ] + build_python = True + build_csharp = False + check_regress = False + clang_info_verbose = False + force_rebuild = False + header_git = False + m_target = None + m_vars = None + j = 0 + refcheck_if = '#ifndef NDEBUG' + trace_if = '#ifndef NDEBUG' + pyodide = state.state_.pyodide + if pyodide: + # Looks like Pyodide sets CXX to (for example) /tmp/tmp8h1meqsj/c++. We + # don't evaluate it here, because that would force a rebuild each time + # because of the command changing. + assert os.environ.get('CXX', None), 'Pyodide build but $CXX not defined.' + compiler = '$CXX' + elif 'CXX' in os.environ: + compiler = os.environ['CXX'] + jlib.log(f'Setting compiler to {os.environ["CXX"]=}.') + elif state.state_.macos: + compiler = 'c++ -std=c++14' + # Add extra flags for MacOS cross-compilation, where ARCHFLAGS can be + # '-arch arm64'. + # + archflags = os.environ.get( 'ARCHFLAGS') + if archflags: + compiler += f' {archflags}' + else: + compiler = 'c++' + + state.state_.show_details = lambda name: False + devenv = 'devenv.com' + if state.state_.windows: + # Search for devenv.com in standard locations. + windows_vs = wdev.WindowsVS() + devenv = windows_vs.devenv + + #jlib.log('{build_dirs.dir_so=}') + details = list() + + while 1: + try: + actions = args.next() + except StopIteration as e: + raise Exception(f'Expected more `-b ...` args such as --python or <actions>') from e + if 0: + pass + elif actions == '-f': + force_rebuild = True + elif actions == '--clang-verbose': + clang_info_verbose = True + elif actions == '-d': + d = args.next() + details.append( d) + def fn(name): + if not name: + return + for detail in details: + if detail in name: + return True + state.state_.show_details = fn + elif actions == '--devenv': + devenv = args.next() + jlib.log( '{devenv=}') + windows_vs = None + if not state.state_.windows: + jlib.log( 'Warning: --devenv was specified, but we are not on Windows so this will have no effect.') + elif actions == '-j': + j = int(args.next()) + elif actions == '--python': + build_python = True + build_csharp = False + elif actions == '--csharp': + build_python = False + build_csharp = True + elif actions == '--regress': + check_regress = True + elif actions == '--refcheck-if': + refcheck_if = args.next() + jlib.log( 'Have set {refcheck_if=}') + elif actions == '--trace-if': + trace_if = args.next() + jlib.log( 'Have set {trace_if=}') + elif actions == '--m-target': + m_target = args.next() + elif actions == '--m-vars': + m_vars = args.next() + elif actions.startswith( '-'): + raise Exception( f'Unrecognised --build flag: {actions}') + else: + break + + if actions == 'all': + actions = '0123' if state.state_.windows else 'm0123' + + dir_so_flags = os.path.basename( build_dirs.dir_so).split( '-') + cflags = os.environ.get('XCXXFLAGS', '') + + windows_build_type = build_dirs.windows_build_type() + so_version = get_so_version( build_dirs) + + for action in actions: + with jlib.LogPrefixScope( f'{action}: '): + jlib.log( '{action=}', 1) + if action == '.': + jlib.log('Ignoring build actions after "." in {actions!r}') + break + + elif action == 'm': + # Build libmupdf.so. + if state.state_.windows: + jlib.log( 'Ignoring `-b m` on Windows as not required.') + else: + jlib.log( 'Building libmupdf.so ...') + command, actual_build_dir, suffix = _get_m_command( build_dirs, j, make_command, m_target, m_vars) + jlib.system( command, prefix=jlib.log_text(), out='log', verbose=1) + + suffix2 = '.dylib' if state.state_.macos else '.so' + p = f'{actual_build_dir}/libmupdf{suffix2}{so_version}' + assert os.path.isfile(p), f'Does not exist: {p=}' + + if actual_build_dir != build_dirs.dir_so: + # This happens when we are being run by + # setup.py - it it might specify '-d + # build/shared-release-x64-py3.8' (which + # will be put into build_dirs.dir_so) but + # the above 'make' command will create + # build/shared-release/libmupdf.so, so we need + # to copy into build/shared-release-x64-py3.8/. + # + jlib.fs_copy( f'{actual_build_dir}/libmupdf{suffix2}', f'{build_dirs.dir_so}/libmupdf{suffix2}', verbose=1) + + elif action == '0': + build_0( + build_dirs, + header_git, + check_regress, + clang_info_verbose, + refcheck_if, + trace_if, + cpp_files, + h_files, + ) + + elif action == '1': + # Compile and link generated C++ code to create libmupdfcpp.so. + if state.state_.windows: + # We build mupdfcpp.dll using the .sln; it will + # contain all C functions internally - there is + # no mupdf.dll. + # + win32_infix = _windows_vs_upgrade( vs_upgrade, build_dirs, devenv) + jlib.log(f'Building mupdfcpp.dll by running devenv ...') + build = f'{windows_build_type}|{build_dirs.cpu.windows_config}' + command = ( + f'cd {build_dirs.dir_mupdf}&&' + f'"{devenv}"' + f' platform/{win32_infix}/mupdf.sln' + f' /Build "{build}"' + ) + projects = ['mupdfcpp', 'libmuthreads'] + if m_target: + projects += m_target.split(',') + for project in projects: + command2 = f'{command} /Project {project}' + jlib.system(command2, verbose=1, out='log') + + jlib.fs_copy( + f'{build_dirs.dir_mupdf}/platform/{win32_infix}/{build_dirs.cpu.windows_subdir}{windows_build_type}/mupdfcpp{build_dirs.cpu.windows_suffix}.dll', + f'{build_dirs.dir_so}/', + verbose=1, + ) + + else: + jlib.log( 'Compiling generated C++ source code to create libmupdfcpp.so ...') + include1 = f'{build_dirs.dir_mupdf}/include' + include2 = f'{build_dirs.dir_mupdf}/platform/c++/include' + cpp_files_text = '' + for i in cpp_files: + cpp_files_text += ' ' + os.path.relpath(i) + libmupdfcpp = f'{build_dirs.dir_so}/libmupdfcpp.so{so_version}' + libmupdf = f'{build_dirs.dir_so}/libmupdf.so{so_version}' + if pyodide: + # Compile/link separately. Otherwise + # emsdk/upstream/bin/llvm-nm: error: a.out: No such + # file or directory + o_files = list() + for cpp_file in cpp_files: + o_file = f'{os.path.relpath(cpp_file)}.o' + o_files.append(o_file) + command = textwrap.dedent( + f''' + {compiler} + -c + -o {o_file} + {build_dirs.cpp_flags} + -fPIC + {cflags} + -I {include1} + -I {include2} + {cpp_file} + ''') + jlib.build( + [include1, include2, cpp_file], + o_file, + command, + force_rebuild, + ) + command = ( textwrap.dedent( + f''' + {compiler} + -o {os.path.relpath(libmupdfcpp)} + -sSIDE_MODULE + {build_dirs.cpp_flags} + -fPIC -shared + -I {include1} + -I {include2} + {" ".join(o_files)} + {link_l_flags(libmupdf)} + ''') + ) + jlib.build( + [include1, include2] + o_files, + libmupdfcpp, + command, + force_rebuild, + ) + + elif 'shared' in dir_so_flags: + link_soname_arg = '' + if state.state_.linux and so_version: + link_soname_arg = f'-Wl,-soname,{os.path.basename(libmupdfcpp)}' + command = ( textwrap.dedent( + f''' + {compiler} + -o {os.path.relpath(libmupdfcpp)} + {link_soname_arg} + {build_dirs.cpp_flags} + -fPIC -shared + {cflags} + -I {include1} + -I {include2} + {cpp_files_text} + {link_l_flags(libmupdf)} + ''') + ) + command_was_run = jlib.build( + [include1, include2] + cpp_files, + libmupdfcpp, + command, + force_rebuild, + ) + if command_was_run: + macos_patch( libmupdfcpp, f'{build_dirs.dir_so}/libmupdf.dylib{so_version}') + if so_version and state.state_.linux: + jlib.system(f'ln -sf libmupdfcpp.so{so_version} {build_dirs.dir_so}/libmupdfcpp.so') + + elif 'fpic' in dir_so_flags: + # We build a .so containing the C and C++ API. This + # might be slightly faster than having separate C and + # C++ API .so files, but probably makes no difference. + # + libmupdfcpp = f'{build_dirs.dir_so}/libmupdfcpp.a' + libmupdf = []#[ f'{build_dirs.dir_so}/libmupdf.a', f'{build_dirs.dir_so}/libmupdf-third.a'] + + # Compile each .cpp file. + ofiles = [] + for cpp_file in cpp_files: + ofile = f'{build_dirs.dir_so}/{os.path.basename(cpp_file)}.o' + ofiles.append( ofile) + command = ( textwrap.dedent( + f''' + {compiler} + {build_dirs.cpp_flags} + -fPIC + -c + {cflags} + -I {include1} + -I {include2} + -o {ofile} + {cpp_file} + ''') + ) + jlib.build( + [include1, include2, cpp_file], + ofile, + command, + force_rebuild, + verbose=True, + ) + + # Create libmupdfcpp.a containing all .cpp.o files. + if 0: + libmupdfcpp_a = f'{build_dirs.dir_so}/libmupdfcpp.a' + command = f'ar cr {libmupdfcpp_a} {" ".join(ofiles)}' + jlib.build( + ofiles, + libmupdfcpp_a, + command, + force_rebuild, + verbose=True, + ) + + # Create libmupdfcpp.so from all .cpp and .c files. + libmupdfcpp_so = f'{build_dirs.dir_so}/libmupdfcpp.so' + alibs = [ + f'{build_dirs.dir_so}/libmupdf.a', + f'{build_dirs.dir_so}/libmupdf-third.a' + ] + command = textwrap.dedent( f''' + {compiler} + {build_dirs.cpp_flags} + -fPIC -shared + -o {libmupdfcpp_so} + {' '.join(ofiles)} + {' '.join(alibs)} + ''') + jlib.build( + ofiles + alibs, + libmupdfcpp_so, + command, + force_rebuild, + verbose=True, + ) + else: + assert 0, f'Leaf must start with "shared-" or "fpic-": build_dirs.dir_so={build_dirs.dir_so}' + + elif action == '2': + # Use SWIG to generate source code for python/C# bindings. + #generated = cpp.Generated(f'{build_dirs.dir_mupdf}/platform/c++') + with open( f'{build_dirs.dir_mupdf}/platform/c++/generated.pickle', 'rb') as f: + generated = pickle.load( f) + generated.swig_cpp = generated.swig_cpp.getvalue() + generated.swig_cpp_python = generated.swig_cpp_python.getvalue() + generated.swig_python = generated.swig_python.getvalue() + generated.swig_csharp = generated.swig_csharp.getvalue() + + if build_python: + jlib.log( 'Generating mupdf_cppyy.py file.') + make_cppyy.make_cppyy( state.state_, build_dirs, generated) + + jlib.log( 'Generating python module source code using SWIG ...') + with jlib.LogPrefixScope( f'swig Python: '): + # Generate C++ code for python module using SWIG. + swig.build_swig( + state.state_, + build_dirs, + generated, + language='python', + swig_command=swig_command, + check_regress=check_regress, + force_rebuild=force_rebuild, + ) + + if build_csharp: + # Generate C# using SWIG. + jlib.log( 'Generating C# module source code using SWIG ...') + with jlib.LogPrefixScope( f'swig C#: '): + swig.build_swig( + state.state_, + build_dirs, + generated, + language='csharp', + swig_command=swig_command, + check_regress=check_regress, + force_rebuild=force_rebuild, + ) + + elif action == 'j': + # Just experimenting. + build_swig_java() + + + elif action == '3': + # Compile code from action=='2' to create Python/C# shared library. + # + if build_python: + jlib.log( 'Compiling/linking generated Python module source code to create _mupdf.{"pyd" if state.state_.windows else "so"} ...') + if build_csharp: + jlib.log( 'Compiling/linking generated C# source code to create mupdfcsharp.{"dll" if state.state_.windows else "so"} ...') + + dir_so_flags = os.path.basename( build_dirs.dir_so).split( '-') + debug = 'debug' in dir_so_flags + + if state.state_.windows: + if build_python: + wp = wdev.WindowsPython(build_dirs.cpu, build_dirs.python_version) + jlib.log( '{wp=}:') + if 0: + # Show contents of include directory. + for dirpath, dirnames, filenames in os.walk( wp.include): + for f in filenames: + p = os.path.join( dirpath, f) + jlib.log( ' {p!r}') + assert os.path.isfile( os.path.join( wp.include, 'Python.h')) + jlib.log( 'Matching python for {build_dirs.cpu=} {wp.version=}: {wp.path=} {wp.include=} {wp.libs=}') + # The swig-generated .cpp file must exist at + # this point. + # + path_cpp = build_dirs.mupdfcpp_swig_cpp('python') + path_cpp = os.path.relpath(path_cpp) # So we don't expose build machine details in __FILE__. + assert os.path.exists(path_cpp), f'SWIG-generated file does not exist: {path_cpp}' + + if 1: + # Build with direct invocation of cl.exe and link.exe. + pf = pipcl.PythonFlags() + path_o = f'{path_cpp}.o' + mupdfcpp_lib = f'{build_dirs.dir_mupdf}/platform/win32/' + + if build_dirs.cpu.bits == 64: + mupdfcpp_lib += 'x64/' + mupdfcpp_lib += 'Debug/' if debug else 'Release/' + mupdfcpp_lib += 'mupdfcpp64.lib' if build_dirs.cpu.bits == 64 else 'mupdfcpp.lib' + build_so_windows( + build_dirs, + path_cpp = path_cpp, + path_so = f'{build_dirs.dir_so}/_mupdf.pyd', + path_lib = f'{build_dirs.dir_so}/_mupdf.lib', + defines = ( + 'FZ_DLL_CLIENT', + 'SWIG_PYTHON_SILENT_MEMLEAK', + ), + includes = ( + f'{build_dirs.dir_mupdf}/include', + f'{build_dirs.dir_mupdf}/platform/c++/include', + wp.include, + ), + libs = mupdfcpp_lib, + libpaths = wp.libs, + debug = debug, + export = 'PyInit__mupdf', + ) + else: + # Use VS devenv. + env_extra = { + 'MUPDF_PYTHON_INCLUDE_PATH': f'{wp.include}', + 'MUPDF_PYTHON_LIBRARY_PATH': f'{wp.libs}', + } + jlib.log('{env_extra=}') + + + # We need to update mtime of the .cpp file to + # force recompile and link, because we run + # devenv with different environmental variables + # depending on the Python for which we are + # building. + # + # [Using /Rebuild or /Clean appears to clean + # the entire solution even if we specify + # /Project.] + # + jlib.log(f'Touching file in case we are building for a different python version: {path_cpp=}') + os.utime(path_cpp) + + win32_infix = _windows_vs_upgrade( vs_upgrade, build_dirs, devenv) + jlib.log('Building mupdfpyswig project') + command = ( + f'cd {build_dirs.dir_mupdf}&&' + f'"{devenv}"' + f' platform/{win32_infix}/mupdfpyswig.sln' + f' /Build "{windows_build_type}Python|{build_dirs.cpu.windows_config}"' + f' /Project mupdfpyswig' + ) + jlib.system(command, verbose=1, out='log', env_extra=env_extra) + + jlib.fs_copy( + f'{build_dirs.dir_mupdf}/platform/{win32_infix}/{build_dirs.cpu.windows_subdir}{windows_build_type}/mupdfpyswig.dll', + f'{build_dirs.dir_so}/_mupdf.pyd', + verbose=1, + ) + + if build_csharp: + # The swig-generated .cpp file must exist at + # this point. + # + path_cpp = build_dirs.mupdfcpp_swig_cpp('csharp') + path_cpp = os.path.relpath(path_cpp) # So we don't expose build machine details in __FILE__. + assert os.path.exists(path_cpp), f'SWIG-generated file does not exist: {path_cpp}' + + if 1: + path_o = f'{path_cpp}.o' + mupdfcpp_lib = f'{build_dirs.dir_mupdf}/platform/win32/' + if build_dirs.cpu.bits == 64: + mupdfcpp_lib += 'x64/' + mupdfcpp_lib += 'Debug/' if debug else 'Release/' + mupdfcpp_lib += 'mupdfcpp64.lib' if build_dirs.cpu.bits == 64 else 'mupdfcpp.lib' + build_so_windows( + build_dirs, + path_cpp = path_cpp, + path_so = f'{build_dirs.dir_so}/mupdfcsharp.dll', + path_lib = f'{build_dirs.dir_so}/mupdfcsharp.lib', + defines = ( + 'FZ_DLL_CLIENT', + ), + includes = ( + f'{build_dirs.dir_mupdf}/include', + f'{build_dirs.dir_mupdf}/platform/c++/include', + ), + libs = mupdfcpp_lib, + debug = debug, + ) + else: + win32_infix = _windows_vs_upgrade( vs_upgrade, build_dirs, devenv) + jlib.log('Building mupdfcsharp project') + command = ( + f'cd {build_dirs.dir_mupdf}&&' + f'"{devenv}"' + f' platform/{win32_infix}/mupdfcsharpswig.sln' + f' /Build "{windows_build_type}Csharp|{build_dirs.cpu.windows_config}"' + f' /Project mupdfcsharpswig' + ) + jlib.system(command, verbose=1, out='log') + + jlib.fs_copy( + f'{build_dirs.dir_mupdf}/platform/{win32_infix}/{build_dirs.cpu.windows_subdir}{windows_build_type}/mupdfcsharpswig.dll', + f'{build_dirs.dir_so}/mupdfcsharp.dll', + verbose=1, + ) + + else: + # Not Windows. + + # We use c++ debug/release flags as implied by + # --dir-so, but all builds output the same file + # mupdf:platform/python/_mupdf.so. We could instead + # generate mupdf.py and _mupdf.so in the --dir-so + # directory? + # + # [While libmupdfcpp.so requires matching + # debug/release build of libmupdf.so, it looks + # like _mupdf.so does not require a matching + # libmupdfcpp.so and libmupdf.so.] + # + flags_compile = '' + flags_link = '' + if build_python: + # We use python-config which appears to + # work better than pkg-config because + # it copes with multiple installed + # python's, e.g. manylinux_2014's + # /opt/python/cp*-cp*/bin/python*. + # + # But... it seems that we should not + # attempt to specify libpython on the link + # command. The manylinux docker containers + # don't actually contain libpython.so, and + # it seems that this deliberate. And the + # link command runs ok. + # + # todo: maybe instead use sysconfig.get_config_vars() ? + # + python_flags = pipcl.PythonFlags() + flags_compile = python_flags.includes + flags_link = python_flags.ldflags + + if state.state_.macos: + # We need this to avoid numerous errors like: + # + # Undefined symbols for architecture x86_64: + # "_PyArg_UnpackTuple", referenced from: + # _wrap_ll_fz_warn(_object*, _object*) in mupdfcpp_swig-0a6733.o + # _wrap_fz_warn(_object*, _object*) in mupdfcpp_swig-0a6733.o + # ... + flags_link += ' -undefined dynamic_lookup' + + jlib.log('flags_compile={flags_compile!r}') + jlib.log('flags_link={flags_link!r}') + + # These are the input files to our g++ command: + # + include1 = f'{build_dirs.dir_mupdf}/include' + include2 = f'{build_dirs.dir_mupdf}/platform/c++/include' + + if 'shared' in dir_so_flags: + libmupdf = f'{build_dirs.dir_so}/libmupdf.so{so_version}' + libmupdfthird = f'' + libmupdfcpp = f'{build_dirs.dir_so}/libmupdfcpp.so{so_version}' + elif 'fpic' in dir_so_flags: + libmupdf = f'{build_dirs.dir_so}/libmupdf.a' + libmupdfthird = f'{build_dirs.dir_so}/libmupdf-third.a' + libmupdfcpp = f'{build_dirs.dir_so}/libmupdfcpp.a' + else: + assert 0, f'Leaf must start with "shared-" or "fpic-": build_dirs.dir_so={build_dirs.dir_so}' + + if build_python: + cpp_path = build_dirs.mupdfcpp_swig_cpp('python') + out_so = f'{build_dirs.dir_so}/_mupdf.so' + elif build_csharp: + cpp_path = build_dirs.mupdfcpp_swig_cpp('csharp') + out_so = f'{build_dirs.dir_so}/mupdfcsharp.so' # todo: append {so_version} ? + cpp_path = os.path.relpath(cpp_path) # So we don't expose build machine details in __FILE__. + if state.state_.openbsd: + # clang needs around 2G on OpenBSD. + # + soft, hard = resource.getrlimit( resource.RLIMIT_DATA) + required = 3 * 2**30 + if soft < required: + if hard < required: + jlib.log( 'Warning: RLIMIT_DATA {hard=} is less than {required=}.') + soft_new = min(hard, required) + resource.setrlimit( resource.RLIMIT_DATA, (soft_new, hard)) + jlib.log( 'Have changed RLIMIT_DATA from {jlib.number_sep(soft)} to {jlib.number_sep(soft_new)}.') + + # We use link_l_flags() to add -L options to search parent + # directories of each .so that we need, and -l with the .so + # leafname without leading 'lib' or trailing '.so'. This + # ensures that at runtime one can set LD_LIBRARY_PATH to + # parent directories and have everything work. + # + + # Build mupdf2.so + if build_python: + cpp2_path = f'{build_dirs.dir_mupdf}/platform/python/mupdfcpp2_swig.cpp' + out2_so = f'{build_dirs.dir_so}/_mupdf2.so' + if jlib.fs_filesize( cpp2_path): + jlib.log( 'Compiling/linking mupdf2') + command = ( textwrap.dedent( + f''' + {compiler} + -o {os.path.relpath(out2_so)} + {os.path.relpath(cpp2_path)} + {build_dirs.cpp_flags} + -fPIC + --shared + {cflags} + -I {include1} + -I {include2} + {flags_compile} + {flags_link2} + {link_l_flags( [libmupdf, libmupdfcpp])} + -Wno-deprecated-declarations + ''') + ) + infiles = [ + cpp2_path, + include1, + include2, + libmupdf, + libmupdfcpp, + ] + jlib.build( + infiles, + out2_so, + command, + force_rebuild, + ) + else: + jlib.fs_remove( out2_so) + jlib.fs_remove( f'{out2_so}.cmd') + + # Build _mupdf.so. + # + # We define SWIG_PYTHON_SILENT_MEMLEAK to avoid generating + # lots of diagnostics `detected a memory leak of type + # 'mupdf::PdfObj *', no destructor found.` when used with + # mupdfpy. However it's not definitely known that these + # diagnostics are spurious - seems to be to do with two + # separate SWIG Python APIs (mupdf and mupdfpy's `extra` + # module) using the same underlying C library. + # + sos = [] + sos.append( f'{build_dirs.dir_so}/libmupdfcpp.so{so_version}') + if os.path.basename( build_dirs.dir_so).startswith( 'shared-'): + sos.append( f'{build_dirs.dir_so}/libmupdf.so{so_version}') + if pyodide: + # Need to use separate compilation/linking. + o_file = f'{os.path.relpath(cpp_path)}.o' + command = ( textwrap.dedent( + f''' + {compiler} + -c + -o {o_file} + {cpp_path} + {build_dirs.cpp_flags} + -fPIC + {cflags} + -I {include1} + -I {include2} + {flags_compile} + -Wno-deprecated-declarations + -Wno-free-nonheap-object + -DSWIG_PYTHON_SILENT_MEMLEAK + ''') + ) + infiles = [ + cpp_path, + include1, + include2, + ] + jlib.build( + infiles, + o_file, + command, + force_rebuild, + ) + + command = ( textwrap.dedent( + f''' + {compiler} + -o {os.path.relpath(out_so)} + -sSIDE_MODULE + {o_file} + {build_dirs.cpp_flags} + -shared + {flags_link} + {link_l_flags( sos)} + ''') + ) + infiles = [ + o_file, + libmupdf, + ] + infiles += sos + + jlib.build( + infiles, + out_so, + command, + force_rebuild, + ) + else: + # Not Pyodide. + command = ( textwrap.dedent( + f''' + {compiler} + -o {os.path.relpath(out_so)} + {cpp_path} + {build_dirs.cpp_flags} + -fPIC + -shared + {cflags} + -I {include1} + -I {include2} + {flags_compile} + -Wno-deprecated-declarations + -Wno-free-nonheap-object + -DSWIG_PYTHON_SILENT_MEMLEAK + {flags_link} + {link_l_flags( sos)} + ''') + ) + infiles = [ + cpp_path, + include1, + include2, + libmupdf, + ] + infiles += sos + + command_was_run = jlib.build( + infiles, + out_so, + command, + force_rebuild, + ) + if command_was_run: + macos_patch( out_so, + f'{build_dirs.dir_so}/libmupdf.dylib{so_version}', + f'{build_dirs.dir_so}/libmupdfcpp.so{so_version}', + ) + else: + raise Exception( 'unrecognised --build action %r' % action) + + +def python_settings(build_dirs, startdir=None): + # We need to set LD_LIBRARY_PATH and PYTHONPATH so that our + # test .py programme can load mupdf.py and _mupdf.so. + if build_dirs.dir_so is None: + # Use no extra environment and default python, e.g. in venv. + jlib.log('build_dirs.dir_so is None, returning empty extra environment and "python"') + return {}, 'python' + + env_extra = {} + env_extra[ 'PYTHONPATH'] = os.path.relpath(build_dirs.dir_so, startdir) + + command_prefix = '' + if state.state_.windows: + # On Windows, it seems that 'py' runs the default + # python. Also, Windows appears to be able to find + # _mupdf.pyd in same directory as mupdf.py. + # + wp = wdev.WindowsPython(build_dirs.cpu, build_dirs.python_version) + python_path = wp.path.replace('\\', '/') # Allows use on Cygwin. + command_prefix = f'"{python_path}"' + else: + pass + # We build _mupdf.so using `-Wl,-rpath='$ORIGIN,-z,origin` (see + # link_l_flags()) so we don't need to set `LD_LIBRARY_PATH`. + # + # But if we did set `LD_LIBRARY_PATH`, it would be with: + # + # env_extra[ 'LD_LIBRARY_PATH'] = os.path.abspath(build_dirs.dir_so) + # + return env_extra, command_prefix + +def make_docs( build_dirs, languages_original): + + languages = languages_original + if languages == 'all': + languages = 'c,c++,python' + languages = languages.split( ',') + + def do_doxygen( name, outdir, path): + ''' + name: + Doxygen PROJECT_NAME of generated documentation + outdir: + Directory in which we run doxygen, so root of generated + documentation will be in <outdir>/html/index.html + path: + Doxygen INPUT setting; this is the path of the directory which + contains the API to document. If a relative path, it should be + relative to <outdir>. + ''' + # We generate a blank doxygen configuration file, make + # some minimal changes, then run doxygen on the modified + # configuration. + # + assert 'docs/generated/' in outdir + jlib.fs_ensure_empty_dir( outdir) + dname = f'{name}.doxygen' + dname2 = os.path.join( outdir, dname) + jlib.system( f'cd {outdir}; rm -f {dname}0; doxygen -g {dname}0', out='return') + with open( dname2+'0') as f: + dtext = f.read() + dtext, n = re.subn( '\nPROJECT_NAME *=.*\n', f'\nPROJECT_NAME = {name}\n', dtext) + assert n == 1 + dtext, n = re.subn( '\nEXTRACT_ALL *=.*\n', f'\nEXTRACT_ALL = YES\n', dtext) + assert n == 1 + dtext, n = re.subn( '\nINPUT *=.*\n', f'\nINPUT = {path}\n', dtext) + assert n == 1 + dtext, n = re.subn( '\nRECURSIVE *=.*\n', f'\nRECURSIVE = YES\n', dtext) + with open( dname2, 'w') as f: + f.write( dtext) + #jlib.system( f'diff -u {dname2}0 {dname2}', raise_errors=False) + command = f'cd {outdir}; doxygen {dname}' + jlib.system( command, out='return', verbose=1) + jlib.log( 'have created: {outdir}/html/index.html') + + out_dir = f'{build_dirs.dir_mupdf}/docs/generated' + + for language in languages: + + if language == 'c': + do_doxygen( 'mupdf', f'{out_dir}/c', f'{build_dirs.dir_mupdf}/include') + + elif language == 'c++': + do_doxygen( 'mupdfcpp', f'{out_dir}/c++', f'{build_dirs.dir_mupdf}/platform/c++/include') + + elif language == 'python': + ld_library_path = os.path.abspath( f'{build_dirs.dir_so}') + jlib.fs_ensure_empty_dir( f'{out_dir}/python') + pythonpath = os.path.relpath( f'{build_dirs.dir_so}', f'{out_dir}/python') + input_relpath = os.path.relpath( f'{build_dirs.dir_so}/mupdf.py', f'{out_dir}/python') + jlib.system( + f'cd {out_dir}/python && LD_LIBRARY_PATH={ld_library_path} PYTHONPATH={pythonpath} pydoc3 -w {input_relpath}', + out='log', + verbose=True, + ) + path = f'{out_dir}/python/mupdf.html' + assert os.path.isfile( path) + + # Add some styling. + # + with open( path) as f: + text = f.read() + + m1 = re.search( '[<]/head[>][<]body[^>]*[>]\n', text) + m2 = re.search( '[<]/body[>]', text) + assert m1 + assert m2 + #jlib.log( '{=m1.start() m1.end() m2.start() m2.end()}') + + a = text[ : m1.start()] + b = textwrap.dedent(''' + <link href="../../../../../css/default.css" rel="stylesheet" type="text/css" /> + <link href="../../../../../css/language-bindings.css" rel="stylesheet" type="text/css" /> + ''') + c = text[ m1.start() : m1.end()] + d = textwrap.dedent(''' + <main style="display:block;"> + <a class="no-underline" href="../../../index.html"> + <div class="banner" role="heading" aria-level="1"> + <h1>MuPDF Python bindings</h1> + </div> + </a> + <div class="outer"> + <div class="inner"> + ''') + e = text[ m1.end() : m2.end()] + f = textwrap.dedent(''' + </div></div> + </main> + ''') + g = text[ m2.end() : ] + text = a + b + c + d + e + f + g + with open( path, 'w') as f: + f.write( text) + jlib.log( 'have created: {path}') + + else: + raise Exception( f'unrecognised language param: {lang}') + + make_docs_index( build_dirs, languages_original) + + +def make_docs_index( build_dirs, languages_original): + # Create index.html with links to the different bindings' + # documentation. + # + #mupdf_dir = os.path.abspath( f'{__file__}/../../..') + out_dir = f'{build_dirs.dir_mupdf}/docs/generated' + top_index_html = f'{out_dir}/index.html' + with open( top_index_html, 'w') as f: + git_id = jlib.git_get_id( build_dirs.dir_mupdf) + git_id = git_id.split( '\n')[0] + f.write( textwrap.dedent( f''' + <!DOCTYPE html> + + <html lang="en"> + <head> + <link href="../../css/default.css" rel="stylesheet" type="text/css" /> + <link href="../../css/language-bindings.css" rel="stylesheet" type="text/css" /> + </head> + <body> + <main style="display:block;"> + <div class="banner" role="heading" aria-level="1"> + <h1>MuPDF bindings</h1> + </div> + <div class="outer"> + <div class="inner"> + <ul> + <li><a href="c/html/index.html">C</a> (generated by Doxygen). + <li><a href="c++/html/index.html">C++</a> (generated by Doxygen). + <li><a href="python/mupdf.html">Python</a> (generated by Pydoc). + </ul> + <small> + <p>Generation:</p> + <ul> + <li>Date: {jlib.date_time()} + <li>Git: {git_id} + <li>Command: <code>./scripts/mupdfwrap.py --doc {languages_original}</code> + </ul> + </small> + </div> + </div> + </main> + </body> + </html> + ''' + )) + jlib.log( 'Have created: {top_index_html}') + + + +def main2(): + + assert not state.state_.cygwin, \ + f'This script does not run properly under Cygwin, use `py ...`' + + # Set default build directory. Can be overridden by '-d'. + # + build_dirs = state.BuildDirs() + + # Set default swig and make. + # + swig_command = 'swig' + make_command = None + + # Whether to use `devenv.com /upgrade`. + # + vs_upgrade = False + + args = jlib.Args( sys.argv[1:]) + arg_i = 0 + while 1: + try: + arg = args.next() + except StopIteration: + break + #log( 'Handling {arg=}') + + arg_i += 1 + + with jlib.LogPrefixScope( f'{arg}: '): + + if arg == '-h' or arg == '--help': + print( __doc__) + + elif arg == '--build' or arg == '-b': + build( build_dirs, swig_command, args, vs_upgrade, make_command) + + elif arg == '--check-headers': + keep_going = False + path = args.next() + if path == '-k': + keep_going = True + path = args.next() + include_dir = os.path.relpath( f'{build_dirs.dir_mupdf}/include') + def paths(): + if path.endswith( '+'): + active = False + for p in jlib.fs_paths( include_dir): + if not active and p == path[:-1]: + active = True + if not active: + continue + if p.endswith( '.h'): + yield p + elif path == 'all': + for p in jlib.fs_paths( include_dir): + if p.endswith( '.h'): + yield p + else: + yield path + failed_paths = [] + for path in paths(): + if path.endswith( '/mupdf/pdf/name-table.h'): + # Not a normal header. + continue + if path.endswith( '.h'): + e = jlib.system( f'cc -I {include_dir} {path}', out='log', raise_errors=False, verbose=1) + if e: + if keep_going: + failed_paths.append( path) + else: + sys.exit( 1) + if failed_paths: + jlib.log( 'Following headers are not self-contained:') + for path in failed_paths: + jlib.log( f' {path}') + sys.exit( 1) + + elif arg == '--compare-fz_usage': + directory = args.next() + compare_fz_usage( tu, directory, fn_usage) + + elif arg == '--diff': + for path in jlib.fs_paths( build_dirs.ref_dir): + #log( '{path=}') + assert path.startswith( build_dirs.ref_dir) + if not path.endswith( '.h') and not path.endswith( '.cpp'): + continue + tail = path[ len( build_dirs.ref_dir):] + path2 = f'{build_dirs.dir_mupdf}/platform/c++/{tail}' + command = f'diff -u {path} {path2}' + jlib.log( 'running: {command}') + jlib.system( + command, + raise_errors=False, + out='log', + ) + + elif arg == '--diff-all': + for a, b in ( + (f'{build_dirs.dir_mupdf}/platform/c++/', f'{build_dirs.dir_mupdf}/platform/c++/'), + (f'{build_dirs.dir_mupdf}/platform/python/', f'{build_dirs.dir_mupdf}/platform/python/') + ): + for dirpath, dirnames, filenames in os.walk( a): + assert dirpath.startswith( a) + root = dirpath[len(a):] + for filename in filenames: + a_path = os.path.join(dirpath, filename) + b_path = os.path.join( b, root, filename) + command = f'diff -u {a_path} {b_path}' + jlib.system( command, out='log', raise_errors=False) + + elif arg == '--doc': + languages = args.next() + make_docs( build_dirs, languages) + + elif arg == '--doc-index': + languages = args.next() + make_docs_index( build_dirs, languages) + + elif arg == '--make': + make_command = args.next() + + elif arg == '--ref': + assert 'mupdfwrap_ref' in build_dirs.ref_dir + jlib.system( + f'rm -r {build_dirs.ref_dir}', + raise_errors=False, + out='log', + ) + jlib.system( + f'rsync -ai {build_dirs.dir_mupdf}/platform/c++/implementation {build_dirs.ref_dir}', + out='log', + ) + jlib.system( + f'rsync -ai {build_dirs.dir_mupdf}/platform/c++/include {build_dirs.ref_dir}', + out='log', + ) + + elif arg == '--dir-so' or arg == '-d': + d = args.next() + build_dirs.set_dir_so( d) + #jlib.log('Have set {build_dirs=}') + + elif arg == '--py-package-multi': + # Investigating different combinations of pip, pyproject.toml, + # setup.py + # + def system(command): + jlib.system(command, verbose=1, out='log') + system( '(rm -r pylocal-multi dist || true)') + system( './setup.py sdist') + system( 'cp -p pyproject.toml pyproject.toml-0') + results = dict() + try: + for toml in 0, 1: + for pip_upgrade in 0, 1: + for do_wheel in 0, 1: + with jlib.LogPrefixScope(f'toml={toml} pip_upgrade={pip_upgrade} do_wheel={do_wheel}: '): + #print(f'jlib.g_log_prefixes={jlib.g_log_prefixes}') + #print(f'jlib.g_log_prefix_scopes.items={jlib.g_log_prefix_scopes.items}') + #print(f'jlib.log_text(""): {jlib.log_text("")}') + result_key = toml, pip_upgrade, do_wheel + jlib.log( '') + jlib.log( '=== {pip_upgrade=} {do_wheel=}') + if toml: + system( 'cp -p pyproject.toml-0 pyproject.toml') + else: + system( 'rm pyproject.toml || true') + system( 'ls -l pyproject.toml || true') + system( + '(rm -r pylocal-multi wheels || true)' + ' && python3 -m venv pylocal-multi' + ' && . pylocal-multi/bin/activate' + ' && pip install clang' + ) + try: + if pip_upgrade: + system( '. pylocal-multi/bin/activate && pip install --upgrade pip') + if do_wheel: + system( '. pylocal-multi/bin/activate && pip install check-wheel-contents') + system( '. pylocal-multi/bin/activate && pip wheel --wheel-dir wheels dist/*') + system( '. pylocal-multi/bin/activate && check-wheel-contents wheels/*') + system( '. pylocal-multi/bin/activate && pip install wheels/*') + else: + system( '. pylocal-multi/bin/activate && pip install dist/*') + #system( './scripts/mupdfwrap_test.py') + system( '. pylocal-multi/bin/activate && python -m mupdf') + except Exception as ee: + e = ee + else: + e = 0 + results[ result_key] = e + jlib.log( '== {e=}') + jlib.log( '== Results:') + for (toml, pip_upgrade, do_wheel), e in results.items(): + jlib.log( ' {toml=} {pip_upgrade=} {do_wheel=}: {e=}') + finally: + system( 'cp -p pyproject.toml-0 pyproject.toml') + + elif arg == '--run-py': + command = '' + while 1: + try: + command += ' ' + args.next() + except StopIteration: + break + + ld_library_path = os.path.abspath( f'{build_dirs.dir_so}') + pythonpath = build_dirs.dir_so + + envs = f'LD_LIBRARY_PATH={ld_library_path} PYTHONPATH={pythonpath}' + command = f'{envs} {command}' + jlib.log( 'running: {command}') + e = jlib.system( + command, + raise_errors=False, + verbose=False, + out='log', + ) + sys.exit(e) + + elif arg == '--show-ast': + filename = args.next() + includes = args.next() + parse.show_ast( filename, includes) + + elif arg == '--swig': + swig_command = args.next() + + elif arg == '--swig-windows-auto': + if state.state_.windows: + import stat + import urllib.request + import zipfile + name = 'swigwin-4.0.2' + + # Download swig .zip file if not already present. + # + if not os.path.exists(f'{name}.zip'): + url = f'http://prdownloads.sourceforge.net/swig/{name}.zip' + jlib.log(f'Downloading Windows SWIG from: {url}') + with urllib.request.urlopen(url) as response: + with open(f'{name}.zip-', 'wb') as f: + shutil.copyfileobj(response, f) + os.rename(f'{name}.zip-', f'{name}.zip') + + # Extract swig from .zip file if not already extracted. + # + swig_local = f'{name}/swig.exe' + if not os.path.exists(swig_local): + # Extract + z = zipfile.ZipFile(f'{name}.zip') + jlib.fs_ensure_empty_dir(f'{name}-0') + z.extractall(f'{name}-0') + os.rename(f'{name}-0/{name}', name) + os.rmdir(f'{name}-0') + + # Need to make swig.exe executable. + swig_local_stat = os.stat(swig_local) + os.chmod(swig_local, swig_local_stat.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + + # Set our <swig> to be the local windows swig.exe. + # + swig_command = swig_local + else: + jlib.log('Ignoring {arg} because not running on Windows') + + elif arg == '--sync-pretty': + destination = args.next() + jlib.log( 'Syncing to {destination=}') + generated = cpp.Generated(f'{build_dirs.dir_mupdf}/platform/c++') + files = generated.h_files + generated.cpp_files + [ + f'{build_dirs.dir_so}/mupdf.py', + f'{build_dirs.dir_mupdf}/platform/c++/fn_usage.txt', + ] + # Generate .html files with syntax colouring for source files. See: + # https://github.com/google/code-prettify + # + files_html = [] + for i in files: + if os.path.splitext( i)[1] not in ( '.h', '.cpp', '.py'): + continue + o = f'{i}.html' + jlib.log( 'converting {i} to {o}') + with open( i) as f: + text = f.read() + with open( o, 'w') as f: + f.write( '<html><body>\n') + f.write( '<script src="https://cdn.jsdelivr.net/gh/google/code-prettify@master/loader/run_prettify.js"></script>\n') + f.write( '<pre class="prettyprint">\n') + f.write( text) + f.write( '</pre>\n') + f.write( '</body></html>\n') + files_html.append( o) + + files += files_html + + # Insert extra './' into each path so that rsync -R uses the + # 'mupdf/...' tail of each local path for the remote path. + # + for i in range( len( files)): + files[i] = files[i].replace( '/mupdf/', '/./mupdf/') + files[i] = files[i].replace( '/tmp/', '/tmp/./') + + jlib.system( f'rsync -aiRz {" ".join( files)} {destination}', verbose=1, out='log') + + elif arg == '--sync-docs': + # We use extra './' so that -R uses remaining path on + # destination. + # + destination = args.next() + jlib.system( f'rsync -aiRz {build_dirs.dir_mupdf}/docs/generated/./ {destination}', verbose=1, out='log') + + elif arg == '--test-cpp': + testfile = os.path.abspath( f'{__file__}/../../../thirdparty/zlib/zlib.3.pdf') + testfile = testfile.replace('\\', '/') + src = f'{build_dirs.dir_mupdf}/scripts/mupdfwrap_test.cpp' + exe = f'{build_dirs.dir_mupdf}/scripts/mupdfwrap_test.cpp.exe' + includes = ( + f' -I {build_dirs.dir_mupdf}/include' + f' -I {build_dirs.dir_mupdf}/platform/c++/include' + ) + cpp_flags = build_dirs.cpp_flags + if state.state_.windows: + win32_infix = _windows_vs_upgrade( vs_upgrade, build_dirs, devenv=None) + windows_build_type = build_dirs.windows_build_type() + lib = f'{build_dirs.dir_mupdf}/platform/{win32_infix}/{build_dirs.cpu.windows_subdir}{windows_build_type}/mupdfcpp{build_dirs.cpu.windows_suffix}.lib' + vs = wdev.WindowsVS() + command = textwrap.dedent(f''' + "{vs.vcvars}"&&"{vs.cl}" + /Tp{src} + {includes} + -D FZ_DLL_CLIENT + {cpp_flags} + /link + {lib} + /out:{exe} + ''') + jlib.system(command, verbose=1) + path = os.environ.get('PATH') + env_extra = dict(PATH = f'{build_dirs.dir_so}{os.pathsep}{path}' if path else build_dirs.dir_so) + jlib.system(f'{exe} {testfile}', verbose=1, env_extra=env_extra) + else: + dir_so_flags = os.path.basename( build_dirs.dir_so).split( '-') + if 'shared' in dir_so_flags: + libmupdf = f'{build_dirs.dir_so}/libmupdf.so' + libmupdfthird = f'' + libmupdfcpp = f'{build_dirs.dir_so}/libmupdfcpp.so' + elif 'fpic' in dir_so_flags: + libmupdf = f'{build_dirs.dir_so}/libmupdf.a' + libmupdfthird = f'{build_dirs.dir_so}/libmupdf-third.a' + libmupdfcpp = f'{build_dirs.dir_so}/libmupdfcpp.a' + else: + assert 0, f'Leaf must start with "shared-" or "fpic-": build_dirs.dir_so={build_dirs.dir_so}' + command = textwrap.dedent(f''' + c++ + -o {exe} + {cpp_flags} + {includes} + {src} + {link_l_flags( [libmupdf, libmupdfcpp])} + ''') + jlib.system(command, verbose=1) + jlib.system( 'pwd', verbose=1) + if state.state_.macos: + jlib.system( f'DYLD_LIBRARY_PATH={build_dirs.dir_so} {exe}', verbose=1) + else: + jlib.system( f'{exe} {testfile}', verbose=1, env_extra=dict(LD_LIBRARY_PATH=build_dirs.dir_so)) + + elif arg == '--test-internal': + _test_get_m_command() + + elif arg == '--test-internal-cpp': + cpp.test() + + elif arg in ('--test-python', '-t', '--test-python-gui'): + + env_extra, command_prefix = python_settings(build_dirs) + script_py = os.path.relpath( f'{build_dirs.dir_mupdf}/scripts/mupdfwrap_gui.py') + if arg == '--test-python-gui': + #env_extra[ 'MUPDF_trace'] = '1' + #env_extra[ 'MUPDF_check_refs'] = '1' + #env_extra[ 'MUPDF_trace_exceptions'] = '1' + command = f'{command_prefix} {script_py} {build_dirs.dir_mupdf}/thirdparty/zlib/zlib.3.pdf' + jlib.system( command, env_extra=env_extra, out='log', verbose=1) + + else: + jlib.log( 'running scripts/mupdfwrap_test.py ...') + script_py = os.path.relpath( f'{build_dirs.dir_mupdf}/scripts/mupdfwrap_test.py') + command = f'{command_prefix} {script_py}' + with open( f'{build_dirs.dir_mupdf}/platform/python/mupdf_test.py.out.txt', 'w') as f: + jlib.system( command, env_extra=env_extra, out='log', verbose=1) + # Repeat with pdf_reference17.pdf if it exists. + path = os.path.relpath( f'{build_dirs.dir_mupdf}/../pdf_reference17.pdf') + if os.path.exists(path): + jlib.log('Running mupdfwrap_test.py on {path}') + command += f' {path}' + jlib.system( command, env_extra=env_extra, out='log', verbose=1) + + # Run mutool.py. + # + mutool_py = os.path.relpath( f'{__file__}/../../mutool.py') + zlib_pdf = os.path.relpath(f'{build_dirs.dir_mupdf}/thirdparty/zlib/zlib.3.pdf') + for args2 in ( + f'trace {zlib_pdf}', + f'convert -o zlib.3.pdf-%d.png {zlib_pdf}', + f'draw -o zlib.3.pdf-%d.png -s tmf -v -y l -w 150 -R 30 -h 200 {zlib_pdf}', + f'draw -o zlib.png -R 10 {zlib_pdf}', + f'clean -gggg {zlib_pdf} zlib.clean.pdf', + ): + command = f'{command_prefix} {mutool_py} {args2}' + jlib.log( 'running: {command}') + jlib.system( f'{command}', env_extra=env_extra, out='log', verbose=1) + + jlib.log( 'Tests ran ok.') + + elif arg == '--test-csharp': + csc, mono, mupdf_cs = csharp.csharp_settings(build_dirs) + + # Our tests look for zlib.3.pdf in their current directory. + testfile = f'{build_dirs.dir_so}/zlib.3.pdf' if state.state_.windows else 'zlib.3.pdf' + jlib.fs_copy( + f'{build_dirs.dir_mupdf}/thirdparty/zlib/zlib.3.pdf', + testfile + ) + # Create test file whose name contains unicode character, which + # scripts/mupdfwrap_test.cs will attempt to open. + testfile2 = testfile + b'\xf0\x90\x90\xb7'.decode() + '.pdf' + jlib.log(f'{testfile=}') + jlib.log(f'{testfile2=}') + jlib.log(f'{testfile2}') + shutil.copy2(testfile, testfile2) + + if 1: + # Build and run simple test. + out = 'test-csharp.exe' + jlib.build( + (f'{build_dirs.dir_mupdf}/scripts/mupdfwrap_test.cs', mupdf_cs), + out, + f'"{csc}" -out:{{OUT}} {{IN}}', + ) + if state.state_.windows: + out_rel = os.path.relpath( out, build_dirs.dir_so) + jlib.system(f'cd {build_dirs.dir_so} && {mono} {out_rel}', verbose=1) + else: + command = f'LD_LIBRARY_PATH={build_dirs.dir_so} {mono} ./{out}' + if state.state_.openbsd: + e = jlib.system( command, verbose=1, raise_errors=False) + if e == 137: + jlib.log( 'Ignoring {e=} on OpenBSD because this occurs in normal operation.') + elif e: + raise Exception( f'command failed: {command}') + else: + jlib.system(f'LD_LIBRARY_PATH={build_dirs.dir_so} {mono} ./{out}', verbose=1) + if 1: + # Build and run test using minimal swig library to test + # handling of Unicode strings. + swig.test_swig_csharp() + + elif arg == '--test-csharp-gui': + csc, mono, mupdf_cs = csharp.csharp_settings(build_dirs) + + # Build and run gui test. + # + # Don't know why Unix/Windows differ in what -r: args are + # required... + # + # We need -unsafe for copying bitmap data from mupdf. + # + references = '-r:System.Drawing -r:System.Windows.Forms' if state.state_.linux else '' + out = 'mupdfwrap_gui.cs.exe' + jlib.build( + (f'{build_dirs.dir_mupdf}/scripts/mupdfwrap_gui.cs', mupdf_cs), + out, + f'"{csc}" -unsafe {references} -out:{{OUT}} {{IN}}' + ) + if state.state_.windows: + # Don't know how to mimic Unix's LD_LIBRARY_PATH, so for + # now we cd into the directory containing our generated + # libraries. + jlib.fs_copy(f'{build_dirs.dir_mupdf}/thirdparty/zlib/zlib.3.pdf', f'{build_dirs.dir_so}/zlib.3.pdf') + # Note that this doesn't work remotely. + out_rel = os.path.relpath( out, build_dirs.dir_so) + jlib.system(f'cd {build_dirs.dir_so} && {mono} {out_rel}', verbose=1) + else: + jlib.fs_copy(f'{build_dirs.dir_mupdf}/thirdparty/zlib/zlib.3.pdf', f'zlib.3.pdf') + jlib.system(f'LD_LIBRARY_PATH={build_dirs.dir_so} {mono} ./{out}', verbose=1) + + elif arg == '--test-python-fitz': + opts = '' + while 1: + arg = args.next() + if arg.startswith('-'): + opts += f' {arg}' + else: + tests = arg + break + startdir = os.path.abspath('../PyMuPDF/tests') + env_extra, command_prefix = python_settings(build_dirs, startdir) + + env_extra['PYTHONPATH'] += f':{os.path.relpath(".", startdir)}' + env_extra['PYTHONPATH'] += f':{os.path.relpath("./scripts", startdir)}' + + #env_extra['PYTHONMALLOC'] = 'malloc' + #env_extra['MUPDF_trace'] = '1' + #env_extra['MUPDF_check_refs'] = '1' + + # -x: stop at first error. + # -s: show stdout/err. + # + if tests == 'all': + jlib.system( + f'cd ../PyMuPDF/tests && py.test-3 {opts}', + env_extra=env_extra, + out='log', + verbose=1, + ) + elif tests == 'iter': + e = 0 + for script in sorted(glob.glob( '../PyMuPDF/tests/test_*.py')): + script = os.path.basename(script) + ee = jlib.system( + f'cd ../PyMuPDF/tests && py.test-3 {opts} {script}', + env_extra=env_extra, + out='log', + verbose=1, + raise_errors=0, + ) + if not e: + e = ee + elif not os.path.isfile(f'../PyMuPDF/tests/{tests}'): + ts = glob.glob("../PyMuPDF/tests/*.py") + ts = [os.path.basename(t) for t in ts] + raise Exception(f'Unrecognised tests={tests}. Should be "all", "iter" or one of {ts}') + else: + jlib.system( + f'cd ../PyMuPDF/tests && py.test-3 {opts} {tests}', + env_extra=env_extra, + out='log', + verbose=1, + ) + + elif arg == '--test-setup.py': + # We use the '.' command to run pylocal/bin/activate rather than 'source', + # because the latter is not portable, e.g. not supported by ksh. The '.' + # command is posix so should work on all shells. + commands = [ + f'cd {build_dirs.dir_mupdf}', + f'python3 -m venv pylocal', + f'. pylocal/bin/activate', + f'pip install clang', + f'python setup.py {extra} install', + f'python scripts/mupdfwrap_test.py', + f'deactivate', + ] + command = 'true' + for c in commands: + command += f' && echo == running: {c}' + command += f' && {c}' + jlib.system( command, verbose=1, out='log') + + elif arg == '--test-swig': + swig.test_swig() + + elif arg in ('--venv' '--venv-force-reinstall'): + force_reinstall = ' --force-reinstall' if arg == '--venv-force-reinstall' else '' + assert arg_i == 1, f'If specified, {arg} should be the first argument.' + venv = f'venv-mupdfwrap-{state.python_version()}-{state.cpu_name()}' + # Oddly, shlex.quote(sys.executable), which puts the name + # inside single quotes, doesn't work - we get error `The + # filename, directory name, or volume label syntax is + # incorrect.`. + if state.state_.openbsd: + # Need system py3-llvm. + jlib.system(f'"{sys.executable}" -m venv --system-site-packages {venv}', out='log', verbose=1) + else: + jlib.system(f'"{sys.executable}" -m venv {venv}', out='log', verbose=1) + + if state.state_.windows: + command_venv_enter = f'{venv}\\Scripts\\activate.bat' + else: + command_venv_enter = f'. {venv}/bin/activate' + + command = f'{command_venv_enter} && python -m pip install --upgrade pip' + + # Required packages are specified by + # setup.py:get_requires_for_build_wheel(). + mupdf_root = os.path.abspath( f'{__file__}/../../../') + sys.path.insert(0, f'{mupdf_root}') + import setup + del sys.path[0] + packages = setup.get_requires_for_build_wheel() + packages = ' '.join(packages) + command += f' && python -m pip install{force_reinstall} --upgrade {packages}' + jlib.system(command, out='log', verbose=1) + + command = f'{command_venv_enter} && python {shlex.quote(sys.argv[0])}' + while 1: + try: + command += f' {shlex.quote(args.next())}' + except StopIteration: + break + command += f' && deactivate' + jlib.system(command, out='log', verbose=1) + + elif arg == '--vs-upgrade': + vs_upgrade = int(args.next()) + + elif arg == '--windows-cmd': + args_tail = '' + while 1: + try: + args_tail += f' {args.next()}' + except StopIteration: + break + command = f'cmd.exe /c "py {sys.argv[0]} {args_tail}"' + jlib.system(command, out='log', verbose=1) + + else: + raise Exception( f'unrecognised arg: {arg}') + + +def write_classextras(path): + ''' + Dumps classes.classextras to file using json, with crude handling of class + instances. + ''' + import json + with open(path, 'w') as f: + class encoder(json.JSONEncoder): + def default( self, obj): + if type(obj).__name__.startswith(('Extra', 'ClassExtra')): + ret = list() + for i in dir( obj): + if not i.startswith( '_'): + ret.append( getattr( obj, i)) + return ret + if callable(obj): + return obj.__name__ + return json.JSONEncoder.default(self, obj) + json.dump( + classes.classextras, + f, + indent=' ', + sort_keys=1, + cls = encoder, + ) + +def main(): + jlib.force_line_buffering() + try: + main2() + except Exception: + jlib.exception_info() + sys.exit(1) + +if __name__ == '__main__': + main2()
