Mercurial > hgrepos > Python2 > PyMuPDF
view mupdf-source/scripts/wrap/__main__.py @ 6:b5f06508363a
PyMuPDF builds on FreeBSD now with "gmake -f Makefile.freebsd".
A virtual environment with requirements from "requirements-build.txt" is required.
| author | Franz Glasner <fzglas.hg@dom66.de> |
|---|---|
| date | Mon, 15 Sep 2025 16:16:51 +0200 |
| parents | b50eed0cc0ef |
| children | 59f1bd90b2a0 |
line wrap: on
line source
#!/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 or state.state_.freebsd): 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()
