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()