1#!/usr/local/bin/python3.8
2# vim:fileencoding=utf-8
3# License: GPLv3 Copyright: 2016, Kovid Goyal <kovid at kovidgoyal.net>
4
5from itertools import count
6import os
7import shutil
8
9from bypy.constants import is64bit
10from bypy.utils import run
11
12WIXP = r'C:\Program Files (x86)\WiX Toolset v3.11'
13if is64bit:
14    UPGRADE_CODE = '5DD881FF-756B-4097-9D82-8C0F11D521EA'
15else:
16    UPGRADE_CODE = 'BEB2A80D-E902-4DAD-ADF9-8BD2DA42CFE1'
17MINVERHUMAN = 'Windows 8'
18calibre_constants = globals()['calibre_constants']
19
20CANDLE = WIXP + r'\bin\candle.exe'
21LIGHT = WIXP + r'\bin\light.exe'
22j, d, a, b = os.path.join, os.path.dirname, os.path.abspath, os.path.basename
23
24
25def create_installer(env):
26    if os.path.exists(env.installer_dir):
27        shutil.rmtree(env.installer_dir)
28    os.makedirs(env.installer_dir)
29
30    with open(j(d(__file__), 'wix-template.xml'), 'rb') as f:
31        template = f.read().decode('utf-8')
32
33    components, smap = get_components_from_files(env)
34    wxs = template.format(
35        app=calibre_constants['appname'],
36        appfolder='Calibre2' if is64bit else 'Calibre',
37        version=calibre_constants['version'],
38        upgrade_code=UPGRADE_CODE,
39        ProgramFilesFolder='ProgramFiles64Folder' if is64bit else 'ProgramFilesFolder',
40        x64=' 64bit' if is64bit else '',
41        minverhuman=MINVERHUMAN,
42        minver='602',
43        fix_wix='<Custom Action="OverwriteWixSetDefaultPerMachineFolder" After="WixSetDefaultPerMachineFolder" />' if is64bit else '',
44        compression='high',
45        app_components=components,
46        exe_map=smap,
47        main_icon=j(env.src_root, 'icons', 'library.ico'),
48        viewer_icon=j(env.src_root, 'icons', 'viewer.ico'),
49        editor_icon=j(env.src_root, 'icons', 'ebook-edit.ico'),
50        web_icon=j(env.src_root, 'icons', 'web.ico'),
51    )
52    with open(j(d(__file__), 'en-us.xml'), 'rb') as f:
53        template = f.read().decode('utf-8')
54    enus = template.format(app=calibre_constants['appname'])
55
56    enusf = j(env.installer_dir, 'en-us.wxl')
57    wxsf = j(env.installer_dir, calibre_constants['appname'] + '.wxs')
58    with open(wxsf, 'wb') as f:
59        f.write(wxs.encode('utf-8'))
60    with open(enusf, 'wb') as f:
61        f.write(enus.encode('utf-8'))
62    wixobj = j(env.installer_dir, calibre_constants['appname'] + '.wixobj')
63    arch = 'x64' if is64bit else 'x86'
64    cmd = [CANDLE, '-nologo', '-arch', arch, '-ext', 'WiXUtilExtension', '-o', wixobj, wxsf]
65    run(*cmd)
66    installer = j(env.dist, '%s%s-%s.msi' % (
67        calibre_constants['appname'], ('-64bit' if is64bit else ''), calibre_constants['version']))
68    license = j(env.src_root, 'LICENSE.rtf')
69    banner = j(env.src_root, 'icons', 'wix-banner.bmp')
70    dialog = j(env.src_root, 'icons', 'wix-dialog.bmp')
71    cmd = [LIGHT, '-nologo', '-ext', 'WixUIExtension',
72           '-cultures:en-us', '-loc', enusf, wixobj,
73           '-ext', 'WixUtilExtension',
74           '-o', installer,
75           '-dWixUILicenseRtf=' + license,
76           '-dWixUIBannerBmp=' + banner,
77           '-dWixUIDialogBmp=' + dialog]
78    cmd.extend([
79        '-sice:ICE60',  # No language in dlls warning
80        '-sice:ICE61',  # Allow upgrading with same version number
81        '-sice:ICE40',  # Re-install mode overridden
82        '-sice:ICE69',  # Shortcut components are part of a different feature than the files they point to
83    ])
84    cmd.append('-sval')  # Disable all checks since they fail when running under ssh
85    run(*cmd)
86
87
88def get_components_from_files(env):
89
90    file_idc = count()
91    file_id_map = {}
92
93    def process_dir(path):
94        components = []
95        for x in os.listdir(path):
96            f = os.path.join(path, x)
97            file_id_map[f] = fid = next(file_idc)
98
99            if os.path.isdir(f):
100                components.append(
101                    '<Directory Id="file_%s" FileSource="%s" Name="%s">' %
102                    (file_id_map[f], f, x))
103                c = process_dir(f)
104                components.extend(c)
105                components.append('</Directory>')
106            else:
107                checksum = 'Checksum="yes"' if x.endswith('.exe') else ''
108                c = [
109                    ('<Component Id="component_%s" Feature="MainApplication" '
110                        'Guid="*">') % (fid,),
111                    ('<File Id="file_%s" Source="%s" Name="%s" ReadOnly="yes" '
112                        'KeyPath="yes" %s/>') %
113                    (fid, f, x, checksum),
114                    '</Component>'
115                ]
116                if x.endswith('.exe') and not x.startswith('pdf'):
117                    # Add the executable to app paths so that users can
118                    # launch it from the run dialog even if it is not on
119                    # the path. See http://msdn.microsoft.com/en-us/library/windows/desktop/ee872121(v=vs.85).aspx
120                    c[-1:-1] = [
121                        ('<RegistryValue Root="HKLM" '
122                            r'Key="SOFTWARE\Microsoft\Windows\CurrentVersion\App '
123                            r'Paths\%s" Value="[#file_%d]" Type="string" />' % (x, fid)),
124                        ('<RegistryValue Root="HKLM" '
125                            r'Key="SOFTWARE\Microsoft\Windows\CurrentVersion\App '
126                            r'Paths\{0}" Name="Path" Value="[APPLICATIONFOLDER]" '
127                            'Type="string" />'.format(x)),
128                    ]
129                components.append('\n'.join(c))
130        return components
131
132    components = process_dir(a(env.base))
133    smap = {}
134    for x in calibre_constants['basenames']['gui']:
135        smap[x] = 'file_%d' % file_id_map[a(j(env.base, x + '.exe'))]
136
137    return '\t\t\t\t' + '\n\t\t\t\t'.join(components), smap
138