changeset 41:71bcc18e306f

MERGE: New upstream PyMuPDF v1.26.5 including MuPDF v1.26.10 BUGS: Needs some additional changes yet. Not yet tested.
author Franz Glasner <fzglas.hg@dom66.de>
date Sat, 11 Oct 2025 15:24:40 +0200
parents 8934ac156ef5 (diff) aa33339d6b8a (current diff)
children 4621bd954a09
files mupdf-source/scripts/jlib.py mupdf-source/scripts/wrap/__main__.py pipcl.py setup.py src/__init__.py
diffstat 13 files changed, 324 insertions(+), 124 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgignore	Sat Oct 11 15:24:40 2025 +0200
@@ -0,0 +1,5 @@
+syntax: regexp
+
+(^|/)_venv.*
+^_.*\.(log|txt)$
+^_tmp/
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/.hgtags	Sat Oct 11 15:24:40 2025 +0200
@@ -0,0 +1,2 @@
+f76e6575dca9fab27786e07528fba717cc4bcd00 v1.26.4+1
+14b91574d44af9549d6db8f43c103eedbca1849a v1.26.4+2
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/Makefile.freebsd	Sat Oct 11 15:24:40 2025 +0200
@@ -0,0 +1,118 @@
+# -*- mode: makefile; coding: utf-8 -*-
+#
+# Needs GNU make (aka gmake) and binutils/ar or LLVM/ar
+# (instead of FreeBSD /usr/bin/ar).
+#
+# Building against an installed mupdf package fails because the libmupdfcpp.so
+# is missing.
+#
+# Prepare:
+#
+# - Create a Python  venv and activate it
+#
+# - Install a LLVM package that has the same version as the libclang
+#   Python package that will be used/installed
+#   (e.g. pkg install llvm18 because of libclang>=18,<19)
+#
+# - In the "bin"-directory of the venv symlink "make" to /usr/local/bin/gmake
+#   and "ar" to /usr/local/llvm18/bin/llvm-ar
+#
+#   * Makefiles have GNU syntax
+#   * ar is called with @-response-files -- which are a GNU feature (also
+#     implemented by LLVM's ar)
+#
+# - At first to not build with tesseract (OCR)
+#
+#     export PYMUPDF_SETUP_MUPDF_TESSERACT=0
+#
+
+.PHONY: all wheel sdist populate-venv check
+
+.SILENT: check
+
+THIS_MAKEFILE_JUSTNAME:=	$(firstword $(MAKEFILE_LIST))
+THIS_MAKEFILE_DIR:=		$(abspath $(dir $(THIS_MAKEFILE_JUSTNAME)))
+
+PYMUPDF_SETUP_MUPDF_BUILD?=	$(THIS_MAKEFILE_DIR)/mupdf-source
+# It would compile with tesseract because its sources are vendored by MuPDF
+PYMUPDF_SETUP_MUPDF_TESSERACT?=	0
+#
+# We use clang instead of libclang: On FreeBSD you have to install
+# llvm to get a libclang.so installed; there is no libclang package
+# with a bundled libclang.so. Additionally, libclang currently is not
+# up-to-date with respect to clang versions >= 19.
+#
+PYMUPDF_SETUP_LIBCLANG?=	clang>=18,<19
+LIBCLANG_LIBRARY_PATH?=		$(CLANG_DIR)/lib
+CLANG_DIR?=			/usr/local/llvm18
+
+TEST=				/bin/test
+LOCALBASE?=			/usr/local
+SYMLINK?=			/bin/ln -s
+PYTHON?=			python3
+PYTHON_PREFIXES!=		$(PYTHON) -c 'import sys; print(sys.prefix); print(sys.base_prefix)'
+
+#
+# Setting these does not work for some parts built by sub-makes.
+# Symlink in the venv instead.
+#
+#CC=	$(CLANG_DIR)/bin/clang
+#CXX=	$(CLANG_DIR)/bin/clang++
+
+# Define _FORTIFY_SOURCE=$(FORTIFY) (if != 0, default 0)
+FORTIFY?=	3
+#
+# If != 0 (default 1):
+#    -fno-delete-null-pointer-checks
+#
+#       Should always be done when fortifying:
+#       https://github.com/ossf/wg-best-practices-os-developers/issues/659
+#
+#    -Werror=implicit-function-declaration
+#    -fstack-clash-protection
+#    -fstack-protector-strong
+#
+EXTRA_CHECKS?=	1
+
+
+all: sdist wheel
+
+
+wheel: check
+	$(TEST) -e $(firstword $(PYTHON_PREFIXES))/bin/make || $(SYMLINK) $(LOCALBASE)/bin/gmake $(firstword $(PYTHON_PREFIXES))/bin/make
+	$(TEST) -e $(firstword $(PYTHON_PREFIXES))/bin/ar || $(SYMLINK) $(CLANG_DIR)/bin/llvm-ar $(firstword $(PYTHON_PREFIXES))/bin/ar
+	$(TEST) -e $(firstword $(PYTHON_PREFIXES))/bin/cc || $(SYMLINK) $(CLANG_DIR)/bin/clang $(firstword $(PYTHON_PREFIXES))/bin/cc
+	$(TEST) -e $(firstword $(PYTHON_PREFIXES))/bin/c++ || $(SYMLINK) $(CLANG_DIR)/bin/clang++ $(firstword $(PYTHON_PREFIXES))/bin/c++
+	$(TEST) -e $(firstword $(PYTHON_PREFIXES))/bin/ld || $(SYMLINK) $(CLANG_DIR)/bin/ld.lld $(firstword $(PYTHON_PREFIXES))/bin/ld
+	FORTIFY=$(FORTIFY) EXTRA_CHECKS=$(EXTRA_CHECKS) PIPCL_VERBOSE=2 LIBCLANG_LIBRARY_PATH=$(LIBCLANG_LIBRARY_PATH) PYMUPDF_SETUP_MUPDF_BUILD=$(PYMUPDF_SETUP_MUPDF_BUILD) PYMUPDF_SETUP_MUPDF_TESSERACT=$(PYMUPDF_SETUP_MUPDF_TESSERACT) PYMUPDF_SETUP_LIBCLANG='$(PYMUPDF_SETUP_LIBCLANG)' $(PYTHON) -m build --wheel --verbose --no-isolation
+
+
+sdist: check
+	PIPCL_VERBOSE=2 LIBCLANG_LIBRARY_PATH=$(LIBCLANG_LIBRARY_PATH) PYMUPDF_SETUP_MUPDF_BUILD=$(PYMUPDF_SETUP_MUPDF_BUILD) PYMUPDF_SETUP_MUPDF_TESSERACT=$(PYMUPDF_SETUP_MUPDF_TESSERACT) PYMUPDF_SETUP_LIBCLANG='$(PYMUPDF_SETUP_LIBCLANG)' $(PYTHON) -m build --sdist --verbose --no-isolation
+
+
+populate-venv:
+ifneq ($(firstword $(PYTHON_PREFIXES)),$(lastword $(PYTHON_PREFIXES)))
+	$(PYTHON) -m pip install -U -r requirements-build.txt
+else
+	$(error Not in a Python virtual environment)
+endif
+
+
+check:
+ifneq ($(firstword $(PYTHON_PREFIXES)),$(lastword $(PYTHON_PREFIXES)))
+	$(PYTHON) -m pip freeze | grep -E '^\s*setuptools-scm==' >/dev/null
+	$(PYTHON) -m pip freeze | grep -E '^\s*build==' >/dev/null
+	$(PYTHON) -m pip freeze | grep -E '^\s*swig==' >/dev/null
+	$(PYTHON) -m pip freeze | grep -E '^\s*clang==18\.' >/dev/null
+	# This covers the "clang" and "libclang" package
+	$(PYTHON) -c 'import clang.cindex' >/dev/null
+	$(TEST) -x $(LOCALBASE)/bin/gmake
+	$(TEST) -x $(LOCALBASE)/bin/cmake
+	$(TEST) -x $(CLANG_DIR)/bin/llvm-ar
+	$(TEST) -x $(CLANG_DIR)/bin/clang
+	$(TEST) -x $(CLANG_DIR)/bin/clang++
+	$(TEST) -x $(CLANG_DIR)/bin/ld.lld
+else
+	$(error Not in a Python virtual environment)
+endif
--- a/PKG-INFO	Sat Oct 11 11:31:38 2025 +0200
+++ /dev/null	Thu Jan 01 00:00:00 1970 +0000
@@ -1,83 +0,0 @@
-Metadata-Version: 2.1
-Name: PyMuPDF
-Version: 1.26.5
-Summary: A high performance Python library for data extraction, analysis, conversion & manipulation of PDF (and other) documents.
-Description-Content-Type: text/markdown
-Author: Artifex
-Author-email: support@artifex.com
-License: Dual Licensed - GNU AFFERO GPL 3.0 or Artifex Commercial License
-Classifier: Development Status :: 5 - Production/Stable
-Classifier: Intended Audience :: Developers
-Classifier: Intended Audience :: Information Technology
-Classifier: Operating System :: MacOS
-Classifier: Operating System :: Microsoft :: Windows
-Classifier: Operating System :: POSIX :: Linux
-Classifier: Programming Language :: C
-Classifier: Programming Language :: C++
-Classifier: Programming Language :: Python :: 3 :: Only
-Classifier: Programming Language :: Python :: Implementation :: CPython
-Classifier: Topic :: Utilities
-Classifier: Topic :: Multimedia :: Graphics
-Classifier: Topic :: Software Development :: Libraries
-Requires-Python: >=3.9
-Project-URL: Documentation, https://pymupdf.readthedocs.io/
-Project-URL: Source, https://github.com/pymupdf/pymupdf
-Project-URL: Tracker, https://github.com/pymupdf/PyMuPDF/issues
-Project-URL: Changelog, https://pymupdf.readthedocs.io/en/latest/changes.html
-
-# PyMuPDF
-
-**PyMuPDF** is a high performance **Python** library for data extraction, analysis, conversion & manipulation of [PDF (and other) documents](https://pymupdf.readthedocs.io/en/latest/the-basics.html#supported-file-types).
-
-# Community
-Join us on **Discord** here: [#pymupdf](https://discord.gg/TSpYGBW4eq)
-
-
-# Installation
-
-**PyMuPDF** requires **Python 3.9 or later**, install using **pip** with:
-
-`pip install PyMuPDF`
-
-There are **no mandatory** external dependencies. However, some [optional features](#pymupdf-optional-features) become available only if additional packages are installed.
-
-You can also try without installing by visiting [PyMuPDF.io](https://pymupdf.io/#examples).
-
-
-# Usage
-
-Basic usage is as follows:
-
-```python
-import pymupdf # imports the pymupdf library
-doc = pymupdf.open("example.pdf") # open a document
-for page in doc: # iterate the document pages
-  text = page.get_text() # get plain text encoded as UTF-8
-
-```
-
-
-# Documentation
-
-Full documentation can be found on [pymupdf.readthedocs.io](https://pymupdf.readthedocs.io).
-
-
-
-# <a id="pymupdf-optional-features"></a>Optional Features
-
-* [fontTools](https://pypi.org/project/fonttools/) for creating font subsets.
-* [pymupdf-fonts](https://pypi.org/project/pymupdf-fonts/) contains some nice fonts for your text output.
-* [Tesseract-OCR](https://github.com/tesseract-ocr/tesseract) for optical character recognition in images and document pages.
-
-
-
-# About
-
-**PyMuPDF** adds **Python** bindings and abstractions to [MuPDF](https://mupdf.com/), a lightweight **PDF**, **XPS**, and **eBook** viewer, renderer, and toolkit. Both **PyMuPDF** and **MuPDF** are maintained and developed by [Artifex Software, Inc](https://artifex.com).
-
-**PyMuPDF** was originally written by [Jorj X. McKie](mailto:jorj.x.mckie@outlook.de).
-
-
-# License and Copyright
-
-**PyMuPDF** is available under [open-source AGPL](https://www.gnu.org/licenses/agpl-3.0.html) and commercial license agreements. If you determine you cannot meet the requirements of the **AGPL**, please contact [Artifex](https://artifex.com/contact/pymupdf-inquiry.php) for more information regarding a commercial license.
--- a/mupdf-source/Makerules	Sat Oct 11 11:31:38 2025 +0200
+++ b/mupdf-source/Makerules	Sat Oct 11 15:24:40 2025 +0200
@@ -105,6 +105,15 @@
   CFLAGS += -ffunction-sections -fdata-sections
 endif
 
+ifneq ($(EXTRA_CHECKS),)
+  ifneq ($(EXTRA_CHECKS),0)
+    CFLAGS += -fno-delete-null-pointer-checks
+    CFLAGS += -Werror=implicit-function-declaration
+    CFLAGS += -fstack-clash-protection
+    CFLAGS += -fstack-protector-strong
+  endif
+endif
+
 ifeq ($(OS),Darwin)
   LDREMOVEUNREACH := -Wl,-dead_strip
   SO := dylib
@@ -139,6 +148,8 @@
     LDFLAGS += -sTOTAL_MEMORY=48MB
   else ifeq ($(OS),Linux)
     LIB_LDFLAGS = -shared -Wl,-soname,$(notdir $@)
+  else ifeq ($(OS),FreeBSD)
+    LIB_LDFLAGS = -shared -Wl,-soname,$(notdir $@)
   else
     LIB_LDFLAGS = -shared
   endif
@@ -153,12 +164,21 @@
 else ifeq ($(build),release)
   CFLAGS += -pipe -O2 -DNDEBUG
   LDFLAGS += $(LDREMOVEUNREACH) -Wl,-s
+  ifneq ($(OS),Darwin)
+    LDFLAGS += -Wl,-z,relro,-z,now
+  endif  
 else ifeq ($(build),small)
   CFLAGS += -pipe -Os -DNDEBUG
   LDFLAGS += $(LDREMOVEUNREACH) -Wl,-s
+  ifneq ($(OS),Darwin)
+    LDFLAGS += -Wl,-z,relro,-z,now
+  endif  
 else ifeq ($(build),valgrind)
   CFLAGS += -pipe -O2 -DNDEBUG -DPACIFY_VALGRIND
   LDFLAGS += $(LDREMOVEUNREACH) -Wl,-s
+  ifneq ($(OS),Darwin)
+    LDFLAGS += -Wl,-z,relro,-z,now
+  endif  
 else ifeq ($(build),sanitize)
   CFLAGS += -pipe -g $(SANITIZE_FLAGS)
   LDFLAGS += -g $(SANITIZE_FLAGS)
@@ -174,6 +194,9 @@
 else ifeq ($(build),native)
   CFLAGS += -pipe -O2 -DNDEBUG -march=native
   LDFLAGS += $(LDREMOVEUNREACH) -Wl,-s
+  ifneq ($(OS),Darwin)
+    LDFLAGS += -Wl,-z,relro,-z,now
+  endif  
 else ifeq ($(build),memento)
   CFLAGS += -pipe -g -DMEMENTO -DMEMENTO_MUPDF_HACKS
   LDFLAGS += -g -rdynamic
--- a/mupdf-source/scripts/jlib.py	Sat Oct 11 11:31:38 2025 +0200
+++ b/mupdf-source/scripts/jlib.py	Sat Oct 11 15:24:40 2025 +0200
@@ -2306,5 +2306,11 @@
             ret += " -Wl,-rpath,'$ORIGIN'"
         else:
             ret += " -Wl,-rpath,'$ORIGIN',-z,origin"
+    if not darwin and (platform.system() != 'Windows'):
+        # *BSD and Linux
+        #   Full RELRO
+        ret += ' -Wl,-z,relro,-z,now'
+        #   Strip
+        ret += ' -Wl,-s'
     #log('{sos=} {ld_origin=} {ret=}')
     return ret.strip()
--- a/mupdf-source/scripts/pipcl.py	Sat Oct 11 11:31:38 2025 +0200
+++ b/mupdf-source/scripts/pipcl.py	Sat Oct 11 15:24:40 2025 +0200
@@ -1772,9 +1772,14 @@
 
     This function can be useful for the `fn_sdist()` callback.
     '''
-    command = 'cd ' + directory + ' && git ls-files'
-    if submodules:
-        command += ' --recurse-submodules'
+    if os.path.isdir(os.path.join(directory, ".hg")):
+        command = 'cd ' + directory + ' && hg files'
+        if submodules:
+            command += ' --subrepos'
+    else:
+        command = 'cd ' + directory + ' && git ls-files'
+        if submodules:
+            command += ' --recurse-submodules'
     log1(f'Running {command=}')
     text = subprocess.check_output( command, shell=True)
     ret = []
@@ -1860,6 +1865,10 @@
 def openbsd():
     return platform.system() == 'OpenBSD'
 
+def freebsd():
+    return platform.system() == 'FreeBSD'
+
+
 class PythonFlags:
     '''
     Compile/link flags for the current python, for example the include path
@@ -2309,7 +2318,7 @@
     return `path`. Useful if Linux shared libraries have been created with
     `-Wl,-soname,...`, where we need to embed the versioned library.
     '''
-    if linux() and os.path.islink(path):
+    if (linux() or freebsd()) and os.path.islink(path):
         path2 = os.path.realpath(path)
         if subprocess.run(f'objdump -p {path2}|grep SONAME', shell=1, check=0).returncode == 0:
             return path2
--- a/mupdf-source/scripts/wrap/__main__.py	Sat Oct 11 11:31:38 2025 +0200
+++ b/mupdf-source/scripts/wrap/__main__.py	Sat Oct 11 15:24:40 2025 +0200
@@ -1537,6 +1537,11 @@
 
     dir_so_flags = os.path.basename( build_dirs.dir_so).split( '-')
     cflags = os.environ.get('XCXXFLAGS', '')
+    if os.environ.get('EXTRA_CHECKS', '1') != '0':
+        cflags += ' -fno-delete-null-pointer-checks'
+        cflags += ' -Werror=implicit-function-declaration'
+        cflags += ' -fstack-clash-protection'
+        cflags += ' -fstack-protector-strong'
 
     windows_build_type = build_dirs.windows_build_type()
     so_version = get_so_version( build_dirs)
@@ -1670,7 +1675,7 @@
 
                     elif 'shared' in dir_so_flags:
                         link_soname_arg = ''
-                        if state.state_.linux and so_version:
+                        if (state.state_.linux or state.state_.freebsd) and so_version:
                             link_soname_arg = f'-Wl,-soname,{os.path.basename(libmupdfcpp)}'
                         command = ( textwrap.dedent(
                                 f'''
@@ -1694,7 +1699,7 @@
                                 )
                         if command_was_run:
                             macos_patch( libmupdfcpp, f'{build_dirs.dir_so}/libmupdf.dylib{so_version}')
-                        if so_version and state.state_.linux:
+                        if so_version and (state.state_.linux or state.state_.freebsd):
                             jlib.system(f'ln -sf libmupdfcpp.so{so_version} {build_dirs.dir_so}/libmupdfcpp.so')
 
                     elif 'fpic' in dir_so_flags:
--- a/mupdf-source/scripts/wrap/state.py	Sat Oct 11 11:31:38 2025 +0200
+++ b/mupdf-source/scripts/wrap/state.py	Sat Oct 11 15:24:40 2025 +0200
@@ -21,6 +21,12 @@
             f'or (OpenBSD only) `pkg_add py3-llvm.`\n'
             )
     clang = None
+else:
+    if os.environ.get('LIBCLANG_LIBRARY_FILE', None):
+        clang.cindex.Config.set_library_file(os.environ['LIBCLANG_LIBRARY_FILE'])
+    elif os.environ.get('LIBCLANG_LIBRARY_PATH', None):
+        clang.cindex.Config.set_library_path(os.environ['LIBCLANG_LIBRARY_PATH'])
+
 
 omit_fns = [
         'fz_open_file_w',
@@ -69,6 +75,7 @@
         self.windows = (self.os_name == 'Windows' or self.os_name.startswith('CYGWIN'))
         self.cygwin = self.os_name.startswith('CYGWIN')
         self.openbsd = self.os_name == 'OpenBSD'
+        self.freebsd = self.os_name == 'FreeBSD'
         self.linux = self.os_name == 'Linux'
         self.macos = self.os_name == 'Darwin'
         self.pyodide = os.environ.get('OS') == 'pyodide'
--- a/pipcl.py	Sat Oct 11 11:31:38 2025 +0200
+++ b/pipcl.py	Sat Oct 11 15:24:40 2025 +0200
@@ -743,7 +743,7 @@
             # Add the files returned by fn_build().
             #
             for item in items:
-                from_, (to_abs, to_rel) = self._fromto(item)
+                from_, (to_abs, to_rel, _) = self._fromto(item)
                 add(from_, to_rel)
 
             # Add <name>-<version>.dist-info/WHEEL.
@@ -841,10 +841,24 @@
                 textb = text.encode('utf8')
                 return add(textb, name)
 
+            def add_symlink(from_, name, linkname=None):
+                check_name(name)
+                assert isinstance(from_, str)
+                assert isinstance(name, str)
+                assert name
+                assert os.path.islink(from_)
+                if linkname is None:
+                    linkname = os.readlink(from_)
+                log2(f'Adding symlink: {os.path.relpath(from_)} => {name} --> {linkname}')
+                ti = tar.gettarinfo(from_, name)
+                tar.addfile(ti)
+
             found_pyproject_toml = False
             for item in items:
-                from_, (to_abs, to_rel) = self._fromto(item)
-                if isinstance(from_, bytes):
+                from_, (to_abs, to_rel, to_symlink) = self._fromto(item, resolve_symlinks=False)
+                if to_symlink:
+                    add_symlink(from_, to_rel, to_symlink)
+                elif isinstance(from_, bytes):
                     add(from_, to_rel)
                 else:
                     if from_.startswith(f'{os.path.abspath(sdist_directory)}/'):
@@ -1105,7 +1119,7 @@
             record.add_content(content, to_rel)
 
         for item in items:
-            from_, (to_abs, to_rel) = self._fromto(item)
+            from_, (to_abs, to_rel, _) = self._fromto(item)
             log0(f'{from_=} {to_abs=} {to_rel=}')
             to_abs2 = f'{root2}/{to_rel}'
             add_file( from_, to_abs2, to_rel)
@@ -1500,7 +1514,7 @@
             ret += '\n'
         return ret
 
-    def _path_relative_to_root(self, path, assert_within_root=True):
+    def _path_relative_to_root(self, path, assert_within_root=True, resolve_symlinks=True):
         '''
         Returns `(path_abs, path_rel)`, where `path_abs` is absolute path and
         `path_rel` is relative to `self.root`.
@@ -1516,14 +1530,21 @@
             p = path
         else:
             p = os.path.join(self.root, path)
+        path_abs = p
+        # always resolve symlinks at first
         p = os.path.realpath(os.path.abspath(p))
         if assert_within_root:
             assert p.startswith(self.root+os.sep) or p == self.root, \
                     f'Path not within root={self.root+os.sep!r}: {path=} {p=}'
         p_rel = os.path.relpath(p, self.root)
-        return p, p_rel
-
-    def _fromto(self, p):
+        if resolve_symlinks or not os.path.islink(path_abs):
+            return p, p_rel, None
+        else:
+            assert os.path.islink(path_abs)
+            p_rel = os.path.relpath(path_abs, self.root)
+            return path_abs, p_rel, os.readlink(path_abs)
+
+    def _fromto(self, p, resolve_symlinks=True):
         '''
         Returns `(from_, (to_abs, to_rel))`.
 
@@ -1567,8 +1588,8 @@
         if to_.startswith( prefix):
             to_ = f'{_normalise2(self.name)}-{self.version}.data/{to_[ len(prefix):]}'
         if isinstance(from_, str):
-            from_, _ = self._path_relative_to_root( from_, assert_within_root=False)
-        to_ = self._path_relative_to_root(to_)
+            from_, _, _ = self._path_relative_to_root( from_, assert_within_root=False, resolve_symlinks=resolve_symlinks)
+        to_ = self._path_relative_to_root(to_, resolve_symlinks=resolve_symlinks)
         assert isinstance(from_, (str, bytes))
         log2(f'returning {from_=} {to_=}')
         return from_, to_
@@ -1890,6 +1911,11 @@
                     {' '.join(path_os)}
                     {linker_extra}
                 '''
+        if os.environ.get('EXTRA_CHECKS', '1') != '0':
+            general_flags += ' -fno-delete-null-pointer-checks'
+            general_flags += ' -Werror=implicit-function-declaration'
+            general_flags += ' -fstack-clash-protection'
+            general_flags += ' -fstack-protector-strong'
     elif pyodide():
         command2 = f'''
                 {linker_command}
@@ -2096,9 +2122,14 @@
 
     This function can be useful for the `fn_sdist()` callback.
     '''
-    command = 'cd ' + directory + ' && git ls-files'
-    if submodules:
-        command += ' --recurse-submodules'
+    if os.path.isdir(os.path.join(directory, ".hg")):
+        command = 'cd ' + directory + ' && hg files'
+        if submodules:
+            command += ' --subrepos'
+    else:
+        command = 'cd ' + directory + ' && git ls-files'
+        if submodules:
+            command += ' --recurse-submodules'
     log1(f'Running {command=}')
     text = subprocess.check_output( command, shell=True)
     ret = []
@@ -2416,6 +2447,9 @@
 def openbsd():
     return platform.system() == 'OpenBSD'
 
+def freebsd():
+    return platform.system() == 'FreeBSD'
+
 
 def show_system():
     '''
@@ -2899,7 +2933,7 @@
 
 def _assert_version_pep_440(version):
     assert re.match(
-                r'^([1-9][0-9]*!)?(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))*((a|b|rc)(0|[1-9][0-9]*))?(\.post(0|[1-9][0-9]*))?(\.dev(0|[1-9][0-9]*))?$',
+                r'^([1-9][0-9]*!)?(0|[1-9][0-9]*)(\.(0|[1-9][0-9]*))*((a|b|rc)(0|[1-9][0-9]*))?(\.post(0|[1-9][0-9]*))?(\.dev(0|[1-9][0-9]*))?(?:\+([a-z0-9]+(?:[-_\.][a-z0-9]+)*))?$',
                 version,
             ), \
             f'Bad version: {version!r}.'
@@ -3013,7 +3047,7 @@
     return `path`. Useful if Linux shared libraries have been created with
     `-Wl,-soname,...`, where we need to embed the versioned library.
     '''
-    if linux() and os.path.islink(path):
+    if (linux() or freebsd()) and os.path.islink(path):
         path2 = os.path.realpath(path)
         if subprocess.run(f'objdump -p {path2}|grep SONAME', shell=1, check=0).returncode == 0:
             return path2
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/requirements-build.txt	Sat Oct 11 15:24:40 2025 +0200
@@ -0,0 +1,12 @@
+# Build in a non-isolated venv
+#
+# cmake is required as a system package: could not build it as Python package
+# on the fly.
+#
+# gmake is to be used!
+#
+setuptools
+setuptools-scm
+build
+swig
+clang>=18,<19
--- a/setup.py	Sat Oct 11 11:31:38 2025 +0200
+++ b/setup.py	Sat Oct 11 15:24:40 2025 +0200
@@ -367,27 +367,63 @@
     Returns `(sha, comment, diff, branch)`, all items are str or None if not
     available.
 
+    Also handles Mercurial repository information.
+
     directory:
         Root of git checkout.
     '''
     sha, comment, diff, branch = '', '', '', ''
-    cp = subprocess.run(
-            f'cd {directory} && (PAGER= git show --pretty=oneline|head -n 1 && git diff)',
-            capture_output=1,
-            shell=1,
-            text=1,
-            )
-    if cp.returncode == 0:
-        sha, _ = cp.stdout.split(' ', 1)
-        comment, diff = _.split('\n', 1)
-    cp = subprocess.run(
-            f'cd {directory} && git rev-parse --abbrev-ref HEAD',
-            capture_output=1,
-            shell=1,
-            text=1,
-            )
-    if cp.returncode == 0:
-        branch = cp.stdout.strip()
+    if os.path.isdir(os.path.join(directory, '.hg')):
+        cp = subprocess.run(
+                f'hg -R {directory} id --id',
+                capture_output=1,
+                shell=1,
+                text=1,
+                )
+        if cp.returncode == 0:
+            sha = cp.stdout.strip()
+        cp = subprocess.run(
+                f'hg -R {directory} diff --git',
+                capture_output=1,
+                shell=1,
+                text=1,
+                )
+        if cp.returncode == 0:
+            diff = cp.stdout
+        cp = subprocess.run(
+                f'hg -R {directory} log --rev . --template "{{branch}}"',
+                capture_output=1,
+                shell=1,
+                text=1,
+                )
+        if cp.returncode == 0:
+            branch = cp.stdout.strip()
+        fp = subprocess.run(
+                f'hg -R {directory} log --rev . --template "{{desc|firstline}}"',
+                capture_output=1,
+                shell=1,
+                text=1,
+                )
+        if cp.returncode == 0:
+            comment = cp.stdout.strip()
+    else:
+        cp = subprocess.run(
+                f'cd {directory} && (PAGER= git show --pretty=oneline|head -n 1 && git diff)',
+                capture_output=1,
+                shell=1,
+                text=1,
+                )
+        if cp.returncode == 0:
+            sha, _ = cp.stdout.split(' ', 1)
+            comment, diff = _.split('\n', 1)
+        cp = subprocess.run(
+                f'cd {directory} && git rev-parse --abbrev-ref HEAD',
+                capture_output=1,
+                shell=1,
+                text=1,
+                )
+        if cp.returncode == 0:
+            branch = cp.stdout.strip()
     log(f'git_info(): directory={directory!r} returning branch={branch!r} sha={sha!r} comment={comment!r}')
     return sha, comment, diff, branch
 
@@ -740,7 +776,11 @@
     version_p_tuple = tuple(int_or_0(i) for i in version_p.split('.'))
     log(f'{swig_version=}')
     text = ''
-    text += f'mupdf_location = {mupdf_location!r}\n'
+    if os.path.isdir(mupdf_location):
+        mupdf_location = mupdf_location.rstrip('/')
+        text += 'mupdf_location = ' + repr(os.path.basename(mupdf_location)) + '\n'
+    else:
+        text += f'mupdf_location = {mupdf_location!r}\n'
     text += f'pymupdf_version = {version_p!r}\n'
     text += f'pymupdf_version_tuple = {version_p_tuple!r}\n'
     text += f'pymupdf_git_sha = {sha!r}\n'
@@ -918,6 +958,10 @@
         log( f'Setting XCFLAGS and XCXXFLAGS to predefine TOFU_CJK_EXT.')
         env_add(env, 'XCFLAGS', '-DTOFU_CJK_EXT')
         env_add(env, 'XCXXFLAGS', '-DTOFU_CJK_EXT')
+        fortify = os.environ.get('FORTIFY', '0')
+        if fortify != '0':
+            env_add(env, 'XCFLAGS', f'-D_FORTIFY_SOURCE={fortify}')
+            env_add(env, 'XCXXFLAGS', f'-D_FORTIFY_SOURCE={fortify}')
 
     if openbsd or freebsd:
         env_add(env, 'CXX', 'c++', ' ')
@@ -1143,6 +1187,9 @@
     debug = 'debug' in mupdf_build_dir_flags
     r_extra = ''
     defines = list()
+    fortify = os.environ.get('FORTIFY', '0')
+    if fortify != '0':
+        defines.append(f'_FORTIFY_SOURCE={fortify}')
     if windows:
         defines.append('FZ_DLL_CLIENT')
         wp = pipcl.wdev.WindowsPython()
@@ -1202,6 +1249,14 @@
         if cxxflags:
             compiler_extra += f' {cxxflags}'
 
+
+    if not darwin and (platform.system() != 'Windows'):
+        # *BSD and Linux
+        #   Full RELRO
+        linker_extra += ' -Wl,-z,relro,-z,now'
+        #   Strip
+        linker_extra += ' -Wl,-s'
+        
     return compiler_extra, linker_extra, includes, defines, optimise, debug, libpaths, libs, libraries, 
 
 
@@ -1254,6 +1309,7 @@
         'Operating System :: MacOS',
         'Operating System :: Microsoft :: Windows',
         'Operating System :: POSIX :: Linux',
+        'Operating System :: POSIX :: BSD :: FreeBSD',
         'Programming Language :: C',
         'Programming Language :: C++',
         'Programming Language :: Python :: 3 :: Only',
@@ -1267,7 +1323,7 @@
 #
 
 # PyMuPDF version.
-version_p = '1.26.5'
+version_p = '1.26.5+XXXFIXME0'
 
 version_mupdf = '1.26.10'
 
@@ -1347,6 +1403,12 @@
         # We can't pip install pytest on pyodide, so specify it here.
         requires_dist.append('pytest')
 
+    #
+    # We need packaging because of extended version parsing (including local
+    # version specifiers.
+    #
+    requires_dist.append('packaging')
+
     p = pipcl.Package(
             name,
             version,
--- a/src/__init__.py	Sat Oct 11 11:31:38 2025 +0200
+++ b/src/__init__.py	Sat Oct 11 15:24:40 2025 +0200
@@ -394,7 +394,7 @@
 
 # Versions as tuples; useful when comparing versions.
 #
-mupdf_version_tuple = tuple( [_int_rc(i) for i in mupdf_version.split('.')])
+mupdf_version_tuple = packaging.version.Version(mupdf_version).release
 
 assert mupdf_version_tuple == (mupdf.FZ_VERSION_MAJOR, mupdf.FZ_VERSION_MINOR, mupdf.FZ_VERSION_PATCH), \
         f'Inconsistent MuPDF version numbers: {mupdf_version_tuple=} != {(mupdf.FZ_VERSION_MAJOR, mupdf.FZ_VERSION_MINOR, mupdf.FZ_VERSION_PATCH)=}'