mirror of
				https://github.com/kovidgoyal/calibre.git
				synced 2025-11-03 19:17:02 -05:00 
			
		
		
		
	
		
			
				
	
	
		
			732 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
			
		
		
	
	
			732 lines
		
	
	
		
			28 KiB
		
	
	
	
		
			Python
		
	
	
	
	
	
#!/usr/bin/env python
 | 
						|
# vim:fileencoding=utf-8
 | 
						|
# License: GPLv3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
 | 
						|
 | 
						|
from __future__ import absolute_import, division, print_function, unicode_literals
 | 
						|
 | 
						|
import errno
 | 
						|
import glob
 | 
						|
import json
 | 
						|
import operator
 | 
						|
import os
 | 
						|
import plistlib
 | 
						|
import runpy
 | 
						|
import shutil
 | 
						|
import stat
 | 
						|
import subprocess
 | 
						|
import sys
 | 
						|
import tempfile
 | 
						|
import time
 | 
						|
import zipfile
 | 
						|
from functools import partial, reduce
 | 
						|
from itertools import repeat
 | 
						|
 | 
						|
from bypy.constants import (
 | 
						|
    OUTPUT_DIR, PREFIX, PYTHON, SRC as CALIBRE_DIR, python_major_minor_version
 | 
						|
)
 | 
						|
from bypy.utils import current_dir, mkdtemp, py_compile, timeit, walk
 | 
						|
 | 
						|
abspath, join, basename, dirname = os.path.abspath, os.path.join, os.path.basename, os.path.dirname
 | 
						|
iv = globals()['init_env']
 | 
						|
calibre_constants = iv['calibre_constants']
 | 
						|
QT_DLLS, QT_PLUGINS, PYQT_MODULES = iv['QT_DLLS'], iv['QT_PLUGINS'], iv['PYQT_MODULES']
 | 
						|
py_ver = '.'.join(map(str, python_major_minor_version()))
 | 
						|
sign_app = runpy.run_path(join(dirname(abspath(__file__)), 'sign.py'))['sign_app']
 | 
						|
 | 
						|
QT_PREFIX = os.path.join(PREFIX, 'qt')
 | 
						|
QT_FRAMEWORKS = [x.replace('5', '') for x in QT_DLLS]
 | 
						|
 | 
						|
ENV = dict(
 | 
						|
    FONTCONFIG_PATH='@executable_path/../Resources/fonts',
 | 
						|
    FONTCONFIG_FILE='@executable_path/../Resources/fonts/fonts.conf',
 | 
						|
    QT_PLUGIN_PATH='@executable_path/../MacOS/qt-plugins',
 | 
						|
    PYTHONIOENCODING='UTF-8',
 | 
						|
    SSL_CERT_FILE='@executable_path/../Resources/resources/mozilla-ca-certs.pem',
 | 
						|
)
 | 
						|
APPNAME, VERSION = calibre_constants['appname'], calibre_constants['version']
 | 
						|
basenames, main_modules, main_functions = calibre_constants['basenames'], calibre_constants['modules'], calibre_constants['functions']
 | 
						|
 | 
						|
 | 
						|
def compile_launcher_lib(contents_dir, gcc, base):
 | 
						|
    print('\tCompiling calibre_launcher.dylib')
 | 
						|
    fd = join(contents_dir, 'Frameworks')
 | 
						|
    dest = join(fd, 'calibre-launcher.dylib')
 | 
						|
    src = join(base, 'util.c')
 | 
						|
    cmd = [gcc] + '-Wall -dynamiclib -std=gnu99'.split() + [src] + \
 | 
						|
        ['-I' + base] + \
 | 
						|
        ['-I%s/python/Python.framework/Versions/Current/Headers' % PREFIX] + \
 | 
						|
        '-current_version 1.0 -compatibility_version 1.0'.split() + \
 | 
						|
        '-fvisibility=hidden -o'.split() + [dest] + \
 | 
						|
        ['-install_name',
 | 
						|
         '@executable_path/../Frameworks/' + os.path.basename(dest)] + \
 | 
						|
        [('-F%s/python' % PREFIX), '-framework', 'Python', '-framework', 'CoreFoundation', '-headerpad_max_install_names']
 | 
						|
    # print('\t'+' '.join(cmd))
 | 
						|
    sys.stdout.flush()
 | 
						|
    subprocess.check_call(cmd)
 | 
						|
    return dest
 | 
						|
 | 
						|
 | 
						|
gcc = os.environ.get('CC', 'clang')
 | 
						|
 | 
						|
 | 
						|
def compile_launchers(contents_dir, xprograms, pyver):
 | 
						|
    base = dirname(abspath(__file__))
 | 
						|
    lib = compile_launcher_lib(contents_dir, gcc, base)
 | 
						|
    with open(join(base, 'launcher.c'), 'rb') as f:
 | 
						|
        src = f.read().decode('utf-8')
 | 
						|
    env, env_vals = [], []
 | 
						|
    for key, val in ENV.items():
 | 
						|
        env.append('"%s"' % key)
 | 
						|
        env_vals.append('"%s"' % val)
 | 
						|
    env = ', '.join(env) + ', '
 | 
						|
    env_vals = ', '.join(env_vals) + ', '
 | 
						|
    src = src.replace('/*ENV_VARS*/', env)
 | 
						|
    src = src.replace('/*ENV_VAR_VALS*/', env_vals)
 | 
						|
    programs = [lib]
 | 
						|
    for program, x in xprograms.items():
 | 
						|
        module, func, ptype = x
 | 
						|
        print('\tCompiling', program)
 | 
						|
        out = join(contents_dir, 'MacOS', program)
 | 
						|
        programs.append(out)
 | 
						|
        psrc = src.replace('**PROGRAM**', program)
 | 
						|
        psrc = psrc.replace('**MODULE**', module)
 | 
						|
        psrc = psrc.replace('**FUNCTION**', func)
 | 
						|
        psrc = psrc.replace('**PYVER**', pyver)
 | 
						|
        psrc = psrc.replace('**IS_GUI**', ('1' if ptype == 'gui' else '0'))
 | 
						|
        fsrc = '/tmp/%s.c' % program
 | 
						|
        with open(fsrc, 'wb') as f:
 | 
						|
            f.write(psrc.encode('utf-8'))
 | 
						|
        cmd = [gcc, '-Wall', '-I' + base, fsrc, lib, '-o', out,
 | 
						|
               '-headerpad_max_install_names']
 | 
						|
        # print('\t'+' '.join(cmd))
 | 
						|
        sys.stdout.flush()
 | 
						|
        subprocess.check_call(cmd)
 | 
						|
    return programs
 | 
						|
 | 
						|
 | 
						|
def flipwritable(fn, mode=None):
 | 
						|
    """
 | 
						|
    Flip the writability of a file and return the old mode. Returns None
 | 
						|
    if the file is already writable.
 | 
						|
    """
 | 
						|
    if os.access(fn, os.W_OK):
 | 
						|
        return None
 | 
						|
    old_mode = os.stat(fn).st_mode
 | 
						|
    os.chmod(fn, stat.S_IWRITE | old_mode)
 | 
						|
    return old_mode
 | 
						|
 | 
						|
 | 
						|
STRIPCMD = ['/usr/bin/strip', '-x', '-S', '-']
 | 
						|
 | 
						|
 | 
						|
def strip_files(files, argv_max=(256 * 1024)):
 | 
						|
    """
 | 
						|
    Strip a list of files
 | 
						|
    """
 | 
						|
    tostrip = [(fn, flipwritable(fn)) for fn in files if os.path.exists(fn)]
 | 
						|
    while tostrip:
 | 
						|
        cmd = list(STRIPCMD)
 | 
						|
        flips = []
 | 
						|
        pathlen = reduce(operator.add, [len(s) + 1 for s in cmd])
 | 
						|
        while pathlen < argv_max:
 | 
						|
            if not tostrip:
 | 
						|
                break
 | 
						|
            added, flip = tostrip.pop()
 | 
						|
            pathlen += len(added) + 1
 | 
						|
            cmd.append(added)
 | 
						|
            flips.append((added, flip))
 | 
						|
        else:
 | 
						|
            cmd.pop()
 | 
						|
            tostrip.append(flips.pop())
 | 
						|
        os.spawnv(os.P_WAIT, cmd[0], cmd)
 | 
						|
        for args in flips:
 | 
						|
            flipwritable(*args)
 | 
						|
 | 
						|
 | 
						|
def flush(func):
 | 
						|
    def ff(*args, **kwargs):
 | 
						|
        sys.stdout.flush()
 | 
						|
        sys.stderr.flush()
 | 
						|
        ret = func(*args, **kwargs)
 | 
						|
        sys.stdout.flush()
 | 
						|
        sys.stderr.flush()
 | 
						|
        return ret
 | 
						|
    return ff
 | 
						|
 | 
						|
 | 
						|
class Freeze(object):
 | 
						|
 | 
						|
    FID = '@executable_path/../Frameworks'
 | 
						|
 | 
						|
    def __init__(self, build_dir, ext_dir, test_runner, test_launchers=False, dont_strip=False, sign_installers=False):
 | 
						|
        self.build_dir = build_dir
 | 
						|
        self.sign_installers = sign_installers
 | 
						|
        self.ext_dir = ext_dir
 | 
						|
        self.test_runner = test_runner
 | 
						|
        self.dont_strip = dont_strip
 | 
						|
        self.contents_dir = join(self.build_dir, 'Contents')
 | 
						|
        self.resources_dir = join(self.contents_dir, 'Resources')
 | 
						|
        self.frameworks_dir = join(self.contents_dir, 'Frameworks')
 | 
						|
        self.site_packages = join(self.resources_dir, 'Python', 'site-packages')
 | 
						|
        self.to_strip = []
 | 
						|
        self.warnings = []
 | 
						|
 | 
						|
        self.run(test_launchers)
 | 
						|
 | 
						|
    def run(self, test_launchers):
 | 
						|
        ret = 0
 | 
						|
        if not test_launchers:
 | 
						|
            if os.path.exists(self.build_dir):
 | 
						|
                shutil.rmtree(self.build_dir)
 | 
						|
            os.makedirs(self.build_dir)
 | 
						|
            self.create_skeleton()
 | 
						|
            self.create_plist()
 | 
						|
 | 
						|
            self.add_python_framework()
 | 
						|
            self.add_site_packages()
 | 
						|
            self.add_stdlib()
 | 
						|
            self.add_qt_frameworks()
 | 
						|
            self.add_calibre_plugins()
 | 
						|
            self.add_podofo()
 | 
						|
            self.add_poppler()
 | 
						|
            self.add_imaging_libs()
 | 
						|
            self.add_fontconfig()
 | 
						|
            self.add_misc_libraries()
 | 
						|
 | 
						|
            self.add_resources()
 | 
						|
            self.compile_py_modules()
 | 
						|
 | 
						|
        self.copy_site()
 | 
						|
        self.create_exe()
 | 
						|
        if not test_launchers and not self.dont_strip:
 | 
						|
            self.strip_files()
 | 
						|
        if not test_launchers:
 | 
						|
            self.create_console_app()
 | 
						|
            self.create_gui_apps()
 | 
						|
 | 
						|
        self.run_tests()
 | 
						|
        ret = self.makedmg(self.build_dir, APPNAME + '-' + VERSION)
 | 
						|
 | 
						|
        return ret
 | 
						|
 | 
						|
    @flush
 | 
						|
    def run_tests(self):
 | 
						|
        cc_dir = os.path.join(self.contents_dir, 'calibre-debug.app', 'Contents')
 | 
						|
        self.test_runner(join(cc_dir, 'MacOS', 'calibre-debug'), self.contents_dir)
 | 
						|
 | 
						|
    @flush
 | 
						|
    def add_resources(self):
 | 
						|
        shutil.copytree('resources', os.path.join(self.resources_dir,
 | 
						|
                                                  'resources'))
 | 
						|
 | 
						|
    @flush
 | 
						|
    def strip_files(self):
 | 
						|
        print('\nStripping files...')
 | 
						|
        strip_files(self.to_strip)
 | 
						|
 | 
						|
    @flush
 | 
						|
    def create_exe(self):
 | 
						|
        print('\nCreating launchers')
 | 
						|
        programs = {}
 | 
						|
        progs = []
 | 
						|
        for x in ('console', 'gui'):
 | 
						|
            progs += list(zip(basenames[x], main_modules[x], main_functions[x], repeat(x)))
 | 
						|
        for program, module, func, ptype in progs:
 | 
						|
            programs[program] = (module, func, ptype)
 | 
						|
        programs = compile_launchers(self.contents_dir, programs, py_ver)
 | 
						|
        for out in programs:
 | 
						|
            self.fix_dependencies_in_lib(out)
 | 
						|
 | 
						|
    @flush
 | 
						|
    def set_id(self, path_to_lib, new_id):
 | 
						|
        old_mode = flipwritable(path_to_lib)
 | 
						|
        subprocess.check_call(['install_name_tool', '-id', new_id, path_to_lib])
 | 
						|
        if old_mode is not None:
 | 
						|
            flipwritable(path_to_lib, old_mode)
 | 
						|
 | 
						|
    @flush
 | 
						|
    def get_dependencies(self, path_to_lib):
 | 
						|
        install_name = subprocess.check_output(['otool', '-D', path_to_lib]).splitlines()[-1].strip()
 | 
						|
        raw = subprocess.check_output(['otool', '-L', path_to_lib]).decode('utf-8')
 | 
						|
        for line in raw.splitlines():
 | 
						|
            if 'compatibility' not in line or line.strip().endswith(':'):
 | 
						|
                continue
 | 
						|
            idx = line.find('(')
 | 
						|
            path = line[:idx].strip()
 | 
						|
            yield path, path == install_name
 | 
						|
 | 
						|
    @flush
 | 
						|
    def get_local_dependencies(self, path_to_lib):
 | 
						|
        for x, is_id in self.get_dependencies(path_to_lib):
 | 
						|
            if x.startswith('@rpath/Qt'):
 | 
						|
                yield x, x[len('@rpath/'):], is_id
 | 
						|
            elif x.startswith('@rpath/libjpeg'):
 | 
						|
                yield x, x[len('@rpath/'):], is_id
 | 
						|
            else:
 | 
						|
                for y in (PREFIX + '/lib/', PREFIX + '/python/Python.framework/'):
 | 
						|
                    if x.startswith(y):
 | 
						|
                        if y == PREFIX + '/python/Python.framework/':
 | 
						|
                            y = PREFIX + '/python/'
 | 
						|
                        yield x, x[len(y):], is_id
 | 
						|
                        break
 | 
						|
 | 
						|
    @flush
 | 
						|
    def change_dep(self, old_dep, new_dep, is_id, path_to_lib):
 | 
						|
        cmd = ['-id', new_dep] if is_id else ['-change', old_dep, new_dep]
 | 
						|
        subprocess.check_call(['install_name_tool'] + cmd + [path_to_lib])
 | 
						|
 | 
						|
    @flush
 | 
						|
    def fix_dependencies_in_lib(self, path_to_lib):
 | 
						|
        self.to_strip.append(path_to_lib)
 | 
						|
        old_mode = flipwritable(path_to_lib)
 | 
						|
        for dep, bname, is_id in self.get_local_dependencies(path_to_lib):
 | 
						|
            ndep = self.FID + '/' + bname
 | 
						|
            self.change_dep(dep, ndep, is_id, path_to_lib)
 | 
						|
        ldeps = list(self.get_local_dependencies(path_to_lib))
 | 
						|
        if ldeps:
 | 
						|
            print('\nFailed to fix dependencies in', path_to_lib)
 | 
						|
            print('Remaining local dependencies:', ldeps)
 | 
						|
            raise SystemExit(1)
 | 
						|
        if old_mode is not None:
 | 
						|
            flipwritable(path_to_lib, old_mode)
 | 
						|
 | 
						|
    @flush
 | 
						|
    def add_python_framework(self):
 | 
						|
        print('\nAdding Python framework')
 | 
						|
        src = join(PREFIX + '/python', 'Python.framework')
 | 
						|
        x = join(self.frameworks_dir, 'Python.framework')
 | 
						|
        curr = os.path.realpath(join(src, 'Versions', 'Current'))
 | 
						|
        currd = join(x, 'Versions', basename(curr))
 | 
						|
        rd = join(currd, 'Resources')
 | 
						|
        os.makedirs(rd)
 | 
						|
        shutil.copy2(join(curr, 'Resources', 'Info.plist'), rd)
 | 
						|
        shutil.copy2(join(curr, 'Python'), currd)
 | 
						|
        self.set_id(join(currd, 'Python'),
 | 
						|
                    self.FID + '/Python.framework/Versions/%s/Python' % basename(curr))
 | 
						|
        # The following is needed for codesign in OS X >= 10.9.5
 | 
						|
        with current_dir(x):
 | 
						|
            os.symlink(basename(curr), 'Versions/Current')
 | 
						|
            for y in ('Python', 'Resources'):
 | 
						|
                os.symlink('Versions/Current/%s' % y, y)
 | 
						|
 | 
						|
    @flush
 | 
						|
    def add_qt_frameworks(self):
 | 
						|
        print('\nAdding Qt Frameworks')
 | 
						|
        for f in QT_FRAMEWORKS:
 | 
						|
            self.add_qt_framework(f)
 | 
						|
        pdir = join(QT_PREFIX, 'plugins')
 | 
						|
        ddir = join(self.contents_dir, 'MacOS', 'qt-plugins')
 | 
						|
        os.mkdir(ddir)
 | 
						|
        for x in QT_PLUGINS:
 | 
						|
            shutil.copytree(join(pdir, x), join(ddir, x))
 | 
						|
        for l in glob.glob(join(ddir, '*/*.dylib')):
 | 
						|
            self.fix_dependencies_in_lib(l)
 | 
						|
            x = os.path.relpath(l, ddir)
 | 
						|
            self.set_id(l, '@executable_path/' + x)
 | 
						|
 | 
						|
    def add_qt_framework(self, f):
 | 
						|
        libname = f
 | 
						|
        f = f + '.framework'
 | 
						|
        src = join(PREFIX, 'qt', 'lib', f)
 | 
						|
        ignore = shutil.ignore_patterns('Headers', '*.h', 'Headers/*')
 | 
						|
        dest = join(self.frameworks_dir, f)
 | 
						|
        shutil.copytree(src, dest, symlinks=True,
 | 
						|
                        ignore=ignore)
 | 
						|
        lib = os.path.realpath(join(dest, libname))
 | 
						|
        rpath = os.path.relpath(lib, self.frameworks_dir)
 | 
						|
        self.set_id(lib, self.FID + '/' + rpath)
 | 
						|
        self.fix_dependencies_in_lib(lib)
 | 
						|
        # The following is needed for codesign in OS X >= 10.9.5
 | 
						|
        # The presence of the .prl file in the root of the framework causes
 | 
						|
        # codesign to fail.
 | 
						|
        with current_dir(dest):
 | 
						|
            for x in os.listdir('.'):
 | 
						|
                if x != 'Versions' and not os.path.islink(x):
 | 
						|
                    os.remove(x)
 | 
						|
 | 
						|
    @flush
 | 
						|
    def create_skeleton(self):
 | 
						|
        c = join(self.build_dir, 'Contents')
 | 
						|
        for x in ('Frameworks', 'MacOS', 'Resources'):
 | 
						|
            os.makedirs(join(c, x))
 | 
						|
        icons = glob.glob(join(CALIBRE_DIR, 'icons', 'icns', '*.iconset'))
 | 
						|
        if not icons:
 | 
						|
            raise SystemExit('Failed to find icns format icons')
 | 
						|
        for x in icons:
 | 
						|
            subprocess.check_call([
 | 
						|
                'iconutil', '-c', 'icns', x, '-o', join(
 | 
						|
                    self.resources_dir, basename(x).partition('.')[0] + '.icns')])
 | 
						|
 | 
						|
    @flush
 | 
						|
    def add_calibre_plugins(self):
 | 
						|
        dest = join(self.frameworks_dir, 'plugins')
 | 
						|
        os.mkdir(dest)
 | 
						|
        plugins = glob.glob(self.ext_dir + '/*.so')
 | 
						|
        if not plugins:
 | 
						|
            raise SystemExit('No calibre plugins found in: ' + self.ext_dir)
 | 
						|
        for f in plugins:
 | 
						|
            shutil.copy2(f, dest)
 | 
						|
            self.fix_dependencies_in_lib(join(dest, basename(f)))
 | 
						|
            if f.endswith('/podofo.so'):
 | 
						|
                self.change_dep('libpodofo.0.9.6.dylib',
 | 
						|
                    '@executable_path/../Frameworks/libpodofo.0.9.6.dylib', False, join(dest, basename(f)))
 | 
						|
 | 
						|
    @flush
 | 
						|
    def create_plist(self):
 | 
						|
        BOOK_EXTENSIONS = calibre_constants['book_extensions']
 | 
						|
        env = dict(**ENV)
 | 
						|
        env['CALIBRE_LAUNCHED_FROM_BUNDLE'] = '1'
 | 
						|
        docs = [{'CFBundleTypeName': 'E-book',
 | 
						|
                 'CFBundleTypeExtensions': list(BOOK_EXTENSIONS),
 | 
						|
                 'CFBundleTypeIconFile': 'book.icns',
 | 
						|
                 'CFBundleTypeRole': 'Viewer',
 | 
						|
                 }]
 | 
						|
 | 
						|
        pl = dict(
 | 
						|
            CFBundleDevelopmentRegion='English',
 | 
						|
            CFBundleDisplayName=APPNAME,
 | 
						|
            CFBundleName=APPNAME,
 | 
						|
            CFBundleIdentifier='net.kovidgoyal.calibre',
 | 
						|
            CFBundleVersion=VERSION,
 | 
						|
            CFBundleShortVersionString=VERSION,
 | 
						|
            CFBundlePackageType='APPL',
 | 
						|
            CFBundleSignature='????',
 | 
						|
            CFBundleExecutable='calibre',
 | 
						|
            CFBundleDocumentTypes=docs,
 | 
						|
            LSMinimumSystemVersion='10.9.5',
 | 
						|
            LSRequiresNativeExecution=True,
 | 
						|
            NSAppleScriptEnabled=False,
 | 
						|
            NSHumanReadableCopyright=time.strftime('Copyright %Y, Kovid Goyal'),
 | 
						|
            CFBundleGetInfoString=('calibre, an E-book management '
 | 
						|
                                   'application. Visit https://calibre-ebook.com for details.'),
 | 
						|
            CFBundleIconFile='calibre.icns',
 | 
						|
            NSHighResolutionCapable=True,
 | 
						|
            LSApplicationCategoryType='public.app-category.productivity',
 | 
						|
            LSEnvironment=env
 | 
						|
        )
 | 
						|
        with open(join(self.contents_dir, 'Info.plist'), 'wb') as p:
 | 
						|
            plistlib.dump(pl, p)
 | 
						|
 | 
						|
    @flush
 | 
						|
    def install_dylib(self, path, set_id=True):
 | 
						|
        shutil.copy2(path, self.frameworks_dir)
 | 
						|
        if set_id:
 | 
						|
            self.set_id(join(self.frameworks_dir, basename(path)),
 | 
						|
                        self.FID + '/' + basename(path))
 | 
						|
        self.fix_dependencies_in_lib(join(self.frameworks_dir, basename(path)))
 | 
						|
 | 
						|
    @flush
 | 
						|
    def add_podofo(self):
 | 
						|
        print('\nAdding PoDoFo')
 | 
						|
        pdf = join(PREFIX, 'lib', 'libpodofo.0.9.6.dylib')
 | 
						|
        self.install_dylib(pdf)
 | 
						|
 | 
						|
    @flush
 | 
						|
    def add_poppler(self):
 | 
						|
        print('\nAdding poppler')
 | 
						|
        for x in ('libpoppler.87.dylib',):
 | 
						|
            self.install_dylib(os.path.join(PREFIX, 'lib', x))
 | 
						|
        for x in ('pdftohtml', 'pdftoppm', 'pdfinfo'):
 | 
						|
            self.install_dylib(os.path.join(PREFIX, 'bin', x), False)
 | 
						|
 | 
						|
    @flush
 | 
						|
    def add_imaging_libs(self):
 | 
						|
        print('\nAdding libjpeg, libpng, libwebp, optipng and mozjpeg')
 | 
						|
        for x in ('jpeg.8', 'png16.16', 'webp.7', 'webpmux.3', 'webpdemux.2'):
 | 
						|
            self.install_dylib(os.path.join(PREFIX, 'lib', 'lib%s.dylib' % x))
 | 
						|
        for x in 'optipng', 'JxrDecApp':
 | 
						|
            self.install_dylib(os.path.join(PREFIX, 'bin', x), False)
 | 
						|
        for x in ('jpegtran', 'cjpeg'):
 | 
						|
            self.install_dylib(os.path.join(PREFIX, 'private', 'mozjpeg', 'bin', x), False)
 | 
						|
 | 
						|
    @flush
 | 
						|
    def add_fontconfig(self):
 | 
						|
        print('\nAdding fontconfig')
 | 
						|
        for x in ('fontconfig.1', 'freetype.6', 'expat.1'):
 | 
						|
            src = os.path.join(PREFIX, 'lib', 'lib' + x + '.dylib')
 | 
						|
            self.install_dylib(src)
 | 
						|
        dst = os.path.join(self.resources_dir, 'fonts')
 | 
						|
        if os.path.exists(dst):
 | 
						|
            shutil.rmtree(dst)
 | 
						|
        src = os.path.join(PREFIX, 'etc', 'fonts')
 | 
						|
        shutil.copytree(src, dst, symlinks=False)
 | 
						|
        fc = os.path.join(dst, 'fonts.conf')
 | 
						|
        with open(fc, 'rb') as f:
 | 
						|
            raw = f.read().decode('utf-8')
 | 
						|
        raw = raw.replace('<dir>/usr/share/fonts</dir>', '''\
 | 
						|
        <dir>/Library/Fonts</dir>
 | 
						|
        <dir>/System/Library/Fonts</dir>
 | 
						|
        <dir>/usr/X11R6/lib/X11/fonts</dir>
 | 
						|
        <dir>/usr/share/fonts</dir>
 | 
						|
        <dir>/var/root/Library/Fonts</dir>
 | 
						|
        <dir>/usr/share/fonts</dir>
 | 
						|
        ''')
 | 
						|
        open(fc, 'wb').write(raw.encode('utf-8'))
 | 
						|
 | 
						|
    @flush
 | 
						|
    def add_misc_libraries(self):
 | 
						|
        for x in (
 | 
						|
                'usb-1.0.0', 'mtp.9', 'chm.0', 'sqlite3.0',
 | 
						|
                'icudata.64', 'icui18n.64', 'icuio.64', 'icuuc.64',
 | 
						|
                'xslt.1', 'exslt.0', 'xml2.2', 'z.1', 'unrar',
 | 
						|
                'crypto.1.0.0', 'ssl.1.0.0', 'iconv.2',  # 'ltdl.7'
 | 
						|
        ):
 | 
						|
            print('\nAdding', x)
 | 
						|
            x = 'lib%s.dylib' % x
 | 
						|
            src = join(PREFIX, 'lib', x)
 | 
						|
            shutil.copy2(src, self.frameworks_dir)
 | 
						|
            dest = join(self.frameworks_dir, x)
 | 
						|
            self.set_id(dest, self.FID + '/' + x)
 | 
						|
            self.fix_dependencies_in_lib(dest)
 | 
						|
 | 
						|
    @flush
 | 
						|
    def add_site_packages(self):
 | 
						|
        print('\nAdding site-packages')
 | 
						|
        os.makedirs(self.site_packages)
 | 
						|
        sys_path = json.loads(subprocess.check_output([
 | 
						|
            PYTHON, '-c', 'import sys, json; json.dump(sys.path, sys.stdout)']))
 | 
						|
        paths = reversed(tuple(map(abspath, [x for x in sys_path if x.startswith('/') and not x.startswith('/Library/')])))
 | 
						|
        upaths = []
 | 
						|
        for x in paths:
 | 
						|
            if x not in upaths and (x.endswith('.egg') or x.endswith('/site-packages')):
 | 
						|
                upaths.append(x)
 | 
						|
        upaths.append(join(CALIBRE_DIR, 'src'))
 | 
						|
        for x in upaths:
 | 
						|
            print('\t', x)
 | 
						|
            tdir = None
 | 
						|
            try:
 | 
						|
                if not os.path.isdir(x):
 | 
						|
                    zf = zipfile.ZipFile(x)
 | 
						|
                    tdir = tempfile.mkdtemp()
 | 
						|
                    zf.extractall(tdir)
 | 
						|
                    x = tdir
 | 
						|
                self.add_modules_from_dir(x)
 | 
						|
                self.add_packages_from_dir(x)
 | 
						|
            finally:
 | 
						|
                if tdir is not None:
 | 
						|
                    shutil.rmtree(tdir)
 | 
						|
        try:
 | 
						|
            shutil.rmtree(os.path.join(self.site_packages, 'calibre', 'plugins'))
 | 
						|
        except OSError as err:
 | 
						|
            if err.errno != errno.ENOENT:
 | 
						|
                raise
 | 
						|
        sp = join(self.resources_dir, 'Python', 'site-packages')
 | 
						|
        for x in os.listdir(join(sp, 'PyQt5')):
 | 
						|
            if x.endswith('.so') and x.rpartition('.')[0] not in PYQT_MODULES and x != 'sip.so':
 | 
						|
                os.remove(join(sp, 'PyQt5', x))
 | 
						|
        os.remove(join(sp, 'PyQt5', 'uic/port_v3/proxy_base.py'))
 | 
						|
        self.remove_bytecode(sp)
 | 
						|
 | 
						|
    @flush
 | 
						|
    def add_modules_from_dir(self, src):
 | 
						|
        for x in glob.glob(join(src, '*.py')) + glob.glob(join(src, '*.so')):
 | 
						|
            dest = os.path.join(self.site_packages, os.path.basename(x))
 | 
						|
            shutil.copy2(x, dest)
 | 
						|
            if x.endswith('.so'):
 | 
						|
                self.fix_dependencies_in_lib(dest)
 | 
						|
 | 
						|
    @flush
 | 
						|
    def add_packages_from_dir(self, src):
 | 
						|
        for x in os.listdir(src):
 | 
						|
            x = join(src, x)
 | 
						|
            if os.path.isdir(x) and os.path.exists(join(x, '__init__.py')):
 | 
						|
                if self.filter_package(basename(x)):
 | 
						|
                    continue
 | 
						|
                self.add_package_dir(x)
 | 
						|
 | 
						|
    @flush
 | 
						|
    def add_package_dir(self, x, dest=None):
 | 
						|
        def ignore(root, files):
 | 
						|
            ans = []
 | 
						|
            for y in files:
 | 
						|
                ext = os.path.splitext(y)[1]
 | 
						|
                if ext not in ('', '.py', '.so') or \
 | 
						|
                        (not ext and not os.path.isdir(join(root, y))):
 | 
						|
                    ans.append(y)
 | 
						|
 | 
						|
            return ans
 | 
						|
        if dest is None:
 | 
						|
            dest = self.site_packages
 | 
						|
        dest = join(dest, basename(x))
 | 
						|
        shutil.copytree(x, dest, symlinks=True, ignore=ignore)
 | 
						|
        self.postprocess_package(x, dest)
 | 
						|
        for x in os.walk(dest):
 | 
						|
            for f in x[-1]:
 | 
						|
                if f.endswith('.so'):
 | 
						|
                    f = join(x[0], f)
 | 
						|
                    self.fix_dependencies_in_lib(f)
 | 
						|
 | 
						|
    @flush
 | 
						|
    def filter_package(self, name):
 | 
						|
        return name in ('Cython', 'modulegraph', 'macholib', 'py2app',
 | 
						|
                        'bdist_mpkg', 'altgraph')
 | 
						|
 | 
						|
    @flush
 | 
						|
    def postprocess_package(self, src_path, dest_path):
 | 
						|
        pass
 | 
						|
 | 
						|
    @flush
 | 
						|
    def add_stdlib(self):
 | 
						|
        print('\nAdding python stdlib')
 | 
						|
        src = PREFIX + '/python/Python.framework/Versions/Current/lib/python'
 | 
						|
        src += py_ver
 | 
						|
        dest = join(self.resources_dir, 'Python', 'lib', 'python')
 | 
						|
        dest += py_ver
 | 
						|
        os.makedirs(dest)
 | 
						|
        for x in os.listdir(src):
 | 
						|
            if x in ('site-packages', 'config', 'test', 'lib2to3', 'lib-tk',
 | 
						|
                     'lib-old', 'idlelib', 'plat-mac', 'plat-darwin', 'site.py'):
 | 
						|
                continue
 | 
						|
            x = join(src, x)
 | 
						|
            if os.path.isdir(x):
 | 
						|
                self.add_package_dir(x, dest)
 | 
						|
            elif os.path.splitext(x)[1] in ('.so', '.py'):
 | 
						|
                shutil.copy2(x, dest)
 | 
						|
                dest2 = join(dest, basename(x))
 | 
						|
                if dest2.endswith('.so'):
 | 
						|
                    self.fix_dependencies_in_lib(dest2)
 | 
						|
 | 
						|
        target = join(self.resources_dir, 'Python', 'lib')
 | 
						|
        self.remove_bytecode(target)
 | 
						|
        for path in walk(target):
 | 
						|
            if path.endswith('.so'):
 | 
						|
                self.fix_dependencies_in_lib(path)
 | 
						|
 | 
						|
    @flush
 | 
						|
    def remove_bytecode(self, dest):
 | 
						|
        for x in os.walk(dest):
 | 
						|
            root = x[0]
 | 
						|
            for f in x[-1]:
 | 
						|
                if os.path.splitext(f) in ('.pyc', '.pyo'):
 | 
						|
                    os.remove(join(root, f))
 | 
						|
 | 
						|
    @flush
 | 
						|
    def compile_py_modules(self):
 | 
						|
        print('\nCompiling Python modules')
 | 
						|
        base = join(self.resources_dir, 'Python')
 | 
						|
        py_compile(base)
 | 
						|
 | 
						|
    def create_app_clone(self, name, specialise_plist, remove_doc_types=True):
 | 
						|
        print('\nCreating ' + name)
 | 
						|
        cc_dir = os.path.join(self.contents_dir, name, 'Contents')
 | 
						|
        exe_dir = join(cc_dir, 'MacOS')
 | 
						|
        os.makedirs(exe_dir)
 | 
						|
        for x in os.listdir(self.contents_dir):
 | 
						|
            if x.endswith('.app'):
 | 
						|
                continue
 | 
						|
            if x == 'Info.plist':
 | 
						|
                with open(join(self.contents_dir, x), 'rb') as r:
 | 
						|
                    plist = plistlib.load(r)
 | 
						|
                specialise_plist(plist)
 | 
						|
                if remove_doc_types:
 | 
						|
                    plist.pop('CFBundleDocumentTypes')
 | 
						|
                exe = plist['CFBundleExecutable']
 | 
						|
                # We cannot symlink the bundle executable as if we do,
 | 
						|
                # codesigning fails
 | 
						|
                plist['CFBundleExecutable'] = exe + '-placeholder-for-codesigning'
 | 
						|
                nexe = join(exe_dir, plist['CFBundleExecutable'])
 | 
						|
                base = os.path.dirname(abspath(__file__))
 | 
						|
                cmd = [gcc, '-Wall', '-Werror', '-DEXE_NAME="%s"' % exe, join(base, 'placeholder.c'), '-o', nexe, '-headerpad_max_install_names']
 | 
						|
                subprocess.check_call(cmd)
 | 
						|
                with open(join(cc_dir, x), 'wb') as p:
 | 
						|
                    plistlib.dump(plist, p)
 | 
						|
            elif x == 'MacOS':
 | 
						|
                for item in os.listdir(join(self.contents_dir, 'MacOS')):
 | 
						|
                    os.symlink('../../../MacOS/' + item, join(exe_dir, item))
 | 
						|
            else:
 | 
						|
                os.symlink(join('../..', x), join(cc_dir, x))
 | 
						|
 | 
						|
    @flush
 | 
						|
    def create_console_app(self):
 | 
						|
        def specialise_plist(plist):
 | 
						|
            plist['LSBackgroundOnly'] = '1'
 | 
						|
            plist['CFBundleIdentifier'] = 'com.calibre-ebook.console'
 | 
						|
            plist['CFBundleExecutable'] = 'calibre-parallel'
 | 
						|
        self.create_app_clone('console.app', specialise_plist)
 | 
						|
 | 
						|
    @flush
 | 
						|
    def create_gui_apps(self):
 | 
						|
        input_formats = sorted(set(json.loads(
 | 
						|
            subprocess.check_output([
 | 
						|
                join(self.contents_dir, 'MacOS', 'calibre-debug'), '-c',
 | 
						|
                'from calibre.customize.ui import all_input_formats; import sys, json; sys.stdout.write(json.dumps(tuple(all_input_formats())))'
 | 
						|
            ])
 | 
						|
        )))
 | 
						|
 | 
						|
        def specialise_plist(launcher, remove_types, plist):
 | 
						|
            plist['CFBundleDisplayName'] = plist['CFBundleName'] = {
 | 
						|
                'ebook-viewer': 'E-book Viewer', 'ebook-edit': 'Edit Book', 'calibre-debug': 'calibre (debug)',
 | 
						|
            }[launcher]
 | 
						|
            plist['CFBundleExecutable'] = launcher
 | 
						|
            if launcher != 'calibre-debug':
 | 
						|
                plist['CFBundleIconFile'] = launcher + '.icns'
 | 
						|
            plist['CFBundleIdentifier'] = 'com.calibre-ebook.' + launcher
 | 
						|
            if not remove_types:
 | 
						|
                e = plist['CFBundleDocumentTypes'][0]
 | 
						|
                exts = 'epub azw3'.split() if launcher == 'ebook-edit' else input_formats
 | 
						|
                e['CFBundleTypeExtensions'] = exts
 | 
						|
        for launcher in ('ebook-viewer', 'ebook-edit', 'calibre-debug'):
 | 
						|
            remove_types = launcher == 'calibre-debug'
 | 
						|
            self.create_app_clone(launcher + '.app', partial(specialise_plist, launcher, remove_types), remove_doc_types=remove_types)
 | 
						|
 | 
						|
    @flush
 | 
						|
    def copy_site(self):
 | 
						|
        base = os.path.dirname(abspath(__file__))
 | 
						|
        shutil.copy2(join(base, 'site.py'), join(self.resources_dir, 'Python',
 | 
						|
                                                 'lib', 'python' + py_ver))
 | 
						|
 | 
						|
    @flush
 | 
						|
    def makedmg(self, d, volname, internet_enable=True, format='ULFO'):
 | 
						|
        ''' Copy a directory d into a dmg named volname '''
 | 
						|
        print('\nSigning...')
 | 
						|
        sys.stdout.flush()
 | 
						|
        destdir = OUTPUT_DIR
 | 
						|
        try:
 | 
						|
            shutil.rmtree(destdir)
 | 
						|
        except EnvironmentError as err:
 | 
						|
            if err.errno != errno.ENOENT:
 | 
						|
                raise
 | 
						|
        os.mkdir(destdir)
 | 
						|
        dmg = os.path.join(destdir, volname + '.dmg')
 | 
						|
        if os.path.exists(dmg):
 | 
						|
            os.unlink(dmg)
 | 
						|
        tdir = tempfile.mkdtemp()
 | 
						|
        appdir = os.path.join(tdir, os.path.basename(d))
 | 
						|
        shutil.copytree(d, appdir, symlinks=True)
 | 
						|
        if self.sign_installers:
 | 
						|
            with timeit() as times:
 | 
						|
                sign_app(appdir)
 | 
						|
            print('Signing completed in %d minutes %d seconds' % tuple(times))
 | 
						|
        os.symlink('/Applications', os.path.join(tdir, 'Applications'))
 | 
						|
        size_in_mb = int(subprocess.check_output(['du', '-s', '-k', tdir]).decode('utf-8').split()[0]) / 1024.
 | 
						|
        cmd = ['/usr/bin/hdiutil', 'create', '-srcfolder', tdir, '-volname', volname, '-format', format]
 | 
						|
        if 190 < size_in_mb < 250:
 | 
						|
            # We need -size 255m because of a bug in hdiutil. When the size of
 | 
						|
            # srcfolder is close to 200MB hdiutil fails with
 | 
						|
            # diskimages-helper: resize request is above maximum size allowed.
 | 
						|
            cmd += ['-size', '255m']
 | 
						|
        print('\nCreating dmg...')
 | 
						|
        with timeit() as times:
 | 
						|
            subprocess.check_call(cmd + [dmg])
 | 
						|
            if internet_enable:
 | 
						|
                subprocess.check_call(['/usr/bin/hdiutil', 'internet-enable', '-yes', dmg])
 | 
						|
        print('dmg created in %d minutes and %d seconds' % tuple(times))
 | 
						|
        shutil.rmtree(tdir)
 | 
						|
        size = os.stat(dmg).st_size / (1024 * 1024.)
 | 
						|
        print('\nInstaller size: %.2fMB\n' % size)
 | 
						|
        return dmg
 | 
						|
 | 
						|
 | 
						|
def main(args, ext_dir, test_runner):
 | 
						|
    build_dir = abspath(join(mkdtemp('frozen-'), APPNAME + '.app'))
 | 
						|
    if args.skip_tests:
 | 
						|
        test_runner = lambda *a: None
 | 
						|
    Freeze(build_dir, ext_dir, test_runner, dont_strip=args.dont_strip, sign_installers=args.sign_installers)
 | 
						|
 | 
						|
 | 
						|
if __name__ == '__main__':
 | 
						|
    args = globals()['args']
 | 
						|
    ext_dir = globals()['ext_dir']
 | 
						|
    run_tests = iv['run_tests']
 | 
						|
    main(args, ext_dir, run_tests)
 |