#!/usr/bin/env python2 # vim:fileencoding=UTF-8:ts=4:sw=4:sta:et:sts=4:ai from __future__ import with_statement, print_function __license__ = 'GPL v3' __copyright__ = '2009, Kovid Goyal ' __docformat__ = 'restructuredtext en' import sys, os, shutil, glob, py_compile, subprocess, re, zipfile, time, textwrap from itertools import chain from setup import (Command, modules, functions, basenames, __version__, __appname__) from setup.build_environment import ( msvc, MT, RC, is64bit, ICU as ICU_DIR, sw as SW, QT_DLLS, QMAKE, QT_PLUGINS, PYQT_MODULES) from setup.installer.windows.wix import WixMixIn OPENSSL_DIR = os.environ.get('OPENSSL_DIR', os.path.join(SW, 'private', 'openssl')) SW = r'C:\cygwin64\home\kovid\sw' IMAGEMAGICK = os.path.join(SW, 'build', 'ImageMagick-*\\VisualMagick\\bin') CRT = r'C:\Microsoft.VC90.CRT' LZMA = os.path.join(SW, *('private/easylzma/build/easylzma-0.0.8'.split('/'))) QT_DIR = subprocess.check_output([QMAKE, '-query', 'QT_INSTALL_PREFIX']).decode('utf-8').strip() VERSION = re.sub('[a-z]\d+', '', __version__) WINVER = VERSION+'.0' machine = 'X64' if is64bit else 'X86' DESCRIPTIONS = { 'calibre' : 'The main calibre program', 'ebook-viewer' : 'The calibre e-book viewer', 'ebook-edit' : 'The calibre e-book editor', 'lrfviewer' : 'Viewer for LRF files', 'ebook-convert': 'Command line interface to the conversion/news download system', 'ebook-meta' : 'Command line interface for manipulating e-book metadata', 'calibredb' : 'Command line interface to the calibre database', 'calibre-launcher' : 'Utility functions common to all executables', 'calibre-debug' : 'Command line interface for calibre debugging/development', 'calibre-customize' : 'Command line interface to calibre plugin system', 'pdfmanipulate' : 'Command line tool to manipulate PDF files', 'calibre-server': 'Standalone calibre content server', 'calibre-parallel': 'calibre worker process', 'calibre-smtp' : 'Command line interface for sending books via email', 'calibre-eject' : 'Helper program for ejecting connected reader devices', } def walk(dir): ''' A nice interface to os.walk ''' for record in os.walk(dir): for f in record[-1]: yield os.path.join(record[0], f) # Remove CRT dep from manifests {{{ def get_manifest_from_dll(dll): import win32api, pywintypes LOAD_LIBRARY_AS_DATAFILE = 2 d = win32api.LoadLibraryEx(os.path.abspath(dll), 0, LOAD_LIBRARY_AS_DATAFILE) try: resources = win32api.EnumResourceNames(d, 24) except pywintypes.error as err: if err.winerror == 1812: return None, None # no resource section (probably a .pyd file) raise if resources: return resources[0], win32api.LoadResource(d, 24, resources[0]) return None, None def update_manifest(dll, rnum, manifest): import win32api h = win32api.BeginUpdateResource(dll, 0) win32api.UpdateResource(h, 24, rnum, manifest) win32api.EndUpdateResource(h, 0) _crt_pat = re.compile(r'Microsoft\.VC\d+\.CRT') def remove_CRT_from_manifest(dll, log=print): from lxml import etree rnum, manifest = get_manifest_from_dll(dll) if manifest is None: return root = etree.fromstring(manifest) found = False for ai in root.xpath('//*[local-name()="assemblyIdentity" and @name]'): name = ai.get('name') if _crt_pat.match(name): p = ai.getparent() pp = p.getparent() pp.remove(p) if len(pp) == 0: pp.getparent().remove(pp) found = True if found: manifest = etree.tostring(root, pretty_print=True) update_manifest(dll, rnum, manifest) log('\t', os.path.basename(dll)) # }}} class Win32Freeze(Command, WixMixIn): description = 'Freeze windows calibre installation' def add_options(self, parser): parser.add_option('--no-ice', default=False, action='store_true', help='Disable ICE checks when building MSI (needed when running' ' from cygwin sshd)') parser.add_option('--msi-compression', '--compress', default='high', help='Compression when generating installer. Set to none to disable') parser.add_option('--keep-site', default=False, action='store_true', help='Keep human readable site.py') parser.add_option('--verbose', default=0, action="count", help="Be more verbose") if not parser.has_option('--dont-strip'): parser.add_option('-x', '--dont-strip', default=False, action='store_true', help='Dont strip the generated binaries (no-op on windows)') def run(self, opts): self.SW = SW self.portable_uncompressed_size = 0 self.opts = opts self.src_root = self.d(self.SRC) self.base = self.j(self.d(self.SRC), 'build', 'winfrozen') self.rc_template = self.j(self.d(self.a(__file__)), 'template.rc') self.py_ver = ''.join(map(str, sys.version_info[:2])) self.lib_dir = self.j(self.base, 'Lib') self.pylib = self.j(self.base, 'pylib.zip') self.dll_dir = self.j(self.base, 'DLLs') self.plugins_dir = os.path.join(self.base, 'plugins2') self.portable_base = self.j(self.d(self.base), 'Calibre Portable') self.obj_dir = self.j(self.src_root, 'build', 'launcher') self.initbase() self.build_launchers() self.build_eject() self.add_plugins() self.freeze() self.embed_manifests() self.install_site_py() self.archive_lib_dir() self.remove_CRT_from_manifests() self.create_installer() if not is64bit: self.build_portable() self.build_portable_installer() self.sign_installers() def remove_CRT_from_manifests(self): ''' The dependency on the CRT is removed from the manifests of all DLLs. This allows the CRT loaded by the .exe files to be used instead. ''' self.info('Removing CRT dependency from manifests of:') for dll in chain(walk(self.dll_dir), walk(self.plugins_dir)): bn = self.b(dll) if bn.lower().rpartition('.')[-1] not in {'dll', 'pyd'}: continue remove_CRT_from_manifest(dll, self.info) def initbase(self): if self.e(self.base): shutil.rmtree(self.base) os.makedirs(self.base) def add_plugins(self): self.info('Adding plugins...') tgt = self.plugins_dir if os.path.exists(tgt): shutil.rmtree(tgt) os.mkdir(tgt) base = self.j(self.SRC, 'calibre', 'plugins') for f in glob.glob(self.j(base, '*.pyd')): # We dont want the manifests as the manifest in the exe will be # used instead shutil.copy2(f, tgt) def fix_pyd_bootstraps_in(self, folder): for dirpath, dirnames, filenames in os.walk(folder): for f in filenames: name, ext = os.path.splitext(f) bpy = self.j(dirpath, name + '.py') if ext == '.pyd' and os.path.exists(bpy): with open(bpy, 'rb') as f: raw = f.read().strip() if (not raw.startswith('def __bootstrap__') or not raw.endswith('__bootstrap__()')): raise Exception('The file %r has non' ' bootstrap code'%self.j(dirpath, f)) for ext in ('.py', '.pyc', '.pyo'): x = self.j(dirpath, name+ext) if os.path.exists(x): os.remove(x) def freeze(self): shutil.copy2(self.j(self.src_root, 'LICENSE'), self.base) self.info('Adding CRT') shutil.copytree(CRT, self.j(self.base, os.path.basename(CRT))) self.info('Adding resources...') tgt = self.j(self.base, 'resources') if os.path.exists(tgt): shutil.rmtree(tgt) shutil.copytree(self.j(self.src_root, 'resources'), tgt) self.info('Adding Qt and python...') shutil.copytree(r'C:\Python%s\DLLs'%self.py_ver, self.dll_dir, ignore=shutil.ignore_patterns('msvc*.dll', 'Microsoft.*')) for x in glob.glob(self.j(OPENSSL_DIR, 'bin', '*.dll')): shutil.copy2(x, self.dll_dir) for x in glob.glob(self.j(ICU_DIR, 'source', 'lib', '*.dll')): shutil.copy2(x, self.dll_dir) for x in QT_DLLS: shutil.copy2(os.path.join(QT_DIR, 'bin', x), self.dll_dir) shutil.copy2(r'C:\windows\system32\python%s.dll'%self.py_ver, self.dll_dir) for dirpath, dirnames, filenames in os.walk(r'C:\Python%s\Lib'%self.py_ver): if os.path.basename(dirpath) == 'pythonwin': continue for f in filenames: if f.lower().endswith('.dll'): f = self.j(dirpath, f) shutil.copy2(f, self.dll_dir) shutil.copy2( r'C:\Python%(v)s\Lib\site-packages\pywin32_system32\pywintypes%(v)s.dll' % dict(v=self.py_ver), self.dll_dir) def ignore_lib(root, items): ans = [] for x in items: ext = os.path.splitext(x)[1] if (not ext and (x in ('demos', 'tests'))) or \ (ext in ('.dll', '.chm', '.htm', '.txt')): ans.append(x) return ans shutil.copytree(r'C:\Python%s\Lib'%self.py_ver, self.lib_dir, ignore=ignore_lib) # Fix win32com sp_dir = self.j(self.lib_dir, 'site-packages') comext = self.j(sp_dir, 'win32comext') shutil.copytree(self.j(comext, 'shell'), self.j(sp_dir, 'win32com', 'shell')) shutil.rmtree(comext) # Fix PyCrypto and Pillow, removing the bootstrap .py modules that load # the .pyd modules, since they do not work when in a zip file for folder in os.listdir(sp_dir): folder = self.j(sp_dir, folder) if os.path.isdir(folder): self.fix_pyd_bootstraps_in(folder) for pat in (r'PyQt5\uic\port_v3', ): x = glob.glob(self.j(self.lib_dir, 'site-packages', pat))[0] shutil.rmtree(x) pyqt = self.j(self.lib_dir, 'site-packages', 'PyQt5') for x in {x for x in os.listdir(pyqt) if x.endswith('.pyd')} - PYQT_MODULES: os.remove(self.j(pyqt, x)) self.info('Adding calibre sources...') for x in glob.glob(self.j(self.SRC, '*')): if os.path.isdir(x): shutil.copytree(x, self.j(sp_dir, self.b(x))) else: shutil.copy(x, self.j(sp_dir, self.b(x))) for x in (r'calibre\manual', r'calibre\trac', 'pythonwin'): deld = self.j(sp_dir, x) if os.path.exists(deld): shutil.rmtree(deld) for x in os.walk(self.j(sp_dir, 'calibre')): for f in x[-1]: if not f.endswith('.py'): os.remove(self.j(x[0], f)) self.info('Byte-compiling all python modules...') for x in ('test', 'lib2to3', 'distutils'): shutil.rmtree(self.j(self.lib_dir, x)) for x in os.walk(self.lib_dir): root = x[0] for f in x[-1]: if f.endswith('.py'): y = self.j(root, f) rel = os.path.relpath(y, self.lib_dir) try: py_compile.compile(y, dfile=rel, doraise=True) os.remove(y) except: self.warn('Failed to byte-compile', y) pyc, pyo = y+'c', y+'o' epyc, epyo, epy = map(os.path.exists, (pyc,pyo,y)) if (epyc or epyo) and epy: os.remove(y) if epyo and epyc: os.remove(pyc) self.info('\nAdding Qt plugins...') qt_prefix = QT_DIR plugdir = self.j(qt_prefix, 'plugins') tdir = self.j(self.base, 'qt_plugins') for d in QT_PLUGINS: self.info('\t', d) imfd = os.path.join(plugdir, d) tg = os.path.join(tdir, d) if os.path.exists(tg): shutil.rmtree(tg) shutil.copytree(imfd, tg) for dirpath, dirnames, filenames in os.walk(tdir): for x in filenames: if not x.endswith('.dll'): os.remove(self.j(dirpath, x)) self.info('\nAdding third party dependencies') self.info('\tAdding misc binary deps') bindir = os.path.join(SW, 'bin') for x in ('pdftohtml', 'pdfinfo', 'pdftoppm', 'jpegtran-calibre', 'cjpeg-calibre'): shutil.copy2(os.path.join(bindir, x+'.exe'), self.base) for x in ('', '.manifest'): fname = 'optipng.exe' + x src = os.path.join(bindir, fname) shutil.copy2(src, self.base) src = os.path.join(self.base, fname) os.rename(src, src.replace('.exe', '-calibre.exe')) for pat in ('*.dll',): for f in glob.glob(os.path.join(bindir, pat)): ok = True for ex in ('expatw', 'testplug'): if ex in f.lower(): ok = False if not ok: continue dest = self.dll_dir shutil.copy2(f, dest) for x in ('zlib1.dll', 'libxml2.dll', 'libxslt.dll', 'libexslt.dll'): msrc = self.j(bindir, x+'.manifest') if os.path.exists(msrc): shutil.copy2(msrc, self.dll_dir) # Copy ImageMagick impath = glob.glob(IMAGEMAGICK)[-1] for pat in ('*.dll', '*.xml'): for f in glob.glob(self.j(impath, pat)): ok = True for ex in ('magick++', 'x11.dll', 'xext.dll'): if ex in f.lower(): ok = False if not ok: continue shutil.copy2(f, self.dll_dir) def embed_manifests(self): self.info('Embedding remaining manifests...') for x in os.walk(self.base): for f in x[-1]: base, ext = os.path.splitext(f) if ext != '.manifest': continue dll = self.j(x[0], base) manifest = self.j(x[0], f) res = 2 if os.path.splitext(dll)[1] == '.exe': res = 1 if os.path.exists(dll): self.run_builder([MT, '-manifest', manifest, '-outputresource:%s;%d'%(dll,res)]) os.remove(manifest) def compress(self): self.info('Compressing app dir using 7-zip') subprocess.check_call([r'C:\Program Files\7-Zip\7z.exe', 'a', '-r', '-scsUTF-8', '-sfx', 'winfrozen', 'winfrozen'], cwd=self.base) def embed_resources(self, module, desc=None, extra_data=None, product_description=None): icon_base = self.j(self.src_root, 'icons') icon_map = {'calibre':'library', 'ebook-viewer':'viewer', 'ebook-edit':'ebook-edit', 'lrfviewer':'viewer', 'calibre-portable':'library'} file_type = 'DLL' if module.endswith('.dll') else 'APP' template = open(self.rc_template, 'rb').read() bname = self.b(module) internal_name = os.path.splitext(bname)[0] icon = icon_map.get(internal_name, 'command-prompt') if internal_name.startswith('calibre-portable-'): icon = 'install' icon = self.j(icon_base, icon+'.ico') if desc is None: defdesc = 'A dynamic link library' if file_type == 'DLL' else \ 'An executable program' desc = DESCRIPTIONS.get(internal_name, defdesc) license = 'GNU GPL v3.0' def e(val): return val.replace('"', r'\"') if product_description is None: product_description = __appname__ + ' - E-book management' rc = template.format( icon=icon, file_type=e(file_type), file_version=e(WINVER.replace('.', ',')), file_version_str=e(WINVER), file_description=e(desc), internal_name=e(internal_name), original_filename=e(bname), product_version=e(WINVER.replace('.', ',')), product_version_str=e(__version__), product_name=e(__appname__), product_description=e(product_description), legal_copyright=e(license), legal_trademarks=e(__appname__ + ' is a registered U.S. trademark number 3,666,525') ) if extra_data: rc += '\nextra extra "%s"'%extra_data tdir = self.obj_dir rcf = self.j(tdir, bname+'.rc') with open(rcf, 'wb') as f: f.write(rc) res = self.j(tdir, bname + '.res') cmd = [RC, '/n', '/fo'+res, rcf] self.run_builder(cmd) return res def install_site_py(self): if not os.path.exists(self.lib_dir): os.makedirs(self.lib_dir) shutil.copy2(self.j(self.d(__file__), 'site.py'), self.lib_dir) y = os.path.join(self.lib_dir, 'site.py') py_compile.compile(y, dfile='site.py', doraise=True) if not self.opts.keep_site: os.remove(y) def run_builder(self, cmd, show_output=False): p = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) buf = [] while p.poll() is None: x = p.stdout.read() + p.stderr.read() if x: buf.append(x) if p.returncode != 0: self.info('Failed to run builder:') self.info(*cmd) self.info(''.join(buf)) self.info('') sys.stdout.flush() sys.exit(1) if show_output: self.info(''.join(buf) + '\n') def build_portable_installer(self): zf = self.a(self.j('dist', 'calibre-portable-%s.zip.lz'%VERSION)) usz = self.portable_uncompressed_size or os.path.getsize(zf) def cc(src, obj): cflags = '/c /EHsc /MT /W4 /Ox /nologo /D_UNICODE /DUNICODE /DPSAPI_VERSION=1'.split() cflags.append(r'/I%s\include'%LZMA) cflags.append('/DUNCOMPRESSED_SIZE=%d'%usz) if self.newer(obj, [src]): self.info('Compiling', obj) cmd = [msvc.cc] + cflags + ['/Fo'+obj, src] self.run_builder(cmd) src = self.j(self.src_root, 'setup', 'installer', 'windows', 'portable-installer.cpp') obj = self.j(self.obj_dir, self.b(src)+'.obj') xsrc = self.j(self.src_root, 'setup', 'installer', 'windows', 'XUnzip.cpp') xobj = self.j(self.obj_dir, self.b(xsrc)+'.obj') cc(src, obj) cc(xsrc, xobj) exe = self.j('dist', 'calibre-portable-installer-%s.exe'%VERSION) if self.newer(exe, [obj, xobj]): self.info('Linking', exe) cmd = [msvc.linker] + ['/INCREMENTAL:NO', '/MACHINE:'+machine, '/LIBPATH:'+self.obj_dir, '/SUBSYSTEM:WINDOWS', '/LIBPATH:'+(LZMA+r'\lib\Release'), '/RELEASE', '/MANIFEST', '/MANIFESTUAC:level="asInvoker" uiAccess="false"', '/ENTRY:wWinMainCRTStartup', '/OUT:'+exe, self.embed_resources(exe, desc='Calibre Portable Installer', extra_data=zf, product_description='Calibre Portable Installer'), xobj, obj, 'User32.lib', 'Shell32.lib', 'easylzma_s.lib', 'Ole32.lib', 'Shlwapi.lib', 'Kernel32.lib', 'Psapi.lib'] self.run_builder(cmd) manifest = exe + '.manifest' with open(manifest, 'r+b') as f: raw = f.read() f.seek(0) f.truncate() # TODO: Add the windows 8 GUID to the compatibility section # after windows 8 is released, see: # http://msdn.microsoft.com/en-us/library/windows/desktop/hh848036(v=vs.85).aspx raw = raw.replace(b'', textwrap.dedent( b'''\ ''')) f.write(raw) self.run_builder([MT, '-manifest', manifest, '-outputresource:%s;1'%exe]) os.remove(manifest) os.remove(zf) def build_portable(self): base = self.portable_base if os.path.exists(base): shutil.rmtree(base) os.makedirs(base) src = self.j(self.src_root, 'setup', 'installer', 'windows', 'portable.c') obj = self.j(self.obj_dir, self.b(src)+'.obj') cflags = '/c /EHsc /MT /W3 /Ox /nologo /D_UNICODE /DUNICODE'.split() if self.newer(obj, [src]): self.info('Compiling', obj) cmd = [msvc.cc] + cflags + ['/Fo'+obj, '/Tc'+src] self.run_builder(cmd) exe = self.j(base, 'calibre-portable.exe') if self.newer(exe, [obj]): self.info('Linking', exe) cmd = [msvc.linker] + ['/INCREMENTAL:NO', '/MACHINE:'+machine, '/LIBPATH:'+self.obj_dir, '/SUBSYSTEM:WINDOWS', '/RELEASE', '/ENTRY:wWinMainCRTStartup', '/OUT:'+exe, self.embed_resources(exe, desc='Calibre Portable', product_description='Calibre Portable'), obj, 'User32.lib'] self.run_builder(cmd) self.info('Creating portable installer') shutil.copytree(self.base, self.j(base, 'Calibre')) os.mkdir(self.j(base, 'Calibre Library')) os.mkdir(self.j(base, 'Calibre Settings')) name = '%s-portable-%s.zip'%(__appname__, __version__) name = self.j('dist', name) with zipfile.ZipFile(name, 'w', zipfile.ZIP_STORED) as zf: self.add_dir_to_zip(zf, base, 'Calibre Portable') self.portable_uncompressed_size = os.path.getsize(name) subprocess.check_call([LZMA + r'\bin\elzma.exe', '-9', '--lzip', name]) def sign_installers(self): self.info('Signing installers...') files = glob.glob(self.j('dist', '*.msi')) + glob.glob(self.j('dist', '*.exe')) if not files: raise ValueError('No installers found') args = ['signtool.exe', 'sign', '/a', '/fd', 'sha256', '/td', 'sha256', '/d', 'calibre - E-book management', '/du', 'http://calibre-ebook.com', '/tr'] def runcmd(cmd): for timeserver in ('http://timestamp.geotrust.com/tsa', 'http://timestamp.comodoca.com/rfc3161',): try: subprocess.check_call(cmd + [timeserver] + files) break except subprocess.CalledProcessError: print ('Signing failed, retrying with different timestamp server') else: raise SystemExit('Signing failed') runcmd(args) def add_dir_to_zip(self, zf, path, prefix=''): ''' Add a directory recursively to the zip file with an optional prefix. ''' if prefix: zi = zipfile.ZipInfo(prefix+'/') zi.external_attr = 16 zf.writestr(zi, '') cwd = os.path.abspath(os.getcwd()) try: os.chdir(path) fp = (prefix + ('/' if prefix else '')).replace('//', '/') for f in os.listdir('.'): arcname = fp + f if os.path.isdir(f): self.add_dir_to_zip(zf, f, prefix=arcname) else: zf.write(f, arcname) finally: os.chdir(cwd) def build_eject(self): self.info('Building calibre-eject.exe') base = self.j(self.src_root, 'setup', 'installer', 'windows') src = self.j(base, 'eject.c') obj = self.j(self.obj_dir, self.b(src)+'.obj') cflags = '/c /EHsc /MD /W3 /Ox /nologo /D_UNICODE'.split() if self.newer(obj, src): cmd = [msvc.cc] + cflags + ['/Fo'+obj, '/Tc'+src] self.run_builder(cmd, show_output=True) exe = self.j(self.base, 'calibre-eject.exe') cmd = [msvc.linker] + ['/MACHINE:'+machine, '/SUBSYSTEM:CONSOLE', '/RELEASE', '/OUT:'+exe] + [self.embed_resources(exe), obj, 'setupapi.lib'] self.run_builder(cmd) def build_launchers(self, debug=False): if not os.path.exists(self.obj_dir): os.makedirs(self.obj_dir) dflags = (['/Zi'] if debug else []) dlflags = (['/DEBUG'] if debug else ['/INCREMENTAL:NO']) base = self.j(self.src_root, 'setup', 'installer', 'windows') sources = [self.j(base, x) for x in ['util.c', 'MemoryModule.c']] headers = [self.j(base, x) for x in ['util.h', 'MemoryModule.h']] objects = [self.j(self.obj_dir, self.b(x)+'.obj') for x in sources] cflags = '/c /EHsc /MD /W3 /Ox /nologo /D_UNICODE'.split() cflags += ['/DPYDLL="python%s.dll"'%self.py_ver, '/IC:/Python%s/include'%self.py_ver] for src, obj in zip(sources, objects): if not self.newer(obj, headers+[src]): continue cmd = [msvc.cc] + cflags + dflags + ['/Fo'+obj, '/Tc'+src] self.run_builder(cmd, show_output=True) dll = self.j(self.obj_dir, 'calibre-launcher.dll') ver = '.'.join(__version__.split('.')[:2]) if self.newer(dll, objects): cmd = [msvc.linker, '/DLL', '/VERSION:'+ver, '/OUT:'+dll, '/nologo', '/MACHINE:'+machine] + dlflags + objects + \ [self.embed_resources(dll), '/LIBPATH:C:/Python%s/libs'%self.py_ver, 'python%s.lib'%self.py_ver, '/delayload:python%s.dll'%self.py_ver] self.info('Linking calibre-launcher.dll') self.run_builder(cmd, show_output=True) src = self.j(base, 'main.c') shutil.copy2(dll, self.base) for typ in ('console', 'gui', ): self.info('Processing %s launchers'%typ) subsys = 'WINDOWS' if typ == 'gui' else 'CONSOLE' for mod, bname, func in zip(modules[typ], basenames[typ], functions[typ]): xflags = list(cflags) if typ == 'gui': xflags += ['/DGUI_APP='] xflags += ['/DMODULE="%s"'%mod, '/DBASENAME="%s"'%bname, '/DFUNCTION="%s"'%func] dest = self.j(self.obj_dir, bname+'.obj') if self.newer(dest, [src]+headers): self.info('Compiling', bname) cmd = [msvc.cc] + xflags + dflags + ['/Tc'+src, '/Fo'+dest] self.run_builder(cmd) exe = self.j(self.base, bname+'.exe') lib = dll.replace('.dll', '.lib') if self.newer(exe, [dest, lib, self.rc_template, __file__]): self.info('Linking', bname) cmd = [msvc.linker] + ['/MACHINE:'+machine, '/LIBPATH:'+self.obj_dir, '/SUBSYSTEM:'+subsys, '/LIBPATH:C:/Python%s/libs'%self.py_ver, '/RELEASE', '/OUT:'+exe] + dlflags + [self.embed_resources(exe), dest, lib] self.run_builder(cmd) def archive_lib_dir(self): self.info('Putting all python code into a zip file for performance') self.zf_timestamp = time.localtime(time.time())[:6] self.zf_names = set() with zipfile.ZipFile(self.pylib, 'w', zipfile.ZIP_STORED) as zf: # Add the .pyds from python and calibre to the zip file for x in (self.plugins_dir, self.dll_dir): for pyd in os.listdir(x): if pyd.endswith('.pyd') and pyd not in { # sqlite_custom has to be a file for # sqlite_load_extension to work 'sqlite_custom.pyd', # calibre_style has to be loaded by Qt therefore it # must be a file 'calibre_style.pyd', # Because of https://github.com/fancycode/MemoryModule/issues/4 # any extensions that use C++ exceptions must be loaded # from files 'unrar.pyd', 'wpd.pyd', 'podofo.pyd', 'progress_indicator.pyd', 'hunspell.pyd', # As per this https://bugs.launchpad.net/bugs/1087816 # on some systems magick.pyd fails to load from memory # on 64 bit 'magick.pyd', # dupypy crashes when loaded from the zip file 'dukpy.pyd', }: self.add_to_zipfile(zf, pyd, x) os.remove(self.j(x, pyd)) # Add everything in Lib except site-packages to the zip file for x in os.listdir(self.lib_dir): if x == 'site-packages': continue self.add_to_zipfile(zf, x, self.lib_dir) sp = self.j(self.lib_dir, 'site-packages') # Special handling for PIL and pywin32 handled = set(['pywin32.pth', 'win32']) pil_dir = glob.glob(self.j(sp, 'Pillow*', 'PIL'))[-1] if is64bit: # PIL can raise exceptions, which cause crashes on 64bit shutil.copytree(pil_dir, self.j(self.dll_dir, 'PIL')) handled.add(self.b(self.d(pil_dir))) base = self.j(sp, 'win32', 'lib') for x in os.listdir(base): if os.path.splitext(x)[1] not in ('.exe',): self.add_to_zipfile(zf, x, base) base = self.d(base) for x in os.listdir(base): if not os.path.isdir(self.j(base, x)): if os.path.splitext(x)[1] not in ('.exe',): self.add_to_zipfile(zf, x, base) handled.add('easy-install.pth') # We dont want the site.py from site-packages handled.add('site.pyo') for d in self.get_pth_dirs(self.j(sp, 'easy-install.pth')): hname = self.b(d) if hname in handled: continue handled.add(hname) for x in os.listdir(d): if x in {'EGG-INFO', 'site.py', 'site.pyc', 'site.pyo'}: continue self.add_to_zipfile(zf, x, d) # The rest of site-packages for x in os.listdir(sp): if x in handled or x.endswith('.egg-info'): continue absp = self.j(sp, x) if os.path.isdir(absp): if not os.listdir(absp): continue self.add_to_zipfile(zf, x, sp) else: self.add_to_zipfile(zf, x, sp) shutil.rmtree(self.lib_dir) def get_pth_dirs(self, pth): base = os.path.dirname(pth) for line in open(pth).readlines(): line = line.strip() if not line or line.startswith('#') or line.startswith('import'): continue candidate = os.path.abspath(self.j(base, line)) if os.path.exists(candidate): if not os.path.isdir(candidate): raise ValueError('%s is not a directory'%candidate) yield candidate def add_to_zipfile(self, zf, name, base, exclude=frozenset()): abspath = self.j(base, name) name = name.replace(os.sep, '/') if name in self.zf_names: raise ValueError('Already added %r to zipfile [%r]'%(name, abspath)) zinfo = zipfile.ZipInfo(filename=name, date_time=self.zf_timestamp) if os.path.isdir(abspath): if not os.listdir(abspath): return zinfo.external_attr = 0o700 << 16 zf.writestr(zinfo, '') for x in os.listdir(abspath): if x not in exclude: self.add_to_zipfile(zf, name + os.sep + x, base) else: ext = os.path.splitext(name)[1].lower() if ext in ('.dll',): raise ValueError('Cannot add %r to zipfile'%abspath) zinfo.external_attr = 0o600 << 16 if ext in ('.py', '.pyc', '.pyo', '.pyd'): with open(abspath, 'rb') as f: zf.writestr(zinfo, f.read()) self.zf_names.add(name)