44
55Examples:
66
7- ./PyMuPDF/scripts/test.py -- m mupdf build test
7+ ./PyMuPDF/scripts/test.py -m mupdf build test
88 Build and test with pre-existing local mupdf/ checkout.
99
1010 ./PyMuPDF/scripts/test.py build test
1313 ./PyMuPDF/scripts/test.py -m 'git:https://git.ghostscript.com/mupdf.git' build test
1414 Build and test with internal checkout of MuPDF master.
1515
16- ./PyMuPDF/scripts/test.py -m 'git:--branch 1.26.x https://github.com/ArtifexSoftware/mupdf.git ' build test
16+ ./PyMuPDF/scripts/test.py -m ': 1.26.x' build test
1717 Build and test using internal checkout of mupdf 1.26.x branch from
1818 Github.
1919
20+ ./PyMuPDF/scripts/test.py install test -i 1.26.3 -k test_2596
21+ Install pymupdf-1.26.3 from pupi.org and test only test_2596.
22+
2023Usage:
2124
2225* Command line arguments are called parameters if they start with `-`,
3134Other:
3235
3336* If we are not already running inside a Python venv, we automatically create a
34- venv and re-run ourselves inside it.
37+ venv and re-run ourselves inside it (also see the -v option) .
3538* Build/wheel/install commands always install into the venv.
3639* Tests use whatever PyMuPDF/MuPDF is currently installed in the venv.
3740* We run tests with pytest.
5558 `setup.py`.]
5659
5760 --build-flavour <build_flavour>
61+ [Obsolete.]
5862 Combination of 'p', 'b', 'd'. See ../setup.py's description of
5963 PYMUPDF_SETUP_FLAVOUR. Default is 'pbd', i.e. self-contained PyMuPDF
6064 wheels including MuPDF build-time files.
7175 --cibw-name <cibw_name>
7276 Name to use when installing cibuildwheel, e.g.:
7377 --cibw-name cibuildwheel==3.0.0b1
78+ --cibw-name git+https://github.com/pypa/cibuildwheel
7479 Default is `cibuildwheel`, i.e. the current release.
7580
7681 --cibw-pyodide 0|1
77- Experimental, make `cibuild ` command build a pyodide wheel.
82+ Experimental, make `cibw ` command build a pyodide wheel.
7883 2025-05-27: this fails when building mupdf C API - `ld -r -b binary
7984 ...` fails with:
8085 emcc: error: binary: No such file or directory ("binary" was expected to be an input file, based on the commandline arguments provided)
9095 --cibw-release-2
9196 Set up so that `cibw` builds only linux-aarch64 wheel.
9297
98+ --cibw-test-project 0|1
99+ If 1, command `cibw` will use a minimal test project instead of the
100+ PyMuPDF directory itself.
101+
102+ The test project uses setjmp/longjmp and C++ throw/catch.
103+
104+ The test checks for current behaviour, so with `--cibw-pyodide 1` it
105+ succeeds if the cibw command fails with the expected error message.
106+
107+ 2025-08-22:
108+ Builds ok on Linux.
109+
110+ Fails at runtime with --cibw-pyodide 1:
111+
112+ With compile/link flags ``:
113+ (+45.0s): remote.py:233:main: jules-devuan: Pyodide has suffered a fatal error. Please report this to the Pyodide maintainers.
114+ (+45.1s): remote.py:233:main: jules-devuan: Stack (most recent call first):
115+ (+45.1s): remote.py:233:main: jules-devuan: File "/tmp/cibw-run-h_pfo0wf/cp312-pyodide_wasm32/venv-test/lib/python3.12/site-packages/foo/__init__.py", line 63 in bar
116+ (+45.1s): remote.py:233:main: jules-devuan: File "<string>The cause of the fatal error was:
117+ (+45.1s): remote.py:233:main: jules-devuan: CppException std::runtime_error: deliberate exception
118+ (+45.1s): remote.py:233:main: jules-devuan: at convertCppException (/home/jules/.cache/cibuildwheel/pyodide-build-0.30.7/0.27.7/xbuildenv/pyodide-root/dist/pyodide.asm.js:10:48959)
119+ (+45.1s): remote.py:233:main: jules-devuan: at API.fatal_error (/home/jules/.cache/cibuildwheel/pyodide-build-0.30.7/0.27.7/xbuildenv/pyodide-root/dist/pyodide.asm.js:10:49253)
120+ (+45.1s): remote.py:233:main: jules-devuan: at main (file:///home/jules/.cache/cibuildwheel/pyodide-build-0.30.7/0.27.7/xbuildenv/pyodide-root/dist/python_cli_entry.mjs:149:13) {
121+ (+45.1s): remote.py:233:main: jules-devuan: ty: 'std::runtime_error',
122+ (+45.1s): remote.py:233:main: jules-devuan: pyodide_fatal_error: true
123+ (+45.1s): remote.py:233:main: jules-devuan: }
124+ (+45.1s): remote.py:233:main: jules-devuan: ", line 1 in <module>
125+ (+45.1s): remote.py:233:main: jules-devuan: CppException std::runtime_error: deliberate exception
126+ (+45.1s): remote.py:233:main: jules-devuan: at convertCppException (/home/jules/.cache/cibuildwheel/pyodide-build-0.30.7/0.27.7/xbuildenv/pyodide-root/dist/pyodide.asm.js:10:48959)
127+ (+45.1s): remote.py:233:main: jules-devuan: at API.fatal_error (/home/jules/.cache/cibuildwheel/pyodide-build-0.30.7/0.27.7/xbuildenv/pyodide-root/dist/pyodide.asm.js:10:49253)
128+ (+45.1s): remote.py:233:main: jules-devuan: at main (file:///home/jules/.cache/cibuildwheel/pyodide-build-0.30.7/0.27.7/xbuildenv/pyodide-root/dist/python_cli_entry.mjs:149:13) {
129+ (+45.1s): remote.py:233:main: jules-devuan: ty: 'std::runtime_error',
130+ (+45.1s): remote.py:233:main: jules-devuan: pyodide_fatal_error: true
131+ (+45.1s): remote.py:233:main: jules-devuan: }
132+
133+ With compile/link flags `-fwasm-exceptions`:
134+ [LinkError: WebAssembly.instantiate(): Import #60 module="env" function="__c_longjmp": tag import requires a WebAssembly.Tag]
135+
136+ With compile/link flags `-fwasm-exceptions -sSUPPORT_LONGJMP=wasm`:
137+ [LinkError: WebAssembly.instantiate(): Import #60 module="env" function="__c_longjmp": tag import requires a WebAssembly.Tag]
138+
93139 -d
94140 Equivalent to `-b debug`.
95141
@@ -341,6 +387,7 @@ def main(argv):
341387 cibw_name = None
342388 cibw_pyodide = None
343389 cibw_pyodide_version = None
390+ cibw_test_project = None
344391 commands = list ()
345392 env_extra = dict ()
346393 graal = False
@@ -424,7 +471,10 @@ def main(argv):
424471 cibw_name = next (args )
425472
426473 elif arg == '--cibw-pyodide' :
427- cibw_pyodide = next (args )
474+ cibw_pyodide = int (next (args ))
475+
476+ elif arg == '--cibw-test-project' :
477+ cibw_test_project = int (next (args ))
428478
429479 elif arg == '-d' :
430480 env_extra ['PYMUPDF_SETUP_MUPDF_BUILD_TYPE' ] = 'debug'
@@ -622,19 +672,13 @@ def main(argv):
622672
623673 elif command == 'cibw' :
624674 # 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'
632675 cibuildwheel (
633676 env_extra ,
634677 cibw_name or 'cibuildwheel' ,
635678 cibw_pyodide ,
636679 cibw_pyodide_version ,
637680 cibw_sdist ,
681+ cibw_test_project ,
638682 )
639683
640684 elif command == 'install' :
@@ -775,7 +819,14 @@ def build(
775819 run (f'pip install{ build_isolation_text } -v --force-reinstall { pymupdf_dir_abs } ' , env_extra = env_extra )
776820
777821
778- def cibuildwheel (env_extra , cibw_name , cibw_pyodide , cibw_pyodide_version , cibw_sdist ):
822+ def cibuildwheel (
823+ env_extra ,
824+ cibw_name ,
825+ cibw_pyodide ,
826+ cibw_pyodide_version ,
827+ cibw_sdist ,
828+ cibw_test_project ,
829+ ):
779830
780831 if cibw_sdist and platform .system () == 'Linux' :
781832 log (f'Building sdist.' )
@@ -823,14 +874,17 @@ def cibuildwheel(env_extra, cibw_name, cibw_pyodide, cibw_pyodide_version, cibw_
823874 CIBW_BUILD = env_extra .get ('CIBW_BUILD' )
824875 log (f'{ CIBW_BUILD = } ' )
825876 if CIBW_BUILD is None :
826- if os .environ .get ('GITHUB_ACTIONS' ) == 'true' :
877+ if cibw_pyodide :
878+ CIBW_BUILD = 'cp312*'
879+ elif os .environ .get ('GITHUB_ACTIONS' ) == 'true' :
827880 # Build/test all supported Python versions.
828881 CIBW_BUILD = 'cp39* cp310* cp311* cp312* cp313*'
829882 else :
830883 # Build/test current Python only.
831884 v = platform .python_version_tuple ()[:2 ]
832885 log (f'{ v = } ' )
833886 CIBW_BUILD = f'cp{ "" .join (v )} *'
887+ log (f'Defaulting to { CIBW_BUILD = } .' )
834888
835889 cibw_pyodide_args = ''
836890 if cibw_pyodide :
@@ -847,7 +901,11 @@ def cibuildwheel(env_extra, cibw_name, cibw_pyodide, cibw_pyodide_version, cibw_
847901 # docker. Note that this will miss any settings in the original
848902 # environment.
849903 env_extra ['CIBW_ENVIRONMENT_PASS_LINUX' ] = ' ' .join (sorted (env_extra .keys ()))
850-
904+
905+ if cibw_test_project :
906+ cibw_do_test_project (env_extra , CIBW_BUILD , cibw_pyodide , cibw_pyodide_args )
907+ return
908+
851909 # Build for lowest (assumed first) Python version.
852910 #
853911 CIBW_BUILD_0 = CIBW_BUILD .split ()[0 ]
@@ -862,6 +920,109 @@ def cibuildwheel(env_extra, cibw_name, cibw_pyodide, cibw_pyodide_version, cibw_
862920 env_extra ['CIBW_BUILD' ] = CIBW_BUILD
863921 run (f'cd { pymupdf_dir } && cibuildwheel{ cibw_pyodide_args } ' , env_extra = env_extra )
864922 run (f'ls -ld { pymupdf_dir } /wheelhouse/*' )
923+
924+
925+ def cibw_do_test_project (env_extra , CIBW_BUILD , cibw_pyodide , cibw_pyodide_args ):
926+ testdir = f'{ pymupdf_dir_abs } /cibw_test'
927+ shutil .rmtree (testdir , ignore_errors = 1 )
928+ os .mkdir (testdir )
929+ with open (f'{ testdir } /setup.py' , 'w' ) as f :
930+ f .write (textwrap .dedent (f'''
931+ import shutil
932+ import sys
933+ import pipcl
934+
935+ def build():
936+ so_leaf = pipcl.build_extension(
937+ name = 'foo',
938+ path_i = 'foo.i',
939+ outdir = 'build',
940+ compiler_extra = { '-fwasm-exceptions -sSUPPORT_LONGJMP=wasm' if cibw_pyodide else '' !r} ,
941+ linker_extra = { '-fwasm-exceptions -sSUPPORT_LONGJMP=wasm' if cibw_pyodide else '' !r} ,
942+ )
943+ return [
944+ ('build/foo.py', 'foo/__init__.py'),
945+ (f'build/{{so_leaf}}', f'foo/'),
946+ ]
947+
948+ p = pipcl.Package(
949+ name = 'foo',
950+ version = '1.2.3',
951+ fn_build = build,
952+ )
953+
954+ build_wheel = p.build_wheel
955+ build_sdist = p.build_sdist
956+ # Handle old-style setup.py command-line usage:
957+ if __name__ == '__main__':
958+ p.handle_argv(sys.argv)
959+ ''' ))
960+ with open (f'{ testdir } /foo.i' , 'w' ) as f :
961+ f .write (textwrap .dedent ('''
962+ %{
963+ #include <setjmp.h>
964+ #include <stdio.h>
965+ #include <string.h>
966+ #include <stdexcept>
967+
968+ static sigjmp_buf jmpbuf;
969+ static int bar0(const char* text)
970+ {
971+ printf("bar(): text: %s\\ \\ n", text);
972+ int len = (int) strlen(text);
973+ printf("bar(): len=%i\\ \\ n", len);
974+ //printf("calling longjmp().\\ n");
975+ //longjmp(jmpbuf, 1);
976+ fflush(stdout);
977+ return len;
978+ }
979+ int bar1(const char* text)
980+ {
981+ int ret = 0;
982+ /*if (setjmp(jmpbuf) == 0)
983+ {
984+ ret = bar0(text);
985+ }
986+ else
987+ {
988+ printf("setjmp() returned non-zero.\\ n");
989+ throw std::runtime_error("deliberate exception");
990+ }*/
991+ throw std::runtime_error("deliberate exception");
992+ return ret;
993+ }
994+ int bar(const char* text)
995+ {
996+ int ret = 0;
997+ try
998+ {
999+ ret = bar1(text);
1000+ }
1001+ catch(std::exception& e)
1002+ {
1003+ printf("Received exception: %s\\ n", e.what());
1004+ }
1005+ return ret;
1006+ }
1007+ %}
1008+ int bar(const char* text);
1009+ ''' ))
1010+ shutil .copy2 (f'{ pymupdf_dir_abs } /pipcl.py' , f'{ testdir } /pipcl.py' )
1011+ shutil .copy2 (f'{ pymupdf_dir_abs } /wdev.py' , f'{ testdir } /wdev.py' )
1012+
1013+ env_extra ['CIBW_BUILD' ] = CIBW_BUILD
1014+ env_extra ['CIBW_TEST_COMMAND' ] = f'python -c "import foo; foo.bar(\\ "some text\\ ")"'
1015+
1016+ if cibw_pyodide :
1017+ # Expect failure.
1018+ e , text = run (f'cd { testdir } && cibuildwheel{ cibw_pyodide_args } ' , env_extra = env_extra , capture = 1 , check = 0 )
1019+ log (f'{ e = } ' )
1020+ log (f'text is:\n { textwrap .indent (text , " " )} ' )
1021+ assert e
1022+ assert 'module="env" function="__c_longjmp": tag import requires a WebAssembly.Tag' in text
1023+ else :
1024+ run (f'cd { testdir } && cibuildwheel{ cibw_pyodide_args } ' , env_extra = env_extra )
1025+ run (f'ls -ld { testdir } /wheelhouse/*' )
8651026
8661027
8671028def build_pyodide_wheel (pyodide_build_version = None ):
0 commit comments