comparison scripts/gh_release.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
comparison
equal deleted inserted replaced
-1:000000000000 1:1d09e1dec1d9
1 #! /usr/bin/env python3
2
3 '''
4 Build+test script for PyMuPDF using cibuildwheel. Mostly for use with github
5 builds.
6
7 We run cibuild manually, in order to build and test PyMuPDF wheels.
8
9 As of 2024-10-08 we also support the old two wheel flavours that make up
10 PyMuPDF:
11
12 PyMuPDFb
13 Not specific to particular versions of Python. Contains shared
14 libraries for the MuPDF C and C++ bindings.
15 PyMuPDF
16 Specific to particular versions of Python. Contains the rest of
17 the PyMuPDF implementation.
18
19 Args:
20 build
21 Build using cibuildwheel.
22 build-devel
23 Build using cibuild with `--platform` set.
24 pip_install <prefix>
25 For internal use. Runs `pip install <prefix>-*<platform_tag>.whl`,
26 where `platform_tag` will be things like 'win32', 'win_amd64',
27 'x86_64`, depending on the python we are running on.
28 venv
29 Run with remaining args inside a venv.
30 test
31 Internal.
32
33 We also look at specific items in the environment. This allows use with Github
34 action inputs, which can't be easily translated into command-line arguments.
35
36 inputs_flavours
37 If '0' or unset, build complete PyMuPDF wheels.
38 If '1', build separate PyMuPDF and PyMuPDFb wheels.
39 inputs_sdist
40 inputs_skeleton
41 Build minimal wheel; for testing only.
42 inputs_wheels_cps:
43 Python versions to build for. E.g. 'cp39* cp313*'.
44 inputs_wheels_default
45 Default value for other inputs_wheels_* if unset.
46 inputs_wheels_linux_aarch64
47 inputs_wheels_linux_auto
48 inputs_wheels_linux_pyodide
49 inputs_wheels_macos_arm64
50 inputs_wheels_macos_auto
51 inputs_wheels_windows_auto
52 If '1' we build the relevant wheels.
53 inputs_PYMUPDF_SETUP_MUPDF_BUILD
54 Used to directly set PYMUPDF_SETUP_MUPDF_BUILD.
55 E.g. 'git:--recursive --depth 1 --shallow-submodules --branch master https://github.com/ArtifexSoftware/mupdf.git'
56 inputs_PYMUPDF_SETUP_MUPDF_BUILD_TYPE
57 Used to directly set PYMUPDF_SETUP_MUPDF_BUILD_TYPE. Note that as of
58 2024-09-10 .github/workflows/build_wheels.yml does not set this.
59 PYMUPDF_SETUP_PY_LIMITED_API
60 If not '0' we build a single wheel for all python versions using the
61 Python Limited API.
62
63 Building for Pyodide
64
65 If `inputs_wheels_linux_pyodide` is true and we are on Linux, we build a
66 Pyodide wheel, using scripts/test.py.
67
68 Set up for use outside Github
69
70 sudo apt install docker.io
71 sudo usermod -aG docker $USER
72
73 Example usage:
74
75 PYMUPDF_SETUP_MUPDF_BUILD=../mupdf py -3.9-32 PyMuPDF/scripts/gh_release.py venv build-devel
76 '''
77
78 import glob
79 import inspect
80 import os
81 import platform
82 import re
83 import shlex
84 import subprocess
85 import sys
86 import textwrap
87
88 import test as test_py
89
90 pymupdf_dir = os.path.abspath( f'{__file__}/../..')
91
92 sys.path.insert(0, pymupdf_dir)
93 import pipcl
94 del sys.path[0]
95
96 log = pipcl.log0
97 run = pipcl.run
98
99
100 def main():
101
102 log( '### main():')
103 log(f'{platform.platform()=}')
104 log(f'{platform.python_version()=}')
105 log(f'{platform.architecture()=}')
106 log(f'{platform.machine()=}')
107 log(f'{platform.processor()=}')
108 log(f'{platform.release()=}')
109 log(f'{platform.system()=}')
110 log(f'{platform.version()=}')
111 log(f'{platform.uname()=}')
112 log(f'{sys.executable=}')
113 log(f'{sys.maxsize=}')
114 log(f'sys.argv ({len(sys.argv)}):')
115 for i, arg in enumerate(sys.argv):
116 log(f' {i}: {arg!r}')
117 log(f'os.environ ({len(os.environ)}):')
118 for k in sorted( os.environ.keys()):
119 v = os.environ[ k]
120 log( f' {k}: {v!r}')
121
122 if test_py.github_workflow_unimportant():
123 return
124
125 valgrind = False
126 if len( sys.argv) == 1:
127 args = iter( ['build'])
128 else:
129 args = iter( sys.argv[1:])
130 while 1:
131 try:
132 arg = next(args)
133 except StopIteration:
134 break
135 if arg == 'build':
136 build(valgrind=valgrind)
137 elif arg == 'build-devel':
138 if platform.system() == 'Linux':
139 p = 'linux'
140 elif platform.system() == 'Windows':
141 p = 'windows'
142 elif platform.system() == 'Darwin':
143 p = 'macos'
144 else:
145 assert 0, f'Unrecognised {platform.system()=}'
146 build(platform_=p)
147 elif arg == 'pip_install':
148 prefix = next(args)
149 d = os.path.dirname(prefix)
150 log( f'{prefix=}')
151 log( f'{d=}')
152 for leaf in os.listdir(d):
153 log( f' {d}/{leaf}')
154 pattern = f'{prefix}-*{platform_tag()}.whl'
155 paths = glob.glob( pattern)
156 log( f'{pattern=} {paths=}')
157 # Follow pipcl.py and look at AUDITWHEEL_PLAT. This allows us to
158 # cope if building for both musl and normal linux.
159 awp = os.environ.get('AUDITWHEEL_PLAT')
160 if awp:
161 paths = [i for i in paths if awp in i]
162 log(f'After selecting AUDITWHEEL_PLAT={awp!r}, {paths=}.')
163 paths = ' '.join( paths)
164 run( f'pip install {paths}')
165 elif arg == 'venv':
166 command = ['python', sys.argv[0]]
167 for arg in args:
168 command.append( arg)
169 venv( command, packages = 'cibuildwheel')
170 elif arg == 'test':
171 project = next(args)
172 package = next(args)
173 test( project, package, valgrind=valgrind)
174 elif arg == '--valgrind':
175 valgrind = int(next(args))
176 else:
177 assert 0, f'Unrecognised {arg=}'
178
179
180 def build( platform_=None, valgrind=False):
181 log( '### build():')
182
183 platform_arg = f' --platform {platform_}' if platform_ else ''
184
185 # Parameters are in os.environ, as that seems to be the only way that
186 # Github workflow .yml files can encode them.
187 #
188 def get_bool(name, default=0):
189 v = os.environ.get(name)
190 if v in ('1', 'true'):
191 return 1
192 elif v in ('0', 'false'):
193 return 0
194 elif v is None:
195 return default
196 else:
197 assert 0, f'Bad environ {name=} {v=}'
198 inputs_flavours = get_bool('inputs_flavours', 1)
199 inputs_sdist = get_bool('inputs_sdist')
200 inputs_skeleton = os.environ.get('inputs_skeleton')
201 inputs_wheels_default = get_bool('inputs_wheels_default', 1)
202 inputs_wheels_linux_aarch64 = get_bool('inputs_wheels_linux_aarch64', inputs_wheels_default)
203 inputs_wheels_linux_auto = get_bool('inputs_wheels_linux_auto', inputs_wheels_default)
204 inputs_wheels_linux_pyodide = get_bool('inputs_wheels_linux_pyodide', 0)
205 inputs_wheels_macos_arm64 = get_bool('inputs_wheels_macos_arm64', 0)
206 inputs_wheels_macos_auto = get_bool('inputs_wheels_macos_auto', inputs_wheels_default)
207 inputs_wheels_windows_auto = get_bool('inputs_wheels_windows_auto', inputs_wheels_default)
208 inputs_wheels_cps = os.environ.get('inputs_wheels_cps')
209 inputs_PYMUPDF_SETUP_MUPDF_BUILD = os.environ.get('inputs_PYMUPDF_SETUP_MUPDF_BUILD')
210 inputs_PYMUPDF_SETUP_MUPDF_BUILD_TYPE = os.environ.get('inputs_PYMUPDF_SETUP_MUPDF_BUILD_TYPE')
211
212 PYMUPDF_SETUP_PY_LIMITED_API = os.environ.get('PYMUPDF_SETUP_PY_LIMITED_API')
213
214 log( f'{inputs_flavours=}')
215 log( f'{inputs_sdist=}')
216 log( f'{inputs_skeleton=}')
217 log( f'{inputs_wheels_default=}')
218 log( f'{inputs_wheels_linux_aarch64=}')
219 log( f'{inputs_wheels_linux_auto=}')
220 log( f'{inputs_wheels_linux_pyodide=}')
221 log( f'{inputs_wheels_macos_arm64=}')
222 log( f'{inputs_wheels_macos_auto=}')
223 log( f'{inputs_wheels_windows_auto=}')
224 log( f'{inputs_wheels_cps=}')
225 log( f'{inputs_PYMUPDF_SETUP_MUPDF_BUILD=}')
226 log( f'{inputs_PYMUPDF_SETUP_MUPDF_BUILD_TYPE=}')
227 log( f'{PYMUPDF_SETUP_PY_LIMITED_API=}')
228
229 # Build Pyodide wheel if specified.
230 #
231 if platform.system() == 'Linux' and inputs_wheels_linux_pyodide:
232 # Pyodide wheels are built by running scripts/test.py, not
233 # cibuildwheel.
234 command = f'{sys.executable} scripts/test.py -P 1'
235 if inputs_PYMUPDF_SETUP_MUPDF_BUILD:
236 command += f' -m {shlex.quote(inputs_PYMUPDF_SETUP_MUPDF_BUILD)}'
237 command += ' pyodide_wheel'
238 run(command)
239
240 # Build sdist(s).
241 #
242 if inputs_sdist:
243 if pymupdf_dir != os.path.abspath( os.getcwd()):
244 log( f'Changing dir to {pymupdf_dir=}')
245 os.chdir( pymupdf_dir)
246 # Create PyMuPDF sdist.
247 run(f'{sys.executable} setup.py sdist')
248 assert glob.glob('dist/pymupdf-*.tar.gz')
249 if inputs_flavours:
250 # Create PyMuPDFb sdist.
251 run(
252 f'{sys.executable} setup.py sdist',
253 env_extra=dict(PYMUPDF_SETUP_FLAVOUR='b'),
254 )
255 assert glob.glob('dist/pymupdfb-*.tar.gz')
256
257 # Build wheels.
258 #
259 if (0
260 or inputs_wheels_linux_aarch64
261 or inputs_wheels_linux_auto
262 or inputs_wheels_macos_arm64
263 or inputs_wheels_macos_auto
264 or inputs_wheels_windows_auto
265 ):
266 env_extra = dict()
267
268 def set_if_unset(name, value):
269 v = os.environ.get(name)
270 if v is None:
271 log( f'Setting environment {name=} to {value=}')
272 env_extra[ name] = value
273 else:
274 log( f'Not changing {name}={v!r} to {value!r}')
275 set_if_unset( 'CIBW_BUILD_VERBOSITY', '1')
276 # We exclude pp* because of `fitz_wrap.obj : error LNK2001: unresolved
277 # external symbol PyUnicode_DecodeRawUnicodeEscape`.
278 # 2024-06-05: musllinux on aarch64 fails because libclang cannot find
279 # libclang.so.
280 #
281 # Note that we had to disable cp313-win32 when 3.13 was experimental
282 # because there was no 64-bit Python-3.13 available via `py
283 # -3.13`. (Win32 builds need to use win64 Python because win32
284 # libclang is broken.)
285 #
286 set_if_unset( 'CIBW_SKIP', 'pp* *i686 cp36* cp37* *musllinux*aarch64*')
287
288 def make_string(*items):
289 ret = list()
290 for item in items:
291 if item:
292 ret.append(item)
293 return ' '.join(ret)
294
295 cps = inputs_wheels_cps if inputs_wheels_cps else 'cp39* cp310* cp311* cp312* cp313*'
296 set_if_unset( 'CIBW_BUILD', cps)
297 for cp in cps.split():
298 m = re.match('cp([0-9]+)[*]', cp)
299 assert m, f'{cps=} {cp=}'
300 v = int(m.group(1))
301 if v == 314:
302 # Need to set CIBW_PRERELEASE_PYTHONS, otherwise cibuildwheel
303 # will refuse.
304 log(f'Setting CIBW_PRERELEASE_PYTHONS for Python version {cp=}.')
305 set_if_unset( 'CIBW_PRERELEASE_PYTHONS', '1')
306
307 if platform.system() == 'Linux':
308 set_if_unset(
309 'CIBW_ARCHS_LINUX',
310 make_string(
311 'auto64' * inputs_wheels_linux_auto,
312 'aarch64' * inputs_wheels_linux_aarch64,
313 ),
314 )
315 if env_extra.get('CIBW_ARCHS_LINUX') == '':
316 log(f'Not running cibuildwheel because CIBW_ARCHS_LINUX is empty string.')
317 return
318
319 if platform.system() == 'Windows':
320 set_if_unset(
321 'CIBW_ARCHS_WINDOWS',
322 make_string(
323 'auto' * inputs_wheels_windows_auto,
324 ),
325 )
326 if env_extra.get('CIBW_ARCHS_WINDOWS') == '':
327 log(f'Not running cibuildwheel because CIBW_ARCHS_WINDOWS is empty string.')
328 return
329
330 if platform.system() == 'Darwin':
331 set_if_unset(
332 'CIBW_ARCHS_MACOS',
333 make_string(
334 'auto' * inputs_wheels_macos_auto,
335 'arm64' * inputs_wheels_macos_arm64,
336 ),
337 )
338 if env_extra.get('CIBW_ARCHS_MACOS') == '':
339 log(f'Not running cibuildwheel because CIBW_ARCHS_MACOS is empty string.')
340 return
341
342 def env_pass(name):
343 '''
344 Adds `name` to CIBW_ENVIRONMENT_PASS_LINUX if required to be available
345 when building wheel with cibuildwheel.
346 '''
347 if platform.system() == 'Linux':
348 v = env_extra.get('CIBW_ENVIRONMENT_PASS_LINUX', '')
349 if v:
350 v += ' '
351 v += name
352 env_extra['CIBW_ENVIRONMENT_PASS_LINUX'] = v
353
354 def env_set(name, value, pass_=False):
355 assert isinstance( value, str)
356 if not name.startswith('CIBW'):
357 assert pass_, f'Non-CIBW* name requires `pass_` to be true. {name=} {value=}.'
358 env_extra[ name] = value
359 if pass_:
360 env_pass(name)
361
362 env_pass('PYMUPDF_SETUP_PY_LIMITED_API')
363
364 if os.environ.get('PYMUPDF_SETUP_LIBCLANG'):
365 env_pass('PYMUPDF_SETUP_LIBCLANG')
366
367 if inputs_skeleton:
368 env_set('PYMUPDF_SETUP_SKELETON', inputs_skeleton, pass_=1)
369
370 if inputs_PYMUPDF_SETUP_MUPDF_BUILD not in ('-', None):
371 log(f'Setting PYMUPDF_SETUP_MUPDF_BUILD to {inputs_PYMUPDF_SETUP_MUPDF_BUILD!r}.')
372 env_set('PYMUPDF_SETUP_MUPDF_BUILD', inputs_PYMUPDF_SETUP_MUPDF_BUILD, pass_=True)
373 env_set('PYMUPDF_SETUP_MUPDF_TGZ', '', pass_=True) # Don't put mupdf in sdist.
374
375 if inputs_PYMUPDF_SETUP_MUPDF_BUILD_TYPE not in ('-', None):
376 log(f'Setting PYMUPDF_SETUP_MUPDF_BUILD_TYPE to {inputs_PYMUPDF_SETUP_MUPDF_BUILD_TYPE!r}.')
377 env_set('PYMUPDF_SETUP_MUPDF_BUILD_TYPE', inputs_PYMUPDF_SETUP_MUPDF_BUILD_TYPE, pass_=True)
378
379 def set_cibuild_test():
380 log( f'set_cibuild_test(): {inputs_skeleton=}')
381 valgrind_text = ''
382 if valgrind:
383 valgrind_text = ' --valgrind 1'
384 env_set('CIBW_TEST_COMMAND', f'python {{project}}/scripts/gh_release.py{valgrind_text} test {{project}} {{package}}')
385
386 if pymupdf_dir != os.path.abspath( os.getcwd()):
387 log( f'Changing dir to {pymupdf_dir=}')
388 os.chdir( pymupdf_dir)
389
390 run('pip install cibuildwheel')
391
392 # We include MuPDF build-time files.
393 flavour_d = True
394
395 if PYMUPDF_SETUP_PY_LIMITED_API != '0':
396 # Build one wheel with oldest python, then fake build with other python
397 # versions so we test everything.
398 log(f'{PYMUPDF_SETUP_PY_LIMITED_API=}')
399 env_pass('PYMUPDF_SETUP_PY_LIMITED_API')
400 CIBW_BUILD_old = env_extra.get('CIBW_BUILD')
401 assert CIBW_BUILD_old is not None
402 cp = cps.split()[0]
403 env_set('CIBW_BUILD', cp)
404 log(f'Building single wheel.')
405 run( f'cibuildwheel{platform_arg}', env_extra=env_extra)
406
407 # Fake-build with all python versions, using the wheel we have
408 # just created. This works by setting PYMUPDF_SETUP_URL_WHEEL
409 # which makes PyMuPDF's setup.py copy an existing wheel instead
410 # of building a wheel itself; it also copes with existing
411 # wheels having extra platform tags (from cibuildwheel's use of
412 # auditwheel).
413 #
414 env_set('PYMUPDF_SETUP_URL_WHEEL', f'file://wheelhouse/', pass_=True)
415
416 set_cibuild_test()
417 env_set('CIBW_BUILD', CIBW_BUILD_old)
418
419 # Disable cibuildwheels use of auditwheel. The wheel was repaired
420 # when it was created above so we don't need to do so again. This
421 # also avoids problems with musl wheels on a Linux glibc host where
422 # auditwheel fails with: `ValueError: Cannot repair wheel, because
423 # required library "libgcc_s-a3a07607.so.1" could not be located`.
424 #
425 env_set('CIBW_REPAIR_WHEEL_COMMAND', '')
426
427 if platform.system() == 'Linux' and env_extra.get('CIBW_ARCHS_LINUX') == 'aarch64':
428 log(f'Testing all Python versions on linux-aarch64 is too slow and is killed by github after 6h.')
429 log(f'Testing on restricted python versions using wheels in wheelhouse/.')
430 # Testing only on first and last python versions.
431 cp1 = cps.split()[0]
432 cp2 = cps.split()[-1]
433 cp = cp1 if cp1 == cp2 else f'{cp1} {cp2}'
434 env_set('CIBW_BUILD', cp)
435 else:
436 log(f'Testing on all python versions using wheels in wheelhouse/.')
437 run( f'cibuildwheel{platform_arg}', env_extra=env_extra)
438
439 elif inputs_flavours:
440 # Build and test PyMuPDF and PyMuPDFb wheels.
441 #
442
443 # First build PyMuPDFb wheel. cibuildwheel will build a single wheel
444 # here, which will work with any python version on current OS.
445 #
446 flavour = 'b'
447 if flavour_d:
448 # Include MuPDF build-time files.
449 flavour += 'd'
450 env_set( 'PYMUPDF_SETUP_FLAVOUR', flavour, pass_=1)
451 run( f'cibuildwheel{platform_arg}', env_extra=env_extra)
452 run( 'echo after {flavour=}')
453 run( 'ls -l wheelhouse')
454
455 # Now set environment to build PyMuPDF wheels. cibuildwheel will build
456 # one for each Python version.
457 #
458
459 # Tell cibuildwheel not to use `auditwheel`, because it cannot cope
460 # with us deliberately putting required libraries into a different
461 # wheel.
462 #
463 # Also, `auditwheel addtag` says `No tags to be added` and terminates
464 # with non-zero. See: https://github.com/pypa/auditwheel/issues/439.
465 #
466 env_set('CIBW_REPAIR_WHEEL_COMMAND_LINUX', '')
467 env_set('CIBW_REPAIR_WHEEL_COMMAND_MACOS', '')
468
469 # We tell cibuildwheel to test these wheels, but also set
470 # CIBW_BEFORE_TEST to make it first run ourselves with the
471 # `pip_install` arg to install the PyMuPDFb wheel. Otherwise
472 # installation of PyMuPDF would fail because it lists the
473 # PyMuPDFb wheel as a prerequisite. We need to use `pip_install`
474 # because wildcards do not work on Windows, and we want to be
475 # careful to avoid incompatible wheels, e.g. 32 vs 64-bit wheels
476 # coexist during Windows builds.
477 #
478 env_set('CIBW_BEFORE_TEST', f'python scripts/gh_release.py pip_install wheelhouse/pymupdfb')
479
480 set_cibuild_test()
481
482 # Build main PyMuPDF wheel.
483 flavour = 'p'
484 env_set( 'PYMUPDF_SETUP_FLAVOUR', flavour, pass_=1)
485 run( f'cibuildwheel{platform_arg}', env_extra=env_extra)
486
487 else:
488 # Build and test wheels which contain everything.
489 #
490 flavour = 'pb'
491 if flavour_d:
492 flavour += 'd'
493 set_cibuild_test()
494 env_set( 'PYMUPDF_SETUP_FLAVOUR', flavour, pass_=1)
495
496 run( f'cibuildwheel{platform_arg}', env_extra=env_extra)
497
498 run( 'ls -lt wheelhouse')
499
500
501 def cpu_bits():
502 return 32 if sys.maxsize == 2**31 - 1 else 64
503
504
505 # Name of venv used by `venv()`.
506 #
507 venv_name = f'venv-pymupdf-{platform.python_version()}-{cpu_bits()}'
508
509 def venv( command=None, packages=None, quick=False, system_site_packages=False):
510 '''
511 Runs remaining args, or the specified command if present, in a venv.
512
513 command:
514 Command as string or list of args. Should usually start with 'python'
515 to run the venv's python.
516 packages:
517 List of packages (or comma-separated string) to install.
518 quick:
519 If true and venv directory already exists, we don't recreate venv or
520 install Python packages in it.
521 '''
522 command2 = ''
523 if platform.system() == 'OpenBSD':
524 # libclang not available from pypi.org, but system py3-llvm package
525 # works. `pip install` should be run with --no-build-isolation and
526 # explicit `pip install swig psutil`.
527 system_site_packages = True
528 #ssp = ' --system-site-packages'
529 log(f'OpenBSD: libclang not available from pypi.org.')
530 log(f'OpenBSD: system package `py3-llvm` must be installed.')
531 log(f'OpenBSD: creating venv with --system-site-packages.')
532 log(f'OpenBSD: `pip install .../PyMuPDF` must be preceded by install of swig etc.')
533 ssp = ' --system-site-packages' if system_site_packages else ''
534 if quick and os.path.isdir(venv_name):
535 log(f'{quick=}: Not creating venv because directory already exists: {venv_name}')
536 command2 += 'true'
537 else:
538 quick = False
539 command2 += f'{sys.executable} -m venv{ssp} {venv_name}'
540 if platform.system() == 'Windows':
541 command2 += f' && {venv_name}\\Scripts\\activate'
542 else:
543 command2 += f' && . {venv_name}/bin/activate'
544 if quick:
545 log(f'{quick=}: Not upgrading pip or installing packages.')
546 else:
547 command2 += ' && python -m pip install --upgrade pip'
548 if packages:
549 if isinstance(packages, str):
550 packages = packages.split(',')
551 command2 += ' && pip install ' + ' '.join(packages)
552 command2 += ' &&'
553 if isinstance( command, str):
554 command2 += ' ' + command
555 else:
556 for arg in command:
557 command2 += ' ' + shlex.quote(arg)
558
559 run( command2)
560
561
562 def test( project, package, valgrind):
563
564 run(f'pip install {test_packages}')
565 if valgrind:
566 log('Installing valgrind.')
567 run(f'sudo apt update')
568 run(f'sudo apt install valgrind')
569 run(f'valgrind --version')
570
571 log('Running PyMuPDF tests under valgrind.')
572 # We ignore memory leaks.
573 run(
574 f'{sys.executable} {project}/tests/run_compound.py'
575 f' valgrind --suppressions={project}/valgrind.supp --error-exitcode=100 --errors-for-leak-kinds=none --fullpath-after='
576 f' pytest {project}/tests'
577 ,
578 env_extra=dict(
579 PYTHONMALLOC='malloc',
580 PYMUPDF_RUNNING_ON_VALGRIND='1',
581 ),
582 )
583 else:
584 run(f'{sys.executable} {project}/tests/run_compound.py pytest {project}/tests')
585
586
587 if platform.system() == 'Windows':
588 def relpath(path, start=None):
589 try:
590 return os.path.relpath(path, start)
591 except ValueError:
592 # os.path.relpath() fails if trying to change drives.
593 return os.path.abspath(path)
594 else:
595 def relpath(path, start=None):
596 return os.path.relpath(path, start)
597
598
599 def platform_tag():
600 bits = cpu_bits()
601 if platform.system() == 'Windows':
602 return 'win32' if bits==32 else 'win_amd64'
603 elif platform.system() in ('Linux', 'Darwin'):
604 assert bits == 64
605 return platform.machine()
606 #return 'x86_64'
607 else:
608 assert 0, f'Unrecognised: {platform.system()=}'
609
610
611 test_packages = 'pytest fontTools pymupdf-fonts flake8 pylint codespell'
612 if platform.system() == 'Windows' and cpu_bits() == 32:
613 # No pillow wheel available, and doesn't build easily.
614 pass
615 else:
616 test_packages += ' pillow'
617 if platform.system().startswith('MSYS_NT-'):
618 # psutil not available on msys2.
619 pass
620 else:
621 test_packages += ' psutil'
622
623
624 if __name__ == '__main__':
625 main()