Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/test_pyodide.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ jobs:

- name: pyodide
run:
python scripts/test.py ${{matrix.args}} pyodide
python scripts/test.py ${{matrix.args}} --cibw-pyodide 1 cibw

# We do not use upload-artifact@v4 because it fails due to us creating
# identically-named wheels.
Expand Down
22 changes: 22 additions & 0 deletions docs/installation.rst
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,28 @@ not generally supported:
command line.


Official PyMuPDF Linux wheels may not install on older Linux systems
--------------------------------------------------------------------

Releases of PyMuPDF are incompatible with older Linux systems.

For example as of 2025-09-03, `pip install pymupdf` does not work on some AWS
Lambda systems - see https://github.com/pymupdf/PyMuPDF/discussions/4631.

This is because official PyMuPDF Linux wheels are built with a version of
glibc determined by the current Python manylinux environment. These wheels are
incompatible with Linux systems that have an older glibc.

The official Python manylinux environment is updated periodically to use newer
glibc versions, so new releases of PyMuPDF become increasingly incompatible
with older Linux systems.

There is nothing that can be done about this, other than updating older Linux
systems, or building PyMuPDF locally from source.

For more details, please see: `Python Packaging Authority <https://www.pypa.io>`_.


Packaging
---------

Expand Down
19 changes: 14 additions & 5 deletions pipcl.py
Original file line number Diff line number Diff line change
Expand Up @@ -415,8 +415,11 @@ def __init__(self,
added.

`to_` identifies what the file should be called within a wheel
or when installing. If `to_` ends with `/`, the leaf of `from_`
is appended to it (and `from_` must not be a `bytes`).
or when installing. If `to_` is empty or `/` we set it to the
leaf of `from_` (`from_` must not be a `bytes`) - i.e. we place
the file in the root directory of the wheel; otherwise if
`to_` ends with `/` the leaf of `from_` is appended to it (and
`from_` must not be a `bytes`).

Initial `$dist-info/` in `_to` is replaced by
`{name}-{version}.dist-info/`; this is useful for license files
Expand Down Expand Up @@ -1446,8 +1449,12 @@ def _fromto(self, p):
`p` is a tuple `(from_, to_)` where `from_` is str/bytes and `to_` is
str. If `from_` is a bytes it is contents of file to add, otherwise the
path of an existing file; non-absolute paths are assumed to be relative
to `self.root`. If `to_` is empty or ends with `/`, we append the leaf
of `from_` (which must be a str).
to `self.root`.

If `to_` is empty or `/` we set it to the leaf of `from_` (which must
be a str) - i.e. we place the file in the root directory of the wheel;
otherwise if `to_` ends with `/` we append the leaf of `from_` (which
must be a str).

If `to_` starts with `$dist-info/`, we replace this with
`self._dist_info_dir()`.
Expand All @@ -1467,7 +1474,9 @@ def _fromto(self, p):
from_, to_ = p
assert isinstance(from_, (str, bytes))
assert isinstance(to_, str)
if to_.endswith('/') or to_=='':
if to_ == '/' or to_ == '':
to_ = os.path.basename(from_)
elif to_.endswith('/'):
to_ += os.path.basename(from_)
prefix = '$dist-info/'
if to_.startswith( prefix):
Expand Down
75 changes: 51 additions & 24 deletions scripts/test.py
Original file line number Diff line number Diff line change
Expand Up @@ -792,7 +792,7 @@ def build(
venv,
wheel,
):
print(f'{build_isolation=}')
log(f'{build_isolation=}')

if build_isolation is None:
# On OpenBSD libclang is not available on pypi.org, so we need to force
Expand Down Expand Up @@ -884,7 +884,8 @@ def cibuildwheel(
log(f'{CIBW_BUILD=}')
if CIBW_BUILD is None:
if cibw_pyodide:
CIBW_BUILD = 'cp312*'
# Using python-3.13 fixes problems with MuPDF's setjmp/longjmp.
CIBW_BUILD = 'cp313*'
elif os.environ.get('GITHUB_ACTIONS') == 'true':
# Build/test all supported Python versions.
CIBW_BUILD = 'cp39* cp310* cp311* cp312* cp313*'
Expand Down Expand Up @@ -939,19 +940,33 @@ def cibw_do_test_project(env_extra, CIBW_BUILD, cibw_pyodide, cibw_pyodide_args)
f.write(textwrap.dedent(f'''
import shutil
import sys
import os
import pipcl

def build():

cc_base, _ = pipcl.base_compiler(cpp=True)
ld_base, _ = pipcl.base_linker(cpp=True)
pipcl.run(f'mkdir -p {testdir}/build')
pipcl.run(f'{{cc_base}} -DNDEBUG -fPIC -c -o {testdir}/build/qwerty.o {testdir}/qwerty.cpp')
pipcl.run(f'{{ld_base}} -o {testdir}/build/libqwerty.so {testdir}/build/qwerty.o')

so_leaf = pipcl.build_extension(
name = 'foo',
path_i = 'foo.i',
outdir = 'build',
compiler_extra = {'-fwasm-exceptions -sSUPPORT_LONGJMP=wasm' if cibw_pyodide else ''!r},
linker_extra = {'-fwasm-exceptions -sSUPPORT_LONGJMP=wasm' if cibw_pyodide else ''!r},
libpaths = '{testdir}/build',
libs = ['qwerty'],
)

return [
('build/foo.py', 'foo/__init__.py'),
(f'build/{{so_leaf}}', f'foo/'),

# 2025-09-03: formally we put libraries in foo.lib; now it seems
# they need to be at top level in wheel.
#
(f'build/libqwerty.so', f'/'),
]

p = pipcl.Package(
Expand All @@ -973,11 +988,17 @@ def build():
#include <stdio.h>
#include <string.h>
#include <stdexcept>

int qwerty(void);

static sigjmp_buf jmpbuf;
static int bar0(const char* text)
{
printf("bar(): text: %s\\\\n", text);
printf("bar(): text: %s\\n", text);

int q = qwerty();
printf("bar(): q=%i\\n", q);

int len = (int) strlen(text);
printf("bar(): len=%i\\\\n", len);
//printf("calling longjmp().\\n");
Expand Down Expand Up @@ -1016,23 +1037,25 @@ def build():
%}
int bar(const char* text);
'''))

with open(f'{testdir}/qwerty.cpp', 'w') as f:
f.write(textwrap.dedent('''
#include <stdio.h>
int qwerty(void)
{
printf("qwerty()\\n");
return 3;
}
'''))

shutil.copy2(f'{pymupdf_dir_abs}/pipcl.py', f'{testdir}/pipcl.py')
shutil.copy2(f'{pymupdf_dir_abs}/wdev.py', f'{testdir}/wdev.py')

env_extra['CIBW_BUILD'] = CIBW_BUILD
env_extra['CIBW_TEST_COMMAND'] = f'python -c "import foo; foo.bar(\\"some text\\")"'
env_extra['CIBW_TEST_COMMAND'] = f'pyodide xbuildenv search --all; python -c "import foo; foo.bar(\\"some text\\")"; true'

if cibw_pyodide:
# Expect failure.
e, text = run(f'cd {testdir} && cibuildwheel{cibw_pyodide_args}', env_extra=env_extra, capture=1, check=0)
log(f'{e=}')
log(f'text is:\n{textwrap.indent(text, " ")}')
assert e
assert 'module="env" function="__c_longjmp": tag import requires a WebAssembly.Tag' in text
log(f'Have detected expected exception from Pyodide.')
else:
run(f'cd {testdir} && cibuildwheel{cibw_pyodide_args}', env_extra=env_extra)
run(f'ls -ld {testdir}/wheelhouse/*')
run(f'cd {testdir} && cibuildwheel{cibw_pyodide_args}', env_extra=env_extra)
run(f'ls -ld {testdir}/wheelhouse/*')


def build_pyodide_wheel(pyodide_build_version=None):
Expand Down Expand Up @@ -1259,14 +1282,18 @@ def getmtime(path):

PYODIDE_ROOT = os.environ.get('PYODIDE_ROOT')
if PYODIDE_ROOT is not None:
# We can't install packages with `pip install`; setup.py will have
# specified pytest in the wheels's <requires_dist>, so it will be
# already installed.
#
log(f'Not installing test packages because {PYODIDE_ROOT=}.')
command = f'{pytest_options} {pytest_arg} -s'
command = f'{pytest_options} {pytest_arg}'
args = shlex.split(command)
print(f'{PYODIDE_ROOT=} so calling pytest.main(args).')
print(f'{command=}')
print(f'args are ({len(args)}):')
log(f'{PYODIDE_ROOT=} so calling pytest.main(args).')
log(f'{command=}')
log(f'args are ({len(args)}):')
for arg in args:
print(f' {arg!r}')
log(f' {arg!r}')
import pytest
pytest.main(args)
return
Expand Down Expand Up @@ -1334,7 +1361,7 @@ def getmtime(path):

# Always start by removing any test_*_fitz.py files.
for p in glob.glob(f'{pymupdf_dir_rel}/tests/test_*_fitz.py'):
print(f'Removing {p=}')
log(f'Removing {p=}')
os.remove(p)
if test_fitz:
# Create copies of each test file, modified to use `pymupdf`
Expand All @@ -1346,7 +1373,7 @@ def getmtime(path):
continue
branch, leaf = os.path.split(p)
p2 = f'{branch}/{leaf[:5]}fitz_{leaf[5:]}'
print(f'Converting {p=} to {p2=}.')
log(f'Converting {p=} to {p2=}.')
with open(p, encoding='utf8') as f:
text = f.read()
text2 = re.sub("([^\'])\\bpymupdf\\b", '\\1fitz', text)
Expand Down
12 changes: 2 additions & 10 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -574,14 +574,10 @@ def get_mupdf(path=None, sha=None):
windows = platform.system() == 'Windows' or platform.system().startswith('CYGWIN')
msys2 = platform.system().startswith('MSYS_NT-')

pyodide_flags = '-fwasm-exceptions'

if os.environ.get('PYODIDE') == '1':
if os.environ.get('OS') != 'pyodide':
log('PYODIDE=1, setting OS=pyodide.')
os.environ['OS'] = 'pyodide'
os.environ['XCFLAGS'] = pyodide_flags
os.environ['XCXXFLAGS'] = pyodide_flags

pyodide = os.environ.get('OS') == 'pyodide'

Expand Down Expand Up @@ -704,8 +700,8 @@ def add(flavour, from_, to_):
add('d', f'{mupdf_build_dir}/libmupdf-threads.a', f'{to_dir_d}/lib/')
elif pyodide:
add('p', f'{mupdf_build_dir}/_mupdf.so', to_dir)
add('b', f'{mupdf_build_dir}/libmupdfcpp.so', 'PyMuPDF.libs/')
add('b', f'{mupdf_build_dir}/libmupdf.so', 'PyMuPDF.libs/')
add('b', f'{mupdf_build_dir}/libmupdfcpp.so', '/')
add('b', f'{mupdf_build_dir}/libmupdf.so', '/')
else:
add('p', f'{mupdf_build_dir}/_mupdf.so', to_dir)
add('b', pipcl.get_soname(f'{mupdf_build_dir}/libmupdfcpp.so'), to_dir)
Expand Down Expand Up @@ -1211,10 +1207,6 @@ def _extension_flags( mupdf_local, mupdf_build_dir, build_type):
if cxxflags:
compiler_extra += f' {cxxflags}'

if pyodide:
compiler_extra += f' {pyodide_flags}'
linker_extra += f' {pyodide_flags}'

return compiler_extra, linker_extra, includes, defines, optimise, debug, libpaths, libs, libraries,


Expand Down
2 changes: 0 additions & 2 deletions src/extra.i
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
%module fitz_extra

%pythoncode %{
# pylint: disable=all
%}
Expand Down
4 changes: 4 additions & 0 deletions tests/test_codespell.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,10 @@ def test_codespell():
'''
Check rebased Python code with codespell.
'''
if os.environ.get('PYODIDE_ROOT'):
print('test_codespell(): not running on Pyodide - cannot run child processes.')
return

if not hasattr(pymupdf, 'mupdf'):
print('Not running codespell with classic implementation.')
return
Expand Down
4 changes: 4 additions & 0 deletions tests/test_flake8.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,10 @@ def test_flake8():
'''
Check rebased Python code with flake8.
'''
if os.environ.get('PYODIDE_ROOT'):
print('test_flake8(): not running on Pyodide - cannot run child processes.')
return

if not hasattr(pymupdf, 'mupdf'):
print(f'Not running flake8 with classic implementation.')
return
Expand Down
8 changes: 8 additions & 0 deletions tests/test_font.py
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,10 @@ def test_2608():
assert text == expected

def test_fontarchive():
if os.environ.get('PYODIDE_ROOT'):
print('test_fontarchive(): not running on Pyodide - we get ValueError: No font code \'notos\' found in pymupdf-fonts..')
return

import subprocess
arch = pymupdf.Archive()
css = pymupdf.css_for_pymupdf_font("notos", archive=arch, name="sans-serif")
Expand Down Expand Up @@ -234,6 +238,10 @@ def test_3887():


def test_4457():
if os.environ.get('PYODIDE_ROOT'):
print('test_4457(): not running on Pyodide - cannot run child processes.')
return

print()
files = (
('https://github.com/user-attachments/files/20862923/test_4457_a.pdf', 'test_4457_a.pdf', None, 4),
Expand Down
Loading