Mercurial > hgrepos > Python2 > PyMuPDF
comparison 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 |
comparison
equal
deleted
inserted
replaced
| 1:1d09e1dec1d9 | 2:b50eed0cc0ef |
|---|---|
| 1 #!/usr/bin/env python3 | |
| 2 | |
| 3 r''' | |
| 4 Support for generating C++ and python wrappers for the mupdf API. | |
| 5 | |
| 6 Overview: | |
| 7 | |
| 8 We generate C++, Python and C# wrappers. | |
| 9 | |
| 10 | |
| 11 C++ wrapping: | |
| 12 | |
| 13 Namespaces: | |
| 14 | |
| 15 All generated functions and classes are in the 'mupdf' namespace. | |
| 16 | |
| 17 Wrapper classes: | |
| 18 | |
| 19 For each MuPDF C struct, we provide a wrapper class with a CamelCase | |
| 20 version of the struct name, e.g. the wrapper for fz_display_list is | |
| 21 mupdf::FzDisplayList. | |
| 22 | |
| 23 These wrapper classes generally have a member `m_internal` that is a | |
| 24 pointer to an instance of the underlying struct. | |
| 25 | |
| 26 Member functions: | |
| 27 | |
| 28 Member functions are provided which wrap all relevant MuPDF C | |
| 29 functions (those with first arg being a pointer to an instance of | |
| 30 the C struct). These methods have the same name as the wrapped | |
| 31 function. | |
| 32 | |
| 33 They generally take args that are references to wrapper classes | |
| 34 instead of pointers to MuPDF C structs, and similarly return | |
| 35 wrapper classes by value instead of returning a pointer to a MuPDF | |
| 36 C struct. | |
| 37 | |
| 38 Reference counting: | |
| 39 | |
| 40 Wrapper classes automatically take care of reference counting, so | |
| 41 user code can freely use instances of wrapper classes as required, | |
| 42 for example making copies and allowing instances to go out of | |
| 43 scope. | |
| 44 | |
| 45 Lifetime-related functions - constructors, copy constructors, | |
| 46 operator= and destructors - make internal calls to | |
| 47 `fz_keep_<structname>()` and `fz_drop_<structname>()` as required. | |
| 48 | |
| 49 Raw constructors that take a pointer to an underlying MuPDF struct | |
| 50 do not call `fz_keep_*()` - it is expected that any supplied MuPDF | |
| 51 struct is already owned. Most of the time user code will not need | |
| 52 to use raw constructors directly. | |
| 53 | |
| 54 Debugging reference counting: | |
| 55 | |
| 56 If environmental variable MUPDF_check_refs is "1", we do | |
| 57 runtime checks of the generated code's handling of structs that | |
| 58 have a reference count (i.e. they have a `int refs;` member). | |
| 59 | |
| 60 If the number of wrapper class instances for a particular MuPDF | |
| 61 struct instance is more than the `.ref` value for that struct | |
| 62 instance, we generate a diagnostic and call `abort()`. | |
| 63 | |
| 64 We also output reference-counting diagnostics each time a | |
| 65 wrapper class constructor, member function or destructor is | |
| 66 called. | |
| 67 | |
| 68 POD wrappers: | |
| 69 | |
| 70 For simple POD structs such as `fz_rect` which are not reference | |
| 71 counted, the wrapper class's `m_internal` can be an instance of | |
| 72 the underlying struct instead of a pointer. Some wrappers for POD | |
| 73 structs take this one step further and embed the struct members | |
| 74 directly in the wrapper class. | |
| 75 | |
| 76 Wrapper functions: | |
| 77 | |
| 78 Class-aware wrappers: | |
| 79 | |
| 80 We provide a class-aware wrapper for each MuPDF C function; these | |
| 81 have the same name as the MuPDF C function and are identical to | |
| 82 the corresponding class member function except that they take an | |
| 83 explicit first arg instead of the implicit C++ `this`. | |
| 84 | |
| 85 Low-level wrappers: | |
| 86 | |
| 87 We provide a low-level wrapper for each C MuPDF function; these | |
| 88 have a `ll_` prefix, do not take a 'fz_context* ctx' arg, and | |
| 89 convert any fz_try..fz_catch exceptions into C++ exceptions. | |
| 90 | |
| 91 Most calling code should use class-aware wrapper functions or | |
| 92 wrapper class methods in preference to these low-level wrapper | |
| 93 functions. | |
| 94 | |
| 95 Text representation of POD data: | |
| 96 | |
| 97 For selected POD MuPDF structs, we provide functions that give a | |
| 98 labelled text representation of the data, for example a `fz_rect` will | |
| 99 be represented like: | |
| 100 | |
| 101 (x0=90.51 y0=160.65 x1=501.39 y1=215.6) | |
| 102 | |
| 103 Text representation of a POD wrapper class: | |
| 104 | |
| 105 * An `operator<< (std::ostream&, <wrapperclass>&)` overload for the wrapper class. | |
| 106 * A member function `std::string to_string();` in the wrapper class. | |
| 107 | |
| 108 Text representation of a MuPDF POD C struct: | |
| 109 | |
| 110 * Function `std::string to_string( const <structname>&);`. | |
| 111 * Function `std::string to_string_<structname>( const <structname>&);`. | |
| 112 | |
| 113 Examples: | |
| 114 | |
| 115 MuPDF C API: | |
| 116 | |
| 117 fz_device *fz_begin_page(fz_context *ctx, fz_document_writer *wri, fz_rect mediabox); | |
| 118 | |
| 119 MuPDF C++ API: | |
| 120 | |
| 121 namespace mupdf | |
| 122 { | |
| 123 struct FzDevice | |
| 124 { | |
| 125 ... | |
| 126 fz_device* m_internal; | |
| 127 }; | |
| 128 | |
| 129 struct FzDocumentWriter | |
| 130 { | |
| 131 ... | |
| 132 FzDevice fz_begin_page(FzRect& mediabox); | |
| 133 ... | |
| 134 fz_document_writer* m_internal; | |
| 135 }; | |
| 136 | |
| 137 FzDevice fz_begin_page(const FzDocumentWriter& wri, FzRect& mediabox); | |
| 138 | |
| 139 fz_device *ll_fz_begin_page(fz_document_writer *wri, fz_rect mediabox); | |
| 140 } | |
| 141 | |
| 142 Environmental variables control runtime diagnostics in debug builds of | |
| 143 generated code: | |
| 144 | |
| 145 MUPDF_trace | |
| 146 If "1", generated code outputs a diagnostic each time it calls | |
| 147 a MuPDF function, showing the args. | |
| 148 | |
| 149 MUPDF_trace_director | |
| 150 If "1", generated code outputs a diagnostic when doing special | |
| 151 handling of MuPDF structs containing function pointers. | |
| 152 | |
| 153 MUPDF_trace_exceptions | |
| 154 If "1", generated code outputs diagnostics when we catch a | |
| 155 MuPDF setjmp/longjmp exception and convert it into a C++ | |
| 156 exception. | |
| 157 | |
| 158 MUPDF_check_refs | |
| 159 If "1", generated code checks MuPDF struct reference counts at | |
| 160 runtime. See below for details. | |
| 161 | |
| 162 Details: | |
| 163 | |
| 164 We use clang-python to parse the MuPDF header files, and generate C++ | |
| 165 headers and source code that gives wrappers for all MuPDF functions. | |
| 166 | |
| 167 We also generate C++ classes that wrap all MuPDF structs, adding in | |
| 168 various constructors and methods that wrap auto-detected MuPDF C | |
| 169 functions, plus explicitly-specified methods that wrap/use MuPDF C | |
| 170 functions. | |
| 171 | |
| 172 More specifically, for each wrapper class: | |
| 173 | |
| 174 Copy constructors/operator=: | |
| 175 | |
| 176 If `fz_keep_<name>()` and `fz_drop_<name>()` exist, we generate | |
| 177 copy constructor and `operator=()` that use these functions. | |
| 178 | |
| 179 Constructors: | |
| 180 | |
| 181 We look for all MuPDF functions called `fz_new_*()` or | |
| 182 `pdf_new_*()` that return a pointer to the wrapped class, and | |
| 183 wrap these into constructors. If any of these constructors have | |
| 184 duplicate prototypes, we cannot provide them as constructors so | |
| 185 instead we provide them as static methods. This is not possible | |
| 186 if the class is not copyable, in which case we include the | |
| 187 constructor code but commented-out and with an explanation. | |
| 188 | |
| 189 Methods: | |
| 190 | |
| 191 We look for all MuPDF functions that take the wrapped struct as | |
| 192 a first arg (ignoring any `fz_context*` arg), and wrap these | |
| 193 into auto-generated class methods. If there are duplicate | |
| 194 prototypes, we comment-out all but the first. | |
| 195 | |
| 196 Auto-generated methods are omitted if a custom method is | |
| 197 defined with the same name. | |
| 198 | |
| 199 Other: | |
| 200 | |
| 201 There are various subleties with wrapper classes for MuPDF | |
| 202 structs that are not copyable etc. | |
| 203 | |
| 204 Internal `fz_context*`'s: | |
| 205 | |
| 206 `mupdf::*` functions and methods generally have the same args | |
| 207 as the MuPDF functions that they wrap except that they don't | |
| 208 take any `fz_context*` parameter. When required, per-thread | |
| 209 `fz_context`'s are generated automatically at runtime, using | |
| 210 `platform/c++/implementation/internal.cpp:internal_context_get()`. | |
| 211 | |
| 212 Extra items: | |
| 213 | |
| 214 `mupdf::metadata_keys`: This is a global const vector of | |
| 215 strings contains the keys that are suitable for passing to | |
| 216 `fz_lookup_metadata()` and its wrappers. | |
| 217 | |
| 218 Output parameters: | |
| 219 | |
| 220 We provide two different ways of wrapping functions with | |
| 221 out-params. | |
| 222 | |
| 223 Using SWIG OUTPUT markers: | |
| 224 | |
| 225 First, in generated C++ prototypes, we use `OUTPUT` as | |
| 226 the name of out-params, which tells SWIG to treat them as | |
| 227 out-params. This works for basic out-params such as `int*`, so | |
| 228 SWIG will generate Python code that returns a tuple and C# code | |
| 229 that takes args marked with the C# keyword `out`. | |
| 230 | |
| 231 Unfortunately SWIG doesn't appear to handle out-params that | |
| 232 are zero terminated strings (`char**`) and cannot generically | |
| 233 handle binary data out-params (often indicated with `unsigned | |
| 234 char**`). Also, SWIG-generated C# out-params are a little | |
| 235 inconvenient compared to returning a C# tuple (requires C# 7 or | |
| 236 later). | |
| 237 | |
| 238 So we provide an additional mechanism in the generated C++. | |
| 239 | |
| 240 Out-params in a struct: | |
| 241 | |
| 242 For each function with out-params, we provide a class | |
| 243 containing just the out-params and a function taking just the | |
| 244 non-out-param args, plus a pointer to the class. This function | |
| 245 fills in the members of this class instead of returning | |
| 246 individual out-params. We then generate extra Python or C# code | |
| 247 that uses these special functions to get the out-params in a | |
| 248 class instance and return them as a tuple in both Python and | |
| 249 C#. | |
| 250 | |
| 251 Binary out-param data: | |
| 252 | |
| 253 Some MuPDF functions return binary data, typically with an | |
| 254 `unsigned char**` out-param. It is not possible to generically | |
| 255 handle these in Python or C# because the size of the returned | |
| 256 buffer is specified elsewhere (for example in a different | |
| 257 out-param or in the return value). So we generate custom Python | |
| 258 and C# code to give a convenient interface, e.g. copying the | |
| 259 returned data into a Python `bytes` object or a C# byte array. | |
| 260 | |
| 261 | |
| 262 Python wrapping: | |
| 263 | |
| 264 We generate a Python module called `mupdf` which directly wraps the C++ API, | |
| 265 using identical names for functions, classes and methods. | |
| 266 | |
| 267 Out-parameters: | |
| 268 | |
| 269 Functions and methods that have out-parameters are modified to return | |
| 270 the out-parameters directly, usually as a tuple. | |
| 271 | |
| 272 Examples: | |
| 273 | |
| 274 `fz_read_best()`: | |
| 275 | |
| 276 MuPDF C function: | |
| 277 | |
| 278 `fz_buffer *fz_read_best(fz_context *ctx, fz_stream *stm, size_t initial, int *truncated);` | |
| 279 | |
| 280 Class-aware C++ wrapper: | |
| 281 | |
| 282 `FzBuffer read_best(FzStream& stm, size_t initial, int *truncated);` | |
| 283 | |
| 284 Class-aware python wrapper: | |
| 285 | |
| 286 `def read_best(stm, initial)` | |
| 287 | |
| 288 and returns: `(buffer, truncated)`, where `buffer` is a SWIG | |
| 289 proxy for a `FzBuffer` instance and `truncated` is an integer. | |
| 290 | |
| 291 `pdf_parse_ind_obj()`: | |
| 292 | |
| 293 MuPDF C function: | |
| 294 | |
| 295 `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);` | |
| 296 | |
| 297 Class-aware C++ wrapper: | |
| 298 | |
| 299 `PdfObj pdf_parse_ind_obj(PdfDocument& doc, const FzStream& f, int *num, int *gen, int64_t *stm_ofs, int *try_repair);` | |
| 300 | |
| 301 Class-aware Python wrapper: | |
| 302 | |
| 303 `def pdf_parse_ind_obj(doc, f)` | |
| 304 | |
| 305 and returns: (ret, num, gen, stm_ofs, try_repair) | |
| 306 | |
| 307 Special handing if `fz_buffer` data: | |
| 308 | |
| 309 Generic data access: | |
| 310 | |
| 311 `mupdf.python_buffer_data(b: bytes)`: | |
| 312 Returns SWIG proxy for an `unsigned char*` that points to | |
| 313 `<b>`'s data. | |
| 314 | |
| 315 `mupdf.raw_to_python_bytes(data, size):` | |
| 316 Returns Python `bytes` instance containing copy of data | |
| 317 specified by `data` (a SWIG proxy for a `const unsigned char* | |
| 318 c`) and `size` (the length of the data). | |
| 319 | |
| 320 Wrappers for `fz_buffer_extract()`: | |
| 321 | |
| 322 These return a Python `bytes` instance containing a copy of the | |
| 323 buffer's data and the buffer is left empty. This is equivalent to | |
| 324 the underlying fz_buffer_extract() function, but it involves an | |
| 325 internal copy of the data. | |
| 326 | |
| 327 New function `fz_buffer_extract_copy` and new method | |
| 328 `FzBuffer.buffer_extract_copy()` are like `fz_buffer_extract()` | |
| 329 except that they don't clear the buffer. They have no direct | |
| 330 analogy in the C API. | |
| 331 | |
| 332 Wrappers for `fz_buffer_storage()`: | |
| 333 | |
| 334 These return `(size, data)` where `data` is a low-level | |
| 335 SWIG representation of the buffer's storage. One can call | |
| 336 `mupdf.raw_to_python_bytes(data, size)` to get a Python `bytes` | |
| 337 object containing a copy of this data. | |
| 338 | |
| 339 Wrappers for `fz_new_buffer_from_copied_data()`: | |
| 340 | |
| 341 These take a Python `bytes` instance. | |
| 342 | |
| 343 One can create an MuPDF buffer that contains a copy of a Python | |
| 344 `bytes` by using the special `mupdf.python_buffer_data()` | |
| 345 function. This returns a SWIG proxy for an `unsigned char*` that | |
| 346 points to the `bytes` instance's data: | |
| 347 | |
| 348 ``` | |
| 349 bs = b'qwerty' | |
| 350 buffer_ = mupdf.new_buffer_from_copied_data(mupdf.python_buffer_data(bs), len(bs)) | |
| 351 ``` | |
| 352 | |
| 353 Functions taking a `va_list` arg: | |
| 354 | |
| 355 We do not provide Python wrappers for functions such as `fz_vsnprintf()`. | |
| 356 | |
| 357 Details: | |
| 358 | |
| 359 The Python module is generated using SWIG. | |
| 360 | |
| 361 Out-parameters: | |
| 362 | |
| 363 Out-parameters are not implemented using SWIG typemaps because it's | |
| 364 very difficult to make things work that way. Instead we internally | |
| 365 create a struct containing the out-params together with C and | |
| 366 Python wrapper functions that use the struct to pass the out-params | |
| 367 back from C into Python. | |
| 368 | |
| 369 The Python function ends up returning the out parameters in the | |
| 370 same order as they occur in the original function's args, prefixed | |
| 371 by the original function's return value if it is not void. | |
| 372 | |
| 373 If a function returns void and has exactly one out-param, the | |
| 374 Python wrapper will return the out-param directly, not as part of a | |
| 375 tuple. | |
| 376 | |
| 377 | |
| 378 Tools required to build: | |
| 379 | |
| 380 Clang: | |
| 381 | |
| 382 Clang versions: | |
| 383 | |
| 384 We work with clang-6 or clang-7, but clang-6 appears to not be able | |
| 385 to cope with function args that are themselves function pointers, | |
| 386 so wrappers for MuPDF functions are omitted from the generated C++ | |
| 387 code. | |
| 388 | |
| 389 Unix: | |
| 390 | |
| 391 It seems that clang-python packages such as Debian's python-clang | |
| 392 and OpenBSD's py3-llvm require us to explicitly specify the | |
| 393 location of libclang, so we search in various locations. | |
| 394 | |
| 395 Alternatively on Linux one can (perhaps in a venv) do: | |
| 396 | |
| 397 pip install libclang | |
| 398 | |
| 399 This makes clang available directly as a Python module. | |
| 400 | |
| 401 On Windows, one must install clang-python with: | |
| 402 | |
| 403 pip install libclang | |
| 404 | |
| 405 setuptools: | |
| 406 Used internally. | |
| 407 | |
| 408 SWIG for Python/C# bindings: | |
| 409 | |
| 410 We work with swig-3 and swig-4. If swig-4 is used, we propagate | |
| 411 doxygen-style comments for structures and functions into the generated | |
| 412 C++ code. | |
| 413 | |
| 414 Mono for C# bindings on Unix. | |
| 415 | |
| 416 | |
| 417 Building Python bindings: | |
| 418 | |
| 419 Build and install the MuPDF Python bindings as module `mupdf` in a Python | |
| 420 virtual environment, using MuPDF's `setup.py` script: | |
| 421 | |
| 422 Linux: | |
| 423 > python3 -m venv pylocal | |
| 424 > . pylocal/bin/activate | |
| 425 (pylocal) > pip install pyqt5 libclang | |
| 426 (pylocal) > cd .../mupdf | |
| 427 (pylocal) > python setup.py install | |
| 428 | |
| 429 Windows: | |
| 430 > py -m venv pylocal | |
| 431 > pylocal\Scripts\activate | |
| 432 (pylocal) > pip install libclang pyqt5 | |
| 433 (pylocal) > cd ...\mupdf | |
| 434 (pylocal) > python setup.py install | |
| 435 | |
| 436 OpenBSD: | |
| 437 [It seems that pip can't install pyqt5 or libclang so instead we | |
| 438 install system packages and use --system-site-packages.] | |
| 439 | |
| 440 > sudo pkg_add py3-llvm py3-qt5 | |
| 441 > python3 -m venv --system-site-packages pylocal | |
| 442 > . pylocal/bin/activate | |
| 443 (pylocal) > cd .../mupdf | |
| 444 (pylocal) > python setup.py install | |
| 445 | |
| 446 Use the mupdf module: | |
| 447 (pylocal) > python | |
| 448 >>> import mupdf | |
| 449 >>> | |
| 450 | |
| 451 Build MuPDF Python bindings without a Python virtual environment, using | |
| 452 scripts/mupdfwrap.py: | |
| 453 | |
| 454 [Have not yet found a way to use clang from python on Windows without a | |
| 455 virtual environment, so this is Unix-only.] | |
| 456 | |
| 457 > cd .../mupdf | |
| 458 | |
| 459 Install required packages: | |
| 460 Debian: | |
| 461 > sudo apt install clang python3-clang python3-dev swig | |
| 462 | |
| 463 OpenBSD: | |
| 464 > pkg_add py3-llvm py3-qt5 | |
| 465 | |
| 466 Build and test: | |
| 467 > ./scripts/mupdfwrap.py -d build/shared-release -b all --test-python | |
| 468 | |
| 469 Use the mupdf module by setting PYTHONPATH: | |
| 470 > PYTHONPATH=build/shared-release python3 | |
| 471 >>> import mupdf | |
| 472 >>> | |
| 473 | |
| 474 | |
| 475 Building C# bindings: | |
| 476 | |
| 477 Build MuPDF C# bindings using scripts/mupdfwrap.py: | |
| 478 | |
| 479 > cd .../mupdf | |
| 480 | |
| 481 Install required packages: | |
| 482 Debian: | |
| 483 > sudo apt install clang python3-clang python3-dev mono-devel | |
| 484 | |
| 485 OpenBSD: | |
| 486 > sudo pkg_add py3-llvm py3-qt5 mono | |
| 487 | |
| 488 Build and test: | |
| 489 > ./scripts/mupdfwrap.py -d build/shared-release -b --csharp all --test-csharp | |
| 490 | |
| 491 | |
| 492 Windows builds: | |
| 493 | |
| 494 Required predefined macros: | |
| 495 | |
| 496 Code that will use the MuPDF DLL must be built with FZ_DLL_CLIENT | |
| 497 predefined. | |
| 498 | |
| 499 The MuPDF DLL itself is built with FZ_DLL predefined. | |
| 500 | |
| 501 DLLs: | |
| 502 | |
| 503 There is no separate C library, instead the C and C++ API are | |
| 504 both in mupdfcpp.dll, which is built by running devenv on | |
| 505 platform/win32/mupdf.sln. | |
| 506 | |
| 507 The Python SWIG library is called _mupdf.pyd which, | |
| 508 despite the name, is a standard Windows DLL, built from | |
| 509 platform/python/mupdfcpp_swig.i.cpp. | |
| 510 | |
| 511 DLL export of functions and data: | |
| 512 | |
| 513 On Windows, include/mupdf/fitz/export.h defines FZ_FUNCTION and FZ_DATA | |
| 514 to __declspec(dllexport) and/or __declspec(dllimport) depending on | |
| 515 whether FZ_DLL or FZ_DLL_CLIENT are defined. | |
| 516 | |
| 517 All MuPDF headers prefix declarations of public global data with | |
| 518 FZ_DATA. | |
| 519 | |
| 520 All generated C++ code prefixes functions with FZ_FUNCTION and data | |
| 521 with FZ_DATA. | |
| 522 | |
| 523 When building mupdfcpp.dll on Windows we link with the auto-generated | |
| 524 platform/c++/windows_mupdf.def file; this lists all C public global | |
| 525 data. | |
| 526 | |
| 527 For reasons that i don't yet understand, we don't seem to need to tag | |
| 528 C functions with FZ_FUNCTION, but this is required for C++ functions | |
| 529 otherwise we get unresolved symbols when building MuPDF client code. | |
| 530 | |
| 531 Building the DLLs: | |
| 532 | |
| 533 We build Windows binaries by running devenv.com directly. We search | |
| 534 for this using scripts/wdev.py. | |
| 535 | |
| 536 Building _mupdf.pyd is tricky because it needs to be built with a | |
| 537 specific Python.h and linked with a specific python.lib. This is done | |
| 538 by setting environmental variables MUPDF_PYTHON_INCLUDE_PATH and | |
| 539 MUPDF_PYTHON_LIBRARY_PATH when running devenv.com, which are referenced | |
| 540 by platform/win32/mupdfpyswig.vcxproj. Thus one cannot easily build | |
| 541 _mupdf.pyd directly from the Visual Studio GUI. | |
| 542 | |
| 543 [In the git history there is code that builds _mupdf.pyd by running the | |
| 544 Windows compiler and linker cl.exe and link.exe directly, which avoids | |
| 545 the complications of going via devenv, at the expense of needing to | |
| 546 know where cl.exe and link.exe are.] | |
| 547 | |
| 548 Usage: | |
| 549 | |
| 550 Args: | |
| 551 | |
| 552 -b [<args>] <actions>: | |
| 553 --build [<args>] <actions>: | |
| 554 Builds some or all of the C++ and python interfaces. | |
| 555 | |
| 556 By default we create source files in: | |
| 557 mupdf/platform/c++/ | |
| 558 mupdf/platform/python/ | |
| 559 | |
| 560 - and .so files in directory specified by --dir-so. | |
| 561 | |
| 562 We avoid unnecessary compiling or running of swig by looking at file | |
| 563 mtimes. We also write commands to .cmd files which allows us to force | |
| 564 rebuilds if commands change. | |
| 565 | |
| 566 args: | |
| 567 --clang-verbose | |
| 568 Generate extra diagnostics in action=0 when looking for | |
| 569 libclang.so. | |
| 570 -d <details> | |
| 571 If specified, we show extra diagnostics when wrapping | |
| 572 functions whose name contains <details>. Can be specified | |
| 573 multiple times. | |
| 574 --devenv <path> | |
| 575 Set path of devenv.com script on Windows. If not specified, | |
| 576 we search for a suitable Visual Studio installation. | |
| 577 -f | |
| 578 Force rebuilds. | |
| 579 -j <N> | |
| 580 Set -j arg used when action 'm' calls make (not | |
| 581 Windows). If <N> is 0 we use the number of CPUs | |
| 582 (from Python's multiprocessing.cpu_count()). | |
| 583 --m-target <target> | |
| 584 Comma-separated list of target(s) to be built by action 'm' | |
| 585 (Unix) or action '1' (Windows). | |
| 586 | |
| 587 On Unix, the specified target(s) are used as Make target(s) | |
| 588 instead of implicit `all`. For example `--m-target libs` | |
| 589 can be used to disable the default building of tools. | |
| 590 | |
| 591 On Windows, for each specified target, `/Project <target>` | |
| 592 is appended to the devenv command. So one can use | |
| 593 `--m-target mutool,muraster` to build mutool.exe and | |
| 594 muraster.exe as well as mupdfcpp64.dll. | |
| 595 --m-vars <text> | |
| 596 Text to insert near start of the action 'm' make command, | |
| 597 typically to set MuPDF build flags, for example: | |
| 598 --m-vars 'HAVE_LIBCRYPTO=no' | |
| 599 --regress | |
| 600 Checks for regressions in generated C++ code and SWIG .i | |
| 601 file (actions 0 and 2 below). If a generated file already | |
| 602 exists and its content differs from our generated content, | |
| 603 show diff and exit with an error. This can be used to check | |
| 604 for regressions when modifying this script. | |
| 605 --refcheck-if <text> | |
| 606 Set text used to determine whether to enabling | |
| 607 reference-checking code. For example use `--refcheck-if | |
| 608 '#if 1'` to always enable, `--refcheck-if '#if 0'` to | |
| 609 always disable. Default is '#ifndef NDEBUG'. | |
| 610 --trace-if <text> | |
| 611 Set text used to determine whether to enabling | |
| 612 runtime diagnostics code. For example use `--trace-if | |
| 613 '#if 1'` to always enable, `--refcheck-if '#if 0'` to | |
| 614 always disable. Default is '#ifndef NDEBUG'. | |
| 615 --python | |
| 616 --csharp | |
| 617 Whether to generated bindings for python or C#. Default is | |
| 618 --python. If specified multiple times, the last wins. | |
| 619 | |
| 620 <actions> is list of single-character actions which are processed in | |
| 621 order. If <actions> is 'all', it is replaced by m0123. | |
| 622 | |
| 623 m: | |
| 624 Builds libmupdf.so by running make in the mupdf/ | |
| 625 directory. Default is release build, but this can be changed | |
| 626 using --dir-so. | |
| 627 | |
| 628 0: | |
| 629 Create C++ source for C++ interface onto the fz_* API. Uses | |
| 630 clang-python to parse the fz_* API. | |
| 631 | |
| 632 Generates various files including: | |
| 633 mupdf/platform/c++/ | |
| 634 implementation/ | |
| 635 classes.cpp | |
| 636 exceptions.cpp | |
| 637 functions.cpp | |
| 638 include/ | |
| 639 classes.h | |
| 640 classes2.h | |
| 641 exceptions.h | |
| 642 functions.h | |
| 643 | |
| 644 If files already contain the generated text, they are not | |
| 645 updated, so that mtimes are unchanged. | |
| 646 | |
| 647 Also removes any other .cpp or .h files from | |
| 648 mupdf/platform/c++/{implementation,include}. | |
| 649 | |
| 650 1: | |
| 651 Compile and link source files created by action=0. | |
| 652 | |
| 653 Generates: | |
| 654 <dir-so>/libmupdfcpp.so | |
| 655 | |
| 656 This gives a C++ interface onto mupdf. | |
| 657 | |
| 658 2: | |
| 659 Run SWIG on the C++ source built by action=0 to generate source | |
| 660 for python interface onto the C++ API. | |
| 661 | |
| 662 For example for Python this generates: | |
| 663 | |
| 664 mupdf/platform/python/mupdfcpp_swig.i | |
| 665 mupdf/platform/python/mupdfcpp_swig.i.cpp | |
| 666 mupdf/build/shared-release/mupdf.py | |
| 667 | |
| 668 Note that this requires action=0 to have been run previously. | |
| 669 | |
| 670 3: | |
| 671 Compile and links the mupdfcpp_swig.i.cpp file created by | |
| 672 action=2. Requires libmupdf.so to be available, e.g. built by | |
| 673 the --libmupdf.so option. | |
| 674 | |
| 675 For example for Python this generates: | |
| 676 | |
| 677 mupdf/build/shared-release/_mupdf.so | |
| 678 | |
| 679 Along with mupdf/platform/python/mupdf.py (generated by | |
| 680 action=2), this implements the mupdf python module. | |
| 681 | |
| 682 .: | |
| 683 Ignores following actions; useful to quickly avoid unnecessary | |
| 684 rebuild if it is known to be unnecessary. | |
| 685 | |
| 686 --check-headers [-k] <which> | |
| 687 Runs cc on header files to check they #include all required headers. | |
| 688 | |
| 689 -k: | |
| 690 If present, we carry on after errors. | |
| 691 which: | |
| 692 If 'all', we run on all headers in .../mupdf/include. Otherwise | |
| 693 if <which> ends with '+', we run on all remaining headers in | |
| 694 .../mupdf/include starting with <which>. Otherwise the name of | |
| 695 header to test. | |
| 696 | |
| 697 --compare-fz_usage <directory> | |
| 698 Finds all fz_*() function calls in git files within <directory>, and | |
| 699 compares with all the fz_*() functions that are wrapped up as class | |
| 700 methods. | |
| 701 | |
| 702 Useful to see what functionality we are missing. | |
| 703 | |
| 704 --diff | |
| 705 Compares generated files with those in the mupdfwrap_ref/ directory | |
| 706 populated by --ref option. | |
| 707 | |
| 708 -d | |
| 709 --dir-so <directory> | |
| 710 Set build directory. | |
| 711 | |
| 712 Default is: build/shared-release | |
| 713 | |
| 714 We use different C++ compile flags depending on release or debug | |
| 715 builds (specifically, the definition of NDEBUG is important because | |
| 716 it must match what was used when libmupdf.so was built). | |
| 717 | |
| 718 If <directory> starts with `build/fpic-`, the C and C++ API are | |
| 719 built as `.a` archives but compiled with -fPIC so that they can be | |
| 720 linked into shared libraries. | |
| 721 | |
| 722 If <directory> is '-' we do not set any paths when running tests | |
| 723 e.g. with --test-python. This is for testing after installing into | |
| 724 a venv. | |
| 725 | |
| 726 Examples: | |
| 727 -d build/shared-debug | |
| 728 -d build/shared-release [default] | |
| 729 | |
| 730 On Windows one can specify the CPU and Python version; we then | |
| 731 use 'py -0f' to find the matching installed Python along with its | |
| 732 Python.h and python.lib. For example: | |
| 733 | |
| 734 -d build/shared-release-x32-py3.8 | |
| 735 -d build/shared-release-x64-py3.9 | |
| 736 | |
| 737 --doc <languages> | |
| 738 Generates documentation for the different APIs in | |
| 739 mupdf/docs/generated/. | |
| 740 | |
| 741 <languages> is either 'all' or a comma-separated list of API languages: | |
| 742 | |
| 743 c | |
| 744 Generate documentation for the C API with doxygen: | |
| 745 include/html/index.html | |
| 746 c++ | |
| 747 Generate documentation for the C++ API with doxygen: | |
| 748 platform/c++/include/html/index.html | |
| 749 python | |
| 750 Generate documentation for the Python API using pydoc3: | |
| 751 platform/python/mupdf.html | |
| 752 | |
| 753 Also see '--sync-docs' option for copying these generated | |
| 754 documentation files elsewhere. | |
| 755 | |
| 756 --make <make-command> | |
| 757 Override make command, e.g. `--make gmake`. | |
| 758 If not specified, we use $MUPDF_MAKE. If this is not set, we use | |
| 759 `make` (or `gmake` on OpenBSD). | |
| 760 | |
| 761 --ref | |
| 762 Copy generated C++ files to mupdfwrap_ref/ directory for use by --diff. | |
| 763 | |
| 764 --run-py <arg> <arg> ... | |
| 765 Runs command with LD_LIBRARY_PATH and PYTHONPATH set up for use with | |
| 766 mupdf.py. | |
| 767 | |
| 768 Exits with same code as the command. | |
| 769 | |
| 770 --swig <swig> | |
| 771 Sets the swig command to use. | |
| 772 | |
| 773 If this is version 4+, we use the <swig> -doxygen to copy | |
| 774 over doxygen-style comments into mupdf.py. Otherwise we use | |
| 775 '%feature("autodoc", "3");' to generate comments with type information | |
| 776 for args in mupdf.py. [These two don't seem to be usable at the same | |
| 777 time in swig-4.] | |
| 778 | |
| 779 --swig-windows-auto | |
| 780 Downloads swig if not present in current directory, extracts | |
| 781 swig.exe and sets things up to use it subsequently. | |
| 782 | |
| 783 --sync-docs <destination> | |
| 784 Use rsync to copy contents of docs/generated/ to remote destination. | |
| 785 | |
| 786 --sync-pretty <destination> | |
| 787 Use rsync to copy generated C++ and Python files to <destination>. Also | |
| 788 uses generates and copies .html versions of these files that use | |
| 789 run_prettify.js from cdn.jsdelivr.net to show embelished content. | |
| 790 | |
| 791 --test-csharp | |
| 792 Tests the experimental C# API. | |
| 793 | |
| 794 --test-python | |
| 795 Tests the python API. | |
| 796 | |
| 797 --test-python-fitz [<options>] all|iter|<script-name> | |
| 798 Tests fitz.py with PyMuPDF. Requires 'pkg_add py3-test' or similar. | |
| 799 options: | |
| 800 Passed to py.test-3. | |
| 801 -x: stop at first error. | |
| 802 -s: show stdout/err. | |
| 803 all: | |
| 804 Runs all tests with py.test-3 | |
| 805 iter: | |
| 806 Runs each test in turn until one fails. | |
| 807 <script-name>: | |
| 808 Runs a single test, e.g.: test_general.py | |
| 809 | |
| 810 --test-setup.py <arg> | |
| 811 Tests that setup.py installs a usable Python mupdf module. | |
| 812 | |
| 813 * Creates a Python virtual environment. | |
| 814 * Activates the Python environment. | |
| 815 * Runs setup.py install. | |
| 816 * Builds C, C++ and Python librariess in build/shared-release. | |
| 817 * Copies build/shared-release/*.so into virtual environment. | |
| 818 * Runs scripts/mupdfwrap_test.py. | |
| 819 * Imports mupdf and checks basic functionality. | |
| 820 * Deactivates the Python environment. | |
| 821 | |
| 822 --venv | |
| 823 If specified, should be the first arg in the command line. | |
| 824 | |
| 825 Re-runs mupdfwrap.py in a Python venv containing libclang | |
| 826 and swig, passing remaining args. | |
| 827 | |
| 828 --vs-upgrade 0 | 1 | |
| 829 If 1, we use a copy of the Windows build file tree | |
| 830 `platform/win32/` called `platform/win32-vs-upgrade`, modifying the | |
| 831 copied files with `devenv.com /upgrade`. | |
| 832 | |
| 833 For example this allows use with Visual Studio 2022 if it doesn't | |
| 834 have the v142 tools installed. | |
| 835 | |
| 836 --windows-cmd ... | |
| 837 Runs mupdfwrap.py via cmd.exe, passing remaining args. Useful to | |
| 838 get from cygwin to native Windows. | |
| 839 | |
| 840 E.g.: | |
| 841 --windows-cmd --venv --swig-windows-auto -b all | |
| 842 | |
| 843 Examples: | |
| 844 | |
| 845 ./scripts/mupdfwrap.py -b all -t | |
| 846 Build all (release build) and test. | |
| 847 | |
| 848 ./scripts/mupdfwrap.py -d build/shared-debug -b all -t | |
| 849 Build all (debug build) and test. | |
| 850 | |
| 851 ./scripts/mupdfwrap.py -b 0 --compare-fz_usage platform/gl | |
| 852 Compare generated class methods with functions called by platform/gl | |
| 853 code. | |
| 854 | |
| 855 python3 -m cProfile -s cumulative ./scripts/mupdfwrap.py --venv -b 0 | |
| 856 Profile generation of C++ source code. | |
| 857 | |
| 858 ./scripts/mupdfwrap.py --venv -b all -t | |
| 859 Build and test on Windows. | |
| 860 | |
| 861 | |
| 862 ''' | |
| 863 | |
| 864 import glob | |
| 865 import multiprocessing | |
| 866 import os | |
| 867 import pickle | |
| 868 import platform | |
| 869 import re | |
| 870 import shlex | |
| 871 import shutil | |
| 872 import sys | |
| 873 import sysconfig | |
| 874 import tempfile | |
| 875 import textwrap | |
| 876 | |
| 877 if platform.system() == 'Windows': | |
| 878 ''' | |
| 879 shlex.quote() is broken. | |
| 880 ''' | |
| 881 def quote(text): | |
| 882 if ' ' in text: | |
| 883 if '"' not in text: | |
| 884 return f'"{text}"' | |
| 885 if "'" not in text: | |
| 886 return f"'{text}'" | |
| 887 assert 0, f'Cannot handle quotes in {text=}' | |
| 888 return text | |
| 889 shlex.quote = quote | |
| 890 | |
| 891 try: | |
| 892 import resource | |
| 893 except ModuleNotFoundError: | |
| 894 # Not available on Windows. | |
| 895 resource = None | |
| 896 | |
| 897 import jlib | |
| 898 import pipcl | |
| 899 import wdev | |
| 900 | |
| 901 from . import classes | |
| 902 from . import cpp | |
| 903 from . import csharp | |
| 904 from . import make_cppyy | |
| 905 from . import parse | |
| 906 from . import state | |
| 907 from . import swig | |
| 908 | |
| 909 clang = state.clang | |
| 910 | |
| 911 | |
| 912 # We use f-strings, so need python-3.6+. | |
| 913 assert sys.version_info[0] == 3 and sys.version_info[1] >= 6, ( | |
| 914 'We require python-3.6+') | |
| 915 | |
| 916 | |
| 917 def compare_fz_usage( | |
| 918 tu, | |
| 919 directory, | |
| 920 fn_usage, | |
| 921 ): | |
| 922 ''' | |
| 923 Looks for fz_ items in git files within <directory> and compares to what | |
| 924 functions we have wrapped in <fn_usage>. | |
| 925 ''' | |
| 926 | |
| 927 filenames = jlib.system( f'cd {directory}; git ls-files .', out='return') | |
| 928 | |
| 929 class FzItem: | |
| 930 def __init__( self, type_, uses_structs=None): | |
| 931 self.type_ = type_ | |
| 932 if self.type_ == 'function': | |
| 933 self.uses_structs = uses_structs | |
| 934 | |
| 935 # Set fz_items to map name to info about function/struct. | |
| 936 # | |
| 937 fz_items = dict() | |
| 938 for cursor in parse.get_members(tu.cursor): | |
| 939 name = cursor.spelling | |
| 940 if not name.startswith( ('fz_', 'pdf_')): | |
| 941 continue | |
| 942 uses_structs = False | |
| 943 if (1 | |
| 944 and name.startswith( ('fz_', 'pdf_')) | |
| 945 and cursor.kind == clang.cindex.CursorKind.FUNCTION_DECL | |
| 946 and ( | |
| 947 cursor.linkage == clang.cindex.LinkageKind.EXTERNAL | |
| 948 or | |
| 949 cursor.is_definition() # Picks up static inline functions. | |
| 950 ) | |
| 951 ): | |
| 952 def uses_struct( type_): | |
| 953 ''' | |
| 954 Returns true if <type_> is a fz struct or pointer to fz struct. | |
| 955 ''' | |
| 956 if type_.kind == clang.cindex.TypeKind.POINTER: | |
| 957 type_ = type_.get_pointee() | |
| 958 type_ = parse.get_name_canonical( type_) | |
| 959 if type_.spelling.startswith( 'struct fz_'): | |
| 960 return True | |
| 961 # Set uses_structs to true if fn returns a fz struct or any | |
| 962 # argument is a fz struct. | |
| 963 if uses_struct( cursor.result_type): | |
| 964 uses_structs = True | |
| 965 else: | |
| 966 for arg in parse.get_args( tu, cursor): | |
| 967 if uses_struct( arg.cursor.type): | |
| 968 uses_structs = True | |
| 969 break | |
| 970 if uses_structs: | |
| 971 pass | |
| 972 #log( 'adding function {name=} {uses_structs=}') | |
| 973 fz_items[ name] = FzItem( 'function', uses_structs) | |
| 974 | |
| 975 directory_names = dict() | |
| 976 for filename in filenames.split( '\n'): | |
| 977 if not filename: | |
| 978 continue | |
| 979 path = os.path.join( directory, filename) | |
| 980 jlib.log( '{filename!r=} {path=}') | |
| 981 with open( path, 'r', encoding='utf-8', errors='replace') as f: | |
| 982 text = f.read() | |
| 983 for m in re.finditer( '(fz_[a-z0-9_]+)', text): | |
| 984 | |
| 985 name = m.group(1) | |
| 986 info = fz_items.get( name) | |
| 987 if info: | |
| 988 if (0 | |
| 989 or (info.type_ == 'function' and info.uses_structs) | |
| 990 or (info.type_ == 'fz-struct') | |
| 991 ): | |
| 992 directory_names.setdefault( name, 0) | |
| 993 directory_names[ name] += 1 | |
| 994 | |
| 995 name_max_len = 0 | |
| 996 for name, n in sorted( directory_names.items()): | |
| 997 name_max_len = max( name_max_len, len( name)) | |
| 998 | |
| 999 n_missing = 0 | |
| 1000 fnnames = sorted( fn_usage.keys()) | |
| 1001 for fnname in fnnames: | |
| 1002 classes_n, cursor = fn_usage[ fnname] | |
| 1003 directory_n = directory_names.get( name, 0) | |
| 1004 if classes_n==0 and directory_n: | |
| 1005 n_missing += 1 | |
| 1006 jlib.log( ' {fnname:40} {classes_n=} {directory_n=}') | |
| 1007 | |
| 1008 jlib.log( '{n_missing}') | |
| 1009 | |
| 1010 | |
| 1011 g_have_done_build_0 = False | |
| 1012 | |
| 1013 | |
| 1014 def _test_get_m_command(): | |
| 1015 ''' | |
| 1016 Tests _get_m_command(). | |
| 1017 ''' | |
| 1018 def test( dir_so, expected_command): | |
| 1019 build_dirs = state.BuildDirs() | |
| 1020 build_dirs.dir_so = dir_so | |
| 1021 command, actual_build_dir = _get_m_command( build_dirs) | |
| 1022 assert command == expected_command, f'\nExpected: {expected_command}\nBut: {command}' | |
| 1023 | |
| 1024 mupdf_root = os.path.abspath( f'{__file__}/../../../') | |
| 1025 infix = 'CXX=c++ ' if state.state_.openbsd else '' | |
| 1026 | |
| 1027 test( | |
| 1028 'shared-release', | |
| 1029 f'cd {mupdf_root} && {infix}gmake HAVE_GLUT=no HAVE_PTHREAD=yes verbose=yes shared=yes build=release build_prefix=shared-', | |
| 1030 ) | |
| 1031 test( | |
| 1032 'mupdfpy-amd64-shared-release', | |
| 1033 f'cd {mupdf_root} && {infix}gmake HAVE_GLUT=no HAVE_PTHREAD=yes verbose=yes shared=yes build=release build_prefix=mupdfpy-amd64-shared-', | |
| 1034 ) | |
| 1035 test( | |
| 1036 'mupdfpy-amd64-fpic-release', | |
| 1037 f'cd {mupdf_root} && CFLAGS="-fPIC" {infix}gmake HAVE_GLUT=no HAVE_PTHREAD=yes verbose=yes build=release build_prefix=mupdfpy-amd64-fpic-', | |
| 1038 ) | |
| 1039 jlib.log( '_get_m_command() ok') | |
| 1040 | |
| 1041 | |
| 1042 def get_so_version( build_dirs): | |
| 1043 ''' | |
| 1044 Returns `.<minor>.<patch>` from include/mupdf/fitz/version.h. | |
| 1045 | |
| 1046 Returns '' on macos. | |
| 1047 ''' | |
| 1048 if state.state_.macos or state.state_.pyodide: | |
| 1049 return '' | |
| 1050 if os.environ.get('USE_SONAME') == 'no': | |
| 1051 return '' | |
| 1052 d = dict() | |
| 1053 def get_v( name): | |
| 1054 path = f'{build_dirs.dir_mupdf}/include/mupdf/fitz/version.h' | |
| 1055 with open( path) as f: | |
| 1056 for line in f: | |
| 1057 m = re.match(f'^#define {name} (.+)\n$', line) | |
| 1058 if m: | |
| 1059 return m.group(1) | |
| 1060 assert 0, f'Cannot find #define of {name=} in {path=}.' | |
| 1061 major = get_v('FZ_VERSION_MAJOR') | |
| 1062 minor = get_v('FZ_VERSION_MINOR') | |
| 1063 patch = get_v('FZ_VERSION_PATCH') | |
| 1064 return f'.{minor}.{patch}' | |
| 1065 | |
| 1066 | |
| 1067 def _get_m_command( build_dirs, j=None, make=None, m_target=None, m_vars=None): | |
| 1068 ''' | |
| 1069 Generates a `make` command for building with `build_dirs.dir_mupdf`. | |
| 1070 | |
| 1071 Returns `(command, actual_build_dir, suffix)`. | |
| 1072 ''' | |
| 1073 assert not state.state_.windows, 'Cannot do "-b m" on Windows; C library is integrated into C++ library built by "-b 01"' | |
| 1074 #jlib.log( '{build_dirs.dir_mupdf=}') | |
| 1075 if not make: | |
| 1076 make = os.environ.get('MUPDF_MAKE') | |
| 1077 if make: | |
| 1078 jlib.log('Overriding from $MUPDF_MAKE: {make=}.') | |
| 1079 if not make: | |
| 1080 if state.state_.openbsd: | |
| 1081 # Need to run gmake, not make. Also for some | |
| 1082 # reason gmake on OpenBSD sets CC to clang, but | |
| 1083 # CXX to g++, so need to force CXX=c++ too. | |
| 1084 # | |
| 1085 make = 'CXX=c++ gmake' | |
| 1086 jlib.log('OpenBSD, using: {make=}.') | |
| 1087 if not make: | |
| 1088 make = 'make' | |
| 1089 | |
| 1090 if j is not None: | |
| 1091 if j == 0: | |
| 1092 j = multiprocessing.cpu_count() | |
| 1093 jlib.log('Setting -j to multiprocessing.cpu_count()={j}') | |
| 1094 make += f' -j {j}' | |
| 1095 flags = os.path.basename( build_dirs.dir_so).split('-') | |
| 1096 make_env = '' | |
| 1097 make_args = ' HAVE_GLUT=no HAVE_PTHREAD=yes verbose=yes barcode=yes' | |
| 1098 if m_vars: | |
| 1099 make_args += f' {m_vars}' | |
| 1100 suffix = None | |
| 1101 for i, flag in enumerate( flags): | |
| 1102 if flag in ('x32', 'x64') or re.match('py[0-9]', flag): | |
| 1103 # setup.py puts cpu and python version | |
| 1104 # elements into the build directory name | |
| 1105 # when creating wheels; we need to ignore | |
| 1106 # them. | |
| 1107 jlib.log('Ignoring {flag=}') | |
| 1108 else: | |
| 1109 if 0: pass # lgtm [py/unreachable-statement] | |
| 1110 elif flag == 'debug': | |
| 1111 make_args += ' build=debug' | |
| 1112 elif flag == 'release': | |
| 1113 make_args += ' build=release' | |
| 1114 elif flag == 'memento': | |
| 1115 make_args += ' build=memento' | |
| 1116 elif flag == 'shared': | |
| 1117 make_args += ' shared=yes' | |
| 1118 suffix = '.so' | |
| 1119 elif flag == 'tesseract': | |
| 1120 make_args += ' HAVE_LEPTONICA=yes HAVE_TESSERACT=yes' | |
| 1121 elif flag == 'bsymbolic': | |
| 1122 make_env += ' XLIB_LDFLAGS=-Wl,-Bsymbolic' | |
| 1123 elif flag in ('Py_LIMITED_API', 'PLA'): | |
| 1124 pass | |
| 1125 elif flag.startswith('Py_LIMITED_API='): # fixme: obsolete. | |
| 1126 pass | |
| 1127 elif flag.startswith('Py_LIMITED_API_'): | |
| 1128 pass | |
| 1129 elif flag.startswith('PLA_'): | |
| 1130 pass | |
| 1131 else: | |
| 1132 jlib.log(f'Ignoring unrecognised flag {flag!r} in {flags!r} in {build_dirs.dir_so!r}') | |
| 1133 make_args += f' OUT=build/{os.path.basename(build_dirs.dir_so)}' | |
| 1134 if m_target: | |
| 1135 for t in m_target.split(','): | |
| 1136 make_args += f' {t}' | |
| 1137 else: | |
| 1138 make_args += f' libs libmupdf-threads' | |
| 1139 command = f'cd {build_dirs.dir_mupdf} &&' | |
| 1140 if make_env: | |
| 1141 command += make_env | |
| 1142 command += f' {make}{make_args}' | |
| 1143 | |
| 1144 return command, build_dirs.dir_so, suffix | |
| 1145 | |
| 1146 _windows_vs_upgrade_cache = dict() | |
| 1147 def _windows_vs_upgrade( vs_upgrade, build_dirs, devenv): | |
| 1148 ''' | |
| 1149 If `vs_upgrade` is true, creates new | |
| 1150 {build_dirs.dir_mupdf}/platform/win32-vs-upgrade/ tree with upgraded .sln | |
| 1151 and .vcxproj files. Returns 'win32-vs-upgrade'. | |
| 1152 | |
| 1153 Otherwise returns 'win32'. | |
| 1154 ''' | |
| 1155 if not vs_upgrade: | |
| 1156 return 'win32' | |
| 1157 key = (build_dirs, devenv) | |
| 1158 infix = _windows_vs_upgrade_cache.get(key) | |
| 1159 if infix is None: | |
| 1160 infix = 'win32-vs-upgrade' | |
| 1161 prefix1 = f'{build_dirs.dir_mupdf}/platform/win32/' | |
| 1162 prefix2 = f'{build_dirs.dir_mupdf}/platform/{infix}/' | |
| 1163 for dirpath, dirnames, filenames in os.walk( prefix1): | |
| 1164 for filename in filenames: | |
| 1165 if os.path.splitext( filename)[ 1] in ( | |
| 1166 '.sln', | |
| 1167 '.vcxproj', | |
| 1168 '.props', | |
| 1169 '.targets', | |
| 1170 '.xml', | |
| 1171 '.c', | |
| 1172 ): | |
| 1173 path1 = f'{dirpath}/{filename}' | |
| 1174 assert path1.startswith(prefix1) | |
| 1175 path2 = prefix2 + path1[ len(prefix1):] | |
| 1176 os.makedirs( os.path.dirname(path2), exist_ok=True) | |
| 1177 jlib.log('Calling shutil.copy2 {path1=} {path2=}') | |
| 1178 shutil.copy2(path1, path2) | |
| 1179 for path in glob.glob( f'{prefix2}*.sln'): | |
| 1180 jlib.system(f'"{devenv}" {path} /upgrade', verbose=1) | |
| 1181 _windows_vs_upgrade_cache[ key] = infix | |
| 1182 jlib.log('returning {infix=}') | |
| 1183 return infix | |
| 1184 | |
| 1185 | |
| 1186 def macos_patch( library, *sublibraries): | |
| 1187 ''' | |
| 1188 Patches `library` so that all references to items in `sublibraries` are | |
| 1189 changed to `@rpath/<leafname>`. | |
| 1190 | |
| 1191 library: | |
| 1192 Path of shared library. | |
| 1193 sublibraries: | |
| 1194 List of paths of shared libraries; these have typically been | |
| 1195 specified with `-l` when `library` was created. | |
| 1196 ''' | |
| 1197 if not state.state_.macos: | |
| 1198 return | |
| 1199 jlib.log( f'macos_patch(): library={library} sublibraries={sublibraries}') | |
| 1200 # Find what shared libraries are used by `library`. | |
| 1201 jlib.system( f'otool -L {library}', out='log') | |
| 1202 command = 'install_name_tool' | |
| 1203 names = [] | |
| 1204 for sublibrary in sublibraries: | |
| 1205 name = jlib.system( f'otool -D {sublibrary}', out='return').strip() | |
| 1206 name = name.split('\n') | |
| 1207 assert len(name) == 2 and name[0] == f'{sublibrary}:', f'{name=}' | |
| 1208 name = name[1] | |
| 1209 # strip trailing so_name. | |
| 1210 leaf = os.path.basename(name) | |
| 1211 m = re.match('^(.+[.]((so)|(dylib)))[0-9.]*$', leaf) | |
| 1212 assert m | |
| 1213 jlib.log(f'Changing {leaf=} to {m.group(1)}') | |
| 1214 leaf = m.group(1) | |
| 1215 command += f' -change {name} @rpath/{leaf}' | |
| 1216 command += f' {library}' | |
| 1217 jlib.system( command, out='log') | |
| 1218 jlib.system( f'otool -L {library}', out='log') | |
| 1219 | |
| 1220 | |
| 1221 def build_0( | |
| 1222 build_dirs, | |
| 1223 header_git, | |
| 1224 check_regress, | |
| 1225 clang_info_verbose, | |
| 1226 refcheck_if, | |
| 1227 trace_if, | |
| 1228 cpp_files, | |
| 1229 h_files, | |
| 1230 ): | |
| 1231 ''' | |
| 1232 Handles `-b 0` - generate C++ bindings source. | |
| 1233 ''' | |
| 1234 # Generate C++ code that wraps the fz_* API. | |
| 1235 | |
| 1236 if state.state_.have_done_build_0: | |
| 1237 # This -b 0 stage modifies global data, for example adding | |
| 1238 # begin() and end() methods to extras[], so must not be run | |
| 1239 # more than once. | |
| 1240 jlib.log( 'Skipping second -b 0') | |
| 1241 return | |
| 1242 | |
| 1243 jlib.log( 'Generating C++ source code ...') | |
| 1244 | |
| 1245 # On 32-bit Windows, libclang doesn't work. So we attempt to run 64-bit `-b | |
| 1246 # 0` to generate C++ code. | |
| 1247 jlib.log1( '{state.state_.windows=} {build_dirs.cpu.bits=}') | |
| 1248 if state.state_.windows and build_dirs.cpu.bits == 32: | |
| 1249 try: | |
| 1250 jlib.log( 'Windows 32-bit: trying dummy call of clang.cindex.Index.create()') | |
| 1251 state.clang.cindex.Index.create() | |
| 1252 except Exception as e: | |
| 1253 py = f'py -{state.python_version()}' | |
| 1254 jlib.log( 'libclang not available on win32; attempting to run separate 64-bit invocation of {sys.argv[0]} with `-b 0`.') | |
| 1255 # We use --venv-force-reinstall to workaround a problem where `pip | |
| 1256 # install libclang` seems to fail to install in the new 64-bit venv | |
| 1257 # if we are in a 'parent' venv created by pip itself. Maybe venv's | |
| 1258 # created by pip are somehow more sticky than plain venv's? | |
| 1259 # | |
| 1260 jlib.system( f'{py} {sys.argv[0]} --venv-force-reinstall -b 0') | |
| 1261 return | |
| 1262 | |
| 1263 namespace = 'mupdf' | |
| 1264 generated = cpp.Generated() | |
| 1265 | |
| 1266 cpp.cpp_source( | |
| 1267 build_dirs.dir_mupdf, | |
| 1268 namespace, | |
| 1269 f'{build_dirs.dir_mupdf}/platform/c++', | |
| 1270 header_git, | |
| 1271 generated, | |
| 1272 check_regress, | |
| 1273 clang_info_verbose, | |
| 1274 refcheck_if, | |
| 1275 trace_if, | |
| 1276 'debug' in build_dirs.dir_so, | |
| 1277 ) | |
| 1278 | |
| 1279 generated.save(f'{build_dirs.dir_mupdf}/platform/c++') | |
| 1280 | |
| 1281 def check_lists_equal(name, expected, actual): | |
| 1282 expected.sort() | |
| 1283 actual.sort() | |
| 1284 if expected != actual: | |
| 1285 text = f'Generated {name} filenames differ from expected:\n' | |
| 1286 text += f' expected {len(expected)}:\n' | |
| 1287 for i in expected: | |
| 1288 text += f' {i}\n' | |
| 1289 text += f' generated {len(actual)}:\n' | |
| 1290 for i in actual: | |
| 1291 text += f' {i}\n' | |
| 1292 raise Exception(text) | |
| 1293 check_lists_equal('C++ source', cpp_files, generated.cpp_files) | |
| 1294 check_lists_equal('C++ headers', h_files, generated.h_files) | |
| 1295 | |
| 1296 for dir_ in ( | |
| 1297 f'{build_dirs.dir_mupdf}/platform/c++/implementation/', | |
| 1298 f'{build_dirs.dir_mupdf}/platform/c++/include/', '.h', | |
| 1299 ): | |
| 1300 for path in jlib.fs_paths( dir_): | |
| 1301 path = path.replace('\\', '/') | |
| 1302 _, ext = os.path.splitext( path) | |
| 1303 if ext not in ('.h', '.cpp'): | |
| 1304 continue | |
| 1305 if path in h_files + cpp_files: | |
| 1306 continue | |
| 1307 jlib.log( 'Removing unknown C++ file: {path}') | |
| 1308 os.remove( path) | |
| 1309 | |
| 1310 jlib.log( 'Wrapper classes that are containers: {generated.container_classnames=}') | |
| 1311 | |
| 1312 # Output info about fz_*() functions that we don't make use | |
| 1313 # of in class methods. | |
| 1314 # | |
| 1315 # This is superseded by automatically finding functions to wrap. | |
| 1316 # | |
| 1317 if 0: # lgtm [py/unreachable-statement] | |
| 1318 jlib.log( 'functions that take struct args and are not used exactly once in methods:') | |
| 1319 num = 0 | |
| 1320 for name in sorted( fn_usage.keys()): | |
| 1321 n, cursor = fn_usage[ name] | |
| 1322 if n == 1: | |
| 1323 continue | |
| 1324 if not fn_has_struct_args( tu, cursor): | |
| 1325 continue | |
| 1326 jlib.log( ' {n} {cursor.displayname} -> {cursor.result_type.spelling}') | |
| 1327 num += 1 | |
| 1328 jlib.log( 'number of functions that we should maybe add wrappers for: {num}') | |
| 1329 | |
| 1330 | |
| 1331 def link_l_flags(sos): | |
| 1332 ld_origin = None | |
| 1333 if state.state_.pyodide: | |
| 1334 # Don't add '-Wl,-rpath*' etc if building for Pyodide. | |
| 1335 ld_origin = False | |
| 1336 ret = jlib.link_l_flags( sos, ld_origin) | |
| 1337 r = os.environ.get('LDFLAGS') | |
| 1338 if r: | |
| 1339 ret += f' {r}' | |
| 1340 return ret | |
| 1341 | |
| 1342 | |
| 1343 def build_so_windows( | |
| 1344 build_dirs, | |
| 1345 path_cpp, | |
| 1346 path_so, | |
| 1347 path_lib, | |
| 1348 *, | |
| 1349 defines=(), | |
| 1350 includes=(), | |
| 1351 libs=(), | |
| 1352 libpaths=(), | |
| 1353 debug=False, | |
| 1354 export=None, | |
| 1355 force_rebuild=False, | |
| 1356 ): | |
| 1357 ''' | |
| 1358 Compiles and links <path_cpp> into DLL <path_so> and .lib <path_lib>. | |
| 1359 ''' | |
| 1360 if isinstance(defines, str): defines = defines, | |
| 1361 if isinstance(includes, str): includes = includes, | |
| 1362 if isinstance(libs, str): libs = libs, | |
| 1363 if isinstance(libpaths, str): libpaths = libpaths, | |
| 1364 vs = wdev.WindowsVS() | |
| 1365 path_cpp_rel = os.path.relpath(path_cpp) | |
| 1366 path_o = f'{path_cpp}.o' | |
| 1367 # Compile. | |
| 1368 command = textwrap.dedent(f''' | |
| 1369 "{vs.vcvars}"&&"{vs.cl}" | |
| 1370 /D "UNICODE" | |
| 1371 /D "_UNICODE" | |
| 1372 /D "_WINDLL" | |
| 1373 /EHsc | |
| 1374 /Fo"{path_o}" | |
| 1375 /GS # Buffer security check. | |
| 1376 /O2 | |
| 1377 /Tp"{path_cpp_rel}" | |
| 1378 /W3 # Warning level, IDE default. | |
| 1379 /Zi # Debug Information Format | |
| 1380 /bigobj | |
| 1381 /c # Compile without linking. | |
| 1382 /diagnostics:caret | |
| 1383 /nologo | |
| 1384 /permissive- | |
| 1385 {'' if debug else '/D "NDEBUG"'} | |
| 1386 {'/MDd' if debug else '/MD'} # Multithread DLL run-time library | |
| 1387 ''') | |
| 1388 if sys.maxsize != 2**31 - 1: | |
| 1389 command += f' /D "WIN64"\n' | |
| 1390 for define in defines: | |
| 1391 command += f' /D "{define}"\n' | |
| 1392 for include in includes: | |
| 1393 command += f' /I"{include}"\n' | |
| 1394 infiles = [path_cpp] + list(includes) | |
| 1395 jlib.build( | |
| 1396 infiles, | |
| 1397 path_o, | |
| 1398 command, | |
| 1399 force_rebuild, | |
| 1400 ) | |
| 1401 # Link | |
| 1402 command = textwrap.dedent(f''' | |
| 1403 "{vs.vcvars}"&&"{vs.link}" | |
| 1404 /DLL # Builds a DLL. | |
| 1405 /IMPLIB:"{path_lib}" # Name of generated .lib. | |
| 1406 /OUT:"{path_so}" # Name of generated .dll. | |
| 1407 {'/DEBUG' if debug else ''} | |
| 1408 {path_o} | |
| 1409 ''') | |
| 1410 for lib in libs: | |
| 1411 command += f' "{lib}"\n' | |
| 1412 for libpath in libpaths: | |
| 1413 command += f' /LIBPATH:"{libpath}"\n' | |
| 1414 if export: | |
| 1415 command += f' /EXPORT:{export}' | |
| 1416 infiles = [path_o] + list(libs) | |
| 1417 jlib.build( | |
| 1418 infiles, | |
| 1419 path_so, | |
| 1420 command, | |
| 1421 force_rebuild, | |
| 1422 ) | |
| 1423 | |
| 1424 | |
| 1425 def build( build_dirs, swig_command, args, vs_upgrade, make_command): | |
| 1426 ''' | |
| 1427 Handles -b ... | |
| 1428 ''' | |
| 1429 cpp_files = [ | |
| 1430 f'{build_dirs.dir_mupdf}/platform/c++/implementation/classes.cpp', | |
| 1431 f'{build_dirs.dir_mupdf}/platform/c++/implementation/classes2.cpp', | |
| 1432 f'{build_dirs.dir_mupdf}/platform/c++/implementation/exceptions.cpp', | |
| 1433 f'{build_dirs.dir_mupdf}/platform/c++/implementation/functions.cpp', | |
| 1434 f'{build_dirs.dir_mupdf}/platform/c++/implementation/internal.cpp', | |
| 1435 f'{build_dirs.dir_mupdf}/platform/c++/implementation/extra.cpp', | |
| 1436 ] | |
| 1437 h_files = [ | |
| 1438 f'{build_dirs.dir_mupdf}/platform/c++/include/mupdf/classes.h', | |
| 1439 f'{build_dirs.dir_mupdf}/platform/c++/include/mupdf/classes2.h', | |
| 1440 f'{build_dirs.dir_mupdf}/platform/c++/include/mupdf/exceptions.h', | |
| 1441 f'{build_dirs.dir_mupdf}/platform/c++/include/mupdf/functions.h', | |
| 1442 f'{build_dirs.dir_mupdf}/platform/c++/include/mupdf/internal.h', | |
| 1443 f'{build_dirs.dir_mupdf}/platform/c++/include/mupdf/extra.h', | |
| 1444 ] | |
| 1445 build_python = True | |
| 1446 build_csharp = False | |
| 1447 check_regress = False | |
| 1448 clang_info_verbose = False | |
| 1449 force_rebuild = False | |
| 1450 header_git = False | |
| 1451 m_target = None | |
| 1452 m_vars = None | |
| 1453 j = 0 | |
| 1454 refcheck_if = '#ifndef NDEBUG' | |
| 1455 trace_if = '#ifndef NDEBUG' | |
| 1456 pyodide = state.state_.pyodide | |
| 1457 if pyodide: | |
| 1458 # Looks like Pyodide sets CXX to (for example) /tmp/tmp8h1meqsj/c++. We | |
| 1459 # don't evaluate it here, because that would force a rebuild each time | |
| 1460 # because of the command changing. | |
| 1461 assert os.environ.get('CXX', None), 'Pyodide build but $CXX not defined.' | |
| 1462 compiler = '$CXX' | |
| 1463 elif 'CXX' in os.environ: | |
| 1464 compiler = os.environ['CXX'] | |
| 1465 jlib.log(f'Setting compiler to {os.environ["CXX"]=}.') | |
| 1466 elif state.state_.macos: | |
| 1467 compiler = 'c++ -std=c++14' | |
| 1468 # Add extra flags for MacOS cross-compilation, where ARCHFLAGS can be | |
| 1469 # '-arch arm64'. | |
| 1470 # | |
| 1471 archflags = os.environ.get( 'ARCHFLAGS') | |
| 1472 if archflags: | |
| 1473 compiler += f' {archflags}' | |
| 1474 else: | |
| 1475 compiler = 'c++' | |
| 1476 | |
| 1477 state.state_.show_details = lambda name: False | |
| 1478 devenv = 'devenv.com' | |
| 1479 if state.state_.windows: | |
| 1480 # Search for devenv.com in standard locations. | |
| 1481 windows_vs = wdev.WindowsVS() | |
| 1482 devenv = windows_vs.devenv | |
| 1483 | |
| 1484 #jlib.log('{build_dirs.dir_so=}') | |
| 1485 details = list() | |
| 1486 | |
| 1487 while 1: | |
| 1488 try: | |
| 1489 actions = args.next() | |
| 1490 except StopIteration as e: | |
| 1491 raise Exception(f'Expected more `-b ...` args such as --python or <actions>') from e | |
| 1492 if 0: | |
| 1493 pass | |
| 1494 elif actions == '-f': | |
| 1495 force_rebuild = True | |
| 1496 elif actions == '--clang-verbose': | |
| 1497 clang_info_verbose = True | |
| 1498 elif actions == '-d': | |
| 1499 d = args.next() | |
| 1500 details.append( d) | |
| 1501 def fn(name): | |
| 1502 if not name: | |
| 1503 return | |
| 1504 for detail in details: | |
| 1505 if detail in name: | |
| 1506 return True | |
| 1507 state.state_.show_details = fn | |
| 1508 elif actions == '--devenv': | |
| 1509 devenv = args.next() | |
| 1510 jlib.log( '{devenv=}') | |
| 1511 windows_vs = None | |
| 1512 if not state.state_.windows: | |
| 1513 jlib.log( 'Warning: --devenv was specified, but we are not on Windows so this will have no effect.') | |
| 1514 elif actions == '-j': | |
| 1515 j = int(args.next()) | |
| 1516 elif actions == '--python': | |
| 1517 build_python = True | |
| 1518 build_csharp = False | |
| 1519 elif actions == '--csharp': | |
| 1520 build_python = False | |
| 1521 build_csharp = True | |
| 1522 elif actions == '--regress': | |
| 1523 check_regress = True | |
| 1524 elif actions == '--refcheck-if': | |
| 1525 refcheck_if = args.next() | |
| 1526 jlib.log( 'Have set {refcheck_if=}') | |
| 1527 elif actions == '--trace-if': | |
| 1528 trace_if = args.next() | |
| 1529 jlib.log( 'Have set {trace_if=}') | |
| 1530 elif actions == '--m-target': | |
| 1531 m_target = args.next() | |
| 1532 elif actions == '--m-vars': | |
| 1533 m_vars = args.next() | |
| 1534 elif actions.startswith( '-'): | |
| 1535 raise Exception( f'Unrecognised --build flag: {actions}') | |
| 1536 else: | |
| 1537 break | |
| 1538 | |
| 1539 if actions == 'all': | |
| 1540 actions = '0123' if state.state_.windows else 'm0123' | |
| 1541 | |
| 1542 dir_so_flags = os.path.basename( build_dirs.dir_so).split( '-') | |
| 1543 cflags = os.environ.get('XCXXFLAGS', '') | |
| 1544 | |
| 1545 windows_build_type = build_dirs.windows_build_type() | |
| 1546 so_version = get_so_version( build_dirs) | |
| 1547 | |
| 1548 for action in actions: | |
| 1549 with jlib.LogPrefixScope( f'{action}: '): | |
| 1550 jlib.log( '{action=}', 1) | |
| 1551 if action == '.': | |
| 1552 jlib.log('Ignoring build actions after "." in {actions!r}') | |
| 1553 break | |
| 1554 | |
| 1555 elif action == 'm': | |
| 1556 # Build libmupdf.so. | |
| 1557 if state.state_.windows: | |
| 1558 jlib.log( 'Ignoring `-b m` on Windows as not required.') | |
| 1559 else: | |
| 1560 jlib.log( 'Building libmupdf.so ...') | |
| 1561 command, actual_build_dir, suffix = _get_m_command( build_dirs, j, make_command, m_target, m_vars) | |
| 1562 jlib.system( command, prefix=jlib.log_text(), out='log', verbose=1) | |
| 1563 | |
| 1564 suffix2 = '.dylib' if state.state_.macos else '.so' | |
| 1565 p = f'{actual_build_dir}/libmupdf{suffix2}{so_version}' | |
| 1566 assert os.path.isfile(p), f'Does not exist: {p=}' | |
| 1567 | |
| 1568 if actual_build_dir != build_dirs.dir_so: | |
| 1569 # This happens when we are being run by | |
| 1570 # setup.py - it it might specify '-d | |
| 1571 # build/shared-release-x64-py3.8' (which | |
| 1572 # will be put into build_dirs.dir_so) but | |
| 1573 # the above 'make' command will create | |
| 1574 # build/shared-release/libmupdf.so, so we need | |
| 1575 # to copy into build/shared-release-x64-py3.8/. | |
| 1576 # | |
| 1577 jlib.fs_copy( f'{actual_build_dir}/libmupdf{suffix2}', f'{build_dirs.dir_so}/libmupdf{suffix2}', verbose=1) | |
| 1578 | |
| 1579 elif action == '0': | |
| 1580 build_0( | |
| 1581 build_dirs, | |
| 1582 header_git, | |
| 1583 check_regress, | |
| 1584 clang_info_verbose, | |
| 1585 refcheck_if, | |
| 1586 trace_if, | |
| 1587 cpp_files, | |
| 1588 h_files, | |
| 1589 ) | |
| 1590 | |
| 1591 elif action == '1': | |
| 1592 # Compile and link generated C++ code to create libmupdfcpp.so. | |
| 1593 if state.state_.windows: | |
| 1594 # We build mupdfcpp.dll using the .sln; it will | |
| 1595 # contain all C functions internally - there is | |
| 1596 # no mupdf.dll. | |
| 1597 # | |
| 1598 win32_infix = _windows_vs_upgrade( vs_upgrade, build_dirs, devenv) | |
| 1599 jlib.log(f'Building mupdfcpp.dll by running devenv ...') | |
| 1600 build = f'{windows_build_type}|{build_dirs.cpu.windows_config}' | |
| 1601 command = ( | |
| 1602 f'cd {build_dirs.dir_mupdf}&&' | |
| 1603 f'"{devenv}"' | |
| 1604 f' platform/{win32_infix}/mupdf.sln' | |
| 1605 f' /Build "{build}"' | |
| 1606 ) | |
| 1607 projects = ['mupdfcpp', 'libmuthreads'] | |
| 1608 if m_target: | |
| 1609 projects += m_target.split(',') | |
| 1610 for project in projects: | |
| 1611 command2 = f'{command} /Project {project}' | |
| 1612 jlib.system(command2, verbose=1, out='log') | |
| 1613 | |
| 1614 jlib.fs_copy( | |
| 1615 f'{build_dirs.dir_mupdf}/platform/{win32_infix}/{build_dirs.cpu.windows_subdir}{windows_build_type}/mupdfcpp{build_dirs.cpu.windows_suffix}.dll', | |
| 1616 f'{build_dirs.dir_so}/', | |
| 1617 verbose=1, | |
| 1618 ) | |
| 1619 | |
| 1620 else: | |
| 1621 jlib.log( 'Compiling generated C++ source code to create libmupdfcpp.so ...') | |
| 1622 include1 = f'{build_dirs.dir_mupdf}/include' | |
| 1623 include2 = f'{build_dirs.dir_mupdf}/platform/c++/include' | |
| 1624 cpp_files_text = '' | |
| 1625 for i in cpp_files: | |
| 1626 cpp_files_text += ' ' + os.path.relpath(i) | |
| 1627 libmupdfcpp = f'{build_dirs.dir_so}/libmupdfcpp.so{so_version}' | |
| 1628 libmupdf = f'{build_dirs.dir_so}/libmupdf.so{so_version}' | |
| 1629 if pyodide: | |
| 1630 # Compile/link separately. Otherwise | |
| 1631 # emsdk/upstream/bin/llvm-nm: error: a.out: No such | |
| 1632 # file or directory | |
| 1633 o_files = list() | |
| 1634 for cpp_file in cpp_files: | |
| 1635 o_file = f'{os.path.relpath(cpp_file)}.o' | |
| 1636 o_files.append(o_file) | |
| 1637 command = textwrap.dedent( | |
| 1638 f''' | |
| 1639 {compiler} | |
| 1640 -c | |
| 1641 -o {o_file} | |
| 1642 {build_dirs.cpp_flags} | |
| 1643 -fPIC | |
| 1644 {cflags} | |
| 1645 -I {include1} | |
| 1646 -I {include2} | |
| 1647 {cpp_file} | |
| 1648 ''') | |
| 1649 jlib.build( | |
| 1650 [include1, include2, cpp_file], | |
| 1651 o_file, | |
| 1652 command, | |
| 1653 force_rebuild, | |
| 1654 ) | |
| 1655 command = ( textwrap.dedent( | |
| 1656 f''' | |
| 1657 {compiler} | |
| 1658 -o {os.path.relpath(libmupdfcpp)} | |
| 1659 -sSIDE_MODULE | |
| 1660 {build_dirs.cpp_flags} | |
| 1661 -fPIC -shared | |
| 1662 -I {include1} | |
| 1663 -I {include2} | |
| 1664 {" ".join(o_files)} | |
| 1665 {link_l_flags(libmupdf)} | |
| 1666 ''') | |
| 1667 ) | |
| 1668 jlib.build( | |
| 1669 [include1, include2] + o_files, | |
| 1670 libmupdfcpp, | |
| 1671 command, | |
| 1672 force_rebuild, | |
| 1673 ) | |
| 1674 | |
| 1675 elif 'shared' in dir_so_flags: | |
| 1676 link_soname_arg = '' | |
| 1677 if state.state_.linux and so_version: | |
| 1678 link_soname_arg = f'-Wl,-soname,{os.path.basename(libmupdfcpp)}' | |
| 1679 command = ( textwrap.dedent( | |
| 1680 f''' | |
| 1681 {compiler} | |
| 1682 -o {os.path.relpath(libmupdfcpp)} | |
| 1683 {link_soname_arg} | |
| 1684 {build_dirs.cpp_flags} | |
| 1685 -fPIC -shared | |
| 1686 {cflags} | |
| 1687 -I {include1} | |
| 1688 -I {include2} | |
| 1689 {cpp_files_text} | |
| 1690 {link_l_flags(libmupdf)} | |
| 1691 ''') | |
| 1692 ) | |
| 1693 command_was_run = jlib.build( | |
| 1694 [include1, include2] + cpp_files, | |
| 1695 libmupdfcpp, | |
| 1696 command, | |
| 1697 force_rebuild, | |
| 1698 ) | |
| 1699 if command_was_run: | |
| 1700 macos_patch( libmupdfcpp, f'{build_dirs.dir_so}/libmupdf.dylib{so_version}') | |
| 1701 if so_version and state.state_.linux: | |
| 1702 jlib.system(f'ln -sf libmupdfcpp.so{so_version} {build_dirs.dir_so}/libmupdfcpp.so') | |
| 1703 | |
| 1704 elif 'fpic' in dir_so_flags: | |
| 1705 # We build a .so containing the C and C++ API. This | |
| 1706 # might be slightly faster than having separate C and | |
| 1707 # C++ API .so files, but probably makes no difference. | |
| 1708 # | |
| 1709 libmupdfcpp = f'{build_dirs.dir_so}/libmupdfcpp.a' | |
| 1710 libmupdf = []#[ f'{build_dirs.dir_so}/libmupdf.a', f'{build_dirs.dir_so}/libmupdf-third.a'] | |
| 1711 | |
| 1712 # Compile each .cpp file. | |
| 1713 ofiles = [] | |
| 1714 for cpp_file in cpp_files: | |
| 1715 ofile = f'{build_dirs.dir_so}/{os.path.basename(cpp_file)}.o' | |
| 1716 ofiles.append( ofile) | |
| 1717 command = ( textwrap.dedent( | |
| 1718 f''' | |
| 1719 {compiler} | |
| 1720 {build_dirs.cpp_flags} | |
| 1721 -fPIC | |
| 1722 -c | |
| 1723 {cflags} | |
| 1724 -I {include1} | |
| 1725 -I {include2} | |
| 1726 -o {ofile} | |
| 1727 {cpp_file} | |
| 1728 ''') | |
| 1729 ) | |
| 1730 jlib.build( | |
| 1731 [include1, include2, cpp_file], | |
| 1732 ofile, | |
| 1733 command, | |
| 1734 force_rebuild, | |
| 1735 verbose=True, | |
| 1736 ) | |
| 1737 | |
| 1738 # Create libmupdfcpp.a containing all .cpp.o files. | |
| 1739 if 0: | |
| 1740 libmupdfcpp_a = f'{build_dirs.dir_so}/libmupdfcpp.a' | |
| 1741 command = f'ar cr {libmupdfcpp_a} {" ".join(ofiles)}' | |
| 1742 jlib.build( | |
| 1743 ofiles, | |
| 1744 libmupdfcpp_a, | |
| 1745 command, | |
| 1746 force_rebuild, | |
| 1747 verbose=True, | |
| 1748 ) | |
| 1749 | |
| 1750 # Create libmupdfcpp.so from all .cpp and .c files. | |
| 1751 libmupdfcpp_so = f'{build_dirs.dir_so}/libmupdfcpp.so' | |
| 1752 alibs = [ | |
| 1753 f'{build_dirs.dir_so}/libmupdf.a', | |
| 1754 f'{build_dirs.dir_so}/libmupdf-third.a' | |
| 1755 ] | |
| 1756 command = textwrap.dedent( f''' | |
| 1757 {compiler} | |
| 1758 {build_dirs.cpp_flags} | |
| 1759 -fPIC -shared | |
| 1760 -o {libmupdfcpp_so} | |
| 1761 {' '.join(ofiles)} | |
| 1762 {' '.join(alibs)} | |
| 1763 ''') | |
| 1764 jlib.build( | |
| 1765 ofiles + alibs, | |
| 1766 libmupdfcpp_so, | |
| 1767 command, | |
| 1768 force_rebuild, | |
| 1769 verbose=True, | |
| 1770 ) | |
| 1771 else: | |
| 1772 assert 0, f'Leaf must start with "shared-" or "fpic-": build_dirs.dir_so={build_dirs.dir_so}' | |
| 1773 | |
| 1774 elif action == '2': | |
| 1775 # Use SWIG to generate source code for python/C# bindings. | |
| 1776 #generated = cpp.Generated(f'{build_dirs.dir_mupdf}/platform/c++') | |
| 1777 with open( f'{build_dirs.dir_mupdf}/platform/c++/generated.pickle', 'rb') as f: | |
| 1778 generated = pickle.load( f) | |
| 1779 generated.swig_cpp = generated.swig_cpp.getvalue() | |
| 1780 generated.swig_cpp_python = generated.swig_cpp_python.getvalue() | |
| 1781 generated.swig_python = generated.swig_python.getvalue() | |
| 1782 generated.swig_csharp = generated.swig_csharp.getvalue() | |
| 1783 | |
| 1784 if build_python: | |
| 1785 jlib.log( 'Generating mupdf_cppyy.py file.') | |
| 1786 make_cppyy.make_cppyy( state.state_, build_dirs, generated) | |
| 1787 | |
| 1788 jlib.log( 'Generating python module source code using SWIG ...') | |
| 1789 with jlib.LogPrefixScope( f'swig Python: '): | |
| 1790 # Generate C++ code for python module using SWIG. | |
| 1791 swig.build_swig( | |
| 1792 state.state_, | |
| 1793 build_dirs, | |
| 1794 generated, | |
| 1795 language='python', | |
| 1796 swig_command=swig_command, | |
| 1797 check_regress=check_regress, | |
| 1798 force_rebuild=force_rebuild, | |
| 1799 ) | |
| 1800 | |
| 1801 if build_csharp: | |
| 1802 # Generate C# using SWIG. | |
| 1803 jlib.log( 'Generating C# module source code using SWIG ...') | |
| 1804 with jlib.LogPrefixScope( f'swig C#: '): | |
| 1805 swig.build_swig( | |
| 1806 state.state_, | |
| 1807 build_dirs, | |
| 1808 generated, | |
| 1809 language='csharp', | |
| 1810 swig_command=swig_command, | |
| 1811 check_regress=check_regress, | |
| 1812 force_rebuild=force_rebuild, | |
| 1813 ) | |
| 1814 | |
| 1815 elif action == 'j': | |
| 1816 # Just experimenting. | |
| 1817 build_swig_java() | |
| 1818 | |
| 1819 | |
| 1820 elif action == '3': | |
| 1821 # Compile code from action=='2' to create Python/C# shared library. | |
| 1822 # | |
| 1823 if build_python: | |
| 1824 jlib.log( 'Compiling/linking generated Python module source code to create _mupdf.{"pyd" if state.state_.windows else "so"} ...') | |
| 1825 if build_csharp: | |
| 1826 jlib.log( 'Compiling/linking generated C# source code to create mupdfcsharp.{"dll" if state.state_.windows else "so"} ...') | |
| 1827 | |
| 1828 dir_so_flags = os.path.basename( build_dirs.dir_so).split( '-') | |
| 1829 debug = 'debug' in dir_so_flags | |
| 1830 | |
| 1831 if state.state_.windows: | |
| 1832 if build_python: | |
| 1833 wp = wdev.WindowsPython(build_dirs.cpu, build_dirs.python_version) | |
| 1834 jlib.log( '{wp=}:') | |
| 1835 if 0: | |
| 1836 # Show contents of include directory. | |
| 1837 for dirpath, dirnames, filenames in os.walk( wp.include): | |
| 1838 for f in filenames: | |
| 1839 p = os.path.join( dirpath, f) | |
| 1840 jlib.log( ' {p!r}') | |
| 1841 assert os.path.isfile( os.path.join( wp.include, 'Python.h')) | |
| 1842 jlib.log( 'Matching python for {build_dirs.cpu=} {wp.version=}: {wp.path=} {wp.include=} {wp.libs=}') | |
| 1843 # The swig-generated .cpp file must exist at | |
| 1844 # this point. | |
| 1845 # | |
| 1846 path_cpp = build_dirs.mupdfcpp_swig_cpp('python') | |
| 1847 path_cpp = os.path.relpath(path_cpp) # So we don't expose build machine details in __FILE__. | |
| 1848 assert os.path.exists(path_cpp), f'SWIG-generated file does not exist: {path_cpp}' | |
| 1849 | |
| 1850 if 1: | |
| 1851 # Build with direct invocation of cl.exe and link.exe. | |
| 1852 pf = pipcl.PythonFlags() | |
| 1853 path_o = f'{path_cpp}.o' | |
| 1854 mupdfcpp_lib = f'{build_dirs.dir_mupdf}/platform/win32/' | |
| 1855 | |
| 1856 if build_dirs.cpu.bits == 64: | |
| 1857 mupdfcpp_lib += 'x64/' | |
| 1858 mupdfcpp_lib += 'Debug/' if debug else 'Release/' | |
| 1859 mupdfcpp_lib += 'mupdfcpp64.lib' if build_dirs.cpu.bits == 64 else 'mupdfcpp.lib' | |
| 1860 build_so_windows( | |
| 1861 build_dirs, | |
| 1862 path_cpp = path_cpp, | |
| 1863 path_so = f'{build_dirs.dir_so}/_mupdf.pyd', | |
| 1864 path_lib = f'{build_dirs.dir_so}/_mupdf.lib', | |
| 1865 defines = ( | |
| 1866 'FZ_DLL_CLIENT', | |
| 1867 'SWIG_PYTHON_SILENT_MEMLEAK', | |
| 1868 ), | |
| 1869 includes = ( | |
| 1870 f'{build_dirs.dir_mupdf}/include', | |
| 1871 f'{build_dirs.dir_mupdf}/platform/c++/include', | |
| 1872 wp.include, | |
| 1873 ), | |
| 1874 libs = mupdfcpp_lib, | |
| 1875 libpaths = wp.libs, | |
| 1876 debug = debug, | |
| 1877 export = 'PyInit__mupdf', | |
| 1878 ) | |
| 1879 else: | |
| 1880 # Use VS devenv. | |
| 1881 env_extra = { | |
| 1882 'MUPDF_PYTHON_INCLUDE_PATH': f'{wp.include}', | |
| 1883 'MUPDF_PYTHON_LIBRARY_PATH': f'{wp.libs}', | |
| 1884 } | |
| 1885 jlib.log('{env_extra=}') | |
| 1886 | |
| 1887 | |
| 1888 # We need to update mtime of the .cpp file to | |
| 1889 # force recompile and link, because we run | |
| 1890 # devenv with different environmental variables | |
| 1891 # depending on the Python for which we are | |
| 1892 # building. | |
| 1893 # | |
| 1894 # [Using /Rebuild or /Clean appears to clean | |
| 1895 # the entire solution even if we specify | |
| 1896 # /Project.] | |
| 1897 # | |
| 1898 jlib.log(f'Touching file in case we are building for a different python version: {path_cpp=}') | |
| 1899 os.utime(path_cpp) | |
| 1900 | |
| 1901 win32_infix = _windows_vs_upgrade( vs_upgrade, build_dirs, devenv) | |
| 1902 jlib.log('Building mupdfpyswig project') | |
| 1903 command = ( | |
| 1904 f'cd {build_dirs.dir_mupdf}&&' | |
| 1905 f'"{devenv}"' | |
| 1906 f' platform/{win32_infix}/mupdfpyswig.sln' | |
| 1907 f' /Build "{windows_build_type}Python|{build_dirs.cpu.windows_config}"' | |
| 1908 f' /Project mupdfpyswig' | |
| 1909 ) | |
| 1910 jlib.system(command, verbose=1, out='log', env_extra=env_extra) | |
| 1911 | |
| 1912 jlib.fs_copy( | |
| 1913 f'{build_dirs.dir_mupdf}/platform/{win32_infix}/{build_dirs.cpu.windows_subdir}{windows_build_type}/mupdfpyswig.dll', | |
| 1914 f'{build_dirs.dir_so}/_mupdf.pyd', | |
| 1915 verbose=1, | |
| 1916 ) | |
| 1917 | |
| 1918 if build_csharp: | |
| 1919 # The swig-generated .cpp file must exist at | |
| 1920 # this point. | |
| 1921 # | |
| 1922 path_cpp = build_dirs.mupdfcpp_swig_cpp('csharp') | |
| 1923 path_cpp = os.path.relpath(path_cpp) # So we don't expose build machine details in __FILE__. | |
| 1924 assert os.path.exists(path_cpp), f'SWIG-generated file does not exist: {path_cpp}' | |
| 1925 | |
| 1926 if 1: | |
| 1927 path_o = f'{path_cpp}.o' | |
| 1928 mupdfcpp_lib = f'{build_dirs.dir_mupdf}/platform/win32/' | |
| 1929 if build_dirs.cpu.bits == 64: | |
| 1930 mupdfcpp_lib += 'x64/' | |
| 1931 mupdfcpp_lib += 'Debug/' if debug else 'Release/' | |
| 1932 mupdfcpp_lib += 'mupdfcpp64.lib' if build_dirs.cpu.bits == 64 else 'mupdfcpp.lib' | |
| 1933 build_so_windows( | |
| 1934 build_dirs, | |
| 1935 path_cpp = path_cpp, | |
| 1936 path_so = f'{build_dirs.dir_so}/mupdfcsharp.dll', | |
| 1937 path_lib = f'{build_dirs.dir_so}/mupdfcsharp.lib', | |
| 1938 defines = ( | |
| 1939 'FZ_DLL_CLIENT', | |
| 1940 ), | |
| 1941 includes = ( | |
| 1942 f'{build_dirs.dir_mupdf}/include', | |
| 1943 f'{build_dirs.dir_mupdf}/platform/c++/include', | |
| 1944 ), | |
| 1945 libs = mupdfcpp_lib, | |
| 1946 debug = debug, | |
| 1947 ) | |
| 1948 else: | |
| 1949 win32_infix = _windows_vs_upgrade( vs_upgrade, build_dirs, devenv) | |
| 1950 jlib.log('Building mupdfcsharp project') | |
| 1951 command = ( | |
| 1952 f'cd {build_dirs.dir_mupdf}&&' | |
| 1953 f'"{devenv}"' | |
| 1954 f' platform/{win32_infix}/mupdfcsharpswig.sln' | |
| 1955 f' /Build "{windows_build_type}Csharp|{build_dirs.cpu.windows_config}"' | |
| 1956 f' /Project mupdfcsharpswig' | |
| 1957 ) | |
| 1958 jlib.system(command, verbose=1, out='log') | |
| 1959 | |
| 1960 jlib.fs_copy( | |
| 1961 f'{build_dirs.dir_mupdf}/platform/{win32_infix}/{build_dirs.cpu.windows_subdir}{windows_build_type}/mupdfcsharpswig.dll', | |
| 1962 f'{build_dirs.dir_so}/mupdfcsharp.dll', | |
| 1963 verbose=1, | |
| 1964 ) | |
| 1965 | |
| 1966 else: | |
| 1967 # Not Windows. | |
| 1968 | |
| 1969 # We use c++ debug/release flags as implied by | |
| 1970 # --dir-so, but all builds output the same file | |
| 1971 # mupdf:platform/python/_mupdf.so. We could instead | |
| 1972 # generate mupdf.py and _mupdf.so in the --dir-so | |
| 1973 # directory? | |
| 1974 # | |
| 1975 # [While libmupdfcpp.so requires matching | |
| 1976 # debug/release build of libmupdf.so, it looks | |
| 1977 # like _mupdf.so does not require a matching | |
| 1978 # libmupdfcpp.so and libmupdf.so.] | |
| 1979 # | |
| 1980 flags_compile = '' | |
| 1981 flags_link = '' | |
| 1982 if build_python: | |
| 1983 # We use python-config which appears to | |
| 1984 # work better than pkg-config because | |
| 1985 # it copes with multiple installed | |
| 1986 # python's, e.g. manylinux_2014's | |
| 1987 # /opt/python/cp*-cp*/bin/python*. | |
| 1988 # | |
| 1989 # But... it seems that we should not | |
| 1990 # attempt to specify libpython on the link | |
| 1991 # command. The manylinux docker containers | |
| 1992 # don't actually contain libpython.so, and | |
| 1993 # it seems that this deliberate. And the | |
| 1994 # link command runs ok. | |
| 1995 # | |
| 1996 # todo: maybe instead use sysconfig.get_config_vars() ? | |
| 1997 # | |
| 1998 python_flags = pipcl.PythonFlags() | |
| 1999 flags_compile = python_flags.includes | |
| 2000 flags_link = python_flags.ldflags | |
| 2001 | |
| 2002 if state.state_.macos: | |
| 2003 # We need this to avoid numerous errors like: | |
| 2004 # | |
| 2005 # Undefined symbols for architecture x86_64: | |
| 2006 # "_PyArg_UnpackTuple", referenced from: | |
| 2007 # _wrap_ll_fz_warn(_object*, _object*) in mupdfcpp_swig-0a6733.o | |
| 2008 # _wrap_fz_warn(_object*, _object*) in mupdfcpp_swig-0a6733.o | |
| 2009 # ... | |
| 2010 flags_link += ' -undefined dynamic_lookup' | |
| 2011 | |
| 2012 jlib.log('flags_compile={flags_compile!r}') | |
| 2013 jlib.log('flags_link={flags_link!r}') | |
| 2014 | |
| 2015 # These are the input files to our g++ command: | |
| 2016 # | |
| 2017 include1 = f'{build_dirs.dir_mupdf}/include' | |
| 2018 include2 = f'{build_dirs.dir_mupdf}/platform/c++/include' | |
| 2019 | |
| 2020 if 'shared' in dir_so_flags: | |
| 2021 libmupdf = f'{build_dirs.dir_so}/libmupdf.so{so_version}' | |
| 2022 libmupdfthird = f'' | |
| 2023 libmupdfcpp = f'{build_dirs.dir_so}/libmupdfcpp.so{so_version}' | |
| 2024 elif 'fpic' in dir_so_flags: | |
| 2025 libmupdf = f'{build_dirs.dir_so}/libmupdf.a' | |
| 2026 libmupdfthird = f'{build_dirs.dir_so}/libmupdf-third.a' | |
| 2027 libmupdfcpp = f'{build_dirs.dir_so}/libmupdfcpp.a' | |
| 2028 else: | |
| 2029 assert 0, f'Leaf must start with "shared-" or "fpic-": build_dirs.dir_so={build_dirs.dir_so}' | |
| 2030 | |
| 2031 if build_python: | |
| 2032 cpp_path = build_dirs.mupdfcpp_swig_cpp('python') | |
| 2033 out_so = f'{build_dirs.dir_so}/_mupdf.so' | |
| 2034 elif build_csharp: | |
| 2035 cpp_path = build_dirs.mupdfcpp_swig_cpp('csharp') | |
| 2036 out_so = f'{build_dirs.dir_so}/mupdfcsharp.so' # todo: append {so_version} ? | |
| 2037 cpp_path = os.path.relpath(cpp_path) # So we don't expose build machine details in __FILE__. | |
| 2038 if state.state_.openbsd: | |
| 2039 # clang needs around 2G on OpenBSD. | |
| 2040 # | |
| 2041 soft, hard = resource.getrlimit( resource.RLIMIT_DATA) | |
| 2042 required = 3 * 2**30 | |
| 2043 if soft < required: | |
| 2044 if hard < required: | |
| 2045 jlib.log( 'Warning: RLIMIT_DATA {hard=} is less than {required=}.') | |
| 2046 soft_new = min(hard, required) | |
| 2047 resource.setrlimit( resource.RLIMIT_DATA, (soft_new, hard)) | |
| 2048 jlib.log( 'Have changed RLIMIT_DATA from {jlib.number_sep(soft)} to {jlib.number_sep(soft_new)}.') | |
| 2049 | |
| 2050 # We use link_l_flags() to add -L options to search parent | |
| 2051 # directories of each .so that we need, and -l with the .so | |
| 2052 # leafname without leading 'lib' or trailing '.so'. This | |
| 2053 # ensures that at runtime one can set LD_LIBRARY_PATH to | |
| 2054 # parent directories and have everything work. | |
| 2055 # | |
| 2056 | |
| 2057 # Build mupdf2.so | |
| 2058 if build_python: | |
| 2059 cpp2_path = f'{build_dirs.dir_mupdf}/platform/python/mupdfcpp2_swig.cpp' | |
| 2060 out2_so = f'{build_dirs.dir_so}/_mupdf2.so' | |
| 2061 if jlib.fs_filesize( cpp2_path): | |
| 2062 jlib.log( 'Compiling/linking mupdf2') | |
| 2063 command = ( textwrap.dedent( | |
| 2064 f''' | |
| 2065 {compiler} | |
| 2066 -o {os.path.relpath(out2_so)} | |
| 2067 {os.path.relpath(cpp2_path)} | |
| 2068 {build_dirs.cpp_flags} | |
| 2069 -fPIC | |
| 2070 --shared | |
| 2071 {cflags} | |
| 2072 -I {include1} | |
| 2073 -I {include2} | |
| 2074 {flags_compile} | |
| 2075 {flags_link2} | |
| 2076 {link_l_flags( [libmupdf, libmupdfcpp])} | |
| 2077 -Wno-deprecated-declarations | |
| 2078 ''') | |
| 2079 ) | |
| 2080 infiles = [ | |
| 2081 cpp2_path, | |
| 2082 include1, | |
| 2083 include2, | |
| 2084 libmupdf, | |
| 2085 libmupdfcpp, | |
| 2086 ] | |
| 2087 jlib.build( | |
| 2088 infiles, | |
| 2089 out2_so, | |
| 2090 command, | |
| 2091 force_rebuild, | |
| 2092 ) | |
| 2093 else: | |
| 2094 jlib.fs_remove( out2_so) | |
| 2095 jlib.fs_remove( f'{out2_so}.cmd') | |
| 2096 | |
| 2097 # Build _mupdf.so. | |
| 2098 # | |
| 2099 # We define SWIG_PYTHON_SILENT_MEMLEAK to avoid generating | |
| 2100 # lots of diagnostics `detected a memory leak of type | |
| 2101 # 'mupdf::PdfObj *', no destructor found.` when used with | |
| 2102 # mupdfpy. However it's not definitely known that these | |
| 2103 # diagnostics are spurious - seems to be to do with two | |
| 2104 # separate SWIG Python APIs (mupdf and mupdfpy's `extra` | |
| 2105 # module) using the same underlying C library. | |
| 2106 # | |
| 2107 sos = [] | |
| 2108 sos.append( f'{build_dirs.dir_so}/libmupdfcpp.so{so_version}') | |
| 2109 if os.path.basename( build_dirs.dir_so).startswith( 'shared-'): | |
| 2110 sos.append( f'{build_dirs.dir_so}/libmupdf.so{so_version}') | |
| 2111 if pyodide: | |
| 2112 # Need to use separate compilation/linking. | |
| 2113 o_file = f'{os.path.relpath(cpp_path)}.o' | |
| 2114 command = ( textwrap.dedent( | |
| 2115 f''' | |
| 2116 {compiler} | |
| 2117 -c | |
| 2118 -o {o_file} | |
| 2119 {cpp_path} | |
| 2120 {build_dirs.cpp_flags} | |
| 2121 -fPIC | |
| 2122 {cflags} | |
| 2123 -I {include1} | |
| 2124 -I {include2} | |
| 2125 {flags_compile} | |
| 2126 -Wno-deprecated-declarations | |
| 2127 -Wno-free-nonheap-object | |
| 2128 -DSWIG_PYTHON_SILENT_MEMLEAK | |
| 2129 ''') | |
| 2130 ) | |
| 2131 infiles = [ | |
| 2132 cpp_path, | |
| 2133 include1, | |
| 2134 include2, | |
| 2135 ] | |
| 2136 jlib.build( | |
| 2137 infiles, | |
| 2138 o_file, | |
| 2139 command, | |
| 2140 force_rebuild, | |
| 2141 ) | |
| 2142 | |
| 2143 command = ( textwrap.dedent( | |
| 2144 f''' | |
| 2145 {compiler} | |
| 2146 -o {os.path.relpath(out_so)} | |
| 2147 -sSIDE_MODULE | |
| 2148 {o_file} | |
| 2149 {build_dirs.cpp_flags} | |
| 2150 -shared | |
| 2151 {flags_link} | |
| 2152 {link_l_flags( sos)} | |
| 2153 ''') | |
| 2154 ) | |
| 2155 infiles = [ | |
| 2156 o_file, | |
| 2157 libmupdf, | |
| 2158 ] | |
| 2159 infiles += sos | |
| 2160 | |
| 2161 jlib.build( | |
| 2162 infiles, | |
| 2163 out_so, | |
| 2164 command, | |
| 2165 force_rebuild, | |
| 2166 ) | |
| 2167 else: | |
| 2168 # Not Pyodide. | |
| 2169 command = ( textwrap.dedent( | |
| 2170 f''' | |
| 2171 {compiler} | |
| 2172 -o {os.path.relpath(out_so)} | |
| 2173 {cpp_path} | |
| 2174 {build_dirs.cpp_flags} | |
| 2175 -fPIC | |
| 2176 -shared | |
| 2177 {cflags} | |
| 2178 -I {include1} | |
| 2179 -I {include2} | |
| 2180 {flags_compile} | |
| 2181 -Wno-deprecated-declarations | |
| 2182 -Wno-free-nonheap-object | |
| 2183 -DSWIG_PYTHON_SILENT_MEMLEAK | |
| 2184 {flags_link} | |
| 2185 {link_l_flags( sos)} | |
| 2186 ''') | |
| 2187 ) | |
| 2188 infiles = [ | |
| 2189 cpp_path, | |
| 2190 include1, | |
| 2191 include2, | |
| 2192 libmupdf, | |
| 2193 ] | |
| 2194 infiles += sos | |
| 2195 | |
| 2196 command_was_run = jlib.build( | |
| 2197 infiles, | |
| 2198 out_so, | |
| 2199 command, | |
| 2200 force_rebuild, | |
| 2201 ) | |
| 2202 if command_was_run: | |
| 2203 macos_patch( out_so, | |
| 2204 f'{build_dirs.dir_so}/libmupdf.dylib{so_version}', | |
| 2205 f'{build_dirs.dir_so}/libmupdfcpp.so{so_version}', | |
| 2206 ) | |
| 2207 else: | |
| 2208 raise Exception( 'unrecognised --build action %r' % action) | |
| 2209 | |
| 2210 | |
| 2211 def python_settings(build_dirs, startdir=None): | |
| 2212 # We need to set LD_LIBRARY_PATH and PYTHONPATH so that our | |
| 2213 # test .py programme can load mupdf.py and _mupdf.so. | |
| 2214 if build_dirs.dir_so is None: | |
| 2215 # Use no extra environment and default python, e.g. in venv. | |
| 2216 jlib.log('build_dirs.dir_so is None, returning empty extra environment and "python"') | |
| 2217 return {}, 'python' | |
| 2218 | |
| 2219 env_extra = {} | |
| 2220 env_extra[ 'PYTHONPATH'] = os.path.relpath(build_dirs.dir_so, startdir) | |
| 2221 | |
| 2222 command_prefix = '' | |
| 2223 if state.state_.windows: | |
| 2224 # On Windows, it seems that 'py' runs the default | |
| 2225 # python. Also, Windows appears to be able to find | |
| 2226 # _mupdf.pyd in same directory as mupdf.py. | |
| 2227 # | |
| 2228 wp = wdev.WindowsPython(build_dirs.cpu, build_dirs.python_version) | |
| 2229 python_path = wp.path.replace('\\', '/') # Allows use on Cygwin. | |
| 2230 command_prefix = f'"{python_path}"' | |
| 2231 else: | |
| 2232 pass | |
| 2233 # We build _mupdf.so using `-Wl,-rpath='$ORIGIN,-z,origin` (see | |
| 2234 # link_l_flags()) so we don't need to set `LD_LIBRARY_PATH`. | |
| 2235 # | |
| 2236 # But if we did set `LD_LIBRARY_PATH`, it would be with: | |
| 2237 # | |
| 2238 # env_extra[ 'LD_LIBRARY_PATH'] = os.path.abspath(build_dirs.dir_so) | |
| 2239 # | |
| 2240 return env_extra, command_prefix | |
| 2241 | |
| 2242 def make_docs( build_dirs, languages_original): | |
| 2243 | |
| 2244 languages = languages_original | |
| 2245 if languages == 'all': | |
| 2246 languages = 'c,c++,python' | |
| 2247 languages = languages.split( ',') | |
| 2248 | |
| 2249 def do_doxygen( name, outdir, path): | |
| 2250 ''' | |
| 2251 name: | |
| 2252 Doxygen PROJECT_NAME of generated documentation | |
| 2253 outdir: | |
| 2254 Directory in which we run doxygen, so root of generated | |
| 2255 documentation will be in <outdir>/html/index.html | |
| 2256 path: | |
| 2257 Doxygen INPUT setting; this is the path of the directory which | |
| 2258 contains the API to document. If a relative path, it should be | |
| 2259 relative to <outdir>. | |
| 2260 ''' | |
| 2261 # We generate a blank doxygen configuration file, make | |
| 2262 # some minimal changes, then run doxygen on the modified | |
| 2263 # configuration. | |
| 2264 # | |
| 2265 assert 'docs/generated/' in outdir | |
| 2266 jlib.fs_ensure_empty_dir( outdir) | |
| 2267 dname = f'{name}.doxygen' | |
| 2268 dname2 = os.path.join( outdir, dname) | |
| 2269 jlib.system( f'cd {outdir}; rm -f {dname}0; doxygen -g {dname}0', out='return') | |
| 2270 with open( dname2+'0') as f: | |
| 2271 dtext = f.read() | |
| 2272 dtext, n = re.subn( '\nPROJECT_NAME *=.*\n', f'\nPROJECT_NAME = {name}\n', dtext) | |
| 2273 assert n == 1 | |
| 2274 dtext, n = re.subn( '\nEXTRACT_ALL *=.*\n', f'\nEXTRACT_ALL = YES\n', dtext) | |
| 2275 assert n == 1 | |
| 2276 dtext, n = re.subn( '\nINPUT *=.*\n', f'\nINPUT = {path}\n', dtext) | |
| 2277 assert n == 1 | |
| 2278 dtext, n = re.subn( '\nRECURSIVE *=.*\n', f'\nRECURSIVE = YES\n', dtext) | |
| 2279 with open( dname2, 'w') as f: | |
| 2280 f.write( dtext) | |
| 2281 #jlib.system( f'diff -u {dname2}0 {dname2}', raise_errors=False) | |
| 2282 command = f'cd {outdir}; doxygen {dname}' | |
| 2283 jlib.system( command, out='return', verbose=1) | |
| 2284 jlib.log( 'have created: {outdir}/html/index.html') | |
| 2285 | |
| 2286 out_dir = f'{build_dirs.dir_mupdf}/docs/generated' | |
| 2287 | |
| 2288 for language in languages: | |
| 2289 | |
| 2290 if language == 'c': | |
| 2291 do_doxygen( 'mupdf', f'{out_dir}/c', f'{build_dirs.dir_mupdf}/include') | |
| 2292 | |
| 2293 elif language == 'c++': | |
| 2294 do_doxygen( 'mupdfcpp', f'{out_dir}/c++', f'{build_dirs.dir_mupdf}/platform/c++/include') | |
| 2295 | |
| 2296 elif language == 'python': | |
| 2297 ld_library_path = os.path.abspath( f'{build_dirs.dir_so}') | |
| 2298 jlib.fs_ensure_empty_dir( f'{out_dir}/python') | |
| 2299 pythonpath = os.path.relpath( f'{build_dirs.dir_so}', f'{out_dir}/python') | |
| 2300 input_relpath = os.path.relpath( f'{build_dirs.dir_so}/mupdf.py', f'{out_dir}/python') | |
| 2301 jlib.system( | |
| 2302 f'cd {out_dir}/python && LD_LIBRARY_PATH={ld_library_path} PYTHONPATH={pythonpath} pydoc3 -w {input_relpath}', | |
| 2303 out='log', | |
| 2304 verbose=True, | |
| 2305 ) | |
| 2306 path = f'{out_dir}/python/mupdf.html' | |
| 2307 assert os.path.isfile( path) | |
| 2308 | |
| 2309 # Add some styling. | |
| 2310 # | |
| 2311 with open( path) as f: | |
| 2312 text = f.read() | |
| 2313 | |
| 2314 m1 = re.search( '[<]/head[>][<]body[^>]*[>]\n', text) | |
| 2315 m2 = re.search( '[<]/body[>]', text) | |
| 2316 assert m1 | |
| 2317 assert m2 | |
| 2318 #jlib.log( '{=m1.start() m1.end() m2.start() m2.end()}') | |
| 2319 | |
| 2320 a = text[ : m1.start()] | |
| 2321 b = textwrap.dedent(''' | |
| 2322 <link href="../../../../../css/default.css" rel="stylesheet" type="text/css" /> | |
| 2323 <link href="../../../../../css/language-bindings.css" rel="stylesheet" type="text/css" /> | |
| 2324 ''') | |
| 2325 c = text[ m1.start() : m1.end()] | |
| 2326 d = textwrap.dedent(''' | |
| 2327 <main style="display:block;"> | |
| 2328 <a class="no-underline" href="../../../index.html"> | |
| 2329 <div class="banner" role="heading" aria-level="1"> | |
| 2330 <h1>MuPDF Python bindings</h1> | |
| 2331 </div> | |
| 2332 </a> | |
| 2333 <div class="outer"> | |
| 2334 <div class="inner"> | |
| 2335 ''') | |
| 2336 e = text[ m1.end() : m2.end()] | |
| 2337 f = textwrap.dedent(''' | |
| 2338 </div></div> | |
| 2339 </main> | |
| 2340 ''') | |
| 2341 g = text[ m2.end() : ] | |
| 2342 text = a + b + c + d + e + f + g | |
| 2343 with open( path, 'w') as f: | |
| 2344 f.write( text) | |
| 2345 jlib.log( 'have created: {path}') | |
| 2346 | |
| 2347 else: | |
| 2348 raise Exception( f'unrecognised language param: {lang}') | |
| 2349 | |
| 2350 make_docs_index( build_dirs, languages_original) | |
| 2351 | |
| 2352 | |
| 2353 def make_docs_index( build_dirs, languages_original): | |
| 2354 # Create index.html with links to the different bindings' | |
| 2355 # documentation. | |
| 2356 # | |
| 2357 #mupdf_dir = os.path.abspath( f'{__file__}/../../..') | |
| 2358 out_dir = f'{build_dirs.dir_mupdf}/docs/generated' | |
| 2359 top_index_html = f'{out_dir}/index.html' | |
| 2360 with open( top_index_html, 'w') as f: | |
| 2361 git_id = jlib.git_get_id( build_dirs.dir_mupdf) | |
| 2362 git_id = git_id.split( '\n')[0] | |
| 2363 f.write( textwrap.dedent( f''' | |
| 2364 <!DOCTYPE html> | |
| 2365 | |
| 2366 <html lang="en"> | |
| 2367 <head> | |
| 2368 <link href="../../css/default.css" rel="stylesheet" type="text/css" /> | |
| 2369 <link href="../../css/language-bindings.css" rel="stylesheet" type="text/css" /> | |
| 2370 </head> | |
| 2371 <body> | |
| 2372 <main style="display:block;"> | |
| 2373 <div class="banner" role="heading" aria-level="1"> | |
| 2374 <h1>MuPDF bindings</h1> | |
| 2375 </div> | |
| 2376 <div class="outer"> | |
| 2377 <div class="inner"> | |
| 2378 <ul> | |
| 2379 <li><a href="c/html/index.html">C</a> (generated by Doxygen). | |
| 2380 <li><a href="c++/html/index.html">C++</a> (generated by Doxygen). | |
| 2381 <li><a href="python/mupdf.html">Python</a> (generated by Pydoc). | |
| 2382 </ul> | |
| 2383 <small> | |
| 2384 <p>Generation:</p> | |
| 2385 <ul> | |
| 2386 <li>Date: {jlib.date_time()} | |
| 2387 <li>Git: {git_id} | |
| 2388 <li>Command: <code>./scripts/mupdfwrap.py --doc {languages_original}</code> | |
| 2389 </ul> | |
| 2390 </small> | |
| 2391 </div> | |
| 2392 </div> | |
| 2393 </main> | |
| 2394 </body> | |
| 2395 </html> | |
| 2396 ''' | |
| 2397 )) | |
| 2398 jlib.log( 'Have created: {top_index_html}') | |
| 2399 | |
| 2400 | |
| 2401 | |
| 2402 def main2(): | |
| 2403 | |
| 2404 assert not state.state_.cygwin, \ | |
| 2405 f'This script does not run properly under Cygwin, use `py ...`' | |
| 2406 | |
| 2407 # Set default build directory. Can be overridden by '-d'. | |
| 2408 # | |
| 2409 build_dirs = state.BuildDirs() | |
| 2410 | |
| 2411 # Set default swig and make. | |
| 2412 # | |
| 2413 swig_command = 'swig' | |
| 2414 make_command = None | |
| 2415 | |
| 2416 # Whether to use `devenv.com /upgrade`. | |
| 2417 # | |
| 2418 vs_upgrade = False | |
| 2419 | |
| 2420 args = jlib.Args( sys.argv[1:]) | |
| 2421 arg_i = 0 | |
| 2422 while 1: | |
| 2423 try: | |
| 2424 arg = args.next() | |
| 2425 except StopIteration: | |
| 2426 break | |
| 2427 #log( 'Handling {arg=}') | |
| 2428 | |
| 2429 arg_i += 1 | |
| 2430 | |
| 2431 with jlib.LogPrefixScope( f'{arg}: '): | |
| 2432 | |
| 2433 if arg == '-h' or arg == '--help': | |
| 2434 print( __doc__) | |
| 2435 | |
| 2436 elif arg == '--build' or arg == '-b': | |
| 2437 build( build_dirs, swig_command, args, vs_upgrade, make_command) | |
| 2438 | |
| 2439 elif arg == '--check-headers': | |
| 2440 keep_going = False | |
| 2441 path = args.next() | |
| 2442 if path == '-k': | |
| 2443 keep_going = True | |
| 2444 path = args.next() | |
| 2445 include_dir = os.path.relpath( f'{build_dirs.dir_mupdf}/include') | |
| 2446 def paths(): | |
| 2447 if path.endswith( '+'): | |
| 2448 active = False | |
| 2449 for p in jlib.fs_paths( include_dir): | |
| 2450 if not active and p == path[:-1]: | |
| 2451 active = True | |
| 2452 if not active: | |
| 2453 continue | |
| 2454 if p.endswith( '.h'): | |
| 2455 yield p | |
| 2456 elif path == 'all': | |
| 2457 for p in jlib.fs_paths( include_dir): | |
| 2458 if p.endswith( '.h'): | |
| 2459 yield p | |
| 2460 else: | |
| 2461 yield path | |
| 2462 failed_paths = [] | |
| 2463 for path in paths(): | |
| 2464 if path.endswith( '/mupdf/pdf/name-table.h'): | |
| 2465 # Not a normal header. | |
| 2466 continue | |
| 2467 if path.endswith( '.h'): | |
| 2468 e = jlib.system( f'cc -I {include_dir} {path}', out='log', raise_errors=False, verbose=1) | |
| 2469 if e: | |
| 2470 if keep_going: | |
| 2471 failed_paths.append( path) | |
| 2472 else: | |
| 2473 sys.exit( 1) | |
| 2474 if failed_paths: | |
| 2475 jlib.log( 'Following headers are not self-contained:') | |
| 2476 for path in failed_paths: | |
| 2477 jlib.log( f' {path}') | |
| 2478 sys.exit( 1) | |
| 2479 | |
| 2480 elif arg == '--compare-fz_usage': | |
| 2481 directory = args.next() | |
| 2482 compare_fz_usage( tu, directory, fn_usage) | |
| 2483 | |
| 2484 elif arg == '--diff': | |
| 2485 for path in jlib.fs_paths( build_dirs.ref_dir): | |
| 2486 #log( '{path=}') | |
| 2487 assert path.startswith( build_dirs.ref_dir) | |
| 2488 if not path.endswith( '.h') and not path.endswith( '.cpp'): | |
| 2489 continue | |
| 2490 tail = path[ len( build_dirs.ref_dir):] | |
| 2491 path2 = f'{build_dirs.dir_mupdf}/platform/c++/{tail}' | |
| 2492 command = f'diff -u {path} {path2}' | |
| 2493 jlib.log( 'running: {command}') | |
| 2494 jlib.system( | |
| 2495 command, | |
| 2496 raise_errors=False, | |
| 2497 out='log', | |
| 2498 ) | |
| 2499 | |
| 2500 elif arg == '--diff-all': | |
| 2501 for a, b in ( | |
| 2502 (f'{build_dirs.dir_mupdf}/platform/c++/', f'{build_dirs.dir_mupdf}/platform/c++/'), | |
| 2503 (f'{build_dirs.dir_mupdf}/platform/python/', f'{build_dirs.dir_mupdf}/platform/python/') | |
| 2504 ): | |
| 2505 for dirpath, dirnames, filenames in os.walk( a): | |
| 2506 assert dirpath.startswith( a) | |
| 2507 root = dirpath[len(a):] | |
| 2508 for filename in filenames: | |
| 2509 a_path = os.path.join(dirpath, filename) | |
| 2510 b_path = os.path.join( b, root, filename) | |
| 2511 command = f'diff -u {a_path} {b_path}' | |
| 2512 jlib.system( command, out='log', raise_errors=False) | |
| 2513 | |
| 2514 elif arg == '--doc': | |
| 2515 languages = args.next() | |
| 2516 make_docs( build_dirs, languages) | |
| 2517 | |
| 2518 elif arg == '--doc-index': | |
| 2519 languages = args.next() | |
| 2520 make_docs_index( build_dirs, languages) | |
| 2521 | |
| 2522 elif arg == '--make': | |
| 2523 make_command = args.next() | |
| 2524 | |
| 2525 elif arg == '--ref': | |
| 2526 assert 'mupdfwrap_ref' in build_dirs.ref_dir | |
| 2527 jlib.system( | |
| 2528 f'rm -r {build_dirs.ref_dir}', | |
| 2529 raise_errors=False, | |
| 2530 out='log', | |
| 2531 ) | |
| 2532 jlib.system( | |
| 2533 f'rsync -ai {build_dirs.dir_mupdf}/platform/c++/implementation {build_dirs.ref_dir}', | |
| 2534 out='log', | |
| 2535 ) | |
| 2536 jlib.system( | |
| 2537 f'rsync -ai {build_dirs.dir_mupdf}/platform/c++/include {build_dirs.ref_dir}', | |
| 2538 out='log', | |
| 2539 ) | |
| 2540 | |
| 2541 elif arg == '--dir-so' or arg == '-d': | |
| 2542 d = args.next() | |
| 2543 build_dirs.set_dir_so( d) | |
| 2544 #jlib.log('Have set {build_dirs=}') | |
| 2545 | |
| 2546 elif arg == '--py-package-multi': | |
| 2547 # Investigating different combinations of pip, pyproject.toml, | |
| 2548 # setup.py | |
| 2549 # | |
| 2550 def system(command): | |
| 2551 jlib.system(command, verbose=1, out='log') | |
| 2552 system( '(rm -r pylocal-multi dist || true)') | |
| 2553 system( './setup.py sdist') | |
| 2554 system( 'cp -p pyproject.toml pyproject.toml-0') | |
| 2555 results = dict() | |
| 2556 try: | |
| 2557 for toml in 0, 1: | |
| 2558 for pip_upgrade in 0, 1: | |
| 2559 for do_wheel in 0, 1: | |
| 2560 with jlib.LogPrefixScope(f'toml={toml} pip_upgrade={pip_upgrade} do_wheel={do_wheel}: '): | |
| 2561 #print(f'jlib.g_log_prefixes={jlib.g_log_prefixes}') | |
| 2562 #print(f'jlib.g_log_prefix_scopes.items={jlib.g_log_prefix_scopes.items}') | |
| 2563 #print(f'jlib.log_text(""): {jlib.log_text("")}') | |
| 2564 result_key = toml, pip_upgrade, do_wheel | |
| 2565 jlib.log( '') | |
| 2566 jlib.log( '=== {pip_upgrade=} {do_wheel=}') | |
| 2567 if toml: | |
| 2568 system( 'cp -p pyproject.toml-0 pyproject.toml') | |
| 2569 else: | |
| 2570 system( 'rm pyproject.toml || true') | |
| 2571 system( 'ls -l pyproject.toml || true') | |
| 2572 system( | |
| 2573 '(rm -r pylocal-multi wheels || true)' | |
| 2574 ' && python3 -m venv pylocal-multi' | |
| 2575 ' && . pylocal-multi/bin/activate' | |
| 2576 ' && pip install clang' | |
| 2577 ) | |
| 2578 try: | |
| 2579 if pip_upgrade: | |
| 2580 system( '. pylocal-multi/bin/activate && pip install --upgrade pip') | |
| 2581 if do_wheel: | |
| 2582 system( '. pylocal-multi/bin/activate && pip install check-wheel-contents') | |
| 2583 system( '. pylocal-multi/bin/activate && pip wheel --wheel-dir wheels dist/*') | |
| 2584 system( '. pylocal-multi/bin/activate && check-wheel-contents wheels/*') | |
| 2585 system( '. pylocal-multi/bin/activate && pip install wheels/*') | |
| 2586 else: | |
| 2587 system( '. pylocal-multi/bin/activate && pip install dist/*') | |
| 2588 #system( './scripts/mupdfwrap_test.py') | |
| 2589 system( '. pylocal-multi/bin/activate && python -m mupdf') | |
| 2590 except Exception as ee: | |
| 2591 e = ee | |
| 2592 else: | |
| 2593 e = 0 | |
| 2594 results[ result_key] = e | |
| 2595 jlib.log( '== {e=}') | |
| 2596 jlib.log( '== Results:') | |
| 2597 for (toml, pip_upgrade, do_wheel), e in results.items(): | |
| 2598 jlib.log( ' {toml=} {pip_upgrade=} {do_wheel=}: {e=}') | |
| 2599 finally: | |
| 2600 system( 'cp -p pyproject.toml-0 pyproject.toml') | |
| 2601 | |
| 2602 elif arg == '--run-py': | |
| 2603 command = '' | |
| 2604 while 1: | |
| 2605 try: | |
| 2606 command += ' ' + args.next() | |
| 2607 except StopIteration: | |
| 2608 break | |
| 2609 | |
| 2610 ld_library_path = os.path.abspath( f'{build_dirs.dir_so}') | |
| 2611 pythonpath = build_dirs.dir_so | |
| 2612 | |
| 2613 envs = f'LD_LIBRARY_PATH={ld_library_path} PYTHONPATH={pythonpath}' | |
| 2614 command = f'{envs} {command}' | |
| 2615 jlib.log( 'running: {command}') | |
| 2616 e = jlib.system( | |
| 2617 command, | |
| 2618 raise_errors=False, | |
| 2619 verbose=False, | |
| 2620 out='log', | |
| 2621 ) | |
| 2622 sys.exit(e) | |
| 2623 | |
| 2624 elif arg == '--show-ast': | |
| 2625 filename = args.next() | |
| 2626 includes = args.next() | |
| 2627 parse.show_ast( filename, includes) | |
| 2628 | |
| 2629 elif arg == '--swig': | |
| 2630 swig_command = args.next() | |
| 2631 | |
| 2632 elif arg == '--swig-windows-auto': | |
| 2633 if state.state_.windows: | |
| 2634 import stat | |
| 2635 import urllib.request | |
| 2636 import zipfile | |
| 2637 name = 'swigwin-4.0.2' | |
| 2638 | |
| 2639 # Download swig .zip file if not already present. | |
| 2640 # | |
| 2641 if not os.path.exists(f'{name}.zip'): | |
| 2642 url = f'http://prdownloads.sourceforge.net/swig/{name}.zip' | |
| 2643 jlib.log(f'Downloading Windows SWIG from: {url}') | |
| 2644 with urllib.request.urlopen(url) as response: | |
| 2645 with open(f'{name}.zip-', 'wb') as f: | |
| 2646 shutil.copyfileobj(response, f) | |
| 2647 os.rename(f'{name}.zip-', f'{name}.zip') | |
| 2648 | |
| 2649 # Extract swig from .zip file if not already extracted. | |
| 2650 # | |
| 2651 swig_local = f'{name}/swig.exe' | |
| 2652 if not os.path.exists(swig_local): | |
| 2653 # Extract | |
| 2654 z = zipfile.ZipFile(f'{name}.zip') | |
| 2655 jlib.fs_ensure_empty_dir(f'{name}-0') | |
| 2656 z.extractall(f'{name}-0') | |
| 2657 os.rename(f'{name}-0/{name}', name) | |
| 2658 os.rmdir(f'{name}-0') | |
| 2659 | |
| 2660 # Need to make swig.exe executable. | |
| 2661 swig_local_stat = os.stat(swig_local) | |
| 2662 os.chmod(swig_local, swig_local_stat.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) | |
| 2663 | |
| 2664 # Set our <swig> to be the local windows swig.exe. | |
| 2665 # | |
| 2666 swig_command = swig_local | |
| 2667 else: | |
| 2668 jlib.log('Ignoring {arg} because not running on Windows') | |
| 2669 | |
| 2670 elif arg == '--sync-pretty': | |
| 2671 destination = args.next() | |
| 2672 jlib.log( 'Syncing to {destination=}') | |
| 2673 generated = cpp.Generated(f'{build_dirs.dir_mupdf}/platform/c++') | |
| 2674 files = generated.h_files + generated.cpp_files + [ | |
| 2675 f'{build_dirs.dir_so}/mupdf.py', | |
| 2676 f'{build_dirs.dir_mupdf}/platform/c++/fn_usage.txt', | |
| 2677 ] | |
| 2678 # Generate .html files with syntax colouring for source files. See: | |
| 2679 # https://github.com/google/code-prettify | |
| 2680 # | |
| 2681 files_html = [] | |
| 2682 for i in files: | |
| 2683 if os.path.splitext( i)[1] not in ( '.h', '.cpp', '.py'): | |
| 2684 continue | |
| 2685 o = f'{i}.html' | |
| 2686 jlib.log( 'converting {i} to {o}') | |
| 2687 with open( i) as f: | |
| 2688 text = f.read() | |
| 2689 with open( o, 'w') as f: | |
| 2690 f.write( '<html><body>\n') | |
| 2691 f.write( '<script src="https://cdn.jsdelivr.net/gh/google/code-prettify@master/loader/run_prettify.js"></script>\n') | |
| 2692 f.write( '<pre class="prettyprint">\n') | |
| 2693 f.write( text) | |
| 2694 f.write( '</pre>\n') | |
| 2695 f.write( '</body></html>\n') | |
| 2696 files_html.append( o) | |
| 2697 | |
| 2698 files += files_html | |
| 2699 | |
| 2700 # Insert extra './' into each path so that rsync -R uses the | |
| 2701 # 'mupdf/...' tail of each local path for the remote path. | |
| 2702 # | |
| 2703 for i in range( len( files)): | |
| 2704 files[i] = files[i].replace( '/mupdf/', '/./mupdf/') | |
| 2705 files[i] = files[i].replace( '/tmp/', '/tmp/./') | |
| 2706 | |
| 2707 jlib.system( f'rsync -aiRz {" ".join( files)} {destination}', verbose=1, out='log') | |
| 2708 | |
| 2709 elif arg == '--sync-docs': | |
| 2710 # We use extra './' so that -R uses remaining path on | |
| 2711 # destination. | |
| 2712 # | |
| 2713 destination = args.next() | |
| 2714 jlib.system( f'rsync -aiRz {build_dirs.dir_mupdf}/docs/generated/./ {destination}', verbose=1, out='log') | |
| 2715 | |
| 2716 elif arg == '--test-cpp': | |
| 2717 testfile = os.path.abspath( f'{__file__}/../../../thirdparty/zlib/zlib.3.pdf') | |
| 2718 testfile = testfile.replace('\\', '/') | |
| 2719 src = f'{build_dirs.dir_mupdf}/scripts/mupdfwrap_test.cpp' | |
| 2720 exe = f'{build_dirs.dir_mupdf}/scripts/mupdfwrap_test.cpp.exe' | |
| 2721 includes = ( | |
| 2722 f' -I {build_dirs.dir_mupdf}/include' | |
| 2723 f' -I {build_dirs.dir_mupdf}/platform/c++/include' | |
| 2724 ) | |
| 2725 cpp_flags = build_dirs.cpp_flags | |
| 2726 if state.state_.windows: | |
| 2727 win32_infix = _windows_vs_upgrade( vs_upgrade, build_dirs, devenv=None) | |
| 2728 windows_build_type = build_dirs.windows_build_type() | |
| 2729 lib = f'{build_dirs.dir_mupdf}/platform/{win32_infix}/{build_dirs.cpu.windows_subdir}{windows_build_type}/mupdfcpp{build_dirs.cpu.windows_suffix}.lib' | |
| 2730 vs = wdev.WindowsVS() | |
| 2731 command = textwrap.dedent(f''' | |
| 2732 "{vs.vcvars}"&&"{vs.cl}" | |
| 2733 /Tp{src} | |
| 2734 {includes} | |
| 2735 -D FZ_DLL_CLIENT | |
| 2736 {cpp_flags} | |
| 2737 /link | |
| 2738 {lib} | |
| 2739 /out:{exe} | |
| 2740 ''') | |
| 2741 jlib.system(command, verbose=1) | |
| 2742 path = os.environ.get('PATH') | |
| 2743 env_extra = dict(PATH = f'{build_dirs.dir_so}{os.pathsep}{path}' if path else build_dirs.dir_so) | |
| 2744 jlib.system(f'{exe} {testfile}', verbose=1, env_extra=env_extra) | |
| 2745 else: | |
| 2746 dir_so_flags = os.path.basename( build_dirs.dir_so).split( '-') | |
| 2747 if 'shared' in dir_so_flags: | |
| 2748 libmupdf = f'{build_dirs.dir_so}/libmupdf.so' | |
| 2749 libmupdfthird = f'' | |
| 2750 libmupdfcpp = f'{build_dirs.dir_so}/libmupdfcpp.so' | |
| 2751 elif 'fpic' in dir_so_flags: | |
| 2752 libmupdf = f'{build_dirs.dir_so}/libmupdf.a' | |
| 2753 libmupdfthird = f'{build_dirs.dir_so}/libmupdf-third.a' | |
| 2754 libmupdfcpp = f'{build_dirs.dir_so}/libmupdfcpp.a' | |
| 2755 else: | |
| 2756 assert 0, f'Leaf must start with "shared-" or "fpic-": build_dirs.dir_so={build_dirs.dir_so}' | |
| 2757 command = textwrap.dedent(f''' | |
| 2758 c++ | |
| 2759 -o {exe} | |
| 2760 {cpp_flags} | |
| 2761 {includes} | |
| 2762 {src} | |
| 2763 {link_l_flags( [libmupdf, libmupdfcpp])} | |
| 2764 ''') | |
| 2765 jlib.system(command, verbose=1) | |
| 2766 jlib.system( 'pwd', verbose=1) | |
| 2767 if state.state_.macos: | |
| 2768 jlib.system( f'DYLD_LIBRARY_PATH={build_dirs.dir_so} {exe}', verbose=1) | |
| 2769 else: | |
| 2770 jlib.system( f'{exe} {testfile}', verbose=1, env_extra=dict(LD_LIBRARY_PATH=build_dirs.dir_so)) | |
| 2771 | |
| 2772 elif arg == '--test-internal': | |
| 2773 _test_get_m_command() | |
| 2774 | |
| 2775 elif arg == '--test-internal-cpp': | |
| 2776 cpp.test() | |
| 2777 | |
| 2778 elif arg in ('--test-python', '-t', '--test-python-gui'): | |
| 2779 | |
| 2780 env_extra, command_prefix = python_settings(build_dirs) | |
| 2781 script_py = os.path.relpath( f'{build_dirs.dir_mupdf}/scripts/mupdfwrap_gui.py') | |
| 2782 if arg == '--test-python-gui': | |
| 2783 #env_extra[ 'MUPDF_trace'] = '1' | |
| 2784 #env_extra[ 'MUPDF_check_refs'] = '1' | |
| 2785 #env_extra[ 'MUPDF_trace_exceptions'] = '1' | |
| 2786 command = f'{command_prefix} {script_py} {build_dirs.dir_mupdf}/thirdparty/zlib/zlib.3.pdf' | |
| 2787 jlib.system( command, env_extra=env_extra, out='log', verbose=1) | |
| 2788 | |
| 2789 else: | |
| 2790 jlib.log( 'running scripts/mupdfwrap_test.py ...') | |
| 2791 script_py = os.path.relpath( f'{build_dirs.dir_mupdf}/scripts/mupdfwrap_test.py') | |
| 2792 command = f'{command_prefix} {script_py}' | |
| 2793 with open( f'{build_dirs.dir_mupdf}/platform/python/mupdf_test.py.out.txt', 'w') as f: | |
| 2794 jlib.system( command, env_extra=env_extra, out='log', verbose=1) | |
| 2795 # Repeat with pdf_reference17.pdf if it exists. | |
| 2796 path = os.path.relpath( f'{build_dirs.dir_mupdf}/../pdf_reference17.pdf') | |
| 2797 if os.path.exists(path): | |
| 2798 jlib.log('Running mupdfwrap_test.py on {path}') | |
| 2799 command += f' {path}' | |
| 2800 jlib.system( command, env_extra=env_extra, out='log', verbose=1) | |
| 2801 | |
| 2802 # Run mutool.py. | |
| 2803 # | |
| 2804 mutool_py = os.path.relpath( f'{__file__}/../../mutool.py') | |
| 2805 zlib_pdf = os.path.relpath(f'{build_dirs.dir_mupdf}/thirdparty/zlib/zlib.3.pdf') | |
| 2806 for args2 in ( | |
| 2807 f'trace {zlib_pdf}', | |
| 2808 f'convert -o zlib.3.pdf-%d.png {zlib_pdf}', | |
| 2809 f'draw -o zlib.3.pdf-%d.png -s tmf -v -y l -w 150 -R 30 -h 200 {zlib_pdf}', | |
| 2810 f'draw -o zlib.png -R 10 {zlib_pdf}', | |
| 2811 f'clean -gggg {zlib_pdf} zlib.clean.pdf', | |
| 2812 ): | |
| 2813 command = f'{command_prefix} {mutool_py} {args2}' | |
| 2814 jlib.log( 'running: {command}') | |
| 2815 jlib.system( f'{command}', env_extra=env_extra, out='log', verbose=1) | |
| 2816 | |
| 2817 jlib.log( 'Tests ran ok.') | |
| 2818 | |
| 2819 elif arg == '--test-csharp': | |
| 2820 csc, mono, mupdf_cs = csharp.csharp_settings(build_dirs) | |
| 2821 | |
| 2822 # Our tests look for zlib.3.pdf in their current directory. | |
| 2823 testfile = f'{build_dirs.dir_so}/zlib.3.pdf' if state.state_.windows else 'zlib.3.pdf' | |
| 2824 jlib.fs_copy( | |
| 2825 f'{build_dirs.dir_mupdf}/thirdparty/zlib/zlib.3.pdf', | |
| 2826 testfile | |
| 2827 ) | |
| 2828 # Create test file whose name contains unicode character, which | |
| 2829 # scripts/mupdfwrap_test.cs will attempt to open. | |
| 2830 testfile2 = testfile + b'\xf0\x90\x90\xb7'.decode() + '.pdf' | |
| 2831 jlib.log(f'{testfile=}') | |
| 2832 jlib.log(f'{testfile2=}') | |
| 2833 jlib.log(f'{testfile2}') | |
| 2834 shutil.copy2(testfile, testfile2) | |
| 2835 | |
| 2836 if 1: | |
| 2837 # Build and run simple test. | |
| 2838 out = 'test-csharp.exe' | |
| 2839 jlib.build( | |
| 2840 (f'{build_dirs.dir_mupdf}/scripts/mupdfwrap_test.cs', mupdf_cs), | |
| 2841 out, | |
| 2842 f'"{csc}" -out:{{OUT}} {{IN}}', | |
| 2843 ) | |
| 2844 if state.state_.windows: | |
| 2845 out_rel = os.path.relpath( out, build_dirs.dir_so) | |
| 2846 jlib.system(f'cd {build_dirs.dir_so} && {mono} {out_rel}', verbose=1) | |
| 2847 else: | |
| 2848 command = f'LD_LIBRARY_PATH={build_dirs.dir_so} {mono} ./{out}' | |
| 2849 if state.state_.openbsd: | |
| 2850 e = jlib.system( command, verbose=1, raise_errors=False) | |
| 2851 if e == 137: | |
| 2852 jlib.log( 'Ignoring {e=} on OpenBSD because this occurs in normal operation.') | |
| 2853 elif e: | |
| 2854 raise Exception( f'command failed: {command}') | |
| 2855 else: | |
| 2856 jlib.system(f'LD_LIBRARY_PATH={build_dirs.dir_so} {mono} ./{out}', verbose=1) | |
| 2857 if 1: | |
| 2858 # Build and run test using minimal swig library to test | |
| 2859 # handling of Unicode strings. | |
| 2860 swig.test_swig_csharp() | |
| 2861 | |
| 2862 elif arg == '--test-csharp-gui': | |
| 2863 csc, mono, mupdf_cs = csharp.csharp_settings(build_dirs) | |
| 2864 | |
| 2865 # Build and run gui test. | |
| 2866 # | |
| 2867 # Don't know why Unix/Windows differ in what -r: args are | |
| 2868 # required... | |
| 2869 # | |
| 2870 # We need -unsafe for copying bitmap data from mupdf. | |
| 2871 # | |
| 2872 references = '-r:System.Drawing -r:System.Windows.Forms' if state.state_.linux else '' | |
| 2873 out = 'mupdfwrap_gui.cs.exe' | |
| 2874 jlib.build( | |
| 2875 (f'{build_dirs.dir_mupdf}/scripts/mupdfwrap_gui.cs', mupdf_cs), | |
| 2876 out, | |
| 2877 f'"{csc}" -unsafe {references} -out:{{OUT}} {{IN}}' | |
| 2878 ) | |
| 2879 if state.state_.windows: | |
| 2880 # Don't know how to mimic Unix's LD_LIBRARY_PATH, so for | |
| 2881 # now we cd into the directory containing our generated | |
| 2882 # libraries. | |
| 2883 jlib.fs_copy(f'{build_dirs.dir_mupdf}/thirdparty/zlib/zlib.3.pdf', f'{build_dirs.dir_so}/zlib.3.pdf') | |
| 2884 # Note that this doesn't work remotely. | |
| 2885 out_rel = os.path.relpath( out, build_dirs.dir_so) | |
| 2886 jlib.system(f'cd {build_dirs.dir_so} && {mono} {out_rel}', verbose=1) | |
| 2887 else: | |
| 2888 jlib.fs_copy(f'{build_dirs.dir_mupdf}/thirdparty/zlib/zlib.3.pdf', f'zlib.3.pdf') | |
| 2889 jlib.system(f'LD_LIBRARY_PATH={build_dirs.dir_so} {mono} ./{out}', verbose=1) | |
| 2890 | |
| 2891 elif arg == '--test-python-fitz': | |
| 2892 opts = '' | |
| 2893 while 1: | |
| 2894 arg = args.next() | |
| 2895 if arg.startswith('-'): | |
| 2896 opts += f' {arg}' | |
| 2897 else: | |
| 2898 tests = arg | |
| 2899 break | |
| 2900 startdir = os.path.abspath('../PyMuPDF/tests') | |
| 2901 env_extra, command_prefix = python_settings(build_dirs, startdir) | |
| 2902 | |
| 2903 env_extra['PYTHONPATH'] += f':{os.path.relpath(".", startdir)}' | |
| 2904 env_extra['PYTHONPATH'] += f':{os.path.relpath("./scripts", startdir)}' | |
| 2905 | |
| 2906 #env_extra['PYTHONMALLOC'] = 'malloc' | |
| 2907 #env_extra['MUPDF_trace'] = '1' | |
| 2908 #env_extra['MUPDF_check_refs'] = '1' | |
| 2909 | |
| 2910 # -x: stop at first error. | |
| 2911 # -s: show stdout/err. | |
| 2912 # | |
| 2913 if tests == 'all': | |
| 2914 jlib.system( | |
| 2915 f'cd ../PyMuPDF/tests && py.test-3 {opts}', | |
| 2916 env_extra=env_extra, | |
| 2917 out='log', | |
| 2918 verbose=1, | |
| 2919 ) | |
| 2920 elif tests == 'iter': | |
| 2921 e = 0 | |
| 2922 for script in sorted(glob.glob( '../PyMuPDF/tests/test_*.py')): | |
| 2923 script = os.path.basename(script) | |
| 2924 ee = jlib.system( | |
| 2925 f'cd ../PyMuPDF/tests && py.test-3 {opts} {script}', | |
| 2926 env_extra=env_extra, | |
| 2927 out='log', | |
| 2928 verbose=1, | |
| 2929 raise_errors=0, | |
| 2930 ) | |
| 2931 if not e: | |
| 2932 e = ee | |
| 2933 elif not os.path.isfile(f'../PyMuPDF/tests/{tests}'): | |
| 2934 ts = glob.glob("../PyMuPDF/tests/*.py") | |
| 2935 ts = [os.path.basename(t) for t in ts] | |
| 2936 raise Exception(f'Unrecognised tests={tests}. Should be "all", "iter" or one of {ts}') | |
| 2937 else: | |
| 2938 jlib.system( | |
| 2939 f'cd ../PyMuPDF/tests && py.test-3 {opts} {tests}', | |
| 2940 env_extra=env_extra, | |
| 2941 out='log', | |
| 2942 verbose=1, | |
| 2943 ) | |
| 2944 | |
| 2945 elif arg == '--test-setup.py': | |
| 2946 # We use the '.' command to run pylocal/bin/activate rather than 'source', | |
| 2947 # because the latter is not portable, e.g. not supported by ksh. The '.' | |
| 2948 # command is posix so should work on all shells. | |
| 2949 commands = [ | |
| 2950 f'cd {build_dirs.dir_mupdf}', | |
| 2951 f'python3 -m venv pylocal', | |
| 2952 f'. pylocal/bin/activate', | |
| 2953 f'pip install clang', | |
| 2954 f'python setup.py {extra} install', | |
| 2955 f'python scripts/mupdfwrap_test.py', | |
| 2956 f'deactivate', | |
| 2957 ] | |
| 2958 command = 'true' | |
| 2959 for c in commands: | |
| 2960 command += f' && echo == running: {c}' | |
| 2961 command += f' && {c}' | |
| 2962 jlib.system( command, verbose=1, out='log') | |
| 2963 | |
| 2964 elif arg == '--test-swig': | |
| 2965 swig.test_swig() | |
| 2966 | |
| 2967 elif arg in ('--venv' '--venv-force-reinstall'): | |
| 2968 force_reinstall = ' --force-reinstall' if arg == '--venv-force-reinstall' else '' | |
| 2969 assert arg_i == 1, f'If specified, {arg} should be the first argument.' | |
| 2970 venv = f'venv-mupdfwrap-{state.python_version()}-{state.cpu_name()}' | |
| 2971 # Oddly, shlex.quote(sys.executable), which puts the name | |
| 2972 # inside single quotes, doesn't work - we get error `The | |
| 2973 # filename, directory name, or volume label syntax is | |
| 2974 # incorrect.`. | |
| 2975 if state.state_.openbsd: | |
| 2976 # Need system py3-llvm. | |
| 2977 jlib.system(f'"{sys.executable}" -m venv --system-site-packages {venv}', out='log', verbose=1) | |
| 2978 else: | |
| 2979 jlib.system(f'"{sys.executable}" -m venv {venv}', out='log', verbose=1) | |
| 2980 | |
| 2981 if state.state_.windows: | |
| 2982 command_venv_enter = f'{venv}\\Scripts\\activate.bat' | |
| 2983 else: | |
| 2984 command_venv_enter = f'. {venv}/bin/activate' | |
| 2985 | |
| 2986 command = f'{command_venv_enter} && python -m pip install --upgrade pip' | |
| 2987 | |
| 2988 # Required packages are specified by | |
| 2989 # setup.py:get_requires_for_build_wheel(). | |
| 2990 mupdf_root = os.path.abspath( f'{__file__}/../../../') | |
| 2991 sys.path.insert(0, f'{mupdf_root}') | |
| 2992 import setup | |
| 2993 del sys.path[0] | |
| 2994 packages = setup.get_requires_for_build_wheel() | |
| 2995 packages = ' '.join(packages) | |
| 2996 command += f' && python -m pip install{force_reinstall} --upgrade {packages}' | |
| 2997 jlib.system(command, out='log', verbose=1) | |
| 2998 | |
| 2999 command = f'{command_venv_enter} && python {shlex.quote(sys.argv[0])}' | |
| 3000 while 1: | |
| 3001 try: | |
| 3002 command += f' {shlex.quote(args.next())}' | |
| 3003 except StopIteration: | |
| 3004 break | |
| 3005 command += f' && deactivate' | |
| 3006 jlib.system(command, out='log', verbose=1) | |
| 3007 | |
| 3008 elif arg == '--vs-upgrade': | |
| 3009 vs_upgrade = int(args.next()) | |
| 3010 | |
| 3011 elif arg == '--windows-cmd': | |
| 3012 args_tail = '' | |
| 3013 while 1: | |
| 3014 try: | |
| 3015 args_tail += f' {args.next()}' | |
| 3016 except StopIteration: | |
| 3017 break | |
| 3018 command = f'cmd.exe /c "py {sys.argv[0]} {args_tail}"' | |
| 3019 jlib.system(command, out='log', verbose=1) | |
| 3020 | |
| 3021 else: | |
| 3022 raise Exception( f'unrecognised arg: {arg}') | |
| 3023 | |
| 3024 | |
| 3025 def write_classextras(path): | |
| 3026 ''' | |
| 3027 Dumps classes.classextras to file using json, with crude handling of class | |
| 3028 instances. | |
| 3029 ''' | |
| 3030 import json | |
| 3031 with open(path, 'w') as f: | |
| 3032 class encoder(json.JSONEncoder): | |
| 3033 def default( self, obj): | |
| 3034 if type(obj).__name__.startswith(('Extra', 'ClassExtra')): | |
| 3035 ret = list() | |
| 3036 for i in dir( obj): | |
| 3037 if not i.startswith( '_'): | |
| 3038 ret.append( getattr( obj, i)) | |
| 3039 return ret | |
| 3040 if callable(obj): | |
| 3041 return obj.__name__ | |
| 3042 return json.JSONEncoder.default(self, obj) | |
| 3043 json.dump( | |
| 3044 classes.classextras, | |
| 3045 f, | |
| 3046 indent=' ', | |
| 3047 sort_keys=1, | |
| 3048 cls = encoder, | |
| 3049 ) | |
| 3050 | |
| 3051 def main(): | |
| 3052 jlib.force_line_buffering() | |
| 3053 try: | |
| 3054 main2() | |
| 3055 except Exception: | |
| 3056 jlib.exception_info() | |
| 3057 sys.exit(1) | |
| 3058 | |
| 3059 if __name__ == '__main__': | |
| 3060 main2() |
