Mercurial > hgrepos > Python2 > PyMuPDF
comparison scripts/test.py @ 1:1d09e1dec1d9 upstream
ADD: PyMuPDF v1.26.4: the original sdist.
It does not yet contain MuPDF. This normally will be downloaded when
building PyMuPDF.
| author | Franz Glasner <fzglas.hg@dom66.de> |
|---|---|
| date | Mon, 15 Sep 2025 11:37:51 +0200 |
| parents | |
| children | a6bc019ac0b2 |
comparison
equal
deleted
inserted
replaced
| -1:000000000000 | 1:1d09e1dec1d9 |
|---|---|
| 1 #! /usr/bin/env python3 | |
| 2 | |
| 3 '''Developer build/test script for PyMuPDF. | |
| 4 | |
| 5 Examples: | |
| 6 | |
| 7 ./PyMuPDF/scripts/test.py --m mupdf build test | |
| 8 Build and test with pre-existing local mupdf/ checkout. | |
| 9 | |
| 10 ./PyMuPDF/scripts/test.py build test | |
| 11 Build and test with default internal download of mupdf. | |
| 12 | |
| 13 ./PyMuPDF/scripts/test.py -m 'git:https://git.ghostscript.com/mupdf.git' build test | |
| 14 Build and test with internal checkout of MuPDF master. | |
| 15 | |
| 16 ./PyMuPDF/scripts/test.py -m 'git:--branch 1.26.x https://github.com/ArtifexSoftware/mupdf.git' build test | |
| 17 Build and test using internal checkout of mupdf 1.26.x branch from | |
| 18 Github. | |
| 19 | |
| 20 Usage: | |
| 21 | |
| 22 * Command line arguments are called parameters if they start with `-`, | |
| 23 otherwise they are called commands. | |
| 24 * Parameters are evaluated first in the order that they were specified. | |
| 25 * Then commands are run in the order in which they were specified. | |
| 26 * Usually command `test` would be specified after a `build`, `install` or | |
| 27 `wheel` command. | |
| 28 * Parameters and commands can be interleaved but it may be clearer to separate | |
| 29 them on the command line. | |
| 30 | |
| 31 Other: | |
| 32 | |
| 33 * If we are not already running inside a Python venv, we automatically create a | |
| 34 venv and re-run ourselves inside it. | |
| 35 * Build/wheel/install commands always install into the venv. | |
| 36 * Tests use whatever PyMuPDF/MuPDF is currently installed in the venv. | |
| 37 * We run tests with pytest. | |
| 38 | |
| 39 * One can generate call traces by setting environment variables in debug | |
| 40 builds. For details see: | |
| 41 https://mupdf.readthedocs.io/en/latest/language-bindings.html#environmental-variables | |
| 42 | |
| 43 Command line args: | |
| 44 | |
| 45 -a <env_name> | |
| 46 Read next space-separated argument(s) from environmental variable | |
| 47 <env_name>. | |
| 48 * Does nothing if <env_name> is unset. | |
| 49 * Useful when running via Github action. | |
| 50 | |
| 51 -b <build> | |
| 52 Set build type for `build` commands. `<build>` should be one of | |
| 53 'release', 'debug', 'memento'. [This makes `build` set environment | |
| 54 variable `PYMUPDF_SETUP_MUPDF_BUILD_TYPE`, which is used by PyMuPDF's | |
| 55 `setup.py`.] | |
| 56 | |
| 57 --build-flavour <build_flavour> | |
| 58 Combination of 'p', 'b', 'd'. See ../setup.py's description of | |
| 59 PYMUPDF_SETUP_FLAVOUR. Default is 'pbd', i.e. self-contained PyMuPDF | |
| 60 wheels including MuPDF build-time files. | |
| 61 | |
| 62 --build-isolation 0|1 | |
| 63 If true (the default on non-OpenBSD systems), we let pip create and use | |
| 64 its own new venv to build PyMuPDF. Otherwise we force pip to use the | |
| 65 current venv. | |
| 66 | |
| 67 --cibw-archs-linux <archs> | |
| 68 Set CIBW_ARCHS_LINUX, e.g. to `auto64 aarch64`. Default is `auto64` so | |
| 69 this allows control over whether to build linux-aarch64 wheels. | |
| 70 | |
| 71 --cibw-name <cibw_name> | |
| 72 Name to use when installing cibuildwheel, e.g.: | |
| 73 --cibw-name cibuildwheel==3.0.0b1 | |
| 74 Default is `cibuildwheel`, i.e. the current release. | |
| 75 | |
| 76 --cibw-pyodide 0|1 | |
| 77 Experimental, make `cibuild` command build a pyodide wheel. | |
| 78 2025-05-27: this fails when building mupdf C API - `ld -r -b binary | |
| 79 ...` fails with: | |
| 80 emcc: error: binary: No such file or directory ("binary" was expected to be an input file, based on the commandline arguments provided) | |
| 81 | |
| 82 --cibw-pyodide-version <cibw_pyodide_version> | |
| 83 Override default Pyodide version to use with `cibuildwheel` command. If | |
| 84 empty string we use cibuildwheel's default. | |
| 85 | |
| 86 --cibw-release-1 | |
| 87 Set up so that `cibw` builds all wheels except linux-aarch64, and sdist | |
| 88 if on Linux. | |
| 89 | |
| 90 --cibw-release-2 | |
| 91 Set up so that `cibw` builds only linux-aarch64 wheel. | |
| 92 | |
| 93 -d | |
| 94 Equivalent to `-b debug`. | |
| 95 | |
| 96 --dummy | |
| 97 Sets PYMUPDF_SETUP_DUMMY=1 which makes setup.py build a dummy wheel | |
| 98 with no content. For internal testing only. | |
| 99 | |
| 100 -e <name>=<value> | |
| 101 Add to environment used in build and test commands. Can be specified | |
| 102 multiple times. | |
| 103 | |
| 104 -f 0|1 | |
| 105 If 1 we also test alias `fitz` as well as `pymupdf`. Default is '0'. | |
| 106 | |
| 107 --gdb 0|1 | |
| 108 Run tests under gdb. Requires user interaction. | |
| 109 | |
| 110 --graal | |
| 111 Use graal - run inside a Graal VM instead of a Python venv. | |
| 112 | |
| 113 As of 2025-08-04 we: | |
| 114 * Clone the latest pyenv and build it. | |
| 115 * Use pyenv to install graalpy. | |
| 116 * Use graalpy to create venv. | |
| 117 | |
| 118 [After the first time, suggest `-v 1` to avoid delay from | |
| 119 updating/building pyenv and recreating the graal venv.] | |
| 120 | |
| 121 --help | |
| 122 -h | |
| 123 Show help. | |
| 124 | |
| 125 -I <implementations> | |
| 126 Set PyMuPDF implementations to test. | |
| 127 <implementations> must contain only these individual characters: | |
| 128 'r' - rebased. | |
| 129 'R' - rebased without optimisations. | |
| 130 Default is 'r'. Also see `PyMuPDF:tests/run_compound.py`. | |
| 131 | |
| 132 -i <install_version> | |
| 133 Set version installed by the 'install' command. | |
| 134 | |
| 135 -k <expression> | |
| 136 Specify which test(s) to run; passed straight through to pytest's `-k`. | |
| 137 For example `-k test_3354`. | |
| 138 | |
| 139 -m <location> | --mupdf <location> | |
| 140 Location of local mupdf/ directory or 'git:...' to be used | |
| 141 when building PyMuPDF. | |
| 142 | |
| 143 This sets environment variable PYMUPDF_SETUP_MUPDF_BUILD, which is used | |
| 144 by PyMuPDF/setup.py. If not specified PyMuPDF will download its default | |
| 145 mupdf .tgz. | |
| 146 | |
| 147 Additionally if <location> starts with ':' we use the remaining text as | |
| 148 the branch name and add https://github.com/ArtifexSoftware/mupdf.git. | |
| 149 | |
| 150 For example: | |
| 151 | |
| 152 -m "git:--branch master https://github.com/ArtifexSoftware/mupdf.git" | |
| 153 -m :master | |
| 154 | |
| 155 -m "git:--branch 1.26.x https://github.com/ArtifexSoftware/mupdf.git" | |
| 156 -m :1.26.x | |
| 157 | |
| 158 --mupdf-clean 0|1 | |
| 159 If 1 we do a clean MuPDF build. | |
| 160 | |
| 161 -M 0|1 | |
| 162 --build-mupdf 0|1 | |
| 163 Whether to rebuild mupdf when we build PyMuPDF. Default is 1. | |
| 164 | |
| 165 -o <os_names> | |
| 166 Control whether we do nothing on the current platform. | |
| 167 * <os_names> is a comma-separated list of names. | |
| 168 * If <os_names> is empty (the default), we always run normally. | |
| 169 * Otherwise we only run if an item in <os_names> matches (case | |
| 170 insensitive) platform.system(). | |
| 171 * For example `-o linux,darwin` will do nothing unless on Linux or | |
| 172 MacOS. | |
| 173 | |
| 174 -p <pytest-options> | |
| 175 Set pytest options; default is ''. | |
| 176 | |
| 177 -P 0|1 | |
| 178 If 1, automatically install required system packages such as | |
| 179 Valgrind. Default is 0. | |
| 180 | |
| 181 --pybind 0|1 | |
| 182 Experimental, for investigating | |
| 183 https://github.com/pymupdf/PyMuPDF/issues/3869. Runs run basic code | |
| 184 inside C++ pybind. Requires `sudo apt install pybind11-dev` or similar. | |
| 185 | |
| 186 --pyodide-build-version <version> | |
| 187 Version of Python package pyodide-build to use with `pyodide` command. | |
| 188 | |
| 189 If None (the default) `pyodide` uses the latest available version. | |
| 190 2025-02-13: pyodide_build_version='0.29.3' works. | |
| 191 | |
| 192 -s 0 | 1 | |
| 193 If 1 (the default), build with Python Limited API/Stable ABI. | |
| 194 [This simply sSets $PYMUPDF_SETUP_PY_LIMITED_API, which is used by | |
| 195 PyMuPDF/setup.py.] | |
| 196 | |
| 197 --show-args: | |
| 198 Show sys.argv and exit. For debugging. | |
| 199 | |
| 200 --sync-paths | |
| 201 Do not run anything, instead write required files/directories/checkouts | |
| 202 to stdout, one per line. This is to help with automated running on | |
| 203 remote machines. | |
| 204 | |
| 205 --system-site-packages 0|1 | |
| 206 If 1, use `--system-site-packages` when creating venv. Defaults is 0. | |
| 207 | |
| 208 --swig <swig> | |
| 209 Use <swig> instead of the `swig` command. | |
| 210 | |
| 211 Unix only: | |
| 212 Clone/update/build swig from a git repository using 'git:' prefix. | |
| 213 | |
| 214 We default to https://github.com/swig/swig.git branch master, so these | |
| 215 are all equivalent: | |
| 216 | |
| 217 --swig 'git:--branch master https://github.com/swig/swig.git' | |
| 218 --swig 'git:--branch master' | |
| 219 --swig git: | |
| 220 | |
| 221 2025-08-18: This fixes building with py_limited_api on python-3.13. | |
| 222 | |
| 223 --swig-quick 0|1 | |
| 224 If 1 and `--swig` starts with 'git:', we do not update/build swig if | |
| 225 already present. | |
| 226 | |
| 227 See description of PYMUPDF_SETUP_SWIG_QUICK in setup.py. | |
| 228 | |
| 229 -t <names> | |
| 230 Pytest test names, comma-separated. Should be relative to PyMuPDF | |
| 231 directory. For example: | |
| 232 -t tests/test_general.py | |
| 233 -t tests/test_general.py::test_subset_fonts | |
| 234 To specify multiple tests, use comma-separated list and/or multiple `-t | |
| 235 <names>` args. | |
| 236 | |
| 237 --timeout <seconds> | |
| 238 Sets timeout when running tests. | |
| 239 | |
| 240 -T <prefix> | |
| 241 Use specified prefix when running pytest, must be one of: | |
| 242 gdb | |
| 243 helgrind | |
| 244 vagrind | |
| 245 | |
| 246 -v <venv> | |
| 247 venv is: | |
| 248 0 - do not use a venv. | |
| 249 1 - Use venv. If it already exists, we assume the existing directory | |
| 250 was created by us earlier and is a valid venv containing all | |
| 251 necessary packages; this saves a little time. | |
| 252 2 - Use venv. | |
| 253 3 - Use venv but delete it first if it already exists. | |
| 254 The default is 2. | |
| 255 | |
| 256 Commands: | |
| 257 | |
| 258 build | |
| 259 Builds and installs PyMuPDF into venv, using `pip install .../PyMuPDF`. | |
| 260 | |
| 261 buildtest | |
| 262 Same as 'build test'. | |
| 263 | |
| 264 cibw | |
| 265 Build and test PyMuPDF wheel(s) using cibuildwheel. Wheels are placed | |
| 266 in directory `wheelhouse`. | |
| 267 * We do not attempt to install wheels. | |
| 268 * So it is generally not useful to do `cibw test`. | |
| 269 | |
| 270 If CIBW_BUILD is unset, we set it as follows: | |
| 271 * On Github we build and test all supported Python versions. | |
| 272 * Otherwise we build and test the current Python version only. | |
| 273 | |
| 274 If CIBW_ARCHS is unset we set $CIBW_ARCHS_WINDOWS, $CIBW_ARCHS_MACOS | |
| 275 and $CIBW_ARCHS_LINUX to auto64 if they are unset. | |
| 276 | |
| 277 install <pymupdf> | |
| 278 Install with `pip install --force-reinstall <pymupdf>`. | |
| 279 | |
| 280 pyodide | |
| 281 Build Pyodide wheel. We clone `emsdk.git`, set it up, and run | |
| 282 `pyodide build`. This runs our setup.py with CC etc set up | |
| 283 to create Pyodide binaries in a wheel called, for example, | |
| 284 `PyMuPDF-1.23.2-cp311-none-emscripten_3_1_32_wasm32.whl`. | |
| 285 | |
| 286 It seems that sys.version must match the Python version inside emsdk; | |
| 287 as of 2025-02-14 this is 3.12. Otherwise we get build errors such as: | |
| 288 [wasm-validator error in function 723] unexpected false: all used features should be allowed, on ... | |
| 289 | |
| 290 test | |
| 291 Runs PyMuPDF's pytest tests. Default is to test rebased and unoptimised | |
| 292 rebased; use `-i` to change this. | |
| 293 | |
| 294 wheel | |
| 295 Build and install wheel. | |
| 296 | |
| 297 | |
| 298 Environment: | |
| 299 PYMUDF_SCRIPTS_TEST_options | |
| 300 Is prepended to command line args. | |
| 301 ''' | |
| 302 | |
| 303 import glob | |
| 304 import os | |
| 305 import platform | |
| 306 import re | |
| 307 import shlex | |
| 308 import shutil | |
| 309 import subprocess | |
| 310 import sys | |
| 311 import textwrap | |
| 312 | |
| 313 | |
| 314 pymupdf_dir_abs = os.path.abspath( f'{__file__}/../..') | |
| 315 | |
| 316 try: | |
| 317 sys.path.insert(0, pymupdf_dir_abs) | |
| 318 import pipcl | |
| 319 finally: | |
| 320 del sys.path[0] | |
| 321 | |
| 322 try: | |
| 323 sys.path.insert(0, f'{pymupdf_dir_abs}/scripts') | |
| 324 import gh_release | |
| 325 finally: | |
| 326 del sys.path[0] | |
| 327 | |
| 328 | |
| 329 pymupdf_dir = pipcl.relpath(pymupdf_dir_abs) | |
| 330 | |
| 331 log = pipcl.log0 | |
| 332 run = pipcl.run | |
| 333 | |
| 334 | |
| 335 def main(argv): | |
| 336 | |
| 337 if github_workflow_unimportant(): | |
| 338 return | |
| 339 | |
| 340 build_isolation = None | |
| 341 cibw_name = None | |
| 342 cibw_pyodide = None | |
| 343 cibw_pyodide_version = None | |
| 344 commands = list() | |
| 345 env_extra = dict() | |
| 346 graal = False | |
| 347 implementations = 'r' | |
| 348 install_version = None | |
| 349 mupdf_sync = None | |
| 350 os_names = list() | |
| 351 system_packages = False | |
| 352 pybind = False | |
| 353 pyodide_build_version = None | |
| 354 pytest_options = '' | |
| 355 pytest_prefix = None | |
| 356 cibw_sdist = None | |
| 357 show_args = False | |
| 358 show_help = False | |
| 359 sync_paths = False | |
| 360 system_site_packages = False | |
| 361 swig = None | |
| 362 swig_quick = None | |
| 363 test_fitz = False | |
| 364 test_names = list() | |
| 365 test_timeout = None | |
| 366 valgrind = False | |
| 367 warnings = list() | |
| 368 venv = 2 | |
| 369 | |
| 370 options = os.environ.get('PYMUDF_SCRIPTS_TEST_options', '') | |
| 371 options = shlex.split(options) | |
| 372 | |
| 373 # Parse args and update the above state. We do this before moving into a | |
| 374 # venv, partly so we can return errors immediately. | |
| 375 # | |
| 376 args = iter(options + argv[1:]) | |
| 377 i = 0 | |
| 378 while 1: | |
| 379 try: | |
| 380 arg = next(args) | |
| 381 except StopIteration: | |
| 382 arg = None | |
| 383 break | |
| 384 | |
| 385 if 0: | |
| 386 pass | |
| 387 | |
| 388 elif arg == '-a': | |
| 389 _name = next(args) | |
| 390 _value = os.environ.get(_name, '') | |
| 391 _args = shlex.split(_value) + list(args) | |
| 392 args = iter(_args) | |
| 393 | |
| 394 elif arg == '-b': | |
| 395 env_extra['PYMUPDF_SETUP_MUPDF_BUILD_TYPE'] = next(args) | |
| 396 | |
| 397 elif arg == '--build-flavour': | |
| 398 env_extra['PYMUPDF_SETUP_FLAVOUR'] = next(args) | |
| 399 | |
| 400 elif arg == '--build-isolation': | |
| 401 build_isolation = int(next(args)) | |
| 402 | |
| 403 elif arg == '--cibw-pyodide-version': | |
| 404 cibw_pyodide_version = next(args) | |
| 405 | |
| 406 elif arg == '--cibw-release-1': | |
| 407 cibw_sdist = True | |
| 408 env_extra['CIBW_ARCHS_LINUX'] = 'auto64' | |
| 409 env_extra['CIBW_ARCHS_MACOS'] = 'auto64' | |
| 410 env_extra['CIBW_ARCHS_WINDOWS'] = 'auto' # win32 and win64. | |
| 411 env_extra['CIBW_SKIP'] = 'pp* *i686 cp36* cp37* *musllinux*aarch64*' | |
| 412 | |
| 413 elif arg == '--cibw-release-2': | |
| 414 env_extra['CIBW_ARCHS_LINUX'] = 'aarch64' | |
| 415 # Testing only first and last python versions because otherwise | |
| 416 # Github times out after 6h. | |
| 417 env_extra['CIBW_BUILD'] = 'cp39* cp313*' | |
| 418 os_names = ['linux'] | |
| 419 | |
| 420 elif arg == '--cibw-archs-linux': | |
| 421 env_extra['CIBW_ARCHS_LINUX'] = next(args) | |
| 422 | |
| 423 elif arg == '--cibw-name': | |
| 424 cibw_name = next(args) | |
| 425 | |
| 426 elif arg == '--cibw-pyodide': | |
| 427 cibw_pyodide = next(args) | |
| 428 | |
| 429 elif arg == '-d': | |
| 430 env_extra['PYMUPDF_SETUP_MUPDF_BUILD_TYPE'] = 'debug' | |
| 431 | |
| 432 elif arg == '--dummy': | |
| 433 env_extra['PYMUPDF_SETUP_DUMMY'] = '1' | |
| 434 env_extra['CIBW_TEST_COMMAND'] = '' | |
| 435 | |
| 436 elif arg == '-e': | |
| 437 _nv = next(args) | |
| 438 assert '=' in _nv, f'-e <name>=<value> does not contain "=": {_nv!r}' | |
| 439 _name, _value = _nv.split('=', 1) | |
| 440 env_extra[_name] = _value | |
| 441 | |
| 442 elif arg == '-f': | |
| 443 test_fitz = int(next(args)) | |
| 444 | |
| 445 elif arg == '--graal': | |
| 446 graal = True | |
| 447 | |
| 448 elif arg in ('-h', '--help'): | |
| 449 show_help = True | |
| 450 | |
| 451 elif arg == '-i': | |
| 452 install_version = next(args) | |
| 453 | |
| 454 elif arg == '-I': | |
| 455 implementations = next(args) | |
| 456 | |
| 457 elif arg == '-k': | |
| 458 pytest_options += f' -k {shlex.quote(next(args))}' | |
| 459 | |
| 460 elif arg in ('-m', '--mupdf'): | |
| 461 _mupdf = next(args) | |
| 462 if _mupdf == '-': | |
| 463 _mupdf = None | |
| 464 elif _mupdf.startswith(':'): | |
| 465 _branch = _mupdf[1:] | |
| 466 _mupdf = 'git:--branch {_branch} https://github.com/ArtifexSoftware/mupdf.git' | |
| 467 os.environ['PYMUPDF_SETUP_MUPDF_BUILD'] = _mupdf | |
| 468 elif _mupdf.startswith('git:') or '://' in _mupdf: | |
| 469 os.environ['PYMUPDF_SETUP_MUPDF_BUILD'] = _mupdf | |
| 470 else: | |
| 471 assert os.path.isdir(_mupdf), f'Not a directory: {_mupdf=}' | |
| 472 os.environ['PYMUPDF_SETUP_MUPDF_BUILD'] = os.path.abspath(_mupdf) | |
| 473 mupdf_sync = _mupdf | |
| 474 | |
| 475 elif arg == '--mupdf-clean': | |
| 476 env_extra['PYMUPDF_SETUP_MUPDF_CLEAN']=next(args) | |
| 477 | |
| 478 elif arg in ('-M', '--build-mupdf'): | |
| 479 env_extra['PYMUPDF_SETUP_MUPDF_REBUILD'] = next(args) | |
| 480 | |
| 481 elif arg == '-o': | |
| 482 os_names += next(args).split(',') | |
| 483 | |
| 484 elif arg == '-p': | |
| 485 pytest_options += f' {next(args)}' | |
| 486 | |
| 487 elif arg == '-P': | |
| 488 system_packages = int(next(args)) | |
| 489 | |
| 490 elif arg == '--pybind': | |
| 491 pybind = int(next(args)) | |
| 492 | |
| 493 elif arg == '--pyodide-build-version': | |
| 494 pyodide_build_version = next(args) | |
| 495 | |
| 496 elif arg == '-s': | |
| 497 _value = next(args) | |
| 498 assert _value in ('0', '1'), f'`-s` must be followed by `0` or `1`, not {_value=}.' | |
| 499 env_extra['PYMUPDF_SETUP_PY_LIMITED_API'] = _value | |
| 500 | |
| 501 elif arg == '--show-args': | |
| 502 show_args = 1 | |
| 503 elif arg == '--sync-paths': | |
| 504 sync_paths = True | |
| 505 | |
| 506 elif arg == '--system-site-packages': | |
| 507 system_site_packages = int(next(args)) | |
| 508 | |
| 509 elif arg == '--swig': | |
| 510 swig = next(args) | |
| 511 | |
| 512 elif arg == '--swig-quick': | |
| 513 swig_quick = int(next(args)) | |
| 514 | |
| 515 elif arg == '-t': | |
| 516 test_names += next(args).split(',') | |
| 517 | |
| 518 elif arg == '--timeout': | |
| 519 test_timeout = float(next(args)) | |
| 520 | |
| 521 elif arg == '-T': | |
| 522 pytest_prefix = next(args) | |
| 523 assert pytest_prefix in ('gdb', 'helgrind', 'valgrind'), \ | |
| 524 f'Unrecognised {pytest_prefix=}, should be one of: gdb valgrind helgrind.' | |
| 525 | |
| 526 elif arg == '-v': | |
| 527 venv = int(next(args)) | |
| 528 assert venv in (0, 1, 2, 3), f'Invalid {venv=} should be 0, 1, 2 or 3.' | |
| 529 | |
| 530 elif arg in ('build', 'cibw', 'install', 'pyodide', 'test', 'wheel'): | |
| 531 commands.append(arg) | |
| 532 | |
| 533 elif arg == 'buildtest': | |
| 534 commands += ['build', 'test'] | |
| 535 | |
| 536 else: | |
| 537 assert 0, f'Unrecognised option/command: {arg=}.' | |
| 538 | |
| 539 # Handle special args --sync-paths, -h, -v, -o first. | |
| 540 # | |
| 541 if sync_paths: | |
| 542 # Just print required files, directories and checkouts. | |
| 543 print(pymupdf_dir) | |
| 544 if mupdf_sync: | |
| 545 print(mupdf_sync) | |
| 546 return | |
| 547 | |
| 548 if show_help: | |
| 549 print(__doc__) | |
| 550 return | |
| 551 | |
| 552 if show_args: | |
| 553 print(f'sys.argv ({len(sys.argv)}):') | |
| 554 for arg in sys.argv: | |
| 555 print(f' {arg!r}') | |
| 556 return | |
| 557 | |
| 558 if os_names: | |
| 559 if platform.system().lower() not in os_names: | |
| 560 log(f'Not running because {platform.system().lower()=} not in {os_names=}') | |
| 561 return | |
| 562 | |
| 563 if commands: | |
| 564 if venv: | |
| 565 # Rerun ourselves inside a venv if not already in a venv. | |
| 566 if not venv_in(): | |
| 567 if graal: | |
| 568 # 2025-07-24: We need the latest pyenv. | |
| 569 graalpy = 'graalpy-24.2.1' | |
| 570 venv_name = f'venv-pymupdf-{graalpy}' | |
| 571 pyenv_dir = f'{pymupdf_dir_abs}/pyenv-git' | |
| 572 os.environ['PYENV_ROOT'] = pyenv_dir | |
| 573 os.environ['PATH'] = f'{pyenv_dir}/bin:{os.environ["PATH"]}' | |
| 574 os.environ['PIPCL_GRAAL_PYTHON'] = sys.executable | |
| 575 | |
| 576 if venv >= 3: | |
| 577 shutil.rmtree(venv_name, ignore_errors=1) | |
| 578 if venv == 1 and os.path.exists(pyenv_dir) and os.path.exists(venv_name): | |
| 579 log(f'{venv=} and {venv_name=} already exists so not building pyenv or creating venv.') | |
| 580 else: | |
| 581 pipcl.git_get('https://github.com/pyenv/pyenv.git', pyenv_dir, branch='master') | |
| 582 run(f'cd {pyenv_dir} && src/configure && make -C src') | |
| 583 run(f'which pyenv') | |
| 584 run(f'pyenv install -v -s {graalpy}') | |
| 585 run(f'{pyenv_dir}/versions/{graalpy}/bin/graalpy -m venv {venv_name}') | |
| 586 e = run(f'. {venv_name}/bin/activate && python {shlex.join(sys.argv)}', | |
| 587 check=False, | |
| 588 ) | |
| 589 else: | |
| 590 venv_name = f'venv-pymupdf-{platform.python_version()}-{int.bit_length(sys.maxsize+1)}' | |
| 591 e = venv_run( | |
| 592 sys.argv, | |
| 593 venv_name, | |
| 594 recreate=(venv>=2), | |
| 595 clean=(venv>=3), | |
| 596 ) | |
| 597 sys.exit(e) | |
| 598 else: | |
| 599 log(f'Warning, no commands specified so nothing to do.') | |
| 600 | |
| 601 # Clone/update/build swig if specified. | |
| 602 swig_binary = pipcl.swig_get(swig, swig_quick) | |
| 603 if swig_binary: | |
| 604 os.environ['PYMUPDF_SETUP_SWIG'] = swig_binary | |
| 605 | |
| 606 # Handle commands. | |
| 607 # | |
| 608 have_installed = False | |
| 609 for command in commands: | |
| 610 log(f'### {command=}.') | |
| 611 if 0: | |
| 612 pass | |
| 613 | |
| 614 elif command in ('build', 'wheel'): | |
| 615 build( | |
| 616 env_extra, | |
| 617 build_isolation=build_isolation, | |
| 618 venv=venv, | |
| 619 wheel=(command=='wheel'), | |
| 620 ) | |
| 621 have_installed = True | |
| 622 | |
| 623 elif command == 'cibw': | |
| 624 # Build wheel(s) with cibuildwheel. | |
| 625 if cibw_pyodide and env_extra.get('CIBW_BUILD') is None: | |
| 626 assert 0, f'Need a Python version for Pyodide.' | |
| 627 CIBW_BUILD = 'cp312*' | |
| 628 env_extra['CIBW_BUILD'] = CIBW_BUILD | |
| 629 log(f'Defaulting to {CIBW_BUILD=} for Pyodide.') | |
| 630 #if cibw_pyodide_version == None: | |
| 631 # cibw_pyodide_version = '0.28.0' | |
| 632 cibuildwheel( | |
| 633 env_extra, | |
| 634 cibw_name or 'cibuildwheel', | |
| 635 cibw_pyodide, | |
| 636 cibw_pyodide_version, | |
| 637 cibw_sdist, | |
| 638 ) | |
| 639 | |
| 640 elif command == 'install': | |
| 641 p = 'pymupdf' | |
| 642 if install_version: | |
| 643 if not install_version.startswith(('==', '>=', '>')): | |
| 644 p = f'{p}==' | |
| 645 p = f'{p}{install_version}' | |
| 646 run(f'pip install --force-reinstall {p}') | |
| 647 have_installed = True | |
| 648 | |
| 649 elif command == 'test': | |
| 650 if not have_installed: | |
| 651 log(f'## Warning: have not built/installed PyMuPDF; testing whatever is already installed.') | |
| 652 test( | |
| 653 env_extra=env_extra, | |
| 654 implementations=implementations, | |
| 655 test_names=test_names, | |
| 656 pytest_options=pytest_options, | |
| 657 test_timeout=test_timeout, | |
| 658 pytest_prefix=pytest_prefix, | |
| 659 test_fitz=test_fitz, | |
| 660 pybind=pybind, | |
| 661 system_packages=system_packages, | |
| 662 venv=venv, | |
| 663 ) | |
| 664 | |
| 665 elif command == 'pyodide': | |
| 666 build_pyodide_wheel(pyodide_build_version=pyodide_build_version) | |
| 667 | |
| 668 else: | |
| 669 assert 0, f'{command=}' | |
| 670 | |
| 671 | |
| 672 def get_env_bool(name, default=0): | |
| 673 v = os.environ.get(name) | |
| 674 if v in ('1', 'true'): | |
| 675 return 1 | |
| 676 elif v in ('0', 'false'): | |
| 677 return 0 | |
| 678 elif v is None: | |
| 679 return default | |
| 680 else: | |
| 681 assert 0, f'Bad environ {name=} {v=}' | |
| 682 | |
| 683 def show_help(): | |
| 684 print(__doc__) | |
| 685 print(venv_info()) | |
| 686 | |
| 687 | |
| 688 def github_workflow_unimportant(): | |
| 689 ''' | |
| 690 Returns true if we are running a Github scheduled workflow but in a | |
| 691 repository not called 'PyMuPDF'. This can be used to avoid consuming | |
| 692 unnecessary Github minutes running workflows on non-main repositories such | |
| 693 as ArtifexSoftware/PyMuPDF-julian. | |
| 694 ''' | |
| 695 GITHUB_EVENT_NAME = os.environ.get('GITHUB_EVENT_NAME') | |
| 696 GITHUB_REPOSITORY = os.environ.get('GITHUB_REPOSITORY') | |
| 697 if GITHUB_EVENT_NAME == 'schedule' and GITHUB_REPOSITORY != 'pymupdf/PyMuPDF': | |
| 698 log(f'## This is an unimportant Github workflow: a scheduled event, not in the main repository `pymupdf/PyMuPDF`.') | |
| 699 log(f'## {GITHUB_EVENT_NAME=}.') | |
| 700 log(f'## {GITHUB_REPOSITORY=}.') | |
| 701 return True | |
| 702 | |
| 703 def venv_info(pytest_args=None): | |
| 704 ''' | |
| 705 Returns string containing information about the venv we use and how to | |
| 706 run tests manually. If specified, `pytest_args` contains the pytest args, | |
| 707 otherwise we use an example. | |
| 708 ''' | |
| 709 pymupdf_dir_rel = gh_release.relpath(pymupdf_dir) | |
| 710 ret = f'Name of venv: {gh_release.venv_name}\n' | |
| 711 if pytest_args is None: | |
| 712 pytest_args = f'{pymupdf_dir_rel}/tests/test_general.py::test_subset_fonts' | |
| 713 if platform.system() == 'Windows': | |
| 714 ret += textwrap.dedent(f''' | |
| 715 Rerun tests manually with rebased implementation: | |
| 716 Enter venv: | |
| 717 {gh_release.venv_name}\\Scripts\\activate | |
| 718 Run specific test in venv: | |
| 719 {gh_release.venv_name}\\Scripts\\python -m pytest {pytest_args} | |
| 720 ''') | |
| 721 else: | |
| 722 ret += textwrap.dedent(f''' | |
| 723 Rerun tests manually with rebased implementation: | |
| 724 Enter venv and run specific test, also under gdb: | |
| 725 . {gh_release.venv_name}/bin/activate | |
| 726 python -m pytest {pytest_args} | |
| 727 gdb --args python -m pytest {pytest_args} | |
| 728 Run without explicitly entering venv, also under gdb: | |
| 729 ./{gh_release.venv_name}/bin/python -m pytest {pytest_args} | |
| 730 gdb --args ./{gh_release.venv_name}/bin/python -m pytest {pytest_args} | |
| 731 ''') | |
| 732 return ret | |
| 733 | |
| 734 | |
| 735 def build( | |
| 736 env_extra, | |
| 737 *, | |
| 738 build_isolation, | |
| 739 venv, | |
| 740 wheel, | |
| 741 ): | |
| 742 print(f'{build_isolation=}') | |
| 743 | |
| 744 if build_isolation is None: | |
| 745 # On OpenBSD libclang is not available on pypi.org, so we need to force | |
| 746 # use of system package py3-llvm with --no-build-isolation, manually | |
| 747 # installing other required packages. | |
| 748 build_isolation = False if platform.system() == 'OpenBSD' else True | |
| 749 | |
| 750 if build_isolation: | |
| 751 # This is the default on non-OpenBSD. | |
| 752 build_isolation_text = '' | |
| 753 else: | |
| 754 # Not using build isolation - i.e. pip will not be using its own clean | |
| 755 # venv, so we need to explicitly install required packages. Manually | |
| 756 # install required packages from pyproject.toml. | |
| 757 sys.path.insert(0, os.path.abspath(f'{__file__}/../..')) | |
| 758 import setup | |
| 759 names = setup.get_requires_for_build_wheel() | |
| 760 del sys.path[0] | |
| 761 if names: | |
| 762 names = ' '.join(names) | |
| 763 if venv == 2: | |
| 764 run( f'python -m pip install --upgrade {names}') | |
| 765 else: | |
| 766 log(f'{venv=}: Not installing packages with pip: {names}') | |
| 767 build_isolation_text = ' --no-build-isolation' | |
| 768 | |
| 769 if wheel: | |
| 770 new_files = pipcl.NewFiles(f'wheelhouse/*.whl') | |
| 771 run(f'pip wheel{build_isolation_text} -w wheelhouse -v {pymupdf_dir_abs}', env_extra=env_extra) | |
| 772 wheel = new_files.get_one() | |
| 773 run(f'pip install --force-reinstall {wheel}') | |
| 774 else: | |
| 775 run(f'pip install{build_isolation_text} -v --force-reinstall {pymupdf_dir_abs}', env_extra=env_extra) | |
| 776 | |
| 777 | |
| 778 def cibuildwheel(env_extra, cibw_name, cibw_pyodide, cibw_pyodide_version, cibw_sdist): | |
| 779 | |
| 780 if cibw_sdist and platform.system() == 'Linux': | |
| 781 log(f'Building sdist.') | |
| 782 run(f'cd {pymupdf_dir_abs} && {sys.executable} setup.py -d wheelhouse sdist', env_extra=env_extra) | |
| 783 sdists = glob.glob(f'{pymupdf_dir_abs}/wheelhouse/pymupdf-*.tar.gz') | |
| 784 log(f'{sdists=}') | |
| 785 assert sdists | |
| 786 | |
| 787 run(f'pip install --upgrade --force-reinstall {cibw_name}') | |
| 788 | |
| 789 # Some general flags. | |
| 790 if 'CIBW_BUILD_VERBOSITY' not in env_extra: | |
| 791 env_extra['CIBW_BUILD_VERBOSITY'] = '1' | |
| 792 if 'CIBW_SKIP' not in env_extra: | |
| 793 env_extra['CIBW_SKIP'] = 'pp* *i686 cp36* cp37* *musllinux* *-win32 *-aarch64' | |
| 794 | |
| 795 # Set what wheels to build, if not already specified. | |
| 796 if 'CIBW_ARCHS' not in env_extra: | |
| 797 if 'CIBW_ARCHS_WINDOWS' not in env_extra: | |
| 798 env_extra['CIBW_ARCHS_WINDOWS'] = 'auto64' | |
| 799 | |
| 800 if 'CIBW_ARCHS_MACOS' not in env_extra: | |
| 801 env_extra['CIBW_ARCHS_MACOS'] = 'auto64' | |
| 802 | |
| 803 if 'CIBW_ARCHS_LINUX' not in env_extra: | |
| 804 env_extra['CIBW_ARCHS_LINUX'] = 'auto64' | |
| 805 | |
| 806 # Tell cibuildwheel not to use `auditwheel` on Linux and MacOS, | |
| 807 # because it cannot cope with us deliberately having required | |
| 808 # libraries in different wheel - specifically in the PyMuPDF wheel. | |
| 809 # | |
| 810 # We cannot use a subset of auditwheel's functionality | |
| 811 # with `auditwheel addtag` because it says `No tags | |
| 812 # to be added` and terminates with non-zero. See: | |
| 813 # https://github.com/pypa/auditwheel/issues/439. | |
| 814 # | |
| 815 env_extra['CIBW_REPAIR_WHEEL_COMMAND_LINUX'] = '' | |
| 816 env_extra['CIBW_REPAIR_WHEEL_COMMAND_MACOS'] = '' | |
| 817 | |
| 818 # Tell cibuildwheel how to test PyMuPDF. | |
| 819 if 'CIBW_TEST_COMMAND' not in env_extra: | |
| 820 env_extra['CIBW_TEST_COMMAND'] = f'python {{project}}/scripts/test.py test' | |
| 821 | |
| 822 # Specify python versions. | |
| 823 CIBW_BUILD = env_extra.get('CIBW_BUILD') | |
| 824 log(f'{CIBW_BUILD=}') | |
| 825 if CIBW_BUILD is None: | |
| 826 if os.environ.get('GITHUB_ACTIONS') == 'true': | |
| 827 # Build/test all supported Python versions. | |
| 828 CIBW_BUILD = 'cp39* cp310* cp311* cp312* cp313*' | |
| 829 else: | |
| 830 # Build/test current Python only. | |
| 831 v = platform.python_version_tuple()[:2] | |
| 832 log(f'{v=}') | |
| 833 CIBW_BUILD = f'cp{"".join(v)}*' | |
| 834 | |
| 835 cibw_pyodide_args = '' | |
| 836 if cibw_pyodide: | |
| 837 cibw_pyodide_args = ' --platform pyodide' | |
| 838 env_extra['HAVE_LIBCRYPTO'] = 'no' | |
| 839 env_extra['PYMUPDF_SETUP_MUPDF_TESSERACT'] = '0' | |
| 840 if cibw_pyodide_version: | |
| 841 # 2025-07-21: there is no --pyodide-version option so we set | |
| 842 # CIBW_PYODIDE_VERSION. | |
| 843 env_extra['CIBW_PYODIDE_VERSION'] = cibw_pyodide_version | |
| 844 env_extra['CIBW_ENABLE'] = 'pyodide-prerelease' | |
| 845 | |
| 846 # Pass all the environment variables we have set, to Linux | |
| 847 # docker. Note that this will miss any settings in the original | |
| 848 # environment. | |
| 849 env_extra['CIBW_ENVIRONMENT_PASS_LINUX'] = ' '.join(sorted(env_extra.keys())) | |
| 850 | |
| 851 # Build for lowest (assumed first) Python version. | |
| 852 # | |
| 853 CIBW_BUILD_0 = CIBW_BUILD.split()[0] | |
| 854 log(f'Building for first Python version {CIBW_BUILD_0}.') | |
| 855 env_extra['CIBW_BUILD'] = CIBW_BUILD_0 | |
| 856 run(f'cd {pymupdf_dir} && cibuildwheel{cibw_pyodide_args}', env_extra=env_extra) | |
| 857 | |
| 858 # Tell cibuildwheel to build and test all specified Python versions; it | |
| 859 # will notice that the wheel we built above supports all versions of | |
| 860 # Python, so will not actually do any builds here. | |
| 861 # | |
| 862 env_extra['CIBW_BUILD'] = CIBW_BUILD | |
| 863 run(f'cd {pymupdf_dir} && cibuildwheel{cibw_pyodide_args}', env_extra=env_extra) | |
| 864 run(f'ls -ld {pymupdf_dir}/wheelhouse/*') | |
| 865 | |
| 866 | |
| 867 def build_pyodide_wheel(pyodide_build_version=None): | |
| 868 ''' | |
| 869 Build Pyodide wheel. | |
| 870 | |
| 871 This runs `pyodide build` inside the PyMuPDF directory, which in turn runs | |
| 872 setup.py in a Pyodide build environment. | |
| 873 ''' | |
| 874 log(f'## Building Pyodide wheel.') | |
| 875 | |
| 876 # Our setup.py does not know anything about Pyodide; we set a few | |
| 877 # required environmental variables here. | |
| 878 # | |
| 879 env_extra = dict() | |
| 880 | |
| 881 # Disable libcrypto because not available in Pyodide. | |
| 882 env_extra['HAVE_LIBCRYPTO'] = 'no' | |
| 883 | |
| 884 # Tell MuPDF to build for Pyodide. | |
| 885 env_extra['OS'] = 'pyodide' | |
| 886 | |
| 887 # Build a single wheel without a separate PyMuPDFb wheel. | |
| 888 env_extra['PYMUPDF_SETUP_FLAVOUR'] = 'pb' | |
| 889 | |
| 890 # 2023-08-30: We set PYMUPDF_SETUP_MUPDF_BUILD_TESSERACT=0 because | |
| 891 # otherwise mupdf thirdparty/tesseract/src/ccstruct/dppoint.cpp fails to | |
| 892 # build because `#include "errcode.h"` finds a header inside emsdk. This is | |
| 893 # pyodide bug https://github.com/pyodide/pyodide/issues/3839. It's fixed in | |
| 894 # https://github.com/pyodide/pyodide/pull/3866 but the fix has not reached | |
| 895 # pypi.org's pyodide-build package. E.g. currently in tag 0.23.4, but | |
| 896 # current devuan pyodide-build is pyodide_build-0.23.4. | |
| 897 # | |
| 898 env_extra['PYMUPDF_SETUP_MUPDF_TESSERACT'] = '0' | |
| 899 setup = pyodide_setup(pymupdf_dir, pyodide_build_version=pyodide_build_version) | |
| 900 command = f'{setup} && echo "### Running pyodide build" && pyodide build --exports whole_archive' | |
| 901 | |
| 902 command = command.replace(' && ', '\n && ') | |
| 903 | |
| 904 run(command, env_extra=env_extra) | |
| 905 | |
| 906 # Copy wheel into `wheelhouse/` so it is picked up as a workflow | |
| 907 # artifact. | |
| 908 # | |
| 909 run(f'ls -l {pymupdf_dir}/dist/') | |
| 910 run(f'mkdir -p {pymupdf_dir}/wheelhouse && cp -p {pymupdf_dir}/dist/* {pymupdf_dir}/wheelhouse/') | |
| 911 run(f'ls -l {pymupdf_dir}/wheelhouse/') | |
| 912 | |
| 913 | |
| 914 def pyodide_setup( | |
| 915 directory, | |
| 916 clean=False, | |
| 917 pyodide_build_version=None, | |
| 918 ): | |
| 919 ''' | |
| 920 Returns a command that will set things up for a pyodide build. | |
| 921 | |
| 922 Args: | |
| 923 directory: | |
| 924 Our command cd's into this directory. | |
| 925 clean: | |
| 926 If true we create an entirely new environment. Otherwise | |
| 927 we reuse any existing emsdk repository and venv. | |
| 928 pyodide_build_version: | |
| 929 Version of Python package pyodide-build; if None we use latest | |
| 930 available version. | |
| 931 2025-02-13: pyodide_build_version='0.29.3' works. | |
| 932 | |
| 933 The returned command does the following: | |
| 934 | |
| 935 * Checkout latest emsdk from https://github.com/emscripten-core/emsdk.git: | |
| 936 * Clone emsdk repository to `emsdk` if not already present. | |
| 937 * Run `git pull -r` inside emsdk checkout. | |
| 938 * Create venv `venv_pyodide_<python_version>` if not already present. | |
| 939 * Activate venv `venv_pyodide_<python_version>`. | |
| 940 * Install/upgrade package `pyodide-build`. | |
| 941 * Run emsdk install scripts and enter emsdk environment. | |
| 942 | |
| 943 Example usage in a build function: | |
| 944 | |
| 945 command = pyodide_setup() | |
| 946 command += ' && pyodide build --exports pyinit' | |
| 947 subprocess.run(command, shell=1, check=1) | |
| 948 ''' | |
| 949 | |
| 950 pv = platform.python_version_tuple()[:2] | |
| 951 assert pv == ('3', '12'), f'Pyodide builds need to be run with Python-3.12 but current Python is {platform.python_version()}.' | |
| 952 command = f'cd {directory}' | |
| 953 | |
| 954 # Clone/update emsdk. We always use the latest emsdk with `git pull`. | |
| 955 # | |
| 956 # 2025-02-13: this works: 2514ec738de72cebbba7f4fdba0cf2fabcb779a5 | |
| 957 # | |
| 958 dir_emsdk = 'emsdk' | |
| 959 if clean: | |
| 960 shutil.rmtree(dir_emsdk, ignore_errors=1) | |
| 961 # 2024-06-25: old `.pyodide-xbuildenv` directory was breaking build, so | |
| 962 # important to remove it here. | |
| 963 shutil.rmtree('.pyodide-xbuildenv', ignore_errors=1) | |
| 964 if not os.path.exists(f'{directory}/{dir_emsdk}'): | |
| 965 command += f' && echo "### Cloning emsdk.git"' | |
| 966 command += f' && git clone https://github.com/emscripten-core/emsdk.git {dir_emsdk}' | |
| 967 command += f' && echo "### Updating checkout {dir_emsdk}"' | |
| 968 command += f' && (cd {dir_emsdk} && git pull -r)' | |
| 969 command += f' && echo "### Checkout {dir_emsdk} is:"' | |
| 970 command += f' && (cd {dir_emsdk} && git show -s --oneline)' | |
| 971 | |
| 972 # Create and enter Python venv. | |
| 973 # | |
| 974 python = sys.executable | |
| 975 venv_pyodide = f'venv_pyodide_{sys.version_info[0]}.{sys.version_info[1]}' | |
| 976 | |
| 977 if not os.path.exists( f'{directory}/{venv_pyodide}'): | |
| 978 command += f' && echo "### Creating venv {venv_pyodide}"' | |
| 979 command += f' && {python} -m venv {venv_pyodide}' | |
| 980 command += f' && . {venv_pyodide}/bin/activate' | |
| 981 command += f' && echo "### Installing Python packages."' | |
| 982 command += f' && python -m pip install --upgrade pip wheel pyodide-build' | |
| 983 if pyodide_build_version: | |
| 984 command += f'=={pyodide_build_version}' | |
| 985 | |
| 986 # Run emsdk install scripts and enter emsdk environment. | |
| 987 # | |
| 988 command += f' && cd {dir_emsdk}' | |
| 989 command += ' && PYODIDE_EMSCRIPTEN_VERSION=$(pyodide config get emscripten_version)' | |
| 990 command += ' && echo "### PYODIDE_EMSCRIPTEN_VERSION is: $PYODIDE_EMSCRIPTEN_VERSION"' | |
| 991 command += ' && echo "### Running ./emsdk install"' | |
| 992 command += ' && ./emsdk install ${PYODIDE_EMSCRIPTEN_VERSION}' | |
| 993 command += ' && echo "### Running ./emsdk activate"' | |
| 994 command += ' && ./emsdk activate ${PYODIDE_EMSCRIPTEN_VERSION}' | |
| 995 command += ' && echo "### Running ./emsdk_env.sh"' | |
| 996 command += ' && . ./emsdk_env.sh' # Need leading `./` otherwise weird 'Not found' error. | |
| 997 | |
| 998 command += ' && cd ..' | |
| 999 return command | |
| 1000 | |
| 1001 | |
| 1002 def test( | |
| 1003 *, | |
| 1004 env_extra, | |
| 1005 implementations, | |
| 1006 venv=False, | |
| 1007 test_names=None, | |
| 1008 pytest_options=None, | |
| 1009 test_timeout=None, | |
| 1010 pytest_prefix=None, | |
| 1011 test_fitz=True, | |
| 1012 pytest_k=None, | |
| 1013 pybind=False, | |
| 1014 system_packages=False, | |
| 1015 ): | |
| 1016 if pybind: | |
| 1017 cpp_path = 'pymupdf_test_pybind.cpp' | |
| 1018 cpp_exe = 'pymupdf_test_pybind.exe' | |
| 1019 cpp = textwrap.dedent(''' | |
| 1020 #include <pybind11/embed.h> | |
| 1021 | |
| 1022 int main() | |
| 1023 { | |
| 1024 pybind11::scoped_interpreter guard{}; | |
| 1025 pybind11::exec(R"( | |
| 1026 print('Hello world', flush=1) | |
| 1027 import pymupdf | |
| 1028 pymupdf.JM_mupdf_show_warnings = 1 | |
| 1029 print(f'{pymupdf.version=}', flush=1) | |
| 1030 doc = pymupdf.Document() | |
| 1031 pymupdf.mupdf.fz_warn('Dummy warning.') | |
| 1032 pymupdf.mupdf.fz_warn('Dummy warning.') | |
| 1033 pymupdf.mupdf.fz_warn('Dummy warning.') | |
| 1034 print(f'{doc=}', flush=1) | |
| 1035 )"); | |
| 1036 } | |
| 1037 ''') | |
| 1038 def fs_read(path): | |
| 1039 try: | |
| 1040 with open(path) as f: | |
| 1041 return f.read() | |
| 1042 except Exception: | |
| 1043 return | |
| 1044 def fs_remove(path): | |
| 1045 try: | |
| 1046 os.remove(path) | |
| 1047 except Exception: | |
| 1048 pass | |
| 1049 cpp_existing = fs_read(cpp_path) | |
| 1050 if cpp == cpp_existing: | |
| 1051 log(f'Not creating {cpp_exe} because unchanged: {cpp_path}') | |
| 1052 else: | |
| 1053 with open(cpp_path, 'w') as f: | |
| 1054 f.write(cpp) | |
| 1055 def getmtime(path): | |
| 1056 try: | |
| 1057 return os.path.getmtime(path) | |
| 1058 except Exception: | |
| 1059 return 0 | |
| 1060 python_config = f'{os.path.realpath(sys.executable)}-config' | |
| 1061 # `--embed` adds `-lpython3.11` to the link command, which appears to | |
| 1062 # be necessary when building an executable. | |
| 1063 flags = run(f'{python_config} --cflags --ldflags --embed', capture=1) | |
| 1064 build_command = f'c++ {cpp_path} -o {cpp_exe} -g -W -Wall {flags}' | |
| 1065 build_path = f'{cpp_exe}.cmd' | |
| 1066 build_command_prev = fs_read(build_path) | |
| 1067 if build_command != build_command_prev or getmtime(cpp_path) >= getmtime(cpp_exe): | |
| 1068 fs_remove(build_path) | |
| 1069 run(build_command) | |
| 1070 with open(build_path, 'w') as f: | |
| 1071 f.write(build_command) | |
| 1072 run(f'./{cpp_exe}') | |
| 1073 return | |
| 1074 | |
| 1075 pymupdf_dir_rel = gh_release.relpath(pymupdf_dir) | |
| 1076 if not pytest_options and pytest_prefix == 'valgrind': | |
| 1077 pytest_options = '-sv' | |
| 1078 if pytest_k: | |
| 1079 pytest_options += f' -k {shlex.quote(pytest_k)}' | |
| 1080 pytest_arg = '' | |
| 1081 if test_names: | |
| 1082 for test_name in test_names: | |
| 1083 pytest_arg += f' {pymupdf_dir_rel}/{test_name}' | |
| 1084 else: | |
| 1085 pytest_arg += f' {pymupdf_dir_rel}/tests' | |
| 1086 python = gh_release.relpath(sys.executable) | |
| 1087 log('Running tests with tests/run_compound.py and pytest.') | |
| 1088 | |
| 1089 PYODIDE_ROOT = os.environ.get('PYODIDE_ROOT') | |
| 1090 if PYODIDE_ROOT is not None: | |
| 1091 log(f'Not installing test packages because {PYODIDE_ROOT=}.') | |
| 1092 command = f'{pytest_options} {pytest_arg} -s' | |
| 1093 args = shlex.split(command) | |
| 1094 print(f'{PYODIDE_ROOT=} so calling pytest.main(args).') | |
| 1095 print(f'{command=}') | |
| 1096 print(f'args are ({len(args)}):') | |
| 1097 for arg in args: | |
| 1098 print(f' {arg!r}') | |
| 1099 import pytest | |
| 1100 pytest.main(args) | |
| 1101 return | |
| 1102 | |
| 1103 if venv >= 2: | |
| 1104 run(f'pip install --upgrade {gh_release.test_packages}') | |
| 1105 else: | |
| 1106 log(f'{venv=}: Not installing test packages: {gh_release.test_packages}') | |
| 1107 run_compound_args = '' | |
| 1108 | |
| 1109 if implementations: | |
| 1110 run_compound_args += f' -i {implementations}' | |
| 1111 | |
| 1112 if test_timeout: | |
| 1113 run_compound_args += f' -t {test_timeout}' | |
| 1114 | |
| 1115 if pytest_prefix in ('valgrind', 'helgrind'): | |
| 1116 if system_packages: | |
| 1117 log('Installing valgrind.') | |
| 1118 run(f'sudo apt update') | |
| 1119 run(f'sudo apt install --upgrade valgrind') | |
| 1120 run(f'valgrind --version') | |
| 1121 | |
| 1122 command = f'{python} {pymupdf_dir_rel}/tests/run_compound.py{run_compound_args}' | |
| 1123 | |
| 1124 if pytest_prefix is None: | |
| 1125 pass | |
| 1126 elif pytest_prefix == 'gdb': | |
| 1127 command += ' gdb --args' | |
| 1128 elif pytest_prefix == 'valgrind': | |
| 1129 env_extra['PYMUPDF_RUNNING_ON_VALGRIND'] = '1' | |
| 1130 env_extra['PYTHONMALLOC'] = 'malloc' | |
| 1131 command += ( | |
| 1132 f' valgrind' | |
| 1133 f' --suppressions={pymupdf_dir_abs}/valgrind.supp' | |
| 1134 f' --trace-children=no' | |
| 1135 f' --num-callers=20' | |
| 1136 f' --error-exitcode=100' | |
| 1137 f' --errors-for-leak-kinds=none' | |
| 1138 f' --fullpath-after=' | |
| 1139 ) | |
| 1140 elif pytest_prefix == 'helgrind': | |
| 1141 env_extra['PYMUPDF_RUNNING_ON_VALGRIND'] = '1' | |
| 1142 env_extra['PYTHONMALLOC'] = 'malloc' | |
| 1143 command = ( | |
| 1144 f' valgrind' | |
| 1145 f' --tool=helgrind' | |
| 1146 f' --trace-children=no' | |
| 1147 f' --num-callers=20' | |
| 1148 f' --error-exitcode=100' | |
| 1149 f' --fullpath-after=' | |
| 1150 ) | |
| 1151 else: | |
| 1152 assert 0, f'Unrecognised {pytest_prefix=}' | |
| 1153 | |
| 1154 if platform.system() == 'Windows': | |
| 1155 # `python -m pytest` doesn't seem to work. | |
| 1156 command += ' pytest' | |
| 1157 else: | |
| 1158 # On OpenBSD `pip install pytest` doesn't seem to install the pytest | |
| 1159 # command, so we use `python -m pytest ...`. | |
| 1160 command += f' {python} -m pytest' | |
| 1161 | |
| 1162 command += f' {pytest_options} {pytest_arg}' | |
| 1163 | |
| 1164 # Always start by removing any test_*_fitz.py files. | |
| 1165 for p in glob.glob(f'{pymupdf_dir_rel}/tests/test_*_fitz.py'): | |
| 1166 print(f'Removing {p=}') | |
| 1167 os.remove(p) | |
| 1168 if test_fitz: | |
| 1169 # Create copies of each test file, modified to use `pymupdf` | |
| 1170 # instead of `fitz`. | |
| 1171 for p in glob.glob(f'{pymupdf_dir_rel}/tests/test_*.py'): | |
| 1172 if os.path.basename(p).startswith('test_fitz_'): | |
| 1173 # Don't recursively generate test_fitz_fitz_foo.py, | |
| 1174 # test_fitz_fitz_fitz_foo.py, ... etc. | |
| 1175 continue | |
| 1176 branch, leaf = os.path.split(p) | |
| 1177 p2 = f'{branch}/{leaf[:5]}fitz_{leaf[5:]}' | |
| 1178 print(f'Converting {p=} to {p2=}.') | |
| 1179 with open(p, encoding='utf8') as f: | |
| 1180 text = f.read() | |
| 1181 text2 = re.sub("([^\'])\\bpymupdf\\b", '\\1fitz', text) | |
| 1182 if p.replace(os.sep, '/') == f'{pymupdf_dir_rel}/tests/test_docs_samples.py'.replace(os.sep, '/'): | |
| 1183 assert text2 == text | |
| 1184 else: | |
| 1185 assert text2 != text, f'Unexpectedly unchanged when creating {p!r} => {p2!r}' | |
| 1186 with open(p2, 'w', encoding='utf8') as f: | |
| 1187 f.write(text2) | |
| 1188 try: | |
| 1189 log(f'Running tests with tests/run_compound.py and pytest.') | |
| 1190 run(command, env_extra=env_extra, timeout=test_timeout) | |
| 1191 | |
| 1192 except subprocess.TimeoutExpired as e: | |
| 1193 log(f'Timeout when running tests.') | |
| 1194 raise | |
| 1195 finally: | |
| 1196 log(f'\n' | |
| 1197 f'[As of 2024-10-10 we get warnings from pytest/Python such as:\n' | |
| 1198 f' DeprecationWarning: builtin type SwigPyPacked has no __module__ attribute\n' | |
| 1199 f'This seems to be due to Swig\'s handling of Py_LIMITED_API.\n' | |
| 1200 f'For details see https://github.com/swig/swig/issues/2881.\n' | |
| 1201 f']' | |
| 1202 ) | |
| 1203 log('\n' + venv_info(pytest_args=f'{pytest_options} {pytest_arg}')) | |
| 1204 | |
| 1205 | |
| 1206 def get_pyproject_required(ppt=None): | |
| 1207 ''' | |
| 1208 Returns space-separated names of required packages in pyproject.toml. We | |
| 1209 do not do a proper parse and rely on the packages being in a single line. | |
| 1210 ''' | |
| 1211 if ppt is None: | |
| 1212 ppt = os.path.abspath(f'{__file__}/../../pyproject.toml') | |
| 1213 with open(ppt) as f: | |
| 1214 for line in f: | |
| 1215 m = re.match('^requires = \\[(.*)\\]$', line) | |
| 1216 if m: | |
| 1217 names = m.group(1).replace(',', ' ').replace('"', '') | |
| 1218 return names | |
| 1219 else: | |
| 1220 assert 0, f'Failed to find "requires" line in {ppt}' | |
| 1221 | |
| 1222 def wrap_get_requires_for_build_wheel(dir_): | |
| 1223 ''' | |
| 1224 Returns space-separated list of required | |
| 1225 packages. Looks at `dir_`/pyproject.toml and calls | |
| 1226 `dir_`/setup.py:get_requires_for_build_wheel(). | |
| 1227 ''' | |
| 1228 dir_abs = os.path.abspath(dir_) | |
| 1229 ret = list() | |
| 1230 ppt = os.path.join(dir_abs, 'pyproject.toml') | |
| 1231 if os.path.exists(ppt): | |
| 1232 ret += get_pyproject_required(ppt) | |
| 1233 if os.path.exists(os.path.join(dir_abs, 'setup.py')): | |
| 1234 sys.path.insert(0, dir_abs) | |
| 1235 try: | |
| 1236 from setup import get_requires_for_build_wheel as foo | |
| 1237 for i in foo(): | |
| 1238 ret.append(i) | |
| 1239 finally: | |
| 1240 del sys.path[0] | |
| 1241 return ' '.join(ret) | |
| 1242 | |
| 1243 | |
| 1244 def venv_in(path=None): | |
| 1245 ''' | |
| 1246 If path is None, returns true if we are in a venv. Otherwise returns true | |
| 1247 only if we are in venv <path>. | |
| 1248 ''' | |
| 1249 if path: | |
| 1250 return os.path.abspath(sys.prefix) == os.path.abspath(path) | |
| 1251 else: | |
| 1252 return sys.prefix != sys.base_prefix | |
| 1253 | |
| 1254 | |
| 1255 def venv_run(args, path, recreate=True, clean=False): | |
| 1256 ''' | |
| 1257 Runs command inside venv and returns termination code. | |
| 1258 | |
| 1259 Args: | |
| 1260 args: | |
| 1261 List of args. | |
| 1262 path: | |
| 1263 Name of venv. | |
| 1264 recreate: | |
| 1265 If false we do not run `<sys.executable> -m venv <path>` if <path> | |
| 1266 already exists. This avoids a delay in the common case where <path> | |
| 1267 is already set up, but fails if <path> exists but does not contain | |
| 1268 a valid venv. | |
| 1269 clean: | |
| 1270 If true we first delete <path>. | |
| 1271 ''' | |
| 1272 if clean: | |
| 1273 log(f'Removing any existing venv {path}.') | |
| 1274 assert path.startswith('venv-') | |
| 1275 shutil.rmtree(path, ignore_errors=1) | |
| 1276 if recreate or not os.path.isdir(path): | |
| 1277 run(f'{sys.executable} -m venv {path}') | |
| 1278 if platform.system() == 'Windows': | |
| 1279 command = f'{path}\\Scripts\\activate && python' | |
| 1280 # shlex not reliable on Windows. | |
| 1281 # Use crude quoting with "...". Seems to work. | |
| 1282 for arg in args: | |
| 1283 assert '"' not in arg | |
| 1284 command += f' "{arg}"' | |
| 1285 else: | |
| 1286 command = f'. {path}/bin/activate && python {shlex.join(args)}' | |
| 1287 e = run(command, check=0) | |
| 1288 return e | |
| 1289 | |
| 1290 | |
| 1291 if __name__ == '__main__': | |
| 1292 try: | |
| 1293 sys.exit(main(sys.argv)) | |
| 1294 except (subprocess.CalledProcessError, subprocess.TimeoutExpired) as e: | |
| 1295 # Terminate relatively quietly, failed commands will usually have | |
| 1296 # generated diagnostics. | |
| 1297 log(f'{e}') | |
| 1298 sys.exit(1) | |
| 1299 # Other exceptions should not happen, and will generate a full Python | |
| 1300 # backtrace etc here. |
