#!/usr/bin/env python
# vim:fileencoding=utf-8
# License: GPLv3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>


import contextlib
import errno
import glob
import os
import re
import runpy
import shutil
import stat
import subprocess
import sys
import zipfile

from bypy.constants import CL, LINK, MT, PREFIX, RC, SIGNTOOL, SW, build_dir, python_major_minor_version, worker_env
from bypy.constants import SRC as CALIBRE_DIR
from bypy.freeze import cleanup_site_packages, extract_extension_modules, freeze_python, path_to_freeze_dir
from bypy.utils import mkdtemp, py_compile, run, walk
from bypy.sign_server import sign_file_in_client
from bypy.authenticode import has_signature

iv = globals()['init_env']
calibre_constants = iv['calibre_constants']
QT_PREFIX = os.path.join(PREFIX, 'qt')
QT_DLLS, QT_PLUGINS, PYQT_MODULES = iv['QT_DLLS'], iv['QT_PLUGINS'], iv['PYQT_MODULES']

APPNAME, VERSION = calibre_constants['appname'], calibre_constants['version']
WINVER = VERSION + '.0'
machine = 'X64'
j, d, a, b = os.path.join, os.path.dirname, os.path.abspath, os.path.basename
create_installer = runpy.run_path(
    j(d(a(__file__)), 'wix.py'), {'calibre_constants': calibre_constants}
)['create_installer']
signatures = set()

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',
    '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',
    'calibre-file-dialog': 'Helper program to show file open/save dialogs',
}

EXE_MANIFEST = '''\
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
  <application xmlns="urn:schemas-microsoft-com:asm.v3">
     <windowsSettings> <longPathAware xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">true</longPathAware> </windowsSettings>
  </application>
  <trustInfo xmlns="urn:schemas-microsoft-com:asm.v2">
    <security>
      <requestedPrivileges xmlns="urn:schemas-microsoft-com:asm.v3">
        <requestedExecutionLevel level="asInvoker" uiAccess="false" />
      </requestedPrivileges>
    </security>
  </trustInfo>
  <compatibility xmlns="urn:schemas-microsoft-com:compatibility.v1">
    <application>
      <supportedOS Id="{8e0f7a12-bfb3-4fe8-b9a5-48fd50a15a9a}" />
    </application>
  </compatibility>
</assembly>
'''


def printf(*args, **kw):
    print(*args, **kw)
    sys.stdout.flush()


def run_compiler(env, *cmd):
    run(*cmd, cwd=env.obj_dir)


def sign_file(path: str) -> None:
    sign_file_in_client(path)
    signatures.add(path)


class Env:

    def __init__(self, build_dir):
        self.python_base = os.path.join(PREFIX, 'private', 'python')
        self.portable_uncompressed_size = 0
        self.src_root = CALIBRE_DIR
        self.base = j(build_dir, 'winfrozen')
        self.app_base = j(self.base, 'app')
        self.rc_template = j(d(a(__file__)), 'template.rc')
        self.py_ver = '.'.join(map(str, python_major_minor_version()))
        self.lib_dir = j(self.app_base, 'Lib')
        self.pylib = j(self.app_base, 'pylib.zip')
        self.dll_dir = j(self.app_base, 'bin')
        self.share_dir = j(self.app_base, 'share')
        self.portable_base = j(d(self.base), 'Calibre Portable')
        self.obj_dir = j(build_dir, 'launcher')
        self.installer_dir = j(build_dir, 'wix')
        self.dist = j(SW, 'dist')


def initbase(env):
    os.makedirs(env.app_base)
    os.mkdir(env.dll_dir)
    os.mkdir(env.share_dir)
    try:
        shutil.rmtree(env.dist)
    except EnvironmentError as err:
        if err.errno != errno.ENOENT:
            raise
    os.mkdir(env.dist)


def freeze(env, ext_dir, incdir):
    shutil.copy2(j(env.src_root, 'LICENSE'), env.base)

    printf('Adding resources...')
    tgt = j(env.app_base, 'resources')
    if os.path.exists(tgt):
        shutil.rmtree(tgt)
    shutil.copytree(j(env.src_root, 'resources'), tgt)

    printf('\tAdding misc binary deps')

    def copybin(x, dest=env.dll_dir):
        shutil.copy2(x, dest)
        with contextlib.suppress(FileNotFoundError):
            shutil.copy2(x + '.manifest', dest)

    bindir = os.path.join(PREFIX, 'bin')
    libdir = os.path.join(PREFIX, 'lib')
    for x in ('pdftohtml', 'pdfinfo', 'pdftoppm', 'pdftotext', 'jpegtran-calibre', 'cjpeg-calibre', 'optipng-calibre', 'cwebp-calibre', 'JXRDecApp-calibre'):
        copybin(os.path.join(bindir, x + '.exe'))
    # piper
    for x in ('espeak-ng-data',):
        shutil.copytree(os.path.join(PREFIX, 'share', x), os.path.join(env.share_dir, x))
    for f in glob.glob(os.path.join(libdir, 'onnxruntime*.dll')):
        copybin(f)
    for f in glob.glob(os.path.join(libdir, 'DirectML*.dll')):
        copybin(f)

    for f in glob.glob(os.path.join(bindir, '*.dll')):
        if re.search(r'(easylzma|icutest)', f.lower()) is None:
            copybin(f)

    ossm = os.path.join(env.dll_dir, 'ossl-modules')
    os.mkdir(ossm)
    for f in glob.glob(os.path.join(libdir, 'ossl-modules', '*.dll')):
        copybin(f, ossm)
    for f in glob.glob(os.path.join(PREFIX, 'ffmpeg', 'bin', '*.dll')):
        copybin(f)

    copybin(os.path.join(env.python_base, 'python%s.dll' % env.py_ver.replace('.', '')))
    copybin(os.path.join(env.python_base, 'python%s.dll' % env.py_ver[0]))
    for x in glob.glob(os.path.join(env.python_base, 'DLLs', '*.dll')):  # dlls needed by python
        copybin(x)
    for f in walk(os.path.join(env.python_base, 'Lib')):
        q = f.lower()
        if q.endswith('.dll') and 'scintilla' not in q and 'pyqtbuild' not in q:
            copybin(f)
    ext_map = extract_extension_modules(ext_dir, env.dll_dir)
    ext_map.update(extract_extension_modules(j(env.python_base, 'DLLs'), env.dll_dir, move=False))

    printf('Adding Qt...')
    for x in QT_DLLS:
        copybin(os.path.join(QT_PREFIX, 'bin', x + '.dll'))
    copybin(os.path.join(QT_PREFIX, 'bin', 'QtWebEngineProcess.exe'))
    plugdir = j(QT_PREFIX, 'plugins')
    tdir = j(env.app_base, 'plugins')
    for d in QT_PLUGINS:
        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 f in walk(tdir):
        if not f.lower().endswith('.dll'):
            os.remove(f)
    for data_file in os.listdir(j(QT_PREFIX, 'resources')):
        shutil.copy2(j(QT_PREFIX, 'resources', data_file), j(env.app_base, 'resources'))
    shutil.copytree(j(QT_PREFIX, 'translations'), j(env.app_base, 'translations'))

    printf('Adding python...')

    def ignore_lib(root, items):
        ans = []
        for x in items:
            ext = os.path.splitext(x)[1].lower()
            if ext in ('.dll', '.chm', '.htm', '.txt'):
                ans.append(x)
        return ans

    shutil.copytree(r'%s\Lib' % env.python_base, env.lib_dir, ignore=ignore_lib)
    install_site_py(env)
    sp_dir = j(env.lib_dir, 'site-packages')

    printf('Adding calibre sources...')
    for x in glob.glob(j(CALIBRE_DIR, 'src', '*')):
        if os.path.isdir(x):
            if os.path.exists(os.path.join(x, '__init__.py')):
                shutil.copytree(x, j(sp_dir, b(x)), ignore=shutil.ignore_patterns('*.pyc', '*.pyo'))
        else:
            shutil.copy(x, j(sp_dir, b(x)))

    ext_map.update(cleanup_site_packages(sp_dir))
    for x in os.listdir(sp_dir):
        os.rename(j(sp_dir, x), j(env.lib_dir, x))
    os.rmdir(sp_dir)
    printf('Extracting extension modules from', env.lib_dir, 'to', env.dll_dir)
    ext_map.update(extract_extension_modules(env.lib_dir, env.dll_dir))

    printf('Byte-compiling all python modules...')
    py_compile(env.lib_dir.replace(os.sep, '/'))
    # from bypy.utils import run_shell
    # run_shell(cwd=env.lib_dir)
    freeze_python(env.lib_dir, env.dll_dir, incdir, ext_map, develop_mode_env_var='CALIBRE_DEVELOP_FROM')
    shutil.rmtree(env.lib_dir)


def embed_manifests(env):
    printf('Embedding remaining manifests...')
    for manifest in walk(env.base):
        dll, ext = os.path.splitext(manifest)
        if ext != '.manifest':
            continue
        res = 2
        if os.path.splitext(dll)[1] == '.exe':
            res = 1
        if os.path.exists(dll) and open(manifest, 'rb').read().strip():
            run(MT, '-manifest', manifest, '-outputresource:%s;%d' % (dll, res))
        os.remove(manifest)


def embed_resources(env, module, desc=None, extra_data=None, product_description=None):
    icon_base = j(env.src_root, 'icons')
    icon_map = {
        'calibre': 'library', 'ebook-viewer': 'viewer', 'ebook-edit': 'ebook-edit',
        'lrfviewer': 'viewer',
    }
    file_type = 'DLL' if module.endswith('.dll') else 'APP'
    with open(env.rc_template, 'rb') as f:
        template = f.read().decode('utf-8')
    bname = b(module)
    internal_name = os.path.splitext(bname)[0]
    icon = icon_map.get(internal_name.replace('-portable', ''), 'command-prompt')
    if internal_name.startswith('calibre-portable-'):
        icon = 'install'
    icon = 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.replace('\\', '/'),
        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 = env.obj_dir
    rcf = j(tdir, bname + '.rc')
    with open(rcf, 'w') as f:
        f.write(rc)
    res = j(tdir, bname + '.res')
    run(RC, '/n', '/fo' + res, rcf)
    return res


def install_site_py(env):
    if not os.path.exists(env.lib_dir):
        os.makedirs(env.lib_dir)
    shutil.copy2(j(d(__file__), 'site.py'), env.lib_dir)


def build_portable_installer(env):
    zf = a(j(env.dist, 'calibre-portable-%s.zip.lz' % VERSION)).replace(os.sep, '/')
    usz = env.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' % PREFIX)
        cflags.append('/DUNCOMPRESSED_SIZE=%d' % usz)
        printf('Compiling', obj)
        cmd = [CL] + cflags + ['/Fo' + obj, src]
        run_compiler(env, *cmd)

    base = d(a(__file__))
    src = j(base, 'portable-installer.cpp')
    obj = j(env.obj_dir, b(src) + '.obj')
    xsrc = j(base, 'XUnzip.cpp')
    xobj = j(env.obj_dir, b(xsrc) + '.obj')
    cc(src, obj)
    cc(xsrc, xobj)

    exe = j(env.dist, 'calibre-portable-installer-%s.exe' % VERSION)
    printf('Linking', exe)
    manifest = exe + '.manifest'
    with open(manifest, 'wb') as f:
        f.write(EXE_MANIFEST.encode('utf-8'))
    cmd = [LINK] + [
        '/INCREMENTAL:NO', '/MACHINE:' + machine,
        '/LIBPATH:' + env.obj_dir, '/SUBSYSTEM:WINDOWS',
        '/LIBPATH:' + (PREFIX + r'\lib'),
        '/RELEASE', '/MANIFEST:EMBED', '/MANIFESTINPUT:' + manifest,
        '/ENTRY:wWinMainCRTStartup',
        '/OUT:' + exe, embed_resources(
            env, 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']
    run(*cmd)
    os.remove(zf)
    os.remove(manifest)


def build_portable(env):
    base = env.portable_base
    if os.path.exists(base):
        shutil.rmtree(base)
    os.makedirs(base)
    root = d(a(__file__))
    src = j(root, 'portable.cpp')
    obj = j(env.obj_dir, b(src) + '.obj')
    cflags = '/c /EHsc /MT /W3 /Ox /nologo /D_UNICODE /DUNICODE'.split()
    launchers = []

    for exe_name in ('calibre.exe', 'ebook-viewer.exe', 'ebook-edit.exe'):
        exe = j(base, exe_name.replace('.exe', '-portable.exe'))
        printf('Compiling', exe)
        cmd = [CL] + cflags + ['/Fo' + obj, '/Tp' + src]
        run_compiler(env, *cmd)
        printf('Linking', exe)
        desc = {
            'calibre.exe': 'Calibre Portable',
            'ebook-viewer.exe': 'Calibre Portable Viewer',
            'ebook-edit.exe': 'Calibre Portable Editor'
        }[exe_name]
        cmd = [LINK] + [
            '/INCREMENTAL:NO', '/MACHINE:' + machine,
            '/LIBPATH:' + env.obj_dir, '/SUBSYSTEM:WINDOWS',
            '/RELEASE',
            '/ENTRY:wWinMainCRTStartup',
            '/OUT:' + exe, embed_resources(env, exe, desc=desc, product_description=desc),
            obj, 'User32.lib', 'Shell32.lib']
        run(*cmd)
        sign_file(exe)

    printf('Creating portable installer')
    shutil.copytree(env.base, j(base, 'Calibre'))
    os.mkdir(j(base, 'Calibre Library'))
    os.mkdir(j(base, 'Calibre Settings'))

    name = '%s-portable-%s.zip' % (APPNAME, VERSION)
    name = j(env.dist, name)
    with zipfile.ZipFile(name, 'w', zipfile.ZIP_STORED) as zf:
        add_dir_to_zip(zf, base, 'Calibre Portable')

    env.portable_uncompressed_size = os.path.getsize(name)
    subprocess.check_call([PREFIX + r'\bin\elzma.exe', '-9', '--lzip', name])


def add_dir_to_zip(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):
                add_dir_to_zip(zf, f, prefix=arcname)
            else:
                zf.write(f, arcname)
    finally:
        os.chdir(cwd)


def build_utils(env):

    def build(src, name, subsys='CONSOLE', libs='setupapi.lib'.split()):
        printf('Building ' + name)
        obj = j(env.obj_dir, os.path.basename(src) + '.obj')
        cflags = '/c /EHsc /MD /W3 /Ox /nologo /D_UNICODE'.split()
        ftype = '/T' + ('c' if src.endswith('.c') else 'p')
        cmd = [CL] + cflags + ['/Fo' + obj, ftype + src]
        run_compiler(env, *cmd)
        exe = j(env.dll_dir, name)
        mf = exe + '.manifest'
        with open(mf, 'wb') as f:
            f.write(EXE_MANIFEST.encode('utf-8'))
        cmd = [LINK] + [
            '/MACHINE:' + machine,
            '/SUBSYSTEM:' + subsys, '/RELEASE', '/MANIFEST:EMBED', '/MANIFESTINPUT:' + mf,
            '/OUT:' + exe] + [embed_resources(env, exe), obj] + libs
        run(*cmd)
    base = d(a(__file__))
    build(j(base, 'file_dialogs.cpp'), 'calibre-file-dialog.exe', 'WINDOWS', 'Ole32.lib Shell32.lib'.split())
    build(j(base, 'eject.c'), 'calibre-eject.exe')


def build_launchers(env, incdir, debug=False):
    if not os.path.exists(env.obj_dir):
        os.makedirs(env.obj_dir)
    dflags = (['/Zi'] if debug else [])
    dlflags = (['/DEBUG'] if debug else ['/INCREMENTAL:NO'])
    base = d(a(__file__))
    sources = [j(base, x) for x in ['util.c', ]]
    objects = [j(env.obj_dir, b(x) + '.obj') for x in sources]
    # Launcher DLL must be built aginast limit API to allow delay loading,
    # which means defining PY_LIMITED_API and linking against python3.lib
    # rather than python3.xx.lib
    cflags = '/c /EHsc /W3 /Ox /nologo /D_UNICODE /DPY_LIMITED_API=0x030A0000L'.split()
    cflags += ['/DPYDLL="python%s.dll"' % env.py_ver.replace('.', ''), '/I%s/include' % env.python_base]
    cflags += [f'/I{path_to_freeze_dir()}', f'/I{incdir}']
    for src, obj in zip(sources, objects):
        cmd = [CL] + cflags + dflags + ['/MD', '/Fo' + obj, '/Tc' + src]
        run_compiler(env, *cmd)

    dll = j(env.obj_dir, 'calibre-launcher.dll')
    ver = '.'.join(VERSION.split('.')[:2])
    cmd = [LINK, '/DLL', '/VERSION:' + ver, '/LTCG', '/OUT:' + dll,
           '/nologo', '/MACHINE:' + machine] + dlflags + objects + \
        [embed_resources(env, dll),
            '/LIBPATH:%s/libs' % env.python_base,
            'delayimp.lib', 'user32.lib', 'shell32.lib',
            'python%s.lib' % env.py_ver.split('.')[0],
            '/delayload:python%s.dll' % env.py_ver.replace('.', '')]
    printf('Linking calibre-launcher.dll')
    run(*cmd)

    src = j(base, 'main.c')
    shutil.copy2(dll, env.dll_dir)
    basenames, modules, functions = calibre_constants['basenames'], calibre_constants['modules'], calibre_constants['functions']
    for typ in ('console', 'gui', ):
        printf('Processing %s launchers' % typ)
        subsys = 'WINDOWS' if typ == 'gui' else 'CONSOLE'
        for mod, bname, func in zip(modules[typ], basenames[typ], functions[typ]):
            cflags = '/c /EHsc /MT /W3 /O1 /nologo /D_UNICODE /DUNICODE /GS-'.split()
            if typ == 'gui':
                cflags += ['/DGUI_APP=']

            cflags += ['/DMODULE=L"%s"' % mod, '/DBASENAME=L"%s"' % bname,
                       '/DFUNCTION=L"%s"' % func]
            dest = j(env.obj_dir, bname + '.obj')
            printf('Compiling', bname)
            cmd = [CL] + cflags + dflags + ['/Tc' + src, '/Fo' + dest]
            run_compiler(env, *cmd)
            exe = j(env.base, bname + '.exe')
            lib = dll.replace('.dll', '.lib')
            u32 = ['user32.lib']
            printf('Linking', bname)
            mf = dest + '.manifest'
            with open(mf, 'wb') as f:
                f.write(EXE_MANIFEST.encode('utf-8'))
            cmd = [LINK] + [
                '/MACHINE:' + machine, '/NODEFAULTLIB', '/ENTRY:start_here',
                '/LIBPATH:' + env.obj_dir, '/SUBSYSTEM:' + subsys,
                '/LIBPATH:%s/libs' % env.python_base, '/RELEASE',
                '/MANIFEST:EMBED', '/MANIFESTINPUT:' + mf,
                '/STACK:2097152',  # Set stack size to 2MB which is what python expects. Default on windows is 1MB
                'user32.lib', 'kernel32.lib',
                '/OUT:' + exe] + u32 + dlflags + [embed_resources(env, exe), dest, lib]
            run(*cmd)


def copy_crt_and_d3d(env):
    printf('Copying CRT and D3D...')
    plat = 'x64'
    for key, val in worker_env.items():
        if 'COMNTOOLS' in key.upper():
            redist_dir = os.path.dirname(os.path.dirname(val.rstrip(os.sep)))
            redist_dir = os.path.join(redist_dir, 'VC', 'Redist', 'MSVC')
            vc_path = glob.glob(os.path.join(redist_dir, '*', plat, '*.CRT'))[0]
            break
    else:
        raise SystemExit('Could not find Visual Studio redistributable CRT')

    sdk_path = os.path.join(
        worker_env['UNIVERSALCRTSDKDIR'], 'Redist', worker_env['WINDOWSSDKVERSION'],
        'ucrt', 'DLLs', plat)
    if not os.path.exists(sdk_path):
        raise SystemExit('Windows 10 Universal CRT redistributable not found at: %r' % sdk_path)
    d3d_path = os.path.join(
        worker_env['WINDOWSSDKDIR'], 'Redist', 'D3D', plat)
    if not os.path.exists(d3d_path):
        raise SystemExit('Windows 10 D3D redistributable not found at: %r' % d3d_path)
    mesa_path = os.path.join(os.environ['MESA'], '64', 'opengl32sw.dll')
    if not os.path.exists(mesa_path):
        raise SystemExit('Mesa DLLs (opengl32sw.dll) not found at: %r' % mesa_path)

    def copy_dll(dll):
        shutil.copy2(dll, env.dll_dir)
        dest = os.path.join(env.dll_dir, b(dll))
        os.chmod(dest, stat.S_IRWXU)

    for dll in glob.glob(os.path.join(d3d_path, '*.dll')):
        if os.path.basename(dll).lower().startswith('d3dcompiler_'):
            copy_dll(dll)
    copy_dll(mesa_path)
    for dll in glob.glob(os.path.join(sdk_path, '*.dll')):
        copy_dll(dll)
    for dll in glob.glob(os.path.join(vc_path, '*.dll')):
        bname = os.path.basename(dll)
        if not bname.startswith('vccorlib') and not bname.startswith('concrt'):
            # Those two DLLs are not required vccorlib is for the CORE CLR
            # I think concrt is the concurrency runtime for C++ which I believe
            # nothing in calibre currently uses
            copy_dll(dll)


def sign_all_pe_files(env):
    for path in walk(env.base):
        if path.rpartition('.')[-1].lower() in ('dll', 'pyd', 'exe') and not has_signature(path):
            sign_file(path)


def main():
    ext_dir = globals()['ext_dir']
    args = globals()['args']
    run_tests = iv['run_tests']
    env = Env(build_dir())
    incdir = mkdtemp('include')
    initbase(env)
    freeze(env, ext_dir, incdir)
    build_launchers(env, incdir)
    build_utils(env)
    embed_manifests(env)
    copy_crt_and_d3d(env)
    if args.sign_installers:
        sign_all_pe_files(env)
    if not args.skip_tests:
        run_tests(os.path.join(env.base, 'calibre-debug.exe'), env.base)
    create_installer(env, args.compression_level)
    build_portable(env)
    build_portable_installer(env)
    if args.sign_installers:
        print(f'Signed {len(signatures)} files')


def develop_launcher():
    import subprocess

    def r(*a):
        subprocess.check_call(list(a))

    r(
        'cl.EXE', '/c', '/EHsc', '/MT', '/W3', '/O1', '/nologo', '/D_UNICODE', '/DUNICODE', '/GS-',
        '/DMODULE="calibre.debug"', '/DBASENAME="calibre-debug"', '/DFUNCTION="main"',
        r'/TcC:\r\src\bypy\windows\main.c', r'/Fo..\launcher\calibre-debug.obj'
    )
    r(
        'link.EXE', '/MACHINE:X86', '/NODEFAULTLIB', '/ENTRY:start_here',
        r'/LIBPATH:..\launcher', '/SUBSYSTEM:CONSOLE',
        r'/LIBPATH:C:\r\sw32\sw\private\python/libs', '/RELEASE',
        '/MANIFEST:EMBED', r'/MANIFESTINPUT:..\launcher\calibre-debug.obj.manifest',
        'user32.lib', 'kernel32.lib', r'/OUT:calibre-debug.exe',
        'user32.lib', '/INCREMENTAL:NO', r'..\launcher\calibre-debug.exe.res',
        r'..\launcher\calibre-debug.obj', r'..\launcher\calibre-launcher.lib'
    )


if __name__ == '__main__':
    main()
