'''
Overview:
Build script for PyMuPDF, supporting PEP-517 and simple command-line usage.
We hard-code the URL of the MuPDF .tar.gz file that we require. This
generally points to a particular source release on mupdf.com.
Default behaviour:
Building an sdist:
We download the MuPDF .tar.gz file and embed within the sdist.
Building PyMuPDF:
If we are not in an sdist we first download the mupdf .tar.gz file.
Then we extract and build MuPDF locally, before building PyMuPDF
itself. So PyMuPDF will always be built with the exact MuPDF
release that we require.
Environmental variables:
If building with system MuPDF (PYMUPDF_SETUP_MUPDF_BUILD is empty string):
CFLAGS
CXXFLAGS
LDFLAGS
Added to c, c++, and link commands.
PYMUPDF_INCLUDES
Colon-separated extra include paths.
PYMUPDF_MUPDF_LIB
Directory containing MuPDF libraries, (libmupdf.so,
libmupdfcpp.so).
PYMUPDF_SETUP_IMPLEMENTATIONS
Must be one of 'a', 'b', 'ab'. If unset we use 'ab'.
If contains 'a' we build original implementation.
If contains 'b' we build rebased implementation.
PYMUPDF_SETUP_DEVENV
Location of devenv.com on Windows. If unset we search for it - see
wdev.py. if that fails we use just 'devenv.com'.
PYMUPDF_SETUP_FLAVOUR
Control building of separate wheels for PyMuPDF.
Must be unset or one of: 'pb', 'p', 'b'.
'pb' or unset:
Build complete wheel `PyMuPDF` with all Python and shared
libraries.
'p':
Wheel `PyMuPDF` excludes all shared libraries that are not
specific to a particular Python version - e.g. on Linux this
will exclude `libmupdf.so` and `libmupdfcpp.so`.
This wheel will require the corresponding PyMuPDFb wheel
- `PyMuPDF-<version>.dist-info/METADATA` will contain
`Requires-Dist: PyMuPDFb ==<version>`.
'b':
Build wheel called `PyMuPDFb` containing only shared libraries
that are not specific to a particular Python version - e.g.
on Linux this will be `libmupdf.so` and `libmupdfcpp.so`.
PYMUPDF_SETUP_MUPDF_BUILD
If set, overrides location of MuPDF when building PyMuPDF:
Empty string:
Build PyMuPDF with the system MuPDF.
A string starting with 'git:':
Use `git clone` to get a MuPDF checkout. We use the
string in the git clone command; it must contain the git
URL from which to clone, and can also contain other `git
clone` args, for example:
PYMUPDF_SETUP_MUPDF_BUILD="git:--branch master https://github.com/ArtifexSoftware/mupdf.git"
Otherwise:
Location of mupdf directory.
PYMUPDF_SETUP_MUPDF_TESSERACT
If '0' we build MuPDF without Tesseract.
PYMUPDF_SETUP_MUPDF_BUILD_TYPE
Unix only. Controls build type of MuPDF. Supported values are:
debug
memento
release (default)
PYMUPDF_SETUP_MUPDF_CLEAN
Unix only. If '1', we do a clean MuPDF build.
PYMUPDF_SETUP_MUPDF_THIRD
If '0' and we are building on Linux with the system MuPDF
(i.e. PYMUPDF_SETUP_MUPDF_BUILD=''), then don't link with
`-lmupdf-third`.
PYMUPDF_SETUP_MUPDF_VS_UPGRADE
If '1' we run mupdf `scripts/mupdfwrap.py` with `--vs-upgrade 1` to
help Windows builds work with Visual Studio versions newer than 2019.
PYMUPDF_SETUP_MUPDF_TGZ
If set, overrides location of MuPDF .tar.gz file:
Empty string:
Do not download MuPDF .tar.gz file. Sdist's will not contain
MuPDF.
A string containing '://':
The URL from which to download the MuPDF .tar.gz file. Leaf
must match mupdf-*.tar.gz.
Otherwise:
The path of local mupdf git checkout. We put all files in this
checkout known to git into a local tar archive.
PYMUPDF_SETUP_MUPDF_OVERWRITE_CONFIG
If '0' we do not overwrite MuPDF's include/mupdf/fitz/config.h with
PyMuPDF's own configuration file, before building MuPDF.
PYMUPDF_SETUP_MUPDF_REBUILD
If 0 we do not (re)build mupdf.
PYMUPDF_SETUP_REBUILD_GIT_DETAILS
If '0' we do not rebuild if only fitz/helper-git-versions.i has
changed.
PYMUPDF_SETUP_SKELETON
If '1' we build minimal wheel for testing.
WDEV_VS_YEAR
If set, we use as Visual Studio year, for example '2019' or '2022'.
WDEV_VS_GRADE
If set, we use as Visual Studio grade, for example 'Community' or
'Professional' or 'Enterprise'.
Known build failures:
Linux:
*musllinux*.
Windows:
pp*:
fitz_wrap.obj : error LNK2001: unresolved external symbol PyUnicode_DecodeRawUnicodeEscape
When using cibuildwheel, one can avoid building these failing wheels with:
CIBW_SKIP='*musllinux* pp*'
'''
import glob
import io
import os
import textwrap
import time
import platform
import shlex
import shutil
import stat
import subprocess
import sys
import tarfile
import urllib.request
import zipfile
import pipcl
_log_prefix = None
def log( text):
global _log_prefix
if not _log_prefix:
p = os.path.abspath( __file__)
p, p1 = os.path.split( p)
p, p0 = os.path.split( p)
_log_prefix = os.path.join( p0, p1)
print(f'{_log_prefix}: {text}', file=sys.stdout)
sys.stdout.flush()
if 1:
log(f'### Starting.')
log(f'__name__: {__name__!r}')
log(f'platform.platform(): {platform.platform()!r}')
log(f'platform.python_version(): {platform.python_version()!r}')
log(f'sys.executable: {sys.executable!r}')
log(f'CPU bits: {32 if sys.maxsize == 2**31 - 1 else 64} {sys.maxsize=}')
log(f'__file__: {__file__!r}')
log(f'os.getcwd(): {os.getcwd()!r}')
log(f'sys.argv ({len(sys.argv)}):')
for i, arg in enumerate(sys.argv):
log(f' {i}: {arg!r}')
log(f'os.environ ({len(os.environ)}):')
for k in sorted( os.environ.keys()):
v = os.environ[ k]
log( f' {k}: {v!r}')
g_flavour = os.environ.get( 'PYMUPDF_SETUP_FLAVOUR', 'pb')
assert g_flavour in ('p', 'b', 'pb'), \
f'Unrecognised {g_flavour=} should be one of: "p", "b", "pb"'
g_root = os.path.abspath( f'{__file__}/..')
assert os.path.abspath( os.getcwd()) == g_root, \
f'Current directory must be the PyMuPDF directory'
def _fs_remove(path):
'''
Removes file or directory, without raising exception if it doesn't exist.
We assert-fail if the path still exists when we return, in case of
permission problems etc.
'''
try:
os.remove( path)
except Exception as e:
pass
if os.path.exists(path):
def error_fn(fn, path, excinfo):
os.chmod(path, stat.S_IWRITE)
fn(path)
shutil.rmtree( path, onerror=error_fn)
assert not os.path.exists( path)
def run(command, check=1):
log(f'Running: {command}')
return subprocess.run( command, shell=1, check=check)
def _git_get_branch( directory):
command = f'cd {directory} && git branch --show-current'
log( f'Running: {command}')
p = subprocess.run(
command,
shell=True,
check=False,
text=True,
stdout=subprocess.PIPE,
)
ret = None
if p.returncode == 0:
ret = p.stdout.strip()
log( f'Have found MuPDF git branch: ret={ret!r}')
return ret
def tar_check(path, mode='r:gz', prefix=None, remove=False):
'''
Checks items in tar file have same <top-directory>, or <prefix> if not None.
We fail if items in tar file have different top-level directory names.
path:
The tar file.
mode:
As tarfile.open().
prefix:
If not None, we fail if tar file's <top-directory> is not <prefix>.
Returns the directory name (which will be <prefix> if not None).
'''
with tarfile.open( path, mode) as t:
items = t.getnames()
assert items
item = items[0]
assert not item.startswith('./') and not item.startswith('../')
s = item.find('/')
if s == -1:
prefix_actual = item + '/'
else:
prefix_actual = item[:s+1]
if prefix:
assert prefix == prefix_actual, f'{path=} {prefix=} {prefix_actual=}'
for item in items[1:]:
assert item.startswith( prefix_actual), f'prefix_actual={prefix_actual!r} != item={item!r}'
return prefix_actual
def tar_extract(path, mode='r:gz', prefix=None, exists='raise'):
'''
Extracts tar file into single local directory.
We fail if items in tar file have different <top-directory>.
path:
The tar file.
mode:
As tarfile.open().
prefix:
If not None, we fail if tar file's <top-directory> is not <prefix>.
exists:
What to do if <top-directory> already exists:
'raise': raise exception.
'remove': remove existing file/directory before extracting.
'return': return without extracting.
Returns the directory name (which will be <prefix> if not None, with '/'
appended if not already present).
'''
prefix_actual = tar_check( path, mode, prefix)
if os.path.exists( prefix_actual):
if exists == 'raise':
raise Exception( f'Path already exists: {prefix_actual!r}')
elif exists == 'remove':
remove( prefix_actual)
elif exists == 'return':
log( f'Not extracting {path} because already exists: {prefix_actual}')
return prefix_actual
else:
assert 0, f'Unrecognised exists={exists!r}'
assert not os.path.exists( prefix_actual), f'Path already exists: {prefix_actual}'
log( f'Extracting {path}')
with tarfile.open( path, mode) as t:
t.extractall()
return prefix_actual
def get_git_id( directory):
'''
Returns `(sha, comment, diff, branch)`, all items are str or None if not
available.
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()
log(f'get_git_id(): directory={directory!r} returning branch={branch!r} sha={sha!r} comment={comment!r}')
return sha, comment, diff, branch
mupdf_tgz = os.path.abspath( f'{__file__}/../mupdf.tgz')
def get_mupdf_internal(out, location=None, sha=None, local_tgz=None):
'''
Gets MuPDF as either a .tgz or a local directory.
Args:
out:
Either 'dir' (we return name of local directory containing mupdf) or 'tgz' (we return
name of local .tgz file containing mupdf).
location:
First, if None we set to hard-coded default URL or git location.
If starts with 'git:', should be remote git location.
Otherwise if containg '://' should be URL for .tgz.
Otherwise shuld path of local mupdf checkout.
sha:
If not None and we use git clone, we checkout this sha.
local_tgz:
If not None, must be local .tgz file.
Returns:
Absolute path of local directory or .tgz containing MuPDF, or None if
we are to use system MuPDF.
'''
log(f'get_mupdf_internal(): {out=} {location=} {sha=}')
assert out in ('dir', 'tgz')
if location is None:
location = 'https://mupdf.com/downloads/archive/mupdf-1.23.7-source.tar.gz'
if location == '':
return
local_dir = None
if local_tgz:
assert os.path.isfile(local_tgz)
elif location.startswith( 'git:'):
location_git = location[4:]
local_dir = 'mupdf-git'
e = run(f'cd {local_dir} && git pull && git submodule update --init', check=False).returncode
if e:
_fs_remove(local_dir)
run(f'git clone --recursive --depth 1 --shallow-submodules {location[4:]} {local_dir}')
run( f'cd {local_dir} && git show --pretty=oneline|head -n 1', check=False)
if sha:
command = f'cd mupdf && git checkout {sha}'
log( f'Running: {command}')
run( f'cd mupdf && git checkout {sha}')
elif '://' in location:
local_tgz = os.path.basename( location)
suffix = '.tar.gz'
assert location.endswith(suffix), f'Unrecognised suffix in remote URL {location=}.'
name = local_tgz[:-len(suffix)]
log( f'Download {location=} {local_tgz=} {name=}')
if os.path.exists(local_tgz):
try:
tar_check(local_tgz, 'r:gz', prefix=f'{name}/')
except Exception as e:
log(f'Not using existing file {local_tgz} because invalid tar data: {e}')
_fs_remove( local_tgz)
if os.path.exists(local_tgz):
log(f'Not downloading from {location} because already present: {local_tgz!r}')
else:
log(f'Downloading from {location=} to {local_tgz=}.')
urllib.request.urlretrieve( location, local_tgz)
assert os.path.exists( local_tgz)
tar_check( local_tgz, 'r:gz', prefix=f'{name}/')
else:
assert os.path.isdir(location), f'Local MuPDF does not exist: {location=}'
local_dir = location
assert bool(local_dir) != bool(local_tgz)
if out == 'dir':
if not local_dir:
assert local_tgz
local_dir = tar_extract( local_tgz, exists='return')
return os.path.abspath( local_dir)
elif out == 'tgz':
if not local_tgz:
assert local_dir
if local_dir.endswith( '/'):
local_dir = local_dir[:-1]
top = os.path.basename(local_dir)
local_tgz = f'{local_dir}.tgz'
log( f'Creating .tgz from git files. {top=} {local_dir=} {local_tgz=}')
_fs_remove( local_tgz)
with tarfile.open( local_tgz, 'w:gz') as f:
for name in pipcl.git_items( local_dir, submodules=True):
path = os.path.join( local_dir, name)
if os.path.isfile( path):
path2 = f'{top}/{name}'
log(f'Adding {path=} {path2=}.')
f.add( path, path2, recursive=False)
return os.path.abspath( local_tgz)
else:
assert 0, f'Unrecognised {out=}'
def get_mupdf_tgz():
'''
Creates .tgz file called containing MuPDF source, for inclusion in an
sdist.
What we do depends on environmental variable PYMUPDF_SETUP_MUPDF_TGZ; see
docs at start of this file for details.
Returns name of top-level directory within the .tgz file.
'''
name = get_mupdf_internal( 'tgz', os.environ.get('PYMUPDF_SETUP_MUPDF_TGZ'))
return name
def get_mupdf(path=None, sha=None):
'''
Downloads and/or extracts mupdf and returns location of mupdf directory.
Exact behaviour depends on environmental variable
PYMUPDF_SETUP_MUPDF_BUILD; see docs at start of this file for details.
'''
m = os.environ.get('PYMUPDF_SETUP_MUPDF_BUILD')
if m is None and os.path.isfile(mupdf_tgz):
log(f'Using local tgz: {mupdf_tgz=}')
return get_mupdf_internal('dir', local_tgz=mupdf_tgz)
return get_mupdf_internal('dir', m)
linux = sys.platform.startswith( 'linux') or 'gnu' in sys.platform
openbsd = sys.platform.startswith( 'openbsd')
freebsd = sys.platform.startswith( 'freebsd')
darwin = sys.platform.startswith( 'darwin')
windows = platform.system() == 'Windows' or platform.system().startswith('CYGWIN')
pyodide = os.environ.get('OS') == 'pyodide'
def _implementations():
v = os.environ.get( 'PYMUPDF_SETUP_IMPLEMENTATIONS', 'ab')
assert v in ('a', 'b', 'ab')
return v
def build():
'''
pipcl.py `build_fn()` callback.
'''
skeleton = os.environ.get( 'PYMUPDF_SETUP_SKELETON')
log( f'{skeleton=}')
if skeleton == '1':
ret = list()
log( f'{g_flavour=}')
run( f'ls -l wheelhouse', check=0)
if 'b' in g_flavour:
with open( 'foo.c', 'w') as f:
f.write( textwrap.dedent( '''
int foo(int x)
{
return x+1;
}
'''))
run(f'cc -fPIC -shared -o {g_root}/libfoo.so foo.c')
ret.append( f'{g_root}/libfoo.so')
ret.append( (f'{g_root}/READMErb.md', '$dist-info/README.md'))
if 'p' in g_flavour:
with open( 'bar.c', 'w') as f:
f.write( textwrap.dedent( '''
int bar(int x)
{
return x+1;
}
'''))
run(f'cc -fPIC -shared -o {g_root}/_bar.so bar.c')
with open( 'bar.py', 'w') as f:
f.write( textwrap.dedent( '''
def bar(x):
return x - 1
'''))
ret.append( f'{g_root}/bar.py')
ret.append( f'{g_root}/_bar.so')
ret.append( (f'{g_root}/README.md', '$dist-info/README.md'))
return ret
elif skeleton == '2':
os.makedirs( 'src-skeleton2', exist_ok=True)
ret = list()
if 1:
cc, _ = pipcl.base_compiler()
with open( 'src-skeleton2/mupdf.c', 'w') as f:
f.write( textwrap.dedent('''
int foo(int x)
{
return x + 1;
}
'''))
run(f'{cc} -o src-skeleton2/libmupdf.so src-skeleton2/mupdf.c -fPIC -shared -Wl,-rpath,\'$ORIGIN\',-z,origin')
ret.append( ('src-skeleton2/libmupdf.so', ''))
if 'p' in g_flavour:
with open( 'src-skeleton2/fitz.i', 'w') as f:
f.write( textwrap.dedent('''
%module fitz
%{
int foo(int x);
int bar(int x)
{
return foo(x) * 2;
}
%}
int bar(int x);
'''))
path_so_leaf_a = pipcl.build_extension(
name = 'fitz',
path_i = 'src-skeleton2/fitz.i',
outdir = 'src-skeleton2',
cpp = False,
libpaths = ['src-skeleton2'],
libs = ['mupdf'],
)
with open( 'src-skeleton2/fitz.i', 'w') as f:
f.write( textwrap.dedent('''
%module fitz_new
%{
int foo(int x);
int bar(int x)
{
return foo(x) * 2;
}
%}
int bar(int x);
'''))
path_so_leaf_b = pipcl.build_extension(
name = 'fitz_new',
path_i = 'src-skeleton2/fitz.i',
outdir = 'src-skeleton2',
cpp = False,
libpaths = ['src-skeleton2'],
libs = ['mupdf'],
)
ret.append( (f'src-skeleton2/{path_so_leaf_a}', ''))
ret.append( (f'src-skeleton2/fitz.py', ''))
ret.append( (f'src-skeleton2/{path_so_leaf_b}', ''))
ret.append( (f'src-skeleton2/fitz_new.py', ''))
ret.append( (f'{g_root}/README.md', '$dist-info/README.md'))
return ret
mupdf_local = get_mupdf()
build_type = os.environ.get( 'PYMUPDF_SETUP_MUPDF_BUILD_TYPE', 'release')
assert build_type in ('debug', 'memento', 'release'), \
f'Unrecognised build_type={build_type!r}'
env_extra = dict()
if mupdf_local:
from_ = f'{g_root}/fitz/_config.h'
to_ = f'{mupdf_local}/include/mupdf/fitz/config.h'
if os.environ.get('PYMUPDF_SETUP_MUPDF_OVERWRITE_CONFIG') == '0':
log( f'Not copying {from_} to {to_}.')
else:
log( f'Copying {from_} to {to_}.')
shutil.copy2( from_, to_)
env_extra[ 'XCFLAGS'] = '-DTOFU_CJK_EXT'
s = os.stat( f'{to_}')
log( f'{to_}: {s} mtime={time.strftime("%F-%T", time.gmtime(s.st_mtime))}')
if windows:
mupdf_build_dir = build_mupdf_windows( mupdf_local, env_extra, build_type)
else:
mupdf_build_dir = build_mupdf_unix( mupdf_local, env_extra, build_type)
log( f'build(): mupdf_build_dir={mupdf_build_dir!r}')
path_so_leaf_a, path_so_leaf_b = _build_extensions(
mupdf_local,
mupdf_build_dir,
build_type,
)
for d in (
mupdf_build_dir,
f'{g_root}/fitz',
f'{g_root}/src',
):
if d:
run(f'ls -l {os.path.relpath(d)}', check=0)
ret_p = list()
ret_b = list()
def add( ret, from_, to_):
ret.append( (from_, to_))
if path_so_leaf_a:
to_dir = 'fitz/'
add( ret_p, f'{g_root}/fitz/__init__.py', to_dir)
add( ret_p, f'{g_root}/fitz/__main__.py', to_dir)
add( ret_p, f'{g_root}/fitz/fitz.py', to_dir)
add( ret_p, f'{g_root}/fitz/table.py', to_dir)
add( ret_p, f'{g_root}/fitz/utils.py', to_dir)
add( ret_p, f'{g_root}/fitz/{path_so_leaf_a}', to_dir)
if mupdf_local:
if windows:
wp = pipcl.wdev.WindowsPython()
add( ret_b, f'{mupdf_build_dir}/mupdfcpp{wp.cpu.windows_suffix}.dll', to_dir)
elif darwin:
add( ret_b, f'{mupdf_build_dir}/libmupdf.dylib', f'{to_dir}libmupdf.dylib')
elif pyodide:
add( ret_b, f'{mupdf_build_dir}/libmupdf.so', 'PyMuPDF.libs/')
else:
add( ret_b, f'{mupdf_build_dir}/libmupdf.so', to_dir)
if path_so_leaf_b:
to_dir = 'fitz_new/' if path_so_leaf_a else 'fitz/'
add( ret_p, f'{g_root}/src/__init__.py', to_dir)
add( ret_p, f'{g_root}/src/__main__.py', to_dir)
add( ret_p, f'{g_root}/src/fitz.py', to_dir)
add( ret_p, f'{g_root}/fitz/table.py', to_dir)
add( ret_p, f'{g_root}/src/utils.py', to_dir)
add( ret_p, f'{g_root}/src/extra.py', to_dir)
add( ret_p, f'{g_root}/src/{path_so_leaf_b}', to_dir)
if mupdf_local:
add( ret_p, f'{mupdf_build_dir}/mupdf.py', to_dir)
if windows:
wp = pipcl.wdev.WindowsPython()
add( ret_p, f'{mupdf_build_dir}/_mupdf.pyd', to_dir)
add( ret_b, f'{mupdf_build_dir}/mupdfcpp{wp.cpu.windows_suffix}.dll', to_dir)
elif darwin:
add( ret_p, f'{mupdf_build_dir}/_mupdf.so', to_dir)
add( ret_b, f'{mupdf_build_dir}/libmupdfcpp.so', to_dir)
add( ret_b, f'{mupdf_build_dir}/libmupdf.dylib', f'{to_dir}libmupdf.dylib')
elif pyodide:
add( ret_p, f'{mupdf_build_dir}/_mupdf.so', to_dir)
add( ret_b, f'{mupdf_build_dir}/libmupdfcpp.so', 'PyMuPDF.libs/')
add( ret_b, f'{mupdf_build_dir}/libmupdf.so', 'PyMuPDF.libs/')
else:
add( ret_p, f'{mupdf_build_dir}/_mupdf.so', to_dir)
add( ret_b, f'{mupdf_build_dir}/libmupdfcpp.so', to_dir)
add( ret_b, f'{mupdf_build_dir}/libmupdf.so', to_dir)
if g_flavour == 'pb':
ret = ret_p + ret_b
elif g_flavour == 'p':
ret = ret_p
elif g_flavour == 'b':
ret = ret_b
else:
assert 0
if g_flavour == 'b':
add( ret, f'{g_root}/READMErb.md', '$dist-info/README.md')
else:
add( ret, f'{g_root}/README.md', '$dist-info/README.md')
for f, t in ret:
log( f'build(): {f} => {t}')
return ret
def env_add(env, name, value, sep=' ', prefix=False, verbose=False):
'''
Appends/prepends `<value>` to `env[name]`.
'''
v = env.get(name)
if verbose:
log(f'Initally: {name}={v!r}')
if v is None:
env[ name] = value
else:
if prefix:
env[ name] = f'{value}{sep}{v}'
else:
env[ name] = f'{v}{sep}{value}'
if verbose:
log(f'Returning with {name}={env[name]!r}')
def build_mupdf_windows( mupdf_local, env, build_type):
assert mupdf_local
wp = pipcl.wdev.WindowsPython()
tesseract = '' if os.environ.get('PYMUPDF_SETUP_MUPDF_TESSERACT') == '0' else 'tesseract-'
windows_build_tail = f'build\\shared-{tesseract}{build_type}-x{wp.cpu.bits}-py{wp.version}'
windows_build_dir = f'{mupdf_local}\\{windows_build_tail}'
devenv = os.environ.get('PYMUPDF_SETUP_DEVENV')
if not devenv:
vs = pipcl.wdev.WindowsVS()
devenv = vs.devenv
if not devenv:
devenv = 'devenv.com'
log( f'Cannot find devenv.com in default locations, using: {devenv!r}')
command = f'cd "{mupdf_local}" && "{sys.executable}" ./scripts/mupdfwrap.py'
if os.environ.get('PYMUPDF_SETUP_MUPDF_VS_UPGRADE') == '1':
command += ' --vs-upgrade 1'
if len(devenv) >= 2:
for q in '"', "'":
if devenv.startswith( q) and devenv.endswith( q):
devenv = devenv[1:-1]
command += f' -d {windows_build_tail} -b --refcheck-if "#if 1" --devenv "{devenv}" '
if 'b' in _implementations():
command += 'all'
else:
command += '01'
env2 = os.environ.copy()
env2.update(env)
if os.environ.get( 'PYMUPDF_SETUP_MUPDF_REBUILD') == '0':
log( f'PYMUPDF_SETUP_MUPDF_REBUILD is "0" so not building MuPDF; would have run with env={env!r}: {command}')
else:
log( f'Building MuPDF by running with {env}={env!r}: {command}')
subprocess.run( command, shell=True, check=True, env=env2)
log( f'Finished building mupdf.')
return windows_build_dir
def build_mupdf_unix( mupdf_local, env, build_type):
'''
Builds MuPDF.
Args:
mupdf_local:
Path of MuPDF directory or None if we are using system MuPDF.
Returns the absolute path of build directory within MuPDF, e.g.
`.../mupdf/build/pymupdf-shared-release`, or `None` if we are using the
system MuPDF.
'''
if not mupdf_local:
log( f'Using system mupdf.')
return None
env = env.copy()
if openbsd or freebsd:
env_add(env, 'CXX', 'clang++', ' ')
archflags = os.environ.get( 'ARCHFLAGS')
if archflags:
env_add(env, 'XCFLAGS', archflags)
env_add(env, 'XLIBS', archflags)
build_prefix = f'PyMuPDF-'
if pyodide:
build_prefix += 'pyodide-'
else:
build_prefix += f'{platform.machine()}-'
build_prefix_extra = os.environ.get( '_PYTHON_HOST_PLATFORM')
if build_prefix_extra:
build_prefix += f'{build_prefix_extra}-'
build_prefix += 'shared-'
if os.environ.get('PYMUPDF_SETUP_MUPDF_TESSERACT') != '0':
build_prefix += 'tesseract-'
unix_build_dir = f'{mupdf_local}/build/{build_prefix}{build_type}'
env_string = ''
for n, v in env.items():
env_string += f' {n}={shlex.quote(v)}'
command = f'cd {mupdf_local} &&{env_string} {sys.executable} ./scripts/mupdfwrap.py -d build/{build_prefix}{build_type} -b '
if 'b' in _implementations() and 'p' in g_flavour:
command += 'all'
else:
command += 'm01'
command += f' && echo {unix_build_dir}:'
command += f' && ls -l {unix_build_dir}'
if os.environ.get( 'PYMUPDF_SETUP_MUPDF_REBUILD') == '0':
log( f'PYMUPDF_SETUP_MUPDF_REBUILD is "0" so not building MuPDF; would have run: {command}')
else:
log( f'Building MuPDF by running: {command}')
subprocess.run( command, shell=True, check=True)
log( f'Finished building mupdf.')
return unix_build_dir
def _fs_update(text, path):
try:
with open( path) as f:
text0 = f.read()
except OSError:
text0 = None
print(f'path={path!r} text==text0={text==text0!r}')
if text != text0:
with open( path, 'w') as f:
f.write( text)
def _build_extensions( mupdf_local, mupdf_build_dir, build_type):
'''
Builds Python extension modules `_fitz` and `_extra`.
Returns (path_so_leaf_a, path_so_leaf_b), the leafnames of the generated
shared libraries within mupdf_build_dir.
'''
path_so_leaf_a = None
path_so_leaf_b = None
if 'a' in _implementations():
path_so_leaf_a = _build_extension_classic( mupdf_local, mupdf_build_dir, build_type)
if 'b' in _implementations():
path_so_leaf_b = _build_extension_rebased( mupdf_local, mupdf_build_dir, build_type)
return path_so_leaf_a, path_so_leaf_b
def _build_extension_classic( mupdf_local, mupdf_build_dir, build_type):
'''
Builds Python extension module `_fitz` for classic implementation.
Returns leafname of the generated shared libraries within mupdf_build_dir.
'''
(compiler_extra, linker_extra, includes, defines, optimise, debug, libpaths, libs, libraries) \
= _extension_flags( mupdf_local, mupdf_build_dir, build_type)
f = io.StringIO()
f.write('%pythoncode %{\n')
def repr_escape(text):
text = repr(text)
text = text.replace('{', '{{')
text = text.replace('}', '}}')
text = text.replace('%', '{chr(37)})')
return 'f' + text
def write_git(name, directory):
sha, comment, diff, branch = get_git_id(directory)
f.write(f'{name}_git_sha = \'{sha}\'\n')
f.write(f'{name}_git_comment = {repr_escape(comment)}\n')
f.write(f'{name}_git_diff = {repr_escape(diff)}\n')
f.write(f'{name}_git_branch = {repr_escape(branch)}\n')
f.write('\n')
write_git('pymupdf', '.')
if mupdf_local:
write_git('mupdf', mupdf_local)
f.write('%}\n')
_fs_update( f.getvalue(), 'fitz/helper-git-versions.i')
if os.environ.get( 'PYMUPDF_SETUP_REBUILD_GIT_DETAILS') == '0':
log( f'Marking fitz/helper-git-versions.i as old because PYMUPDF_SETUP_REBUILD_GIT_DETAILS=0')
os.utime( 'fitz/helper-git-versions.i', (1, 1))
if windows:
compiler_extra_c = ''
else:
compiler_extra_c = (
' -Wno-incompatible-pointer-types'
' -Wno-pointer-sign'
' -Wno-sign-compare'
)
prerequisites_swig = glob.glob( f'{g_root}/fitz/*.i')
if os.environ.get( 'PYMUPDF_SETUP_REBUILD_GIT_DETAILS') == '0':
for i, p in enumerate( prerequisites_swig):
if p.endswith( 'helper-git-versions.i'):
del prerequisites_swig[i]
break
else:
assert 0, f'Cannot find *helper-git-versions.i in prerequisites_swig: {prerequisites_swig}'
path_so_leaf_a = pipcl.build_extension(
name = 'fitz',
path_i = f'{g_root}/fitz/fitz.i',
outdir = f'{g_root}/fitz',
includes = includes,
defines = defines,
libpaths = libpaths,
libs = libs,
compiler_extra = compiler_extra + compiler_extra_c,
linker_extra = linker_extra,
optimise = optimise,
debug = debug,
cpp = False,
prerequisites_swig = prerequisites_swig,
prerequisites_compile = f'{mupdf_local}/include',
prerequisites_link = libraries,
)
return path_so_leaf_a
def _build_extension_rebased( mupdf_local, mupdf_build_dir, build_type):
'''
Builds Python extension module `_extra` for rebased implementation.
Returns leafname of the generated shared libraries within mupdf_build_dir.
'''
(compiler_extra, linker_extra, includes, defines, optimise, debug, libpaths, libs, libraries) \
= _extension_flags( mupdf_local, mupdf_build_dir, build_type)
if mupdf_local:
includes = (
f'{mupdf_local}/platform/c++/include',
f'{mupdf_local}/include',
)
log('Building PyMuPDF rebased.')
compile_extra_cpp = ''
if darwin:
compile_extra_cpp += ' -Wno-non-pod-varargs'
compile_extra_cpp += ' -std=c++14'
if windows:
wp = pipcl.wdev.WindowsPython()
libs = f'mupdfcpp{wp.cpu.windows_suffix}.lib'
else:
libs = ('mupdf', 'mupdfcpp')
libraries = [
f'{mupdf_build_dir}/libmupdf.so'
f'{mupdf_build_dir}/libmupdfcpp.so'
]
path_so_leaf_b = pipcl.build_extension(
name = 'extra',
path_i = f'{g_root}/src/extra.i',
outdir = f'{g_root}/src',
includes = includes,
defines = defines,
libpaths = libpaths,
libs = libs,
compiler_extra = compiler_extra + compile_extra_cpp,
linker_extra = linker_extra,
optimise = optimise,
debug = debug,
prerequisites_swig = None,
prerequisites_compile = f'{mupdf_local}/include',
prerequisites_link = libraries,
)
return path_so_leaf_b
def _extension_flags( mupdf_local, mupdf_build_dir, build_type):
'''
Returns various flags to pass to pipcl.build_extension().
'''
compiler_extra = ''
linker_extra = ''
if build_type == 'memento':
compiler_extra += ' -DMEMENTO'
if mupdf_build_dir:
mupdf_build_dir_flags = os.path.basename( mupdf_build_dir).split( '-')
else:
mupdf_build_dir_flags = [build_type]
optimise = 'release' in mupdf_build_dir_flags
debug = 'debug' in mupdf_build_dir_flags
r_extra = ''
if windows:
defines = ('FZ_DLL_CLIENT',)
wp = pipcl.wdev.WindowsPython()
if os.environ.get('PYMUPDF_SETUP_MUPDF_VS_UPGRADE') == '1':
infix = 'win32-vs-upgrade'
else:
infix = 'win32'
build_type_infix = 'Debug' if debug else 'Release'
libpaths = (
f'{mupdf_local}\\platform\\{infix}\\{wp.cpu.windows_subdir}{build_type_infix}',
f'{mupdf_local}\\platform\\{infix}\\{wp.cpu.windows_subdir}{build_type_infix}Tesseract',
)
libs = f'mupdfcpp{wp.cpu.windows_suffix}.lib'
libraries = f'{mupdf_local}\\platform\\{infix}\\{wp.cpu.windows_subdir}{build_type_infix}\\{libs}'
compiler_extra = ''
else:
libs = ['mupdf']
defines = None
compiler_extra += (
' -Wall'
' -Wno-deprecated-declarations'
' -Wno-unused-const-variable'
)
if mupdf_local:
libpaths = (mupdf_build_dir,)
libraries = f'{mupdf_build_dir}/{libs[0]}'
if openbsd:
compiler_extra += ' -Wno-deprecated-declarations'
else:
libpaths = os.environ.get('PYMUPDF_MUPDF_LIB')
libraries = None
if libpaths:
libpaths = libpaths.split(':')
if mupdf_local:
includes = (
f'{mupdf_local}/include',
f'{mupdf_local}/include/mupdf',
f'{mupdf_local}/thirdparty/freetype/include',
)
else:
includes = list()
pi = os.environ.get('PYMUPDF_INCLUDES')
if pi:
includes += pi.split(':')
pmi = os.environ.get('PYMUPDF_MUPDF_INCLUDE')
if pmi:
includes.append(pmi)
ldflags = os.environ.get('LDFLAGS')
if ldflags:
linker_extra += f' {ldflags}'
cflags = os.environ.get('CFLAGS')
if cflags:
compiler_extra += f' {cflags}'
cxxflags = os.environ.get('CXXFLAGS')
if cxxflags:
compiler_extra += f' {cxxflags}'
return compiler_extra, linker_extra, includes, defines, optimise, debug, libpaths, libs, libraries,
def sdist():
ret = list()
for p in pipcl.git_items( g_root):
if p.startswith(
(
'docs/',
'signatures/',
'.',
)
):
pass
else:
ret.append(p)
tgz = get_mupdf_tgz()
if tgz:
ret.append((tgz, mupdf_tgz))
return ret
classifier = [
'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers',
'Intended Audience :: Information Technology',
'Operating System :: MacOS',
'Operating System :: Microsoft :: Windows',
'Operating System :: POSIX :: Linux',
'Programming Language :: C',
'Programming Language :: C++',
'Programming Language :: Python :: 3 :: Only',
'Programming Language :: Python :: Implementation :: CPython',
'Topic :: Utilities',
'Topic :: Multimedia :: Graphics',
'Topic :: Software Development :: Libraries',
]
with open( f'{g_root}/README.md', encoding='utf-8') as f:
readme_ = f.read()
with open( f'{g_root}/READMErb.md', encoding='utf-8') as f:
readme_rb = f.read()
version = '1.23.7'
version_b = '1.23.7'
tag_python = None
requires_dist = None,
summary = 'A high performance Python library for data extraction, analysis, conversion & manipulation of PDF (and other) documents.'
if g_flavour == 'pb':
name = 'PyMuPDF'
readme = readme_
elif g_flavour == 'p':
name = 'PyMuPDF'
readme = readme_
requires_dist = f'PyMuPDFb =={version_b}'
elif g_flavour == 'b':
name = 'PyMuPDFb'
summary = 'MuPDF shared libraries for PyMuPDF.'
readme = readme_rb
tag_python = 'py3'
version = version_b
else:
assert 0, f'Unrecognised flavour: {g_flavour}'
p = pipcl.Package(
name,
version,
summary = summary,
description = readme,
description_content_type = 'text/markdown',
classifier = classifier,
author = 'Artifex',
author_email = 'support@artifex.com',
requires_dist = requires_dist,
requires_python = '>=3.8',
license = 'GNU AFFERO GPL 3.0',
project_url = [
('Documentation, https://pymupdf.readthedocs.io/'),
('Source, https://github.com/pymupdf/pymupdf'),
('Tracker, https://github.com/pymupdf/PyMuPDF/issues'),
('Changelog, https://pymupdf.readthedocs.io/en/latest/changes.html'),
],
fn_build=build,
fn_sdist=sdist,
tag_python=tag_python,
wheel_compresslevel = 9,
)
build_wheel = p.build_wheel
build_sdist = p.build_sdist
import sys
if __name__ == '__main__':
p.handle_argv(sys.argv)