Mercurial > hgrepos > Python2 > PyMuPDF
comparison setup.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 | 5ab937c03c27 a6bc019ac0b2 |
comparison
equal
deleted
inserted
replaced
| -1:000000000000 | 1:1d09e1dec1d9 |
|---|---|
| 1 #! /usr/bin/env python3 | |
| 2 | |
| 3 ''' | |
| 4 Overview: | |
| 5 | |
| 6 Build script for PyMuPDF, supporting PEP-517 and simple command-line usage. | |
| 7 | |
| 8 We hard-code the URL of the MuPDF .tar.gz file that we require. This | |
| 9 generally points to a particular source release on mupdf.com. | |
| 10 | |
| 11 Default behaviour: | |
| 12 | |
| 13 Building an sdist: | |
| 14 As of 2024-002-28 we no longer download the MuPDF .tar.gz file and | |
| 15 embed it within the sdist. Instead it will be downloaded at build | |
| 16 time. | |
| 17 | |
| 18 Building PyMuPDF: | |
| 19 We first download the hard-coded mupdf .tar.gz file. | |
| 20 | |
| 21 Then we extract and build MuPDF locally, before building PyMuPDF | |
| 22 itself. So PyMuPDF will always be built with the exact MuPDF | |
| 23 release that we require. | |
| 24 | |
| 25 | |
| 26 Environmental variables: | |
| 27 | |
| 28 If building with system MuPDF (PYMUPDF_SETUP_MUPDF_BUILD is empty string): | |
| 29 | |
| 30 CFLAGS | |
| 31 CXXFLAGS | |
| 32 LDFLAGS | |
| 33 Added to c, c++, and link commands. | |
| 34 | |
| 35 PYMUPDF_INCLUDES | |
| 36 Colon-separated extra include paths. | |
| 37 | |
| 38 PYMUPDF_MUPDF_LIB | |
| 39 Directory containing MuPDF libraries, (libmupdf.so, | |
| 40 libmupdfcpp.so). | |
| 41 | |
| 42 PYMUPDF_SETUP_DEVENV | |
| 43 Location of devenv.com on Windows. If unset we search for it - see | |
| 44 wdev.py. if that fails we use just 'devenv.com'. | |
| 45 | |
| 46 PYMUPDF_SETUP_DUMMY | |
| 47 If 1, we build dummy sdist and wheel with no files. | |
| 48 | |
| 49 PYMUPDF_SETUP_FLAVOUR | |
| 50 Control building of separate wheels for PyMuPDF. | |
| 51 | |
| 52 Must be unset or a combination of 'p', 'b' and 'd'. | |
| 53 | |
| 54 Default is 'pbd'. | |
| 55 | |
| 56 'p': | |
| 57 Generated wheel contains PyMuPDF code. | |
| 58 'b': | |
| 59 Generated wheel contains MuPDF libraries; these are independent of | |
| 60 the Python version. | |
| 61 'd': | |
| 62 Generated wheel contains includes and libraries for MuPDF. | |
| 63 | |
| 64 If 'p' is included, the generated wheel is called PyMuPDF. | |
| 65 Otherwise if 'b' is included the generated wheel is called PyMuPDFb. | |
| 66 Otherwise if 'd' is included the generated wheel is called PyMuPDFd. | |
| 67 | |
| 68 For example: | |
| 69 | |
| 70 'pb': a `PyMuPDF` wheel with PyMuPDF runtime files and MuPDF | |
| 71 runtime shared libraries. | |
| 72 | |
| 73 'b': a `PyMuPDFb` wheel containing MuPDF runtime shared libraries. | |
| 74 | |
| 75 'pbd' a `PyMuPDF` wheel with PyMuPDF runtime files and MuPDF | |
| 76 runtime shared libraries, plus MuPDF build-time files (includes, | |
| 77 *.lib files on Windows). | |
| 78 | |
| 79 'd': a `PyMuPDFd` wheel containing MuPDF build-time files | |
| 80 (includes, *.lib files on Windows). | |
| 81 | |
| 82 PYMUPDF_SETUP_LIBCLANG | |
| 83 For internal testing. | |
| 84 | |
| 85 PYMUPDF_SETUP_MUPDF_BUILD | |
| 86 If unset or '-', use internal hard-coded default MuPDF location. | |
| 87 Otherwise overrides location of MuPDF when building PyMuPDF: | |
| 88 Empty string: | |
| 89 Build PyMuPDF with the system MuPDF. | |
| 90 A string starting with 'git:': | |
| 91 Use `git clone` to get a MuPDF checkout. We use the | |
| 92 string in the git clone command; it must contain the git | |
| 93 URL from which to clone, and can also contain other `git | |
| 94 clone` args, for example: | |
| 95 PYMUPDF_SETUP_MUPDF_BUILD="git:--branch master https://github.com/ArtifexSoftware/mupdf.git" | |
| 96 Otherwise: | |
| 97 Location of mupdf directory. | |
| 98 | |
| 99 PYMUPDF_SETUP_MUPDF_BSYMBOLIC | |
| 100 If '0' we do not link libmupdf.so with -Bsymbolic. | |
| 101 | |
| 102 PYMUPDF_SETUP_MUPDF_TESSERACT | |
| 103 If '0' we build MuPDF without Tesseract. | |
| 104 | |
| 105 PYMUPDF_SETUP_MUPDF_BUILD_TYPE | |
| 106 Unix only. Controls build type of MuPDF. Supported values are: | |
| 107 debug | |
| 108 memento | |
| 109 release (default) | |
| 110 | |
| 111 PYMUPDF_SETUP_MUPDF_CLEAN | |
| 112 Unix only. If '1', we do a clean MuPDF build. | |
| 113 | |
| 114 PYMUPDF_SETUP_MUPDF_REFCHECK_IF | |
| 115 Should be preprocessor statement to enable MuPDF reference count | |
| 116 checking. | |
| 117 | |
| 118 As of 2024-09-27, MuPDF default is `#ifndef NDEBUG`. | |
| 119 | |
| 120 PYMUPDF_SETUP_MUPDF_TRACE_IF | |
| 121 Should be preprocessor statement to enable MuPDF runtime diagnostics in | |
| 122 response to environment variables such as MUPDF_trace. | |
| 123 | |
| 124 As of 2024-09-27, MuPDF default is `#ifndef NDEBUG`. | |
| 125 | |
| 126 PYMUPDF_SETUP_MUPDF_THIRD | |
| 127 If '0' and we are building on Linux with the system MuPDF | |
| 128 (i.e. PYMUPDF_SETUP_MUPDF_BUILD=''), then don't link with | |
| 129 `-lmupdf-third`. | |
| 130 | |
| 131 PYMUPDF_SETUP_MUPDF_VS_UPGRADE | |
| 132 If '1' we run mupdf `scripts/mupdfwrap.py` with `--vs-upgrade 1` to | |
| 133 help Windows builds work with Visual Studio versions newer than 2019. | |
| 134 | |
| 135 PYMUPDF_SETUP_MUPDF_TGZ | |
| 136 If set, overrides location of MuPDF .tar.gz file: | |
| 137 Empty string: | |
| 138 Do not download MuPDF .tar.gz file. Sdist's will not contain | |
| 139 MuPDF. | |
| 140 | |
| 141 A string containing '://': | |
| 142 The URL from which to download the MuPDF .tar.gz file. Leaf | |
| 143 must match mupdf-*.tar.gz. | |
| 144 | |
| 145 Otherwise: | |
| 146 The path of local mupdf git checkout. We put all files in this | |
| 147 checkout known to git into a local tar archive. | |
| 148 | |
| 149 PYMUPDF_SETUP_MUPDF_OVERWRITE_CONFIG | |
| 150 If '0' we do not overwrite MuPDF's include/mupdf/fitz/config.h with | |
| 151 PyMuPDF's own configuration file, before building MuPDF. | |
| 152 | |
| 153 PYMUPDF_SETUP_MUPDF_REBUILD | |
| 154 If 0 we do not (re)build mupdf. | |
| 155 | |
| 156 PYMUPDF_SETUP_PY_LIMITED_API | |
| 157 If not '0', we build for current Python's stable ABI. | |
| 158 | |
| 159 However if unset and we are on Python-3.13 or later, we do | |
| 160 not build for the stable ABI because as of 2025-03-04 SWIG | |
| 161 generates incorrect stable ABI code with Python-3.13 - see: | |
| 162 https://github.com/swig/swig/issues/3059 | |
| 163 | |
| 164 PYMUPDF_SETUP_URL_WHEEL | |
| 165 If set, we use an existing wheel instead of building a new wheel. | |
| 166 | |
| 167 If starts with `http://` or `https://`: | |
| 168 If ends with '/', we append our wheel name and download. Otherwise | |
| 169 we download directly. | |
| 170 | |
| 171 If starts with `file://`: | |
| 172 If ends with '/' we look for a matching wheel name, `using | |
| 173 pipcl.wheel_name_match()` to cope with differing platform tags, | |
| 174 for example our `manylinux2014_x86_64` will match with an existing | |
| 175 wheel with `manylinux2014_x86_64.manylinux_2_17_x86_64`. | |
| 176 | |
| 177 Any other prefix is an error. | |
| 178 | |
| 179 PYMUPDF_SETUP_SWIG | |
| 180 If set, we use this instead of `swig`. | |
| 181 | |
| 182 WDEV_VS_YEAR | |
| 183 If set, we use as Visual Studio year, for example '2019' or '2022'. | |
| 184 | |
| 185 WDEV_VS_GRADE | |
| 186 If set, we use as Visual Studio grade, for example 'Community' or | |
| 187 'Professional' or 'Enterprise'. | |
| 188 ''' | |
| 189 | |
| 190 import glob | |
| 191 import io | |
| 192 import os | |
| 193 import textwrap | |
| 194 import time | |
| 195 import platform | |
| 196 import re | |
| 197 import shlex | |
| 198 import shutil | |
| 199 import stat | |
| 200 import subprocess | |
| 201 import sys | |
| 202 import tarfile | |
| 203 import traceback | |
| 204 import urllib.request | |
| 205 import zipfile | |
| 206 | |
| 207 import pipcl | |
| 208 | |
| 209 | |
| 210 log = pipcl.log0 | |
| 211 | |
| 212 run = pipcl.run | |
| 213 | |
| 214 | |
| 215 if 1: | |
| 216 # For debugging. | |
| 217 log(f'### Starting.') | |
| 218 pipcl.show_system() | |
| 219 | |
| 220 | |
| 221 PYMUPDF_SETUP_FLAVOUR = os.environ.get( 'PYMUPDF_SETUP_FLAVOUR', 'pbd') | |
| 222 for i in PYMUPDF_SETUP_FLAVOUR: | |
| 223 assert i in 'pbd', f'Unrecognised flag "{i} in {PYMUPDF_SETUP_FLAVOUR=}. Should be one of "p", "b", "d"' | |
| 224 | |
| 225 g_root = os.path.abspath( f'{__file__}/..') | |
| 226 | |
| 227 # Name of file that identifies that we are in a PyMuPDF sdist. | |
| 228 g_pymupdfb_sdist_marker = 'pymupdfb_sdist' | |
| 229 | |
| 230 python_version_tuple = tuple(int(x) for x in platform.python_version_tuple()[:2]) | |
| 231 | |
| 232 PYMUPDF_SETUP_PY_LIMITED_API = os.environ.get('PYMUPDF_SETUP_PY_LIMITED_API') | |
| 233 assert PYMUPDF_SETUP_PY_LIMITED_API in (None, '', '0', '1'), \ | |
| 234 f'Should be "", "0", "1" or undefined: {PYMUPDF_SETUP_PY_LIMITED_API=}.' | |
| 235 if PYMUPDF_SETUP_PY_LIMITED_API is None and python_version_tuple >= (3, 13): | |
| 236 log(f'Not defaulting to Python limited api because {platform.python_version_tuple()=}.') | |
| 237 g_py_limited_api = False | |
| 238 else: | |
| 239 g_py_limited_api = (PYMUPDF_SETUP_PY_LIMITED_API != '0') | |
| 240 | |
| 241 PYMUPDF_SETUP_URL_WHEEL = os.environ.get('PYMUPDF_SETUP_URL_WHEEL') | |
| 242 log(f'{PYMUPDF_SETUP_URL_WHEEL=}') | |
| 243 | |
| 244 PYMUPDF_SETUP_DUMMY = os.environ.get('PYMUPDF_SETUP_DUMMY') | |
| 245 log(f'{PYMUPDF_SETUP_DUMMY=}') | |
| 246 | |
| 247 PYMUPDF_SETUP_SWIG = os.environ.get('PYMUPDF_SETUP_SWIG') | |
| 248 | |
| 249 def _fs_remove(path): | |
| 250 ''' | |
| 251 Removes file or directory, without raising exception if it doesn't exist. | |
| 252 | |
| 253 We assert-fail if the path still exists when we return, in case of | |
| 254 permission problems etc. | |
| 255 ''' | |
| 256 # First try deleting `path` as a file. | |
| 257 try: | |
| 258 os.remove( path) | |
| 259 except Exception as e: | |
| 260 pass | |
| 261 | |
| 262 if os.path.exists(path): | |
| 263 # Try deleting `path` as a directory. Need to use | |
| 264 # shutil.rmtree() callback to handle permission problems; see: | |
| 265 # https://docs.python.org/3/library/shutil.html#rmtree-example | |
| 266 # | |
| 267 def error_fn(fn, path, excinfo): | |
| 268 # Clear the readonly bit and reattempt the removal. | |
| 269 os.chmod(path, stat.S_IWRITE) | |
| 270 fn(path) | |
| 271 shutil.rmtree( path, onerror=error_fn) | |
| 272 | |
| 273 assert not os.path.exists( path) | |
| 274 | |
| 275 | |
| 276 def _git_get_branch( directory): | |
| 277 command = f'cd {directory} && git branch --show-current' | |
| 278 log( f'Running: {command}') | |
| 279 p = subprocess.run( | |
| 280 command, | |
| 281 shell=True, | |
| 282 check=False, | |
| 283 text=True, | |
| 284 stdout=subprocess.PIPE, | |
| 285 ) | |
| 286 ret = None | |
| 287 if p.returncode == 0: | |
| 288 ret = p.stdout.strip() | |
| 289 log( f'Have found MuPDF git branch: ret={ret!r}') | |
| 290 return ret | |
| 291 | |
| 292 | |
| 293 def tar_check(path, mode='r:gz', prefix=None, remove=False): | |
| 294 ''' | |
| 295 Checks items in tar file have same <top-directory>, or <prefix> if not None. | |
| 296 | |
| 297 We fail if items in tar file have different top-level directory names. | |
| 298 | |
| 299 path: | |
| 300 The tar file. | |
| 301 mode: | |
| 302 As tarfile.open(). | |
| 303 prefix: | |
| 304 If not None, we fail if tar file's <top-directory> is not <prefix>. | |
| 305 | |
| 306 Returns the directory name (which will be <prefix> if not None). | |
| 307 ''' | |
| 308 with tarfile.open( path, mode) as t: | |
| 309 items = t.getnames() | |
| 310 assert items | |
| 311 item = items[0] | |
| 312 assert not item.startswith('./') and not item.startswith('../') | |
| 313 s = item.find('/') | |
| 314 if s == -1: | |
| 315 prefix_actual = item + '/' | |
| 316 else: | |
| 317 prefix_actual = item[:s+1] | |
| 318 if prefix: | |
| 319 assert prefix == prefix_actual, f'{path=} {prefix=} {prefix_actual=}' | |
| 320 for item in items[1:]: | |
| 321 assert item.startswith( prefix_actual), f'prefix_actual={prefix_actual!r} != item={item!r}' | |
| 322 return prefix_actual | |
| 323 | |
| 324 | |
| 325 def tar_extract(path, mode='r:gz', prefix=None, exists='raise'): | |
| 326 ''' | |
| 327 Extracts tar file into single local directory. | |
| 328 | |
| 329 We fail if items in tar file have different <top-directory>. | |
| 330 | |
| 331 path: | |
| 332 The tar file. | |
| 333 mode: | |
| 334 As tarfile.open(). | |
| 335 prefix: | |
| 336 If not None, we fail if tar file's <top-directory> is not <prefix>. | |
| 337 exists: | |
| 338 What to do if <top-directory> already exists: | |
| 339 'raise': raise exception. | |
| 340 'remove': remove existing file/directory before extracting. | |
| 341 'return': return without extracting. | |
| 342 | |
| 343 Returns the directory name (which will be <prefix> if not None, with '/' | |
| 344 appended if not already present). | |
| 345 ''' | |
| 346 prefix_actual = tar_check( path, mode, prefix) | |
| 347 if os.path.exists( prefix_actual): | |
| 348 if exists == 'raise': | |
| 349 raise Exception( f'Path already exists: {prefix_actual!r}') | |
| 350 elif exists == 'remove': | |
| 351 remove( prefix_actual) | |
| 352 elif exists == 'return': | |
| 353 log( f'Not extracting {path} because already exists: {prefix_actual}') | |
| 354 return prefix_actual | |
| 355 else: | |
| 356 assert 0, f'Unrecognised exists={exists!r}' | |
| 357 assert not os.path.exists( prefix_actual), f'Path already exists: {prefix_actual}' | |
| 358 log( f'Extracting {path}') | |
| 359 with tarfile.open( path, mode) as t: | |
| 360 t.extractall() | |
| 361 return prefix_actual | |
| 362 | |
| 363 | |
| 364 def git_info( directory): | |
| 365 ''' | |
| 366 Returns `(sha, comment, diff, branch)`, all items are str or None if not | |
| 367 available. | |
| 368 | |
| 369 directory: | |
| 370 Root of git checkout. | |
| 371 ''' | |
| 372 sha, comment, diff, branch = '', '', '', '' | |
| 373 cp = subprocess.run( | |
| 374 f'cd {directory} && (PAGER= git show --pretty=oneline|head -n 1 && git diff)', | |
| 375 capture_output=1, | |
| 376 shell=1, | |
| 377 text=1, | |
| 378 ) | |
| 379 if cp.returncode == 0: | |
| 380 sha, _ = cp.stdout.split(' ', 1) | |
| 381 comment, diff = _.split('\n', 1) | |
| 382 cp = subprocess.run( | |
| 383 f'cd {directory} && git rev-parse --abbrev-ref HEAD', | |
| 384 capture_output=1, | |
| 385 shell=1, | |
| 386 text=1, | |
| 387 ) | |
| 388 if cp.returncode == 0: | |
| 389 branch = cp.stdout.strip() | |
| 390 log(f'git_info(): directory={directory!r} returning branch={branch!r} sha={sha!r} comment={comment!r}') | |
| 391 return sha, comment, diff, branch | |
| 392 | |
| 393 | |
| 394 def git_patch(directory, patch, hard=False): | |
| 395 ''' | |
| 396 Applies string <patch> with `git patch` in <directory>. | |
| 397 | |
| 398 If <hard> is true we clean the tree with `git checkout .` and then apply | |
| 399 the patch. | |
| 400 | |
| 401 Otherwise we apply patch only if it is not already applied; this might fail | |
| 402 if there are conflicting changes in the tree. | |
| 403 ''' | |
| 404 log(f'Applying patch in {directory}:\n{textwrap.indent(patch, " ")}') | |
| 405 if not patch: | |
| 406 return | |
| 407 # Carriage returns break `git apply` so we use `newline='\n'` in open(). | |
| 408 path = os.path.abspath(f'{directory}/pymupdf_patch.txt') | |
| 409 with open(path, 'w', newline='\n') as f: | |
| 410 f.write(patch) | |
| 411 log(f'Using patch file: {path}') | |
| 412 if hard: | |
| 413 run(f'cd {directory} && git checkout .') | |
| 414 run(f'cd {directory} && git apply {path}') | |
| 415 log(f'Have applied patch in {directory}.') | |
| 416 else: | |
| 417 e = run( f'cd {directory} && git apply --check --reverse {path}', check=0) | |
| 418 if e == 0: | |
| 419 log(f'Not patching {directory} because already patched.') | |
| 420 else: | |
| 421 run(f'cd {directory} && git apply {path}') | |
| 422 log(f'Have applied patch in {directory}.') | |
| 423 run(f'cd {directory} && git diff') | |
| 424 | |
| 425 | |
| 426 mupdf_tgz = os.path.abspath( f'{__file__}/../mupdf.tgz') | |
| 427 | |
| 428 def get_mupdf_internal(out, location=None, sha=None, local_tgz=None): | |
| 429 ''' | |
| 430 Gets MuPDF as either a .tgz or a local directory. | |
| 431 | |
| 432 Args: | |
| 433 out: | |
| 434 Either 'dir' (we return name of local directory containing mupdf) or 'tgz' (we return | |
| 435 name of local .tgz file containing mupdf). | |
| 436 location: | |
| 437 First, if None we set to hard-coded default URL or git location. | |
| 438 If starts with 'git:', should be remote git location. | |
| 439 Otherwise if containing '://' should be URL for .tgz. | |
| 440 Otherwise should path of local mupdf checkout. | |
| 441 sha: | |
| 442 If not None and we use git clone, we checkout this sha. | |
| 443 local_tgz: | |
| 444 If not None, must be local .tgz file. | |
| 445 Returns: | |
| 446 (path, location): | |
| 447 `path` is absolute path of local directory or .tgz containing | |
| 448 MuPDF, or None if we are to use system MuPDF. | |
| 449 | |
| 450 `location_out` is `location` if not None, else the hard-coded | |
| 451 default location. | |
| 452 | |
| 453 ''' | |
| 454 log(f'get_mupdf_internal(): {out=} {location=} {sha=}') | |
| 455 assert out in ('dir', 'tgz') | |
| 456 if location is None: | |
| 457 location = f'https://mupdf.com/downloads/archive/mupdf-{version_mupdf}-source.tar.gz' | |
| 458 #location = 'git:--branch master https://github.com/ArtifexSoftware/mupdf.git' | |
| 459 | |
| 460 if location == '': | |
| 461 # Use system mupdf. | |
| 462 return None, location | |
| 463 | |
| 464 local_dir = None | |
| 465 if local_tgz: | |
| 466 assert os.path.isfile(local_tgz) | |
| 467 elif location.startswith( 'git:'): | |
| 468 location_git = location[4:] | |
| 469 local_dir = 'mupdf-git' | |
| 470 | |
| 471 # Try to update existing checkout. | |
| 472 e = run(f'cd {local_dir} && git pull && git submodule update --init', check=False) | |
| 473 if e: | |
| 474 # No existing git checkout, so do a fresh clone. | |
| 475 _fs_remove(local_dir) | |
| 476 gitargs = location[4:] | |
| 477 run(f'git clone --recursive --depth 1 --shallow-submodules {gitargs} {local_dir}') | |
| 478 | |
| 479 # Show sha of checkout. | |
| 480 run( f'cd {local_dir} && git show --pretty=oneline|head -n 1', check=False) | |
| 481 if sha: | |
| 482 run( f'cd {local_dir} && git checkout {sha}') | |
| 483 elif '://' in location: | |
| 484 # Download .tgz. | |
| 485 local_tgz = os.path.basename( location) | |
| 486 suffix = '.tar.gz' | |
| 487 assert location.endswith(suffix), f'Unrecognised suffix in remote URL {location=}.' | |
| 488 name = local_tgz[:-len(suffix)] | |
| 489 log( f'Download {location=} {local_tgz=} {name=}') | |
| 490 if os.path.exists(local_tgz): | |
| 491 try: | |
| 492 tar_check(local_tgz, 'r:gz', prefix=f'{name}/') | |
| 493 except Exception as e: | |
| 494 log(f'Not using existing file {local_tgz} because invalid tar data: {e}') | |
| 495 _fs_remove( local_tgz) | |
| 496 if os.path.exists(local_tgz): | |
| 497 log(f'Not downloading from {location} because already present: {local_tgz!r}') | |
| 498 else: | |
| 499 log(f'Downloading from {location=} to {local_tgz=}.') | |
| 500 urllib.request.urlretrieve( location, local_tgz + '-') | |
| 501 os.rename(local_tgz + '-', local_tgz) | |
| 502 assert os.path.exists( local_tgz) | |
| 503 tar_check( local_tgz, 'r:gz', prefix=f'{name}/') | |
| 504 else: | |
| 505 assert os.path.isdir(location), f'Local MuPDF does not exist: {location=}' | |
| 506 local_dir = location | |
| 507 | |
| 508 assert bool(local_dir) != bool(local_tgz) | |
| 509 if out == 'dir': | |
| 510 if not local_dir: | |
| 511 assert local_tgz | |
| 512 local_dir = tar_extract( local_tgz, exists='return') | |
| 513 return os.path.abspath( local_dir), location | |
| 514 elif out == 'tgz': | |
| 515 if not local_tgz: | |
| 516 # Create .tgz containing git files in `local_dir`. | |
| 517 assert local_dir | |
| 518 if local_dir.endswith( '/'): | |
| 519 local_dir = local_dir[:-1] | |
| 520 top = os.path.basename(local_dir) | |
| 521 local_tgz = f'{local_dir}.tgz' | |
| 522 log( f'Creating .tgz from git files. {top=} {local_dir=} {local_tgz=}') | |
| 523 _fs_remove( local_tgz) | |
| 524 with tarfile.open( local_tgz, 'w:gz') as f: | |
| 525 for name in pipcl.git_items( local_dir, submodules=True): | |
| 526 path = os.path.join( local_dir, name) | |
| 527 if os.path.isfile( path): | |
| 528 path2 = f'{top}/{name}' | |
| 529 log(f'Adding {path=} {path2=}.') | |
| 530 f.add( path, path2, recursive=False) | |
| 531 return os.path.abspath( local_tgz), location | |
| 532 else: | |
| 533 assert 0, f'Unrecognised {out=}' | |
| 534 | |
| 535 | |
| 536 | |
| 537 def get_mupdf_tgz(): | |
| 538 ''' | |
| 539 Creates .tgz file called containing MuPDF source, for inclusion in an | |
| 540 sdist. | |
| 541 | |
| 542 What we do depends on environmental variable PYMUPDF_SETUP_MUPDF_TGZ; see | |
| 543 docs at start of this file for details. | |
| 544 | |
| 545 Returns name of top-level directory within the .tgz file. | |
| 546 ''' | |
| 547 name, location = get_mupdf_internal( 'tgz', os.environ.get('PYMUPDF_SETUP_MUPDF_TGZ')) | |
| 548 return name, location | |
| 549 | |
| 550 | |
| 551 def get_mupdf(path=None, sha=None): | |
| 552 ''' | |
| 553 Downloads and/or extracts mupdf and returns (path, location) where `path` | |
| 554 is the local mupdf directory and `location` is where it came from. | |
| 555 | |
| 556 Exact behaviour depends on environmental variable | |
| 557 PYMUPDF_SETUP_MUPDF_BUILD; see docs at start of this file for details. | |
| 558 ''' | |
| 559 m = os.environ.get('PYMUPDF_SETUP_MUPDF_BUILD') | |
| 560 if m == '-': | |
| 561 # This allows easy specification in Github actions. | |
| 562 m = None | |
| 563 if m is None and os.path.isfile(mupdf_tgz): | |
| 564 # This makes us use tgz inside sdist. | |
| 565 log(f'Using local tgz: {mupdf_tgz=}') | |
| 566 return get_mupdf_internal('dir', local_tgz=mupdf_tgz) | |
| 567 return get_mupdf_internal('dir', m) | |
| 568 | |
| 569 | |
| 570 linux = sys.platform.startswith( 'linux') or 'gnu' in sys.platform | |
| 571 openbsd = sys.platform.startswith( 'openbsd') | |
| 572 freebsd = sys.platform.startswith( 'freebsd') | |
| 573 darwin = sys.platform.startswith( 'darwin') | |
| 574 windows = platform.system() == 'Windows' or platform.system().startswith('CYGWIN') | |
| 575 msys2 = platform.system().startswith('MSYS_NT-') | |
| 576 | |
| 577 pyodide_flags = '-fwasm-exceptions' | |
| 578 | |
| 579 if os.environ.get('PYODIDE') == '1': | |
| 580 if os.environ.get('OS') != 'pyodide': | |
| 581 log('PYODIDE=1, setting OS=pyodide.') | |
| 582 os.environ['OS'] = 'pyodide' | |
| 583 os.environ['XCFLAGS'] = pyodide_flags | |
| 584 os.environ['XCXXFLAGS'] = pyodide_flags | |
| 585 | |
| 586 pyodide = os.environ.get('OS') == 'pyodide' | |
| 587 | |
| 588 def build(): | |
| 589 ''' | |
| 590 pipcl.py `build_fn()` callback. | |
| 591 ''' | |
| 592 #pipcl.show_sysconfig() | |
| 593 | |
| 594 if PYMUPDF_SETUP_DUMMY == '1': | |
| 595 log(f'{PYMUPDF_SETUP_DUMMY=} Building dummy wheel with no files.') | |
| 596 return list() | |
| 597 | |
| 598 # Download MuPDF. | |
| 599 # | |
| 600 mupdf_local, mupdf_location = get_mupdf() | |
| 601 if mupdf_local: | |
| 602 mupdf_version_tuple = get_mupdf_version(mupdf_local) | |
| 603 # else we cannot determine version this way and do not use it | |
| 604 | |
| 605 build_type = os.environ.get( 'PYMUPDF_SETUP_MUPDF_BUILD_TYPE', 'release') | |
| 606 assert build_type in ('debug', 'memento', 'release'), \ | |
| 607 f'Unrecognised build_type={build_type!r}' | |
| 608 | |
| 609 overwrite_config = os.environ.get('PYMUPDF_SETUP_MUPDF_OVERWRITE_CONFIG', '1') == '1' | |
| 610 | |
| 611 PYMUPDF_SETUP_MUPDF_REFCHECK_IF = os.environ.get('PYMUPDF_SETUP_MUPDF_REFCHECK_IF') | |
| 612 PYMUPDF_SETUP_MUPDF_TRACE_IF = os.environ.get('PYMUPDF_SETUP_MUPDF_TRACE_IF') | |
| 613 | |
| 614 # Build MuPDF shared libraries. | |
| 615 # | |
| 616 if windows: | |
| 617 mupdf_build_dir = build_mupdf_windows( | |
| 618 mupdf_local, | |
| 619 build_type, | |
| 620 overwrite_config, | |
| 621 g_py_limited_api, | |
| 622 PYMUPDF_SETUP_MUPDF_REFCHECK_IF, | |
| 623 PYMUPDF_SETUP_MUPDF_TRACE_IF, | |
| 624 ) | |
| 625 else: | |
| 626 if 'p' not in PYMUPDF_SETUP_FLAVOUR and 'b' not in PYMUPDF_SETUP_FLAVOUR: | |
| 627 # We only need MuPDF headers, so no point building MuPDF. | |
| 628 log(f'Not building MuPDF because not Windows and {PYMUPDF_SETUP_FLAVOUR=}.') | |
| 629 mupdf_build_dir = None | |
| 630 else: | |
| 631 mupdf_build_dir = build_mupdf_unix( | |
| 632 mupdf_local, | |
| 633 build_type, | |
| 634 overwrite_config, | |
| 635 g_py_limited_api, | |
| 636 PYMUPDF_SETUP_MUPDF_REFCHECK_IF, | |
| 637 PYMUPDF_SETUP_MUPDF_TRACE_IF, | |
| 638 PYMUPDF_SETUP_SWIG, | |
| 639 ) | |
| 640 log( f'build(): mupdf_build_dir={mupdf_build_dir!r}') | |
| 641 | |
| 642 # Build rebased `extra` module. | |
| 643 # | |
| 644 if 'p' in PYMUPDF_SETUP_FLAVOUR: | |
| 645 path_so_leaf = _build_extension( | |
| 646 mupdf_local, | |
| 647 mupdf_build_dir, | |
| 648 build_type, | |
| 649 g_py_limited_api, | |
| 650 ) | |
| 651 else: | |
| 652 log(f'Not building extension.') | |
| 653 path_so_leaf = None | |
| 654 | |
| 655 # Generate list of (from, to) items to return to pipcl. What we add depends | |
| 656 # on PYMUPDF_SETUP_FLAVOUR. | |
| 657 # | |
| 658 ret = list() | |
| 659 def add(flavour, from_, to_): | |
| 660 assert flavour in 'pbd' | |
| 661 if flavour in PYMUPDF_SETUP_FLAVOUR: | |
| 662 ret.append((from_, to_)) | |
| 663 | |
| 664 to_dir = 'pymupdf/' | |
| 665 to_dir_d = f'{to_dir}/mupdf-devel' | |
| 666 | |
| 667 # Add implementation files. | |
| 668 add('p', f'{g_root}/src/__init__.py', to_dir) | |
| 669 add('p', f'{g_root}/src/__main__.py', to_dir) | |
| 670 add('p', f'{g_root}/src/pymupdf.py', to_dir) | |
| 671 add('p', f'{g_root}/src/table.py', to_dir) | |
| 672 add('p', f'{g_root}/src/utils.py', to_dir) | |
| 673 add('p', f'{g_root}/src/_wxcolors.py', to_dir) | |
| 674 add('p', f'{g_root}/src/_apply_pages.py', to_dir) | |
| 675 add('p', f'{g_root}/src/build/extra.py', to_dir) | |
| 676 if path_so_leaf: | |
| 677 add('p', f'{g_root}/src/build/{path_so_leaf}', to_dir) | |
| 678 | |
| 679 # Add support for `fitz` backwards compatibility. | |
| 680 add('p', f'{g_root}/src/fitz___init__.py', 'fitz/__init__.py') | |
| 681 add('p', f'{g_root}/src/fitz_table.py', 'fitz/table.py') | |
| 682 add('p', f'{g_root}/src/fitz_utils.py', 'fitz/utils.py') | |
| 683 | |
| 684 if mupdf_local: | |
| 685 # Add MuPDF Python API. | |
| 686 add('p', f'{mupdf_build_dir}/mupdf.py', to_dir) | |
| 687 | |
| 688 # Add MuPDF shared libraries. | |
| 689 if windows: | |
| 690 wp = pipcl.wdev.WindowsPython() | |
| 691 add('p', f'{mupdf_build_dir}/_mupdf.pyd', to_dir) | |
| 692 add('b', f'{mupdf_build_dir}/mupdfcpp{wp.cpu.windows_suffix}.dll', to_dir) | |
| 693 | |
| 694 # Add Windows .lib files. | |
| 695 mupdf_build_dir2 = _windows_lib_directory(mupdf_local, build_type) | |
| 696 add('d', f'{mupdf_build_dir2}/mupdfcpp{wp.cpu.windows_suffix}.lib', f'{to_dir_d}/lib/') | |
| 697 if mupdf_version_tuple >= (1, 26): | |
| 698 # MuPDF-1.25+ language bindings build also builds libmuthreads. | |
| 699 add('d', f'{mupdf_build_dir2}/libmuthreads.lib', f'{to_dir_d}/lib/') | |
| 700 elif darwin: | |
| 701 add('p', f'{mupdf_build_dir}/_mupdf.so', to_dir) | |
| 702 add('b', f'{mupdf_build_dir}/libmupdfcpp.so', to_dir) | |
| 703 add('b', f'{mupdf_build_dir}/libmupdf.dylib', to_dir) | |
| 704 add('d', f'{mupdf_build_dir}/libmupdf-threads.a', f'{to_dir_d}/lib/') | |
| 705 elif pyodide: | |
| 706 add('p', f'{mupdf_build_dir}/_mupdf.so', to_dir) | |
| 707 add('b', f'{mupdf_build_dir}/libmupdfcpp.so', 'PyMuPDF.libs/') | |
| 708 add('b', f'{mupdf_build_dir}/libmupdf.so', 'PyMuPDF.libs/') | |
| 709 else: | |
| 710 add('p', f'{mupdf_build_dir}/_mupdf.so', to_dir) | |
| 711 add('b', pipcl.get_soname(f'{mupdf_build_dir}/libmupdfcpp.so'), to_dir) | |
| 712 add('b', pipcl.get_soname(f'{mupdf_build_dir}/libmupdf.so'), to_dir) | |
| 713 add('d', f'{mupdf_build_dir}/libmupdf-threads.a', f'{to_dir_d}/lib/') | |
| 714 | |
| 715 if 'd' in PYMUPDF_SETUP_FLAVOUR: | |
| 716 # Add MuPDF C and C++ headers to `ret_d`. Would prefer to use | |
| 717 # pipcl.git_items() but hard-coded mupdf tree is not a git | |
| 718 # checkout. | |
| 719 # | |
| 720 for root in ( | |
| 721 f'{mupdf_local}/include', | |
| 722 f'{mupdf_local}/platform/c++/include', | |
| 723 ): | |
| 724 for dirpath, dirnames, filenames in os.walk(root): | |
| 725 for filename in filenames: | |
| 726 if not filename.endswith('.h'): | |
| 727 continue | |
| 728 header_abs = os.path.join(dirpath, filename) | |
| 729 assert header_abs.startswith(root) | |
| 730 header_rel = header_abs[len(root)+1:] | |
| 731 add('d', f'{header_abs}', f'{to_dir_d}/include/{header_rel}') | |
| 732 | |
| 733 # Add a .py file containing location of MuPDF. | |
| 734 try: | |
| 735 sha, comment, diff, branch = git_info(g_root) | |
| 736 except Exception as e: | |
| 737 log(f'Failed to get git information: {e}') | |
| 738 sha, comment, diff, branch = (None, None, None, None) | |
| 739 swig = PYMUPDF_SETUP_SWIG or 'swig' | |
| 740 swig_version_text = run(f'{swig} --version', capture=1) | |
| 741 m = re.search('\nSWIG Version ([^\n]+)', swig_version_text) | |
| 742 log(f'{swig_version_text=}') | |
| 743 assert m, f'Unrecognised {swig_version_text=}' | |
| 744 swig_version = m.group(1) | |
| 745 def int_or_0(text): | |
| 746 try: | |
| 747 return int(text) | |
| 748 except Exception: | |
| 749 return 0 | |
| 750 swig_version_tuple = tuple(int_or_0(i) for i in swig_version.split('.')) | |
| 751 log(f'{swig_version=}') | |
| 752 text = '' | |
| 753 text += f'mupdf_location = {mupdf_location!r}\n' | |
| 754 text += f'pymupdf_version = {version_p!r}\n' | |
| 755 text += f'pymupdf_git_sha = {sha!r}\n' | |
| 756 text += f'pymupdf_git_diff = {diff!r}\n' | |
| 757 text += f'pymupdf_git_branch = {branch!r}\n' | |
| 758 text += f'swig_version = {swig_version!r}\n' | |
| 759 text += f'swig_version_tuple = {swig_version_tuple!r}\n' | |
| 760 add('p', text.encode(), f'{to_dir}/_build.py') | |
| 761 | |
| 762 # Add single README file. | |
| 763 if 'p' in PYMUPDF_SETUP_FLAVOUR: | |
| 764 add('p', f'{g_root}/README.md', '$dist-info/README.md') | |
| 765 elif 'b' in PYMUPDF_SETUP_FLAVOUR: | |
| 766 add('b', f'{g_root}/READMEb.md', '$dist-info/README.md') | |
| 767 elif 'd' in PYMUPDF_SETUP_FLAVOUR: | |
| 768 add('d', f'{g_root}/READMEd.md', '$dist-info/README.md') | |
| 769 | |
| 770 return ret | |
| 771 | |
| 772 | |
| 773 def env_add(env, name, value, sep=' ', prepend=False, verbose=False): | |
| 774 ''' | |
| 775 Appends/prepends `<value>` to `env[name]`. | |
| 776 | |
| 777 If `name` is not in `env`, we use os.environ[name] if it exists. | |
| 778 ''' | |
| 779 v = env.get(name) | |
| 780 if verbose: | |
| 781 log(f'Initally: {name}={v!r}') | |
| 782 if v is None: | |
| 783 v = os.environ.get(name) | |
| 784 if v is None: | |
| 785 env[ name] = value | |
| 786 else: | |
| 787 if prepend: | |
| 788 env[ name] = f'{value}{sep}{v}' | |
| 789 else: | |
| 790 env[ name] = f'{v}{sep}{value}' | |
| 791 if verbose: | |
| 792 log(f'Returning with {name}={env[name]!r}') | |
| 793 | |
| 794 | |
| 795 def build_mupdf_windows( | |
| 796 mupdf_local, | |
| 797 build_type, | |
| 798 overwrite_config, | |
| 799 g_py_limited_api, | |
| 800 PYMUPDF_SETUP_MUPDF_REFCHECK_IF, | |
| 801 PYMUPDF_SETUP_MUPDF_TRACE_IF, | |
| 802 ): | |
| 803 | |
| 804 assert mupdf_local | |
| 805 | |
| 806 if overwrite_config: | |
| 807 mupdf_config_h = f'{mupdf_local}/include/mupdf/fitz/config.h' | |
| 808 prefix = '#define TOFU_CJK_EXT 1 /* PyMuPDF override. */\n' | |
| 809 with open(mupdf_config_h) as f: | |
| 810 text = f.read() | |
| 811 if text.startswith(prefix): | |
| 812 print(f'Not modifying {mupdf_config_h} because already has prefix {prefix!r}.') | |
| 813 else: | |
| 814 print(f'Prefixing {mupdf_config_h} with {prefix!r}.') | |
| 815 text = prefix + text | |
| 816 st = os.stat(mupdf_config_h) | |
| 817 with open(mupdf_config_h, 'w') as f: | |
| 818 f.write(text) | |
| 819 os.utime(mupdf_config_h, (st.st_atime, st.st_mtime)) | |
| 820 | |
| 821 wp = pipcl.wdev.WindowsPython() | |
| 822 tesseract = '' if os.environ.get('PYMUPDF_SETUP_MUPDF_TESSERACT') == '0' else 'tesseract-' | |
| 823 windows_build_tail = f'build\\shared-{tesseract}{build_type}' | |
| 824 if g_py_limited_api: | |
| 825 windows_build_tail += f'-Py_LIMITED_API_{pipcl.current_py_limited_api()}' | |
| 826 windows_build_tail += f'-x{wp.cpu.bits}-py{wp.version}' | |
| 827 windows_build_dir = f'{mupdf_local}\\{windows_build_tail}' | |
| 828 #log( f'Building mupdf.') | |
| 829 devenv = os.environ.get('PYMUPDF_SETUP_DEVENV') | |
| 830 if not devenv: | |
| 831 try: | |
| 832 # Prefer VS-2022 as that is what Github provide in windows-2022. | |
| 833 log(f'Looking for Visual Studio 2022.') | |
| 834 vs = pipcl.wdev.WindowsVS(year=2022) | |
| 835 except Exception as e: | |
| 836 log(f'Failed to find VS-2022:\n' | |
| 837 f'{textwrap.indent(traceback.format_exc(), " ")}' | |
| 838 ) | |
| 839 log(f'Looking for any Visual Studio.') | |
| 840 vs = pipcl.wdev.WindowsVS() | |
| 841 log(f'vs:\n{vs.description_ml(" ")}') | |
| 842 devenv = vs.devenv | |
| 843 if not devenv: | |
| 844 devenv = 'devenv.com' | |
| 845 log( f'Cannot find devenv.com in default locations, using: {devenv!r}') | |
| 846 command = f'cd "{mupdf_local}" && "{sys.executable}" ./scripts/mupdfwrap.py' | |
| 847 if os.environ.get('PYMUPDF_SETUP_MUPDF_VS_UPGRADE') == '1': | |
| 848 command += ' --vs-upgrade 1' | |
| 849 | |
| 850 # Would like to simply do f'... --devenv {shutil.quote(devenv)}', but | |
| 851 # it looks like if `devenv` has spaces then `shutil.quote()` puts it | |
| 852 # inside single quotes, which then appear to be ignored when run by | |
| 853 # subprocess.run(). | |
| 854 # | |
| 855 # So instead we strip any enclosing quotes and the enclose with | |
| 856 # double-quotes. | |
| 857 # | |
| 858 if len(devenv) >= 2: | |
| 859 for q in '"', "'": | |
| 860 if devenv.startswith( q) and devenv.endswith( q): | |
| 861 devenv = devenv[1:-1] | |
| 862 command += f' -d {windows_build_tail}' | |
| 863 command += f' -b' | |
| 864 if PYMUPDF_SETUP_MUPDF_REFCHECK_IF: | |
| 865 command += f' --refcheck-if "{PYMUPDF_SETUP_MUPDF_REFCHECK_IF}"' | |
| 866 if PYMUPDF_SETUP_MUPDF_TRACE_IF: | |
| 867 command += f' --trace-if "{PYMUPDF_SETUP_MUPDF_TRACE_IF}"' | |
| 868 command += f' --devenv "{devenv}"' | |
| 869 command += f' all' | |
| 870 if os.environ.get( 'PYMUPDF_SETUP_MUPDF_REBUILD') == '0': | |
| 871 log( f'PYMUPDF_SETUP_MUPDF_REBUILD is "0" so not building MuPDF; would have run: {command}') | |
| 872 else: | |
| 873 log( f'Building MuPDF by running: {command}') | |
| 874 subprocess.run( command, shell=True, check=True) | |
| 875 log( f'Finished building mupdf.') | |
| 876 | |
| 877 return windows_build_dir | |
| 878 | |
| 879 | |
| 880 def _windows_lib_directory(mupdf_local, build_type): | |
| 881 ret = f'{mupdf_local}/platform/win32/' | |
| 882 if _cpu_bits() == 64: | |
| 883 ret += 'x64/' | |
| 884 if build_type == 'release': | |
| 885 ret += 'Release/' | |
| 886 elif build_type == 'debug': | |
| 887 ret += 'Debug/' | |
| 888 else: | |
| 889 assert 0, f'Unrecognised {build_type=}.' | |
| 890 return ret | |
| 891 | |
| 892 | |
| 893 def _cpu_bits(): | |
| 894 if sys.maxsize == 2**31 - 1: | |
| 895 return 32 | |
| 896 return 64 | |
| 897 | |
| 898 | |
| 899 def build_mupdf_unix( | |
| 900 mupdf_local, | |
| 901 build_type, | |
| 902 overwrite_config, | |
| 903 g_py_limited_api, | |
| 904 PYMUPDF_SETUP_MUPDF_REFCHECK_IF, | |
| 905 PYMUPDF_SETUP_MUPDF_TRACE_IF, | |
| 906 PYMUPDF_SETUP_SWIG, | |
| 907 ): | |
| 908 ''' | |
| 909 Builds MuPDF. | |
| 910 | |
| 911 Args: | |
| 912 mupdf_local: | |
| 913 Path of MuPDF directory or None if we are using system MuPDF. | |
| 914 | |
| 915 Returns the absolute path of build directory within MuPDF, e.g. | |
| 916 `.../mupdf/build/pymupdf-shared-release`, or `None` if we are using the | |
| 917 system MuPDF. | |
| 918 ''' | |
| 919 if not mupdf_local: | |
| 920 log( f'Using system mupdf.') | |
| 921 return None | |
| 922 | |
| 923 env = dict() | |
| 924 if overwrite_config: | |
| 925 # By predefining TOFU_CJK_EXT here, we don't need to modify | |
| 926 # MuPDF's include/mupdf/fitz/config.h. | |
| 927 log( f'Setting XCFLAGS and XCXXFLAGS to predefine TOFU_CJK_EXT.') | |
| 928 env_add(env, 'XCFLAGS', '-DTOFU_CJK_EXT') | |
| 929 env_add(env, 'XCXXFLAGS', '-DTOFU_CJK_EXT') | |
| 930 | |
| 931 if openbsd or freebsd: | |
| 932 env_add(env, 'CXX', 'c++', ' ') | |
| 933 | |
| 934 if darwin and os.environ.get('GITHUB_ACTIONS') == 'true': | |
| 935 if os.environ.get('ImageOS') == 'macos13': | |
| 936 # On Github macos13 we need to use Clang/LLVM (Homebrew) 15.0.7, | |
| 937 # otherwise mupdf:thirdparty/tesseract/src/api/baseapi.cpp fails to | |
| 938 # compile with: | |
| 939 # | |
| 940 # thirdparty/tesseract/src/api/baseapi.cpp:150:25: error: 'recursive_directory_iterator' is unavailable: introduced in macOS 10.15 | |
| 941 # | |
| 942 # See: | |
| 943 # https://github.com/actions/runner-images/blob/main/images/macos/macos-13-Readme.md | |
| 944 # | |
| 945 log(f'Using llvm@15 clang and clang++') | |
| 946 cl15 = pipcl.run(f'brew --prefix llvm@15', capture=1) | |
| 947 log(f'{cl15=}') | |
| 948 cl15 = cl15.strip() | |
| 949 pipcl.run(f'ls -lL {cl15}') | |
| 950 pipcl.run(f'ls -lL {cl15}/bin') | |
| 951 cc = f'{cl15}/bin/clang' | |
| 952 cxx = f'{cl15}/bin/clang++' | |
| 953 env['CC'] = cc | |
| 954 env['CXX'] = cxx | |
| 955 | |
| 956 # Show compiler versions. | |
| 957 cc = env.get('CC', 'cc') | |
| 958 cxx = env.get('CXX', 'c++') | |
| 959 pipcl.run(f'{cc} --version') | |
| 960 pipcl.run(f'{cxx} --version') | |
| 961 | |
| 962 # Add extra flags for MacOS cross-compilation, where ARCHFLAGS can be | |
| 963 # '-arch arm64'. | |
| 964 # | |
| 965 archflags = os.environ.get( 'ARCHFLAGS') | |
| 966 if archflags: | |
| 967 env_add(env, 'XCFLAGS', archflags) | |
| 968 env_add(env, 'XLIBS', archflags) | |
| 969 | |
| 970 mupdf_version_tuple = get_mupdf_version(mupdf_local) | |
| 971 | |
| 972 # We specify a build directory path containing 'pymupdf' so that we | |
| 973 # coexist with non-PyMuPDF builds (because PyMuPDF builds have a | |
| 974 # different config.h). | |
| 975 # | |
| 976 # We also append further text to try to allow different builds to | |
| 977 # work if they reuse the mupdf directory. | |
| 978 # | |
| 979 # Using platform.machine() (e.g. 'amd64') ensures that different | |
| 980 # builds of mupdf on a shared filesystem can coexist. Using | |
| 981 # $_PYTHON_HOST_PLATFORM allows cross-compiled cibuildwheel builds | |
| 982 # to coexist, e.g. on github. | |
| 983 # | |
| 984 # Have experimented with looking at getconf_ARG_MAX to decide whether to | |
| 985 # omit `PyMuPDF-` from the build directory, to avoid command-too-long | |
| 986 # errors with mupdf-1.26. But it seems that `getconf ARG_MAX` returns | |
| 987 # a system limit, not the actual limit of the current shell, and there | |
| 988 # doesn't seem to be a way to find the current shell's limit. | |
| 989 # | |
| 990 build_prefix = f'PyMuPDF-' | |
| 991 if mupdf_version_tuple >= (1, 26): | |
| 992 # Avoid link command length problems seen on musllinux. | |
| 993 build_prefix = '' | |
| 994 if pyodide: | |
| 995 build_prefix += 'pyodide-' | |
| 996 else: | |
| 997 build_prefix += f'{platform.machine()}-' | |
| 998 build_prefix_extra = os.environ.get( '_PYTHON_HOST_PLATFORM') | |
| 999 if build_prefix_extra: | |
| 1000 build_prefix += f'{build_prefix_extra}-' | |
| 1001 build_prefix += 'shared-' | |
| 1002 if msys2: | |
| 1003 # Error in mupdf/scripts/tesseract/endianness.h: | |
| 1004 # #error "I don't know what architecture this is!" | |
| 1005 log(f'msys2: building MuPDF without tesseract.') | |
| 1006 elif os.environ.get('PYMUPDF_SETUP_MUPDF_TESSERACT') == '0': | |
| 1007 log(f'PYMUPDF_SETUP_MUPDF_TESSERACT=0 so building mupdf without tesseract.') | |
| 1008 else: | |
| 1009 build_prefix += 'tesseract-' | |
| 1010 if ( | |
| 1011 linux | |
| 1012 and os.environ.get('PYMUPDF_SETUP_MUPDF_BSYMBOLIC', '1') == '1' | |
| 1013 ): | |
| 1014 log(f'Appending `bsymbolic-` to MuPDF build path.') | |
| 1015 build_prefix += 'bsymbolic-' | |
| 1016 log(f'{g_py_limited_api=}') | |
| 1017 if g_py_limited_api: | |
| 1018 build_prefix += f'Py_LIMITED_API_{pipcl.current_py_limited_api()}-' | |
| 1019 unix_build_dir = f'{mupdf_local}/build/{build_prefix}{build_type}' | |
| 1020 PYMUPDF_SETUP_MUPDF_CLEAN = os.environ.get('PYMUPDF_SETUP_MUPDF_CLEAN') | |
| 1021 if PYMUPDF_SETUP_MUPDF_CLEAN == '1': | |
| 1022 log(f'{PYMUPDF_SETUP_MUPDF_CLEAN=}, deleting {unix_build_dir=}.') | |
| 1023 shutil.rmtree(unix_build_dir, ignore_errors=1) | |
| 1024 # We need MuPDF's Python bindings, so we build MuPDF with | |
| 1025 # `mupdf/scripts/mupdfwrap.py` instead of running `make`. | |
| 1026 # | |
| 1027 command = f'cd {mupdf_local} &&' | |
| 1028 for n, v in env.items(): | |
| 1029 command += f' {n}={shlex.quote(v)}' | |
| 1030 command += f' {sys.executable} ./scripts/mupdfwrap.py' | |
| 1031 if PYMUPDF_SETUP_SWIG: | |
| 1032 command += f' --swig {shlex.quote(PYMUPDF_SETUP_SWIG)}' | |
| 1033 command += f' -d build/{build_prefix}{build_type} -b' | |
| 1034 #command += f' --m-target libs' | |
| 1035 if PYMUPDF_SETUP_MUPDF_REFCHECK_IF: | |
| 1036 command += f' --refcheck-if "{PYMUPDF_SETUP_MUPDF_REFCHECK_IF}"' | |
| 1037 if PYMUPDF_SETUP_MUPDF_TRACE_IF: | |
| 1038 command += f' --trace-if "{PYMUPDF_SETUP_MUPDF_TRACE_IF}"' | |
| 1039 if 'p' in PYMUPDF_SETUP_FLAVOUR: | |
| 1040 command += ' all' | |
| 1041 else: | |
| 1042 command += ' m01' # No need for C++/Python bindings. | |
| 1043 command += f' && echo {unix_build_dir}:' | |
| 1044 command += f' && ls -l {unix_build_dir}' | |
| 1045 | |
| 1046 if os.environ.get( 'PYMUPDF_SETUP_MUPDF_REBUILD') == '0': | |
| 1047 log( f'PYMUPDF_SETUP_MUPDF_REBUILD is "0" so not building MuPDF; would have run: {command}') | |
| 1048 else: | |
| 1049 log( f'Building MuPDF by running: {command}') | |
| 1050 subprocess.run( command, shell=True, check=True) | |
| 1051 log( f'Finished building mupdf.') | |
| 1052 | |
| 1053 return unix_build_dir | |
| 1054 | |
| 1055 | |
| 1056 def get_mupdf_version(mupdf_dir): | |
| 1057 path = f'{mupdf_dir}/include/mupdf/fitz/version.h' | |
| 1058 with open(path) as f: | |
| 1059 text = f.read() | |
| 1060 v0 = re.search('#define FZ_VERSION_MAJOR ([0-9]+)', text) | |
| 1061 v1 = re.search('#define FZ_VERSION_MINOR ([0-9]+)', text) | |
| 1062 v2 = re.search('#define FZ_VERSION_PATCH ([0-9]+)', text) | |
| 1063 assert v0 and v1 and v2, f'Cannot find MuPDF version numbers in {path=}.' | |
| 1064 v0 = int(v0.group(1)) | |
| 1065 v1 = int(v1.group(1)) | |
| 1066 v2 = int(v2.group(1)) | |
| 1067 return v0, v1, v2 | |
| 1068 | |
| 1069 def _fs_update(text, path): | |
| 1070 try: | |
| 1071 with open( path) as f: | |
| 1072 text0 = f.read() | |
| 1073 except OSError: | |
| 1074 text0 = None | |
| 1075 print(f'path={path!r} text==text0={text==text0!r}') | |
| 1076 if text != text0: | |
| 1077 with open( path, 'w') as f: | |
| 1078 f.write( text) | |
| 1079 | |
| 1080 | |
| 1081 def _build_extension( mupdf_local, mupdf_build_dir, build_type, g_py_limited_api): | |
| 1082 ''' | |
| 1083 Builds Python extension module `_extra`. | |
| 1084 | |
| 1085 Returns leafname of the generated shared libraries within mupdf_build_dir. | |
| 1086 ''' | |
| 1087 (compiler_extra, linker_extra, includes, defines, optimise, debug, libpaths, libs, libraries) \ | |
| 1088 = _extension_flags( mupdf_local, mupdf_build_dir, build_type) | |
| 1089 log(f'_build_extension(): {g_py_limited_api=} {defines=}') | |
| 1090 if mupdf_local: | |
| 1091 includes = ( | |
| 1092 f'{mupdf_local}/platform/c++/include', | |
| 1093 f'{mupdf_local}/include', | |
| 1094 ) | |
| 1095 | |
| 1096 # Build rebased extension module. | |
| 1097 log('Building PyMuPDF rebased.') | |
| 1098 compile_extra_cpp = '' | |
| 1099 if darwin: | |
| 1100 # Avoids `error: cannot pass object of non-POD type | |
| 1101 # 'std::nullptr_t' through variadic function; call will abort at | |
| 1102 # runtime` when compiling `mupdf::pdf_dict_getl(..., nullptr)`. | |
| 1103 compile_extra_cpp += ' -Wno-non-pod-varargs' | |
| 1104 # Avoid errors caused by mupdf's C++ bindings' exception classes | |
| 1105 # not having `nothrow` to match the base exception class. | |
| 1106 compile_extra_cpp += ' -std=c++14' | |
| 1107 if windows: | |
| 1108 wp = pipcl.wdev.WindowsPython() | |
| 1109 libs = f'mupdfcpp{wp.cpu.windows_suffix}.lib' | |
| 1110 else: | |
| 1111 libs = ('mupdf', 'mupdfcpp') | |
| 1112 libraries = [ | |
| 1113 f'{mupdf_build_dir}/libmupdf.so' | |
| 1114 f'{mupdf_build_dir}/libmupdfcpp.so' | |
| 1115 ] | |
| 1116 | |
| 1117 path_so_leaf = pipcl.build_extension( | |
| 1118 name = 'extra', | |
| 1119 path_i = f'{g_root}/src/extra.i', | |
| 1120 outdir = f'{g_root}/src/build', | |
| 1121 includes = includes, | |
| 1122 defines = defines, | |
| 1123 libpaths = libpaths, | |
| 1124 libs = libs, | |
| 1125 compiler_extra = compiler_extra + compile_extra_cpp, | |
| 1126 linker_extra = linker_extra, | |
| 1127 optimise = optimise, | |
| 1128 debug = debug, | |
| 1129 prerequisites_swig = None, | |
| 1130 prerequisites_compile = f'{mupdf_local}/include', | |
| 1131 prerequisites_link = libraries, | |
| 1132 py_limited_api = g_py_limited_api, | |
| 1133 swig = PYMUPDF_SETUP_SWIG, | |
| 1134 ) | |
| 1135 | |
| 1136 return path_so_leaf | |
| 1137 | |
| 1138 | |
| 1139 def _extension_flags( mupdf_local, mupdf_build_dir, build_type): | |
| 1140 ''' | |
| 1141 Returns various flags to pass to pipcl.build_extension(). | |
| 1142 ''' | |
| 1143 compiler_extra = '' | |
| 1144 linker_extra = '' | |
| 1145 if build_type == 'memento': | |
| 1146 compiler_extra += ' -DMEMENTO' | |
| 1147 if mupdf_build_dir: | |
| 1148 mupdf_build_dir_flags = os.path.basename( mupdf_build_dir).split( '-') | |
| 1149 else: | |
| 1150 mupdf_build_dir_flags = [build_type] | |
| 1151 optimise = 'release' in mupdf_build_dir_flags | |
| 1152 debug = 'debug' in mupdf_build_dir_flags | |
| 1153 r_extra = '' | |
| 1154 defines = list() | |
| 1155 if windows: | |
| 1156 defines.append('FZ_DLL_CLIENT') | |
| 1157 wp = pipcl.wdev.WindowsPython() | |
| 1158 if os.environ.get('PYMUPDF_SETUP_MUPDF_VS_UPGRADE') == '1': | |
| 1159 # MuPDF C++ build uses a parallel build tree with updated VS files. | |
| 1160 infix = 'win32-vs-upgrade' | |
| 1161 else: | |
| 1162 infix = 'win32' | |
| 1163 build_type_infix = 'Debug' if debug else 'Release' | |
| 1164 libpaths = ( | |
| 1165 f'{mupdf_local}\\platform\\{infix}\\{wp.cpu.windows_subdir}{build_type_infix}', | |
| 1166 f'{mupdf_local}\\platform\\{infix}\\{wp.cpu.windows_subdir}{build_type_infix}Tesseract', | |
| 1167 ) | |
| 1168 libs = f'mupdfcpp{wp.cpu.windows_suffix}.lib' | |
| 1169 libraries = f'{mupdf_local}\\platform\\{infix}\\{wp.cpu.windows_subdir}{build_type_infix}\\{libs}' | |
| 1170 compiler_extra = '' | |
| 1171 else: | |
| 1172 libs = ['mupdf'] | |
| 1173 compiler_extra += ( | |
| 1174 ' -Wall' | |
| 1175 ' -Wno-deprecated-declarations' | |
| 1176 ' -Wno-unused-const-variable' | |
| 1177 ) | |
| 1178 if mupdf_local: | |
| 1179 libpaths = (mupdf_build_dir,) | |
| 1180 libraries = f'{mupdf_build_dir}/{libs[0]}' | |
| 1181 if openbsd: | |
| 1182 compiler_extra += ' -Wno-deprecated-declarations' | |
| 1183 else: | |
| 1184 libpaths = os.environ.get('PYMUPDF_MUPDF_LIB') | |
| 1185 libraries = None | |
| 1186 if libpaths: | |
| 1187 libpaths = libpaths.split(':') | |
| 1188 | |
| 1189 if mupdf_local: | |
| 1190 includes = ( | |
| 1191 f'{mupdf_local}/include', | |
| 1192 f'{mupdf_local}/include/mupdf', | |
| 1193 f'{mupdf_local}/thirdparty/freetype/include', | |
| 1194 ) | |
| 1195 else: | |
| 1196 # Use system MuPDF. | |
| 1197 includes = list() | |
| 1198 pi = os.environ.get('PYMUPDF_INCLUDES') | |
| 1199 if pi: | |
| 1200 includes += pi.split(':') | |
| 1201 pmi = os.environ.get('PYMUPDF_MUPDF_INCLUDE') | |
| 1202 if pmi: | |
| 1203 includes.append(pmi) | |
| 1204 ldflags = os.environ.get('LDFLAGS') | |
| 1205 if ldflags: | |
| 1206 linker_extra += f' {ldflags}' | |
| 1207 cflags = os.environ.get('CFLAGS') | |
| 1208 if cflags: | |
| 1209 compiler_extra += f' {cflags}' | |
| 1210 cxxflags = os.environ.get('CXXFLAGS') | |
| 1211 if cxxflags: | |
| 1212 compiler_extra += f' {cxxflags}' | |
| 1213 | |
| 1214 if pyodide: | |
| 1215 compiler_extra += f' {pyodide_flags}' | |
| 1216 linker_extra += f' {pyodide_flags}' | |
| 1217 | |
| 1218 return compiler_extra, linker_extra, includes, defines, optimise, debug, libpaths, libs, libraries, | |
| 1219 | |
| 1220 | |
| 1221 def sdist(): | |
| 1222 ret = list() | |
| 1223 if PYMUPDF_SETUP_DUMMY == '1': | |
| 1224 return ret | |
| 1225 | |
| 1226 if PYMUPDF_SETUP_FLAVOUR == 'b': | |
| 1227 # Create a minimal sdist that will build/install a dummy PyMuPDFb. | |
| 1228 for p in ( | |
| 1229 'setup.py', | |
| 1230 'pipcl.py', | |
| 1231 'wdev.py', | |
| 1232 'pyproject.toml', | |
| 1233 ): | |
| 1234 ret.append(p) | |
| 1235 ret.append( | |
| 1236 ( | |
| 1237 b'This file indicates that we are a PyMuPDFb sdist and should build/install a dummy PyMuPDFb package.\n', | |
| 1238 g_pymupdfb_sdist_marker, | |
| 1239 ) | |
| 1240 ) | |
| 1241 return ret | |
| 1242 | |
| 1243 for p in pipcl.git_items( g_root): | |
| 1244 if p.startswith( | |
| 1245 ( | |
| 1246 'docs/', | |
| 1247 'signatures/', | |
| 1248 '.', | |
| 1249 ) | |
| 1250 ): | |
| 1251 pass | |
| 1252 else: | |
| 1253 ret.append(p) | |
| 1254 if 0: | |
| 1255 tgz, mupdf_location = get_mupdf_tgz() | |
| 1256 if tgz: | |
| 1257 ret.append((tgz, mupdf_tgz)) | |
| 1258 else: | |
| 1259 log(f'Not including MuPDF .tgz in sdist.') | |
| 1260 return ret | |
| 1261 | |
| 1262 | |
| 1263 classifier = [ | |
| 1264 'Development Status :: 5 - Production/Stable', | |
| 1265 'Intended Audience :: Developers', | |
| 1266 'Intended Audience :: Information Technology', | |
| 1267 'Operating System :: MacOS', | |
| 1268 'Operating System :: Microsoft :: Windows', | |
| 1269 'Operating System :: POSIX :: Linux', | |
| 1270 'Programming Language :: C', | |
| 1271 'Programming Language :: C++', | |
| 1272 'Programming Language :: Python :: 3 :: Only', | |
| 1273 'Programming Language :: Python :: Implementation :: CPython', | |
| 1274 'Topic :: Utilities', | |
| 1275 'Topic :: Multimedia :: Graphics', | |
| 1276 'Topic :: Software Development :: Libraries', | |
| 1277 ] | |
| 1278 | |
| 1279 # We generate different wheels depending on PYMUPDF_SETUP_FLAVOUR. | |
| 1280 # | |
| 1281 | |
| 1282 # PyMuPDF version. | |
| 1283 version_p = '1.26.4' | |
| 1284 | |
| 1285 version_mupdf = '1.26.7' | |
| 1286 | |
| 1287 # PyMuPDFb version. This is the PyMuPDF version whose PyMuPDFb wheels we will | |
| 1288 # (re)use if generating separate PyMuPDFb wheels. Though as of PyMuPDF-1.24.11 | |
| 1289 # (2024-10-03) we no longer use PyMuPDFb wheels so this is actually unused. | |
| 1290 # | |
| 1291 version_b = '1.26.3' | |
| 1292 | |
| 1293 if os.path.exists(f'{g_root}/{g_pymupdfb_sdist_marker}'): | |
| 1294 | |
| 1295 # We are in a PyMuPDFb sdist. We specify a dummy package so that pip builds | |
| 1296 # from sdists work - pip's build using PyMuPDF's sdist will already create | |
| 1297 # the required binaries, but pip will still see `requires_dist` set to | |
| 1298 # 'PyMuPDFb', so will also download and build PyMuPDFb's sdist. | |
| 1299 # | |
| 1300 log(f'Specifying dummy PyMuPDFb wheel.') | |
| 1301 | |
| 1302 def get_requires_for_build_wheel(config_settings=None): | |
| 1303 return list() | |
| 1304 | |
| 1305 p = pipcl.Package( | |
| 1306 'PyMuPDFb', | |
| 1307 version_b, | |
| 1308 summary = 'Dummy PyMuPDFb wheel', | |
| 1309 description = '', | |
| 1310 author = 'Artifex', | |
| 1311 author_email = 'support@artifex.com', | |
| 1312 license = 'GNU AFFERO GPL 3.0', | |
| 1313 tag_python = 'py3', | |
| 1314 ) | |
| 1315 | |
| 1316 else: | |
| 1317 # A normal PyMuPDF package. | |
| 1318 | |
| 1319 with open( f'{g_root}/README.md', encoding='utf-8') as f: | |
| 1320 readme_p = f.read() | |
| 1321 | |
| 1322 with open( f'{g_root}/READMEb.md', encoding='utf-8') as f: | |
| 1323 readme_b = f.read() | |
| 1324 | |
| 1325 with open( f'{g_root}/READMEd.md', encoding='utf-8') as f: | |
| 1326 readme_d = f.read() | |
| 1327 | |
| 1328 tag_python = None | |
| 1329 requires_dist = list() | |
| 1330 entry_points = None | |
| 1331 | |
| 1332 if 'p' in PYMUPDF_SETUP_FLAVOUR: | |
| 1333 version = version_p | |
| 1334 name = 'PyMuPDF' | |
| 1335 readme = readme_p | |
| 1336 summary = 'A high performance Python library for data extraction, analysis, conversion & manipulation of PDF (and other) documents.' | |
| 1337 if 'b' not in PYMUPDF_SETUP_FLAVOUR: | |
| 1338 requires_dist.append(f'PyMuPDFb =={version_b}') | |
| 1339 # Create a `pymupdf` command. | |
| 1340 entry_points = textwrap.dedent(''' | |
| 1341 [console_scripts] | |
| 1342 pymupdf = pymupdf.__main__:main | |
| 1343 ''') | |
| 1344 elif 'b' in PYMUPDF_SETUP_FLAVOUR: | |
| 1345 version = version_b | |
| 1346 name = 'PyMuPDFb' | |
| 1347 readme = readme_b | |
| 1348 summary = 'MuPDF shared libraries for PyMuPDF.' | |
| 1349 tag_python = 'py3' | |
| 1350 elif 'd' in PYMUPDF_SETUP_FLAVOUR: | |
| 1351 version = version_b | |
| 1352 name = 'PyMuPDFd' | |
| 1353 readme = readme_d | |
| 1354 summary = 'MuPDF build-time files for PyMuPDF.' | |
| 1355 tag_python = 'py3' | |
| 1356 else: | |
| 1357 assert 0, f'Unrecognised {PYMUPDF_SETUP_FLAVOUR=}.' | |
| 1358 | |
| 1359 if os.environ.get('PYODIDE_ROOT'): | |
| 1360 # We can't pip install pytest on pyodide, so specify it here. | |
| 1361 requires_dist.append('pytest') | |
| 1362 | |
| 1363 p = pipcl.Package( | |
| 1364 name, | |
| 1365 version, | |
| 1366 summary = summary, | |
| 1367 description = readme, | |
| 1368 description_content_type = 'text/markdown', | |
| 1369 classifier = classifier, | |
| 1370 author = 'Artifex', | |
| 1371 author_email = 'support@artifex.com', | |
| 1372 requires_dist = requires_dist, | |
| 1373 requires_python = '>=3.9', | |
| 1374 license = 'Dual Licensed - GNU AFFERO GPL 3.0 or Artifex Commercial License', | |
| 1375 project_url = [ | |
| 1376 ('Documentation, https://pymupdf.readthedocs.io/'), | |
| 1377 ('Source, https://github.com/pymupdf/pymupdf'), | |
| 1378 ('Tracker, https://github.com/pymupdf/PyMuPDF/issues'), | |
| 1379 ('Changelog, https://pymupdf.readthedocs.io/en/latest/changes.html'), | |
| 1380 ], | |
| 1381 | |
| 1382 entry_points = entry_points, | |
| 1383 | |
| 1384 fn_build=build, | |
| 1385 fn_sdist=sdist, | |
| 1386 | |
| 1387 tag_python=tag_python, | |
| 1388 py_limited_api=g_py_limited_api, | |
| 1389 | |
| 1390 # 30MB: 9 ZIP_DEFLATED | |
| 1391 # 28MB: 9 ZIP_BZIP2 | |
| 1392 # 23MB: 9 ZIP_LZMA | |
| 1393 #wheel_compression = zipfile.ZIP_DEFLATED if (darwin or pyodide) else zipfile.ZIP_LZMA, | |
| 1394 wheel_compresslevel = 9, | |
| 1395 ) | |
| 1396 | |
| 1397 def get_requires_for_build_wheel(config_settings=None): | |
| 1398 ''' | |
| 1399 Adds to pyproject.toml:[build-system]:requires, allowing programmatic | |
| 1400 control over what packages we require. | |
| 1401 ''' | |
| 1402 def platform_release_tuple(): | |
| 1403 r = platform.release() | |
| 1404 r = r.split('.') | |
| 1405 r = tuple(int(i) for i in r) | |
| 1406 log(f'platform_release_tuple() returning {r=}.') | |
| 1407 return r | |
| 1408 | |
| 1409 ret = list() | |
| 1410 libclang = os.environ.get('PYMUPDF_SETUP_LIBCLANG') | |
| 1411 if libclang: | |
| 1412 print(f'Overriding to use {libclang=}.') | |
| 1413 ret.append(libclang) | |
| 1414 elif openbsd: | |
| 1415 print(f'OpenBSD: libclang not available via pip; assuming `pkg_add py3-llvm`.') | |
| 1416 elif darwin and platform.machine() == 'arm64': | |
| 1417 print(f'MacOS/arm64: forcing use of libclang 16.0.6 because 18.1.1 known to fail with `clang.cindex.TranslationUnitLoadError: Error parsing translation unit.`') | |
| 1418 ret.append('libclang==16.0.6') | |
| 1419 elif darwin and platform_release_tuple() < (18,): | |
| 1420 # There are still of problems when building on old macos. | |
| 1421 ret.append('libclang==14.0.6') | |
| 1422 else: | |
| 1423 ret.append('libclang') | |
| 1424 if msys2: | |
| 1425 print(f'msys2: pip install of swig does not build; assuming `pacman -S swig`.') | |
| 1426 elif openbsd: | |
| 1427 print(f'OpenBSD: pip install of swig does not build; assuming `pkg_add swig`.') | |
| 1428 else: | |
| 1429 ret.append( 'swig') | |
| 1430 return ret | |
| 1431 | |
| 1432 | |
| 1433 if PYMUPDF_SETUP_URL_WHEEL: | |
| 1434 def build_wheel( | |
| 1435 wheel_directory, | |
| 1436 config_settings=None, | |
| 1437 metadata_directory=None, | |
| 1438 p=p, | |
| 1439 ): | |
| 1440 ''' | |
| 1441 Instead of building wheel, we look for and copy a wheel from location | |
| 1442 specified by PYMUPDF_SETUP_URL_WHEEL. | |
| 1443 ''' | |
| 1444 log(f'{PYMUPDF_SETUP_URL_WHEEL=}') | |
| 1445 log(f'{p.wheel_name()=}') | |
| 1446 url = PYMUPDF_SETUP_URL_WHEEL | |
| 1447 if url.startswith(('http://', 'https://')): | |
| 1448 leaf = p.wheel_name() | |
| 1449 out_path = f'{wheel_directory}{leaf}' | |
| 1450 out_path_temp = out_path + '-' | |
| 1451 if url.endswith('/'): | |
| 1452 url += leaf | |
| 1453 log(f'Downloading from {url=} to {out_path_temp=}.') | |
| 1454 urllib.request.urlretrieve(url, out_path_temp) | |
| 1455 elif url.startswith(f'file://'): | |
| 1456 in_path = url[len('file://'):] | |
| 1457 log(f'{in_path=}') | |
| 1458 if in_path.endswith('/'): | |
| 1459 # Look for matching wheel within this directory. | |
| 1460 wheels = glob.glob(f'{in_path}*.whl') | |
| 1461 log(f'{len(wheels)=}') | |
| 1462 for in_path in wheels: | |
| 1463 log(f'{in_path=}') | |
| 1464 leaf = os.path.basename(in_path) | |
| 1465 if p.wheel_name_match(leaf): | |
| 1466 log(f'Match: {in_path=}') | |
| 1467 break | |
| 1468 else: | |
| 1469 message = f'Cannot find matching for {p.wheel_name()=} in ({len(wheels)=}):\n' | |
| 1470 wheels_text = '' | |
| 1471 for wheel in wheels: | |
| 1472 wheels_text += f' {wheel}\n' | |
| 1473 assert 0, f'Cannot find matching for {p.wheel_name()=} in:\n{wheels_text}' | |
| 1474 else: | |
| 1475 leaf = os.path.basename(in_path) | |
| 1476 out_path = os.path.join(wheel_directory, leaf) | |
| 1477 out_path_temp = out_path + '-' | |
| 1478 log(f'Copying from {in_path=} to {out_path_temp=}.') | |
| 1479 shutil.copy2(in_path, out_path_temp) | |
| 1480 else: | |
| 1481 assert 0, f'Unrecognised prefix in {PYMUPDF_SETUP_URL_WHEEL=}.' | |
| 1482 | |
| 1483 log(f'Renaming from:\n {out_path_temp}\nto:\n {out_path}.') | |
| 1484 os.rename(out_path_temp, out_path) | |
| 1485 return os.path.basename(out_path) | |
| 1486 else: | |
| 1487 build_wheel = p.build_wheel | |
| 1488 | |
| 1489 build_sdist = p.build_sdist | |
| 1490 | |
| 1491 | |
| 1492 if __name__ == '__main__': | |
| 1493 p.handle_argv(sys.argv) |
