mirror of
				https://github.com/kovidgoyal/calibre.git
				synced 2025-10-30 18:22:25 -04:00 
			
		
		
		
	
		
			
				
	
	
		
			763 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			763 lines
		
	
	
		
			29 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
| #!/usr/bin/env python
 | |
| 
 | |
| __license__   = 'GPL v3'
 | |
| __copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
 | |
| __docformat__ = 'restructuredtext en'
 | |
| 
 | |
| import errno
 | |
| import glob
 | |
| import json
 | |
| import os
 | |
| import shlex
 | |
| import shutil
 | |
| import subprocess
 | |
| import sys
 | |
| import sysconfig
 | |
| import textwrap
 | |
| from functools import partial
 | |
| from typing import NamedTuple
 | |
| 
 | |
| from setup import SRC, Command, isbsd, isfreebsd, ishaiku, islinux, ismacos, iswindows
 | |
| 
 | |
| isunix = islinux or ismacos or isbsd or ishaiku
 | |
| 
 | |
| py_lib = os.path.join(sys.prefix, 'libs', 'python{}{}.lib'.format(*sys.version_info[:2]))
 | |
| 
 | |
| 
 | |
| class CompileCommand(NamedTuple):
 | |
|     cmd: list[str]
 | |
|     src: str
 | |
|     dest: str
 | |
| 
 | |
| 
 | |
| class LinkCommand(NamedTuple):
 | |
|     cmd: list[str]
 | |
|     objects: list[str]
 | |
|     dest: str
 | |
| 
 | |
| 
 | |
| def walk(path='.'):
 | |
|     for dirpath, dirnames, filenames in os.walk(path):
 | |
|         for f in filenames:
 | |
|             yield os.path.join(dirpath, f)
 | |
| 
 | |
| 
 | |
| def init_symbol_name(name):
 | |
|     prefix = 'PyInit_'
 | |
|     return prefix + name
 | |
| 
 | |
| 
 | |
| def absolutize(paths):
 | |
|     return list({x if os.path.isabs(x) else os.path.join(SRC, x.replace('/', os.sep)) for x in paths})
 | |
| 
 | |
| 
 | |
| class Extension:
 | |
| 
 | |
|     def __init__(self, name, sources, **kwargs):
 | |
|         self.data = d = {}
 | |
|         self.name = d['name'] = name
 | |
|         self.sources = d['sources'] = absolutize(sources)
 | |
|         self.needs_cxx = d['needs_cxx'] = bool([1 for x in self.sources if os.path.splitext(x)[1] in ('.cpp', '.c++', '.cxx')])
 | |
|         self.needs_py2 = d['needs_py2'] = kwargs.get('needs_py2', False)
 | |
|         self.headers = d['headers'] = absolutize(kwargs.get('headers', []))
 | |
|         self.sip_files = d['sip_files'] = absolutize(kwargs.get('sip_files', []))
 | |
|         self.needs_exceptions = d['needs_exceptions'] = kwargs.get('needs_exceptions', False)
 | |
|         self.qt_modules = d['qt_modules'] = kwargs.get('qt_modules', ['widgets'])
 | |
|         self.inc_dirs = d['inc_dirs'] = absolutize(kwargs.get('inc_dirs', []))
 | |
|         self.lib_dirs = d['lib_dirs'] = absolutize(kwargs.get('lib_dirs', []))
 | |
|         self.extra_objs = d['extra_objs'] = absolutize(kwargs.get('extra_objs', []))
 | |
|         self.error = d['error'] = kwargs.get('error', None)
 | |
|         self.libraries = d['libraries'] = kwargs.get('libraries', [])
 | |
|         self.cflags = d['cflags'] = kwargs.get('cflags', [])
 | |
|         self.uses_icu = 'icuuc' in self.libraries
 | |
|         self.ldflags = d['ldflags'] = kwargs.get('ldflags', [])
 | |
|         self.optional = d['options'] = kwargs.get('optional', False)
 | |
|         self.needs_cxx_std = kwargs.get('needs_c++')
 | |
|         self.needs_c_std = kwargs.get('needs_c')
 | |
|         self.only_build_for = kwargs.get('only', '')
 | |
| 
 | |
| 
 | |
| def lazy_load(name):
 | |
|     if name.startswith('!'):
 | |
|         name = name[1:]
 | |
|     from setup import build_environment
 | |
|     try:
 | |
|         return getattr(build_environment, name)
 | |
|     except AttributeError:
 | |
|         raise ImportError(f'The setup.build_environment module has no symbol named: {name}')
 | |
| 
 | |
| 
 | |
| def expand_file_list(items, is_paths=True, cross_compile_for='native'):
 | |
|     if not items:
 | |
|         return []
 | |
|     ans = []
 | |
|     for item in items:
 | |
|         if item.startswith('!'):
 | |
|             if cross_compile_for == 'native' or not item.endswith('_dirs'):
 | |
|                 item = lazy_load(item)
 | |
|                 if hasattr(item, 'rjust'):
 | |
|                     item = [item]
 | |
|                 items = expand_file_list(item, is_paths=is_paths, cross_compile_for=cross_compile_for)
 | |
|             else:
 | |
|                 pkg, category = item[1:].split('_')[:2]
 | |
|                 if category == 'inc':
 | |
|                     category = 'include'
 | |
|                 items = [f'bypy/b/windows/64/{pkg}/{category}']
 | |
|                 items = expand_file_list(item, is_paths=is_paths, cross_compile_for=cross_compile_for)
 | |
|             ans.extend(items)
 | |
|         else:
 | |
|             if '*' in item:
 | |
|                 ans.extend(expand_file_list(sorted(glob.glob(os.path.join(SRC, item))), is_paths=is_paths, cross_compile_for=cross_compile_for))
 | |
|             else:
 | |
|                 item = [item]
 | |
|                 if is_paths:
 | |
|                     item = absolutize(item)
 | |
|                 ans.extend(item)
 | |
|     return ans
 | |
| 
 | |
| 
 | |
| def is_ext_allowed(cross_compile_for: str, ext: Extension) -> bool:
 | |
|     only = ext.only_build_for
 | |
|     if only:
 | |
|         if islinux and only == cross_compile_for:
 | |
|             return True
 | |
|         only = set(only.split())
 | |
|         q = set(filter(lambda x: globals()['is' + x], ['bsd', 'freebsd', 'haiku', 'linux', 'macos', 'windows']))
 | |
|         return len(q.intersection(only)) > 0
 | |
|     return True
 | |
| 
 | |
| 
 | |
| def parse_extension(ext, compiling_for='native'):
 | |
|     ext = ext.copy()
 | |
|     only = ext.pop('only', None)
 | |
|     kw = {}
 | |
|     name = ext.pop('name')
 | |
|     get_key = 'linux_'
 | |
|     if iswindows:
 | |
|         get_key = 'windows_'
 | |
|     elif ismacos:
 | |
|         get_key = 'macos_'
 | |
|     elif isbsd:
 | |
|         get_key = 'bsd_'
 | |
|     elif isfreebsd:
 | |
|         get_key = 'freebsd_'
 | |
|     elif ishaiku:
 | |
|         get_key = 'haiku_'
 | |
|     if compiling_for == 'windows':
 | |
|         get_key = 'windows_'
 | |
| 
 | |
|     def get(k, default=''):
 | |
|         ans = ext.pop(k, default)
 | |
|         ans = ext.pop(get_key + k, ans)
 | |
|         return ans
 | |
|     for k in 'libraries qt_private ldflags cflags error'.split():
 | |
|         kw[k] = expand_file_list(get(k).split(), is_paths=False)
 | |
|     defines = get('defines')
 | |
|     if defines:
 | |
|         if 'cflags' not in kw:
 | |
|             kw['cflags'] = []
 | |
|         cflags = kw['cflags']
 | |
|         prefix = '/D' if get_key == 'windows_' else '-D'
 | |
|         cflags.extend(prefix + x for x in defines.split())
 | |
|     for k in 'inc_dirs lib_dirs sources headers sip_files'.split():
 | |
|         v = get(k)
 | |
|         if v:
 | |
|             kw[k] = expand_file_list(v.split())
 | |
|     kw.update(ext)
 | |
|     kw['only'] = only
 | |
|     return Extension(name, **kw)
 | |
| 
 | |
| 
 | |
| def read_extensions():
 | |
|     if hasattr(read_extensions, 'extensions'):
 | |
|         return read_extensions.extensions
 | |
|     with open(os.path.dirname(os.path.abspath(__file__)) + '/extensions.json', 'rb') as f:
 | |
|         ans = read_extensions.extensions = json.load(f)
 | |
|     return ans
 | |
| 
 | |
| 
 | |
| def get_python_include_paths():
 | |
|     ans = []
 | |
|     for name in sysconfig.get_path_names():
 | |
|         if 'include' in name:
 | |
|             ans.append(name)
 | |
| 
 | |
|     def gp(x):
 | |
|         return sysconfig.get_path(x)
 | |
| 
 | |
|     return sorted(frozenset(filter(None, map(gp, sorted(ans)))))
 | |
| 
 | |
| 
 | |
| is_macos_universal_build = ismacos and 'universal2' in sysconfig.get_platform()
 | |
| 
 | |
| 
 | |
| def basic_windows_flags(debug=False):
 | |
|     cflags = '/c /nologo /W3 /EHsc /O2 /utf-8'.split()
 | |
|     cflags.append('/Zi' if debug else '/DNDEBUG')
 | |
|     suffix = ('d' if debug else '')
 | |
|     cflags.append('/MD' + suffix)
 | |
|     ldflags = f'/DLL /nologo /INCREMENTAL:NO /NODEFAULTLIB:libcmt{suffix}.lib'.split()
 | |
|     if debug:
 | |
|         ldflags.append('/DEBUG')
 | |
|     # cflags = '/c /nologo /Ox /MD /W3 /EHsc /Zi'.split()
 | |
|     # ldflags = '/DLL /nologo /INCREMENTAL:NO /DEBUG'.split()
 | |
|     cflags.append('/GS-')
 | |
|     return cflags, ldflags
 | |
| 
 | |
| 
 | |
| class Environment(NamedTuple):
 | |
|     cc: str
 | |
|     cxx: str
 | |
|     linker: str
 | |
|     base_cflags: list[str]
 | |
|     base_cxxflags: list[str]
 | |
|     base_ldflags: list[str]
 | |
|     cflags: list[str]
 | |
|     ldflags: list[str]
 | |
|     make: str
 | |
|     internal_inc_prefix: str
 | |
|     external_inc_prefix: str
 | |
|     libdir_prefix: str
 | |
|     lib_prefix: str
 | |
|     lib_suffix: str
 | |
|     obj_suffix: str
 | |
|     cc_input_c_flag: str
 | |
|     cc_input_cpp_flag: str
 | |
|     cc_output_flag: str
 | |
|     platform_name: str
 | |
|     dest_ext: str
 | |
|     std_prefix: str
 | |
| 
 | |
|     def inc_dirs_to_cflags(self, dirs) -> list[str]:
 | |
|         return [self.external_inc_prefix+x for x in dirs]
 | |
| 
 | |
|     def lib_dirs_to_ldflags(self, dirs) -> list[str]:
 | |
|         return [self.libdir_prefix+x for x in dirs if x]
 | |
| 
 | |
|     def libraries_to_ldflags(self, libs):
 | |
|         def map_name(x):
 | |
|             if '/' in x:
 | |
|                 return x
 | |
|             return self.lib_prefix+x+self.lib_suffix
 | |
|         return list(map(map_name, libs))
 | |
| 
 | |
| 
 | |
| def init_env(debug=False, sanitize=False, compiling_for='native'):
 | |
|     from setup.build_environment import NMAKE, win_cc, win_inc, win_ld, win_lib
 | |
|     linker = None
 | |
|     internal_inc_prefix = external_inc_prefix = '-I'
 | |
|     libdir_prefix = '-L'
 | |
|     lib_prefix = '-l'
 | |
|     lib_suffix = ''
 | |
|     std_prefix = '-std='
 | |
|     obj_suffix = '.o'
 | |
|     cc_input_c_flag = cc_input_cpp_flag = '-c'
 | |
|     cc_output_flag = '-o'
 | |
|     platform_name = 'linux'
 | |
|     dest_ext = '.so'
 | |
|     if isunix:
 | |
|         cc = os.environ.get('CC', 'gcc')
 | |
|         cxx = os.environ.get('CXX', 'g++')
 | |
|         debug = '-ggdb' if debug else ''
 | |
|         cflags = os.environ.get('OVERRIDE_CFLAGS',
 | |
|             f'-Wall -DNDEBUG {debug} -fno-strict-aliasing -pipe -O3')
 | |
|         cflags = shlex.split(cflags) + ['-fPIC']
 | |
|         ldflags = os.environ.get('OVERRIDE_LDFLAGS', '-Wall')
 | |
|         ldflags = shlex.split(ldflags)
 | |
|         base_cflags = shlex.split(os.environ.get('CFLAGS', ''))
 | |
|         base_cxxflags = shlex.split(os.environ.get('CXXFLAGS', ''))
 | |
|         base_ldflags = shlex.split(os.environ.get('LDFLAGS', ''))
 | |
|         cflags += base_cflags
 | |
|         ldflags += base_ldflags
 | |
|         cflags += ['-fvisibility=hidden']
 | |
|         if sanitize:
 | |
|             cflags.append('-fsanitize=address')
 | |
| 
 | |
|     if islinux:
 | |
|         cflags.append('-pthread')
 | |
|         if sys.stdout.isatty():
 | |
|             base_cflags.append('-fdiagnostics-color=always')
 | |
|             cflags.append('-fdiagnostics-color=always')
 | |
|         ldflags.append('-shared')
 | |
| 
 | |
|     if isbsd:
 | |
|         cflags.append('-pthread')
 | |
|         ldflags.append('-shared')
 | |
| 
 | |
|     if ishaiku:
 | |
|         cflags.append('-lpthread')
 | |
|         ldflags.append('-shared')
 | |
| 
 | |
|     if islinux or isbsd or ishaiku:
 | |
|         cflags.extend('-I' + x for x in get_python_include_paths())
 | |
|         ldlib = sysconfig.get_config_var('LIBDIR')
 | |
|         if ldlib:
 | |
|             ldflags += ['-L' + ldlib]
 | |
|         ldlib = sysconfig.get_config_var('VERSION')
 | |
|         if ldlib:
 | |
|             ldflags += ['-lpython' + ldlib + sys.abiflags]
 | |
|         ldflags += (sysconfig.get_config_var('LINKFORSHARED') or '').split()
 | |
| 
 | |
|     if ismacos:
 | |
|         platform_name = 'macos'
 | |
|         if is_macos_universal_build:
 | |
|             cflags.extend(['-arch', 'x86_64', '-arch', 'arm64'])
 | |
|             ldflags.extend(['-arch', 'x86_64', '-arch', 'arm64'])
 | |
|         cflags.append('-D_OSX')
 | |
|         ldflags.extend('-bundle -undefined dynamic_lookup'.split())
 | |
|         cflags.extend(['-fno-common', '-dynamic'])
 | |
|         cflags.extend('-I' + x for x in get_python_include_paths())
 | |
| 
 | |
|     if iswindows or compiling_for == 'windows':
 | |
|         platform_name = 'windows'
 | |
|         std_prefix = '/std:'
 | |
|         cc = cxx = win_cc
 | |
|         linker = win_ld
 | |
|         cflags, ldflags = basic_windows_flags(debug)
 | |
|         base_cflags, base_cxxflags, base_ldflags = [], [], []
 | |
|         if compiling_for == 'windows':
 | |
|             cc = cxx = 'clang-cl'
 | |
|             linker = 'lld-link'
 | |
|             splat = '.build-cache/xwin/root'
 | |
|             cflags.append('-fcolor-diagnostics')
 | |
|             cflags.append('-fansi-escape-codes')
 | |
|             for I in 'sdk/include/um sdk/include/cppwinrt sdk/include/shared sdk/include/ucrt crt/include sdk/include/winrt'.split():
 | |
|                 cflags.append('/external:I')
 | |
|                 cflags.append(f'{splat}/{I}')
 | |
|             for L in 'sdk/lib/um crt/lib sdk/lib/ucrt'.split():
 | |
|                 ldflags.append(f'/libpath:{splat}/{L}')
 | |
|         else:
 | |
|             for p in win_inc:
 | |
|                 cflags.append('-I'+p)
 | |
|             for p in win_lib:
 | |
|                 if p:
 | |
|                     ldflags.append('/LIBPATH:'+p)
 | |
|         internal_inc_prefix = external_inc_prefix = '/I'
 | |
|         libdir_prefix = '/libpath:'
 | |
|         lib_prefix = ''
 | |
|         lib_suffix = '.lib'
 | |
|         cc_input_c_flag = '/Tc'
 | |
|         cc_input_cpp_flag = '/Tp'
 | |
|         cc_output_flag = '/Fo'
 | |
|         obj_suffix = '.obj'
 | |
|         dest_ext = '.pyd'
 | |
|         if compiling_for == 'windows':
 | |
|             external_inc_prefix = '/external:I'
 | |
|             dest_ext = '.cross-windows-x64' + dest_ext
 | |
|             obj_suffix = '.cross-windows-x64' + obj_suffix
 | |
|             cflags.append('/external:I')
 | |
|             cflags.append('bypy/b/windows/64/pkg/python/private/python/include')
 | |
|             ldflags.append('/libpath:' + 'bypy/b/windows/64/pkg/python/private/python/libs')
 | |
|         else:
 | |
|             cflags.extend('-I' + x for x in get_python_include_paths())
 | |
|             ldflags.append('/LIBPATH:'+os.path.join(sysconfig.get_config_var('prefix'), 'libs'))
 | |
|     return Environment(
 | |
|         platform_name=platform_name, dest_ext=dest_ext, std_prefix=std_prefix,
 | |
|         base_cflags=base_cflags, base_cxxflags=base_cxxflags, base_ldflags=base_ldflags,
 | |
|         cc=cc, cxx=cxx, cflags=cflags, ldflags=ldflags, linker=linker, make=NMAKE if iswindows else 'make', lib_prefix=lib_prefix,
 | |
|         obj_suffix=obj_suffix, cc_input_c_flag=cc_input_c_flag, cc_input_cpp_flag=cc_input_cpp_flag, cc_output_flag=cc_output_flag,
 | |
|         internal_inc_prefix=internal_inc_prefix, external_inc_prefix=external_inc_prefix, libdir_prefix=libdir_prefix, lib_suffix=lib_suffix)
 | |
| 
 | |
| 
 | |
| class Build(Command):
 | |
| 
 | |
|     short_description = 'Build calibre C/C++ extension modules'
 | |
|     DEFAULT_OUTPUTDIR = os.path.abspath(os.path.join(SRC, 'calibre', 'plugins'))
 | |
|     DEFAULT_BUILDDIR = os.path.abspath(os.path.join(os.path.dirname(SRC), 'build'))
 | |
| 
 | |
|     description = textwrap.dedent('''\
 | |
|         calibre depends on several python extensions written in C/C++.
 | |
|         This command will compile them. You can influence the compile
 | |
|         process by several environment variables, listed below:
 | |
| 
 | |
|            CC      - C Compiler defaults to gcc
 | |
|            CXX     - C++ Compiler, defaults to g++
 | |
|            CFLAGS  - Extra compiler flags
 | |
|            LDFLAGS - Extra linker flags
 | |
| 
 | |
|            POPPLER_INC_DIR - poppler header files
 | |
|            POPPLER_LIB_DIR - poppler-qt4 library
 | |
| 
 | |
|            PODOFO_INC_DIR - podofo header files
 | |
|            PODOFO_LIB_DIR - podofo library files
 | |
| 
 | |
|            QMAKE          - Path to qmake
 | |
|         ''')
 | |
| 
 | |
|     def add_options(self, parser):
 | |
|         choices = [e['name'] for e in read_extensions()]+['all', 'headless']
 | |
|         parser.add_option('-1', '--only', choices=choices, default='all',
 | |
|                 help=('Build only the named extension. Available: '+ ', '.join(choices)+'. Default:%default'))
 | |
|         parser.add_option('--no-compile', default=False, action='store_true',
 | |
|                 help='Skip compiling all C/C++ extensions.')
 | |
|         parser.add_option('--build-dir', default=None,
 | |
|             help='Path to directory in which to place object files during the build process, defaults to "build"')
 | |
|         parser.add_option('--output-dir', default=None,
 | |
|             help='Path to directory in which to place the built extensions. Defaults to src/calibre/plugins')
 | |
|         parser.add_option('--debug', default=False, action='store_true',
 | |
|             help='Build in debug mode')
 | |
|         parser.add_option('--sanitize', default=False, action='store_true',
 | |
|             help='Build with sanitization support. Run with LD_PRELOAD=$(gcc -print-file-name=libasan.so)')
 | |
|         parser.add_option('--cross-compile-extensions', choices='windows disabled'.split(), default='disabled',
 | |
|             help=('Cross compile extensions for other platforms. Useful for development.'
 | |
|                 ' Currently supports of windows extensions on Linux. Remember to run ./setup.py xwin first to install the Windows SDK locally. '))
 | |
| 
 | |
|     def dump_db(self, name, db):
 | |
|         os.makedirs('build', exist_ok=True)
 | |
|         existing = []
 | |
|         try:
 | |
|             with open(f'build/{name}_commands.json', 'rb') as f:
 | |
|                 existing = json.load(f)
 | |
|         except FileNotFoundError:
 | |
|             pass
 | |
|         combined = {x['output']: x for x in existing}
 | |
|         for x in db:
 | |
|             combined[x['output']] = x
 | |
|         try:
 | |
|             with open(f'build/{name}_commands.json', 'w') as f:
 | |
|                 json.dump(tuple(combined.values()), f, indent=2)
 | |
|         except OSError as err:
 | |
|             if err.errno != errno.EROFS:
 | |
|                 raise
 | |
| 
 | |
|     def run(self, opts):
 | |
|         from setup.parallel_build import create_job, parallel_build
 | |
|         if opts.no_compile:
 | |
|             self.info('--no-compile specified, skipping compilation')
 | |
|             return
 | |
|         self.compiling_for = 'native'
 | |
|         if islinux and opts.cross_compile_extensions == 'windows':
 | |
|             self.compiling_for = 'windows'
 | |
|             if not os.path.exists('.build-cache/xwin/root'):
 | |
|                 subprocess.check_call([sys.executable, 'setup.py', 'xwin'])
 | |
|         self.env = init_env(debug=opts.debug)
 | |
|         self.windows_cross_env = init_env(debug=opts.debug, compiling_for='windows')
 | |
|         all_extensions = tuple(map(partial(parse_extension, compiling_for=self.compiling_for), read_extensions()))
 | |
|         self.build_dir = os.path.abspath(opts.build_dir or self.DEFAULT_BUILDDIR)
 | |
|         self.output_dir = os.path.abspath(opts.output_dir or self.DEFAULT_OUTPUTDIR)
 | |
|         self.obj_dir = os.path.join(self.build_dir, 'objects')
 | |
|         for x in (self.output_dir, self.obj_dir):
 | |
|             os.makedirs(x, exist_ok=True)
 | |
|         pyqt_extensions, extensions = [], []
 | |
|         for ext in all_extensions:
 | |
|             if opts.only != 'all' and opts.only != ext.name:
 | |
|                 continue
 | |
|             if not is_ext_allowed(self.compiling_for, ext):
 | |
|                 continue
 | |
|             if ext.error:
 | |
|                 if ext.optional:
 | |
|                     self.warn(ext.error)
 | |
|                     continue
 | |
|                 else:
 | |
|                     raise Exception(ext.error)
 | |
|             (pyqt_extensions if ext.sip_files else extensions).append(ext)
 | |
| 
 | |
|         jobs = []
 | |
|         objects_map = {}
 | |
|         self.info(f'Building {len(extensions)+len(pyqt_extensions)} extensions')
 | |
|         ccdb = []
 | |
|         for ext in all_extensions:
 | |
|             if ext in pyqt_extensions:
 | |
|                 continue
 | |
|             cmds, objects = self.get_compile_commands(ext, ccdb)
 | |
|             objects_map[id(ext)] = objects
 | |
|             if ext in extensions:
 | |
|                 for cmd in cmds:
 | |
|                     jobs.append(create_job(cmd.cmd))
 | |
|         self.dump_db('compile', ccdb)
 | |
|         if jobs:
 | |
|             self.info(f'Compiling {len(jobs)} files...')
 | |
|             if not parallel_build(jobs, self.info):
 | |
|                 raise SystemExit(1)
 | |
|         jobs, link_commands, lddb = [], [], []
 | |
|         for ext in all_extensions:
 | |
|             if ext in pyqt_extensions:
 | |
|                 continue
 | |
|             objects = objects_map[id(ext)]
 | |
|             cmd = self.get_link_command(ext, objects, lddb)
 | |
|             if ext in extensions and cmd is not None:
 | |
|                 link_commands.append(cmd)
 | |
|                 jobs.append(create_job(cmd.cmd))
 | |
|         self.dump_db('link', lddb)
 | |
|         if jobs:
 | |
|             self.info(f'Linking {len(jobs)} files...')
 | |
|             if not parallel_build(jobs, self.info):
 | |
|                 raise SystemExit(1)
 | |
|             for cmd in link_commands:
 | |
|                 self.post_link_cleanup(cmd)
 | |
| 
 | |
|         jobs = []
 | |
|         sbf_map = {}
 | |
|         for ext in pyqt_extensions:
 | |
|             cmd, sbf, cwd = self.get_sip_commands(ext)
 | |
|             sbf_map[id(ext)] = sbf
 | |
|             if cmd is not None:
 | |
|                 jobs.append(create_job(cmd, cwd=cwd))
 | |
|         if jobs:
 | |
|             self.info(f'SIPing {len(jobs)} files...')
 | |
|             if not parallel_build(jobs, self.info):
 | |
|                 raise SystemExit(1)
 | |
|         for ext in pyqt_extensions:
 | |
|             sbf = sbf_map[id(ext)]
 | |
|             if not os.path.exists(sbf):
 | |
|                 self.build_pyqt_extension(ext, sbf)
 | |
| 
 | |
|         if opts.only in {'all', 'headless'}:
 | |
|             self.build_headless()
 | |
| 
 | |
|     def dest(self, ext, env):
 | |
|         return os.path.join(self.output_dir, getattr(ext, 'name', ext))+env.dest_ext
 | |
| 
 | |
|     def env_for_compilation_db(self, ext):
 | |
|         if is_ext_allowed('native', ext):
 | |
|             return self.env
 | |
|         if ext.only_build_for == 'windows':
 | |
|             return self.windows_cross_env
 | |
| 
 | |
|     def get_compile_commands(self, ext, db):
 | |
|         obj_dir = self.j(self.obj_dir, ext.name)
 | |
| 
 | |
|         def get(src: str, env: Environment, for_tooling: bool = False) -> CompileCommand:
 | |
|             compiler = env.cxx if ext.needs_cxx else env.cc
 | |
|             obj = self.j(obj_dir, os.path.splitext(self.b(src))[0]+env.obj_suffix)
 | |
|             inf = env.cc_input_cpp_flag if src.endswith(('.cpp', '.cxx')) else env.cc_input_c_flag
 | |
|             sinc = [inf, src]
 | |
|             if env.cc_output_flag.startswith('/'):
 | |
|                 if for_tooling:  # clangd gets confused by cl.exe style source and output flags
 | |
|                     oinc = ['-o', obj]
 | |
|                 else:
 | |
|                     oinc = [env.cc_output_flag + obj]
 | |
|                     sinc = [inf + src]
 | |
|             else:
 | |
|                 oinc = [env.cc_output_flag, obj]
 | |
|             einc = env.inc_dirs_to_cflags(ext.inc_dirs)
 | |
|             if env.cc_output_flag.startswith('/'):
 | |
|                 cflags = ['/DCALIBRE_MODINIT_FUNC=PyMODINIT_FUNC']
 | |
|             else:
 | |
|                 return_type = 'PyObject*'
 | |
|                 extern_decl = 'extern "C"' if ext.needs_cxx else ''
 | |
|                 cflags = [
 | |
|                     '-DCALIBRE_MODINIT_FUNC='
 | |
|                     f'{extern_decl} __attribute__ ((visibility ("default"))) {return_type}']
 | |
|             if ext.needs_cxx and ext.needs_cxx_std:
 | |
|                 if env.cc_output_flag.startswith('/') and ext.needs_cxx == '11':
 | |
|                     ext.needs_cxx = '14'
 | |
|                 cflags.append(env.std_prefix + 'c++' + ext.needs_cxx_std)
 | |
| 
 | |
|             if ext.needs_c_std and not env.std_prefix.startswith('/'):
 | |
|                 cflags.append(env.std_prefix + 'c' + ext.needs_c_std)
 | |
| 
 | |
|             cmd = [compiler] + env.cflags + cflags + ext.cflags + einc + sinc + oinc
 | |
|             return CompileCommand(cmd, src, obj)
 | |
| 
 | |
|         objects = []
 | |
|         ans = []
 | |
|         os.makedirs(obj_dir, exist_ok=True)
 | |
| 
 | |
|         for src in ext.sources:
 | |
|             cc = get(src, self.windows_cross_env if self.compiling_for == 'windows' else self.env)
 | |
|             objects.append(cc.dest)
 | |
|             if self.newer(cc.dest, [src]+ext.headers):
 | |
|                 ans.append(cc)
 | |
|             env = self.env_for_compilation_db(ext)
 | |
|             if env is not None:
 | |
|                 cc = get(src, env, for_tooling=True)
 | |
|                 db.append({
 | |
|                     'arguments': cc.cmd, 'directory': os.getcwd(), 'file': os.path.relpath(src, os.getcwd()),
 | |
|                     'output': os.path.relpath(cc.dest, os.getcwd())})
 | |
|         return ans, objects
 | |
| 
 | |
|     def get_link_command(self, ext, objects, lddb):
 | |
| 
 | |
|         def get(env: Environment) -> LinkCommand:
 | |
|             dest = self.dest(ext, env)
 | |
|             compiler = env.cxx if ext.needs_cxx else env.cc
 | |
|             linker = env.linker or compiler
 | |
|             cmd = [linker]
 | |
|             elib = env.lib_dirs_to_ldflags(ext.lib_dirs)
 | |
|             xlib = env.libraries_to_ldflags(ext.libraries)
 | |
|             all_objects = sorted(objects + ext.extra_objs)
 | |
|             if iswindows or env is self.windows_cross_env:
 | |
|                 pre_ld_flags = []
 | |
|                 if ext.uses_icu:
 | |
|                     # windows has its own ICU libs that don't work
 | |
|                     pre_ld_flags = elib
 | |
|                 cmd += pre_ld_flags + env.ldflags + ext.ldflags + elib + xlib + \
 | |
|                     ['/EXPORT:' + init_symbol_name(ext.name)] + all_objects + ['/OUT:'+dest]
 | |
|             else:
 | |
|                 cmd += all_objects + ['-o', dest] + env.ldflags + ext.ldflags + elib + xlib
 | |
|             return LinkCommand(cmd, objects, dest)
 | |
| 
 | |
|         env = self.env_for_compilation_db(ext)
 | |
|         if env is not None:
 | |
|             ld = get(env)
 | |
|             lddb.append({'arguments': ld.cmd, 'directory': os.getcwd(), 'output': os.path.relpath(ld.dest, os.getcwd())})
 | |
| 
 | |
|         env = self.windows_cross_env if self.compiling_for == 'windows' else self.env
 | |
|         lc = get(env)
 | |
|         if self.newer(lc.dest, objects+ext.extra_objs):
 | |
|             return lc
 | |
| 
 | |
|     def post_link_cleanup(self, link_command):
 | |
|         if iswindows:
 | |
|             dest = link_command.dest
 | |
|             for x in ('.exp', '.lib'):
 | |
|                 x = os.path.splitext(dest)[0]+x
 | |
|                 if os.path.exists(x):
 | |
|                     os.remove(x)
 | |
| 
 | |
|     def check_call(self, *args, **kwargs):
 | |
|         '''print cmdline if an error occurred
 | |
| 
 | |
|         If something is missing (cmake e.g.) you get a non-informative error
 | |
|          self.check_call(qmc + [ext.name+'.pro'])
 | |
|          so you would have to look at the source to see the actual command.
 | |
|         '''
 | |
|         try:
 | |
|             subprocess.check_call(*args, **kwargs)
 | |
|         except:
 | |
|             cmdline = ' '.join([f'"{arg}"' if ' ' in arg else arg for arg in args[0]])
 | |
|             print(f'Error while executing: {cmdline}\n')
 | |
|             raise
 | |
| 
 | |
|     def build_headless(self):
 | |
|         from setup.parallel_build import cpu_count
 | |
|         if iswindows or ishaiku:
 | |
|             return  # Don't have headless operation on these platforms
 | |
|         from setup.build_environment import CMAKE, sw
 | |
|         self.info('\n####### Building headless QPA plugin', '#'*7)
 | |
|         a = absolutize
 | |
|         headers = a([
 | |
|             'calibre/headless/headless_backingstore.h',
 | |
|             'calibre/headless/headless_integration.h',
 | |
|         ])
 | |
|         sources = a([
 | |
|             'calibre/headless/main.cpp',
 | |
|             'calibre/headless/headless_backingstore.cpp',
 | |
|             'calibre/headless/headless_integration.cpp',
 | |
|         ])
 | |
|         others = a(['calibre/headless/headless.json'])
 | |
|         target = self.dest('headless', self.env)
 | |
|         if not ismacos:
 | |
|             target = target.replace('headless', 'libheadless')
 | |
|         if not self.newer(target, headers + sources + others):
 | |
|             return
 | |
| 
 | |
|         bdir = self.j(self.build_dir, 'headless')
 | |
|         if os.path.exists(bdir):
 | |
|             shutil.rmtree(bdir)
 | |
|         cmd = [CMAKE]
 | |
|         if is_macos_universal_build:
 | |
|             cmd += ['-DCMAKE_OSX_ARCHITECTURES=x86_64;arm64']
 | |
|         if sw and os.path.exists(os.path.join(sw, 'qt')):
 | |
|             cmd += ['-DCMAKE_SYSTEM_PREFIX_PATH=' + os.path.join(sw, 'qt').replace(os.sep, '/')]
 | |
|         os.makedirs(bdir)
 | |
|         cwd = os.getcwd()
 | |
|         os.chdir(bdir)
 | |
|         try:
 | |
|             self.check_call(cmd + ['-S', os.path.dirname(sources[0])])
 | |
|             self.check_call([self.env.make] + [f'-j{cpu_count or 1}'])
 | |
|         finally:
 | |
|             os.chdir(cwd)
 | |
|         os.rename(self.j(bdir, 'libheadless.so'), target)
 | |
| 
 | |
|     def create_sip_build_skeleton(self, src_dir, ext):
 | |
|         from setup.build_environment import pyqt_sip_abi_version
 | |
|         abi_version = ''
 | |
|         if pyqt_sip_abi_version():
 | |
|             abi_version = f'abi-version = "{pyqt_sip_abi_version()}"'
 | |
|         sipf = ext.sip_files[0]
 | |
|         needs_exceptions = 'true' if ext.needs_exceptions else 'false'
 | |
|         with open(os.path.join(src_dir, 'pyproject.toml'), 'w') as f:
 | |
|             f.write(f'''
 | |
| [build-system]
 | |
| requires = ["sip >=5.3", "PyQt-builder >=1"]
 | |
| build-backend = "sipbuild.api"
 | |
| 
 | |
| [tool.sip]
 | |
| project-factory = "pyqtbuild:PyQtProject"
 | |
| 
 | |
| [tool.sip.project]
 | |
| sip-files-dir = "."
 | |
| {abi_version}
 | |
| 
 | |
| [project]
 | |
| name = "{ext.name}"
 | |
| 
 | |
| [tool.sip.builder]
 | |
| qmake-settings = [
 | |
|     """QMAKE_CC = {self.env.cc}""",
 | |
|     """QMAKE_CXX = {self.env.cxx}""",
 | |
|     """QMAKE_LINK = {self.env.linker or self.env.cxx}""",
 | |
|     """QMAKE_CFLAGS += {shlex.join(self.env.base_cflags)}""",
 | |
|     """QMAKE_CXXFLAGS += {shlex.join(self.env.base_cxxflags)}""",
 | |
|     """QMAKE_LFLAGS += {shlex.join(self.env.base_ldflags)}""",
 | |
| ]
 | |
| 
 | |
| [tool.sip.bindings.{ext.name}]
 | |
| headers = {sorted(ext.headers)}
 | |
| sources = {sorted(ext.sources)}
 | |
| exceptions = {needs_exceptions}
 | |
| include-dirs = {ext.inc_dirs}
 | |
| qmake-QT = {ext.qt_modules}
 | |
| sip-file = {os.path.basename(sipf)!r}
 | |
| ''')
 | |
|         shutil.copy2(sipf, src_dir)
 | |
| 
 | |
|     def get_sip_commands(self, ext):
 | |
|         from setup.build_environment import QMAKE
 | |
|         pyqt_dir = self.j(self.build_dir, 'pyqt')
 | |
|         src_dir = self.j(pyqt_dir, ext.name)
 | |
|         # TODO: Handle building extensions with multiple SIP files.
 | |
|         sipf = ext.sip_files[0]
 | |
|         sbf = self.j(src_dir, self.b(sipf)+'.sbf')
 | |
|         cmd = None
 | |
|         cwd = None
 | |
|         if self.newer(sbf, [sipf] + ext.headers + ext.sources):
 | |
|             shutil.rmtree(src_dir, ignore_errors=True)
 | |
|             os.makedirs(src_dir)
 | |
|             self.create_sip_build_skeleton(src_dir, ext)
 | |
|             cwd = src_dir
 | |
|             cmd = [
 | |
|                 sys.executable, '-m', 'sipbuild.tools.build',
 | |
|                 '--verbose', '--no-make', '--qmake', QMAKE
 | |
|             ]
 | |
|         return cmd, sbf, cwd
 | |
| 
 | |
|     def build_pyqt_extension(self, ext, sbf):
 | |
|         self.info(f'\n####### Building {ext.name} extension', '#'*7)
 | |
|         src_dir = os.path.dirname(sbf)
 | |
|         cwd = os.getcwd()
 | |
|         try:
 | |
|             os.chdir(os.path.join(src_dir, 'build'))
 | |
|             env = os.environ.copy()
 | |
|             if is_macos_universal_build:
 | |
|                 env['ARCHS'] = 'x86_64 arm64'
 | |
|             self.check_call([self.env.make] + ([] if iswindows else [f'-j{os.cpu_count() or 1}']), env=env)
 | |
|             e = 'pyd' if iswindows else 'so'
 | |
|             m = glob.glob(f'{ext.name}/{ext.name}.*{e}')
 | |
|             if not m:
 | |
|                 raise SystemExit(f'No built PyQt extension file in {os.path.join(os.getcwd(), ext.name)}')
 | |
|             if len(m) != 1:
 | |
|                 raise SystemExit(f'Found extra PyQt extension files: {m}')
 | |
|             shutil.copy2(m[0], self.dest(ext, self.env))
 | |
|             with open(sbf, 'w') as f:
 | |
|                 f.write('done')
 | |
|         finally:
 | |
|             os.chdir(cwd)
 | |
| 
 | |
|     def clean(self):
 | |
|         self.output_dir = self.DEFAULT_OUTPUTDIR
 | |
|         extensions = map(parse_extension, read_extensions())
 | |
|         env = init_env()
 | |
|         for ext in extensions:
 | |
|             dest = self.dest(ext, env)
 | |
|             b, d = os.path.basename(dest), os.path.dirname(dest)
 | |
|             b = b.split('.')[0] + '.*'
 | |
|             for x in glob.glob(os.path.join(d, b)):
 | |
|                 if os.path.exists(x):
 | |
|                     os.remove(x)
 | |
|         build_dir = self.DEFAULT_BUILDDIR
 | |
|         if os.path.exists(build_dir):
 | |
|             shutil.rmtree(build_dir)
 |