Logo Search packages:      
Sourcecode: calibre version File versions

main.py

#!/usr/bin/env python
# vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai
from __future__ import with_statement

__license__   = 'GPL v3'
__copyright__ = '2009, Kovid Goyal <kovid@kovidgoyal.net>'
__docformat__ = 'restructuredtext en'

import sys, os, shutil, plistlib, subprocess, glob, zipfile, tempfile, \
    py_compile, stat, operator
abspath, join, basename = os.path.abspath, os.path.join, os.path.basename

l = {}
exec open('setup.py').read() in l
VERSION = l['VERSION']
APPNAME = l['APPNAME']
scripts = l['scripts']
basenames = l['basenames']
main_functions = l['main_functions']
main_modules = l['main_modules']
LICENSE = open('LICENSE', 'rb').read()
ENV = dict(
        PYTHONPATH='@executable_path/../Resources/Python/site-packages',
        PYTHONHOME='@executable_path/../Resources/Python',
        FC_CONFIG_DIR='@executable_path/../Resources/fonts',
        MAGICK_HOME='@executable_path/../Frameworks/ImageMagick',
        QT_PLUGIN_PATH='@executable_path/../MacOS',
        PYTHONDONTWRITEBYTECODE='1',
        PYTHONIOENCODING='utf-8:replace',
        PYTHONOPTIMIZE='2',
        )

SW = os.environ.get('SW', '/sw')

def compile_launchers(contents_dir, xprograms):
    gcc = os.environ.get('CC', 'gcc')
    base = os.path.dirname(__file__)
    src = open(join(base, 'launcher.c'), 'rb').read()
    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 = []
    for program, module in xprograms.items():
        print '\tCompiling', program
        out = join(contents_dir, 'MacOS', program)
        programs.append(out)
        psrc = src.replace('**PROGRAM**', program)
        psrc = psrc.replace('**MODULE**', module)
        fsrc = '/tmp/%s.c'%program
        with open(fsrc, 'wb') as f:
            f.write(psrc)
        cmd = [gcc, '-Wall', '-arch', 'x86_64',
            '-I%s/python/Python.framework/Headers'%SW,
            fsrc, '-o', out, '-F%s/python'%SW,
            '-framework', 'Python', '-framework', 'CoreFoundation',
            '-headerpad_max_install_names']
        print ' '.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

def thin(path):
    try:
        subprocess.check_call(['lipo', path, '-verify_arch', 'ppc64'])
        print '\tThinning', path
    except:
        return
    else:
        subprocess.check_call(['lipo', path, '-thin', 'x86_64', '-output', path])

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]
    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 Py2App(object):

    FID = '@executable_path/../Frameworks'

    def __init__(self, build_dir):
        self.build_dir = build_dir
        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.version_info = '.'.join(map(str, sys.version_info[:2]))
        self.to_strip = []
        self.warnings = []

    def warn(self, *args):
        self.warnings.append(args)
        prefix = '' if args and args[0].startswith('WARNING:') else 'WARNING: '
        sys.stdout.write(prefix+' '.join(args)+'\n')
        sys.stdout.flush()


    def run(self):
        self.create_skeleton()
        self.create_plist()

        self.add_python_framework()
        self.add_qt_frameworks()
        self.add_calibre_plugins()
        self.add_podofo()
        self.add_poppler()
        self.add_libjpeg()
        self.add_libpng()
        self.add_fontconfig()
        self.add_imagemagick()
        self.add_misc_libraries()

        self.add_site_packages()
        self.add_stdlib()
        self.compile_py_modules()

        self.create_console_app()

        self.copy_launcher_and_site()
        self.create_exe()
        self.thin_to_x86_64()
        self.strip_files()

        ret = self.makedmg(self.build_dir, APPNAME+'-'+VERSION+'-x86_64')
        sys.stdout.flush()
        sys.stderr.flush()

        print '\nThere were', len(self.warnings), 'warnings'
        for w in list(self.warnings):
            print
            self.warn(*w)
        return ret

    @flush
    def thin_to_x86_64(self):
        print '\nThinning to x86_64'
        for y in (self.frameworks_dir, join(self.resources_dir, 'Python')):
            for x in os.walk(y):
                for f in x[-1]:
                    f = join(x[0], f)
                    if not os.path.isfile(f): continue
                    for t in ('.so', '.dylib', '/Python'):
                        if f.endswith(t):
                            thin(f)
                            break

    @flush
    def strip_files(self):
        print '\nStripping files...'
        strip_files(self.to_strip)

    @flush
    def create_exe(self):
        print '\nCreating launchers'
        programs = {}
        for program, module in zip(basenames['console'],
                main_modules['console'])+zip(basenames['gui'],
                main_modules['gui']):
            programs[program] = module
        programs = compile_launchers(self.contents_dir, programs)
        for out in programs:
            self.fix_dependencies_in_lib(out)
        for module in main_modules['console'] + main_modules['gui']:
            base = join(*module.split('.'))+'.py'
            shutil.copy2(join('src', base),
                    join(self.resources_dir, 'Python', 'site-packages', base))

    @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):
        raw = subprocess.Popen(['otool', '-L', path_to_lib],
                stdout=subprocess.PIPE).stdout.read()
        for line in raw.splitlines():
            if 'compatibility' not in line or line.strip().endswith(':'):
                continue
            idx = line.find('(')
            path = line[:idx].strip()
            yield path

    @flush
    def get_local_dependencies(self, path_to_lib):
        for x in self.get_dependencies(path_to_lib):
            for y in (SW+'/lib/', '/usr/local/lib/', SW+'/qt/lib/',
                    SW+'/python/'):
                if x.startswith(y):
                    yield x, x[len(y):]
                    break

    @flush
    def change_dep(self, old_dep, new_dep, path_to_lib):
        print '\tResolving dependency %s to'%old_dep, new_dep
        subprocess.check_call(['install_name_tool', '-change', old_dep, new_dep,
            path_to_lib])

    @flush
    def fix_dependencies_in_lib(self, path_to_lib):
        print '\nFixing dependencies in', path_to_lib
        self.to_strip.append(path_to_lib)
        old_mode = flipwritable(path_to_lib)
        for dep, bname in self.get_local_dependencies(path_to_lib):
            ndep = self.FID+'/'+bname
            self.change_dep(dep, ndep, path_to_lib)
        if list(self.get_local_dependencies(path_to_lib)):
            raise Exception('Failed to resolve deps in: '+path_to_lib)
        if old_mode is not None:
            flipwritable(path_to_lib, old_mode)

    @flush
    def add_python_framework(self):
        print '\nAdding Python framework'
        src = join(SW, '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))

    @flush
    def add_qt_frameworks(self):
        for f in ('QtCore', 'QtGui', 'QtXml', 'QtNetwork', 'QtSvg', 'QtWebkit',
                'phonon'):
            self.add_qt_framework(f)
        for d in glob.glob(join(SW, 'qt', 'plugins', '*')):
            shutil.copytree(d, join(self.contents_dir, 'MacOS', basename(d)))
        for l in glob.glob(join(self.contents_dir, 'MacOS', '*/*.dylib')):
            self.fix_dependencies_in_lib(l)
            x = os.path.relpath(l, join(self.contents_dir, 'MacOS'))
            self.set_id(l, '@executable_path/'+x)

    @flush
    def add_qt_framework(self, f):
        libname = f
        f = f+'.framework'
        src = join(SW, '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)

    @flush
    def create_skeleton(self):
        c = join(self.build_dir, 'Contents')
        for x in ('Frameworks', 'MacOS', 'Resources'):
            os.makedirs(join(c, x))
        x = 'library.icns'
        shutil.copyfile(join('icons', x), join(self.resources_dir, x))

    @flush
    def add_calibre_plugins(self):
        dest = join(self.frameworks_dir, 'plugins')
        os.mkdir(dest)
        for f in glob.glob('src/calibre/plugins/*.so'):
            shutil.copy2(f, dest)
            self.fix_dependencies_in_lib(join(dest, basename(f)))
            if 'podofo' in f:
                self.change_dep('libpodofo.0.6.99.dylib',
                self.FID+'/'+'libpodofo.0.6.99.dylib', join(dest, basename(f)))


    @flush
    def create_plist(self):
        env = dict(**ENV)
        env['CALIBRE_LAUNCHED_FROM_BUNDLE']='1';

        pl = dict(
                CFBundleDevelopmentRegion='English',
                CFBundleDisplayName=APPNAME,
                CFBundleName=APPNAME,
                CFBundleIdentifier='net.kovidgoyal.calibre',
                CFBundleVersion=VERSION,
                CFBundlePackageType='APPL',
                CFBundleSignature='????',
                CFBundleExecutable='calibre',
                LSMinimumSystemVersion='10.5.2',
                LSRequiresNativeExecution=True,
                NSAppleScriptEnabled=False,
                NSHumanReadableCopyright='Copyright 2008, Kovid Goyal',
                CFBundleGetInfoString=('calibre, an E-book management '
                'application. Visit http://calibre.kovidgoyal.net for details.'),
                CFBundleIconFile='library.icns',
                LSMultipleInstancesProhibited=True,
                LSEnvironment=env
        )
        plistlib.writePlist(pl, join(self.contents_dir, 'Info.plist'))

    @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(SW, 'lib', 'libpodofo.0.6.99.dylib')
        self.install_dylib(pdf)

    @flush
    def add_poppler(self):
        print '\nAdding poppler'
        for x in ('libpoppler.4.dylib', 'libpoppler-qt4.3.dylib'):
            self.install_dylib(os.path.join(SW, 'lib', x))
        self.install_dylib(os.path.join(SW, 'bin', 'pdftohtml'), False)

    @flush
    def add_libjpeg(self):
        print '\nAdding libjpeg'
        self.install_dylib(os.path.join(SW, 'lib', 'libjpeg.7.dylib'))

    @flush
    def add_libpng(self):
        print '\nAdding libpng'
        self.install_dylib(os.path.join(SW, 'lib', 'libpng12.0.dylib'))

    @flush
    def add_fontconfig(self):
        print '\nAdding fontconfig'
        for x in ('fontconfig.1', 'freetype.6', 'expat.1'):
            src = os.path.join(SW, '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(SW, 'etc', 'fonts')
        shutil.copytree(src, dst, symlinks=False)
        fc = os.path.join(dst, 'fonts.conf')
        raw = open(fc, 'rb').read()
        raw = raw.replace('<dir>/usr/share/fonts</dir>', '''\
        <dir>/Library/Fonts</dir>
        <dir>/Network/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)

    @flush
    def add_imagemagick(self):
        print '\nAdding ImageMagick'
        for x in ('Wand', 'Core'):
            self.install_dylib(os.path.join(SW, 'lib', 'libMagick%s.2.dylib'%x))
        idir = glob.glob(os.path.join(SW, 'lib', 'ImageMagick-*'))[-1]
        dest = os.path.join(self.frameworks_dir, 'ImageMagick', 'lib')
        if not os.path.exists(dest):
            os.makedirs(dest)
        dest = os.path.join(dest, os.path.basename(idir))
        if os.path.exists(dest):
            shutil.rmtree(dest)
        shutil.copytree(idir, dest, True)
        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 add_misc_libraries(self):
        for x in ('usb', 'unrar', 'readline.6.0'):
            print '\nAdding', x
            x = 'lib%s.dylib'%x
            shutil.copy2(join(SW, 'lib', x), self.frameworks_dir)
            self.set_id(join(self.frameworks_dir, x), self.FID+'/'+x)

    @flush
    def add_site_packages(self):
        print '\nAdding site-packages'
        self.site_packages = join(self.resources_dir, 'Python', 'site-packages')
        os.makedirs(self.site_packages)
        paths = reversed(map(abspath, [x for x in sys.path if x.startswith('/')]))
        upaths = []
        for x in paths:
            if x.endswith('/PIL') or 'site-packages' not in x:
                continue
            if x not in upaths:
                upaths.append(x)
        upaths.append(os.path.expanduser('~/build/calibre/src'))
        for x in upaths:
            tdir = None
            try:
                if not os.path.isdir(x):
                    try:
                        zf = zipfile.ZipFile(x)
                    except:
                        self.warn(x, 'is neither a directory nor a zipfile')
                        continue
                    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)
        self.remove_bytecode(join(self.resources_dir, 'Python', 'site-packages'))

    @flush
    def add_modules_from_dir(self, src):
        for x in glob.glob(join(src, '*.py'))+glob.glob(join(src, '*.so')):
            dest = join(self.site_packages, basename(x))
            shutil.copy2(x, self.site_packages)
            if x.endswith('.so'):
                self.fix_dependencies_in_lib(x)

    @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 = join(SW, 'python/Python.framework/Versions/Current/lib/python')
        src += self.version_info
        dest = join(self.resources_dir, 'Python', 'lib', 'python')
        dest += self.version_info
        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)
        self.remove_bytecode(join(self.resources_dir, 'Python', 'lib'))

    @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')
        for x in os.walk(base):
            root = x[0]
            for f in x[-1]:
                if f.endswith('.py'):
                    y = join(root, f)
                    rel = os.path.relpath(y, base)
                    try:
                        py_compile.compile(y, dfile=rel, doraise=True)
                        os.remove(y)
                    except:
                        self.warn('WARNING: Failed to byte-compile', y)

    @flush
    def create_console_app(self):
        print '\nCreating console.app'
        cc_dir = os.path.join(self.contents_dir, 'console.app', 'Contents')
        os.makedirs(cc_dir)
        for x in os.listdir(self.contents_dir):
            if x == 'console.app':
                continue
            if x == 'Info.plist':
                plist = plistlib.readPlist(join(self.contents_dir, x))
                plist['LSUIElement'] = '1'
                plistlib.writePlist(plist, join(cc_dir, x))
            else:
                os.symlink(join('../..', x),
                           join(cc_dir, x))

    @flush
    def copy_launcher_and_site(self):
        base = os.path.dirname(__file__)
        shutil.copy2(join(base, 'launcher.py'), self.resources_dir)
        shutil.copy2(join(base, 'site.py'), join(self.resources_dir, 'Python',
            'lib', 'python'+self.version_info))


    @flush
    def makedmg(self, d, volname,
                destdir='dist',
                internet_enable=True,
                format='UDBZ'):
        ''' Copy a directory d into a dmg named volname '''
        print '\nCreating dmg'
        sys.stdout.flush()
        if not os.path.exists(destdir):
            os.makedirs(destdir)
        dmg = os.path.join(destdir, volname+'.dmg')
        if os.path.exists(dmg):
            os.unlink(dmg)
        subprocess.check_call(['/usr/bin/hdiutil', 'create', '-srcfolder', os.path.abspath(d),
                               '-volname', volname, '-format', format, dmg])
        if internet_enable:
           subprocess.check_call(['/usr/bin/hdiutil', 'internet-enable', '-yes', dmg])
        size = os.stat(dmg).st_size/(1024*1024.)
        print '\nInstaller size: %.2fMB\n'%size
        return dmg

def test_exe():
    build_dir = abspath(join('build', APPNAME+'.app'))
    py2app = Py2App(build_dir)
    py2app.create_exe()
    return 0


def main():
    if 'test_exe' in sys.argv:
        return test_exe()
    build_dir = abspath(join('build', APPNAME+'.app'))
    if os.path.exists(build_dir):
        shutil.rmtree(build_dir)
    os.makedirs(build_dir)
    py2app = Py2App(build_dir)
    py2app.run()
    return 0


if __name__ == '__main__':
    sys.exit(main())

Generated by  Doxygen 1.6.0   Back to index