__main__.py 128 KB

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