1# -*- coding: iso-8859-1 -*-
2# Copyright (C) 2005, 2006 Martin von L�wis
3# Licensed to PSF under a Contributor Agreement.
4# The bdist_wininst command proper
5# based on bdist_wininst
6"""
7Implements the bdist_msi command.
8"""
9import sys, os
10from sysconfig import get_python_version
11
12from distutils.core import Command
13from distutils.dir_util import remove_tree
14from distutils.version import StrictVersion
15from distutils.errors import DistutilsOptionError
16from distutils import log
17from distutils.util import get_platform
18
19import msilib
20from msilib import schema, sequence, text
21from msilib import Directory, Feature, Dialog, add_data
22
23class PyDialog(Dialog):
24    """Dialog class with a fixed layout: controls at the top, then a ruler,
25    then a list of buttons: back, next, cancel. Optionally a bitmap at the
26    left."""
27    def __init__(self, *args, **kw):
28        """Dialog(database, name, x, y, w, h, attributes, title, first,
29        default, cancel, bitmap=true)"""
30        Dialog.__init__(self, *args)
31        ruler = self.h - 36
32        #if kw.get("bitmap", True):
33        #    self.bitmap("Bitmap", 0, 0, bmwidth, ruler, "PythonWin")
34        self.line("BottomLine", 0, ruler, self.w, 0)
35
36    def title(self, title):
37        "Set the title text of the dialog at the top."
38        # name, x, y, w, h, flags=Visible|Enabled|Transparent|NoPrefix,
39        # text, in VerdanaBold10
40        self.text("Title", 15, 10, 320, 60, 0x30003,
41                  r"{\VerdanaBold10}%s" % title)
42
43    def back(self, title, next, name = "Back", active = 1):
44        """Add a back button with a given title, the tab-next button,
45        its name in the Control table, possibly initially disabled.
46
47        Return the button, so that events can be associated"""
48        if active:
49            flags = 3 # Visible|Enabled
50        else:
51            flags = 1 # Visible
52        return self.pushbutton(name, 180, self.h-27 , 56, 17, flags, title, next)
53
54    def cancel(self, title, next, name = "Cancel", active = 1):
55        """Add a cancel button with a given title, the tab-next button,
56        its name in the Control table, possibly initially disabled.
57
58        Return the button, so that events can be associated"""
59        if active:
60            flags = 3 # Visible|Enabled
61        else:
62            flags = 1 # Visible
63        return self.pushbutton(name, 304, self.h-27, 56, 17, flags, title, next)
64
65    def next(self, title, next, name = "Next", active = 1):
66        """Add a Next button with a given title, the tab-next button,
67        its name in the Control table, possibly initially disabled.
68
69        Return the button, so that events can be associated"""
70        if active:
71            flags = 3 # Visible|Enabled
72        else:
73            flags = 1 # Visible
74        return self.pushbutton(name, 236, self.h-27, 56, 17, flags, title, next)
75
76    def xbutton(self, name, title, next, xpos):
77        """Add a button with a given title, the tab-next button,
78        its name in the Control table, giving its x position; the
79        y-position is aligned with the other buttons.
80
81        Return the button, so that events can be associated"""
82        return self.pushbutton(name, int(self.w*xpos - 28), self.h-27, 56, 17, 3, title, next)
83
84class bdist_msi (Command):
85
86    description = "create a Microsoft Installer (.msi) binary distribution"
87
88    user_options = [('bdist-dir=', None,
89                     "temporary directory for creating the distribution"),
90                    ('plat-name=', 'p',
91                     "platform name to embed in generated filenames "
92                     "(default: %s)" % get_platform()),
93                    ('keep-temp', 'k',
94                     "keep the pseudo-installation tree around after " +
95                     "creating the distribution archive"),
96                    ('target-version=', None,
97                     "require a specific python version" +
98                     " on the target system"),
99                    ('no-target-compile', 'c',
100                     "do not compile .py to .pyc on the target system"),
101                    ('no-target-optimize', 'o',
102                     "do not compile .py to .pyo (optimized)"
103                     "on the target system"),
104                    ('dist-dir=', 'd',
105                     "directory to put final built distributions in"),
106                    ('skip-build', None,
107                     "skip rebuilding everything (for testing/debugging)"),
108                    ('install-script=', None,
109                     "basename of installation script to be run after"
110                     "installation or before deinstallation"),
111                    ('pre-install-script=', None,
112                     "Fully qualified filename of a script to be run before "
113                     "any files are installed.  This script need not be in the "
114                     "distribution"),
115                   ]
116
117    boolean_options = ['keep-temp', 'no-target-compile', 'no-target-optimize',
118                       'skip-build']
119
120    all_versions = ['2.0', '2.1', '2.2', '2.3', '2.4',
121                    '2.5', '2.6', '2.7', '2.8', '2.9',
122                    '3.0', '3.1', '3.2', '3.3', '3.4',
123                    '3.5', '3.6', '3.7', '3.8', '3.9']
124    other_version = 'X'
125
126    def initialize_options (self):
127        self.bdist_dir = None
128        self.plat_name = None
129        self.keep_temp = 0
130        self.no_target_compile = 0
131        self.no_target_optimize = 0
132        self.target_version = None
133        self.dist_dir = None
134        self.skip_build = None
135        self.install_script = None
136        self.pre_install_script = None
137        self.versions = None
138
139    def finalize_options (self):
140        self.set_undefined_options('bdist', ('skip_build', 'skip_build'))
141
142        if self.bdist_dir is None:
143            bdist_base = self.get_finalized_command('bdist').bdist_base
144            self.bdist_dir = os.path.join(bdist_base, 'msi')
145
146        short_version = get_python_version()
147        if (not self.target_version) and self.distribution.has_ext_modules():
148            self.target_version = short_version
149
150        if self.target_version:
151            self.versions = [self.target_version]
152            if not self.skip_build and self.distribution.has_ext_modules()\
153               and self.target_version != short_version:
154                raise DistutilsOptionError, \
155                      "target version can only be %s, or the '--skip-build'" \
156                      " option must be specified" % (short_version,)
157        else:
158            self.versions = list(self.all_versions)
159
160        self.set_undefined_options('bdist',
161                                   ('dist_dir', 'dist_dir'),
162                                   ('plat_name', 'plat_name'),
163                                   )
164
165        if self.pre_install_script:
166            raise DistutilsOptionError, "the pre-install-script feature is not yet implemented"
167
168        if self.install_script:
169            for script in self.distribution.scripts:
170                if self.install_script == os.path.basename(script):
171                    break
172            else:
173                raise DistutilsOptionError, \
174                      "install_script '%s' not found in scripts" % \
175                      self.install_script
176        self.install_script_key = None
177    # finalize_options()
178
179
180    def run (self):
181        if not self.skip_build:
182            self.run_command('build')
183
184        install = self.reinitialize_command('install', reinit_subcommands=1)
185        install.prefix = self.bdist_dir
186        install.skip_build = self.skip_build
187        install.warn_dir = 0
188
189        install_lib = self.reinitialize_command('install_lib')
190        # we do not want to include pyc or pyo files
191        install_lib.compile = 0
192        install_lib.optimize = 0
193
194        if self.distribution.has_ext_modules():
195            # If we are building an installer for a Python version other
196            # than the one we are currently running, then we need to ensure
197            # our build_lib reflects the other Python version rather than ours.
198            # Note that for target_version!=sys.version, we must have skipped the
199            # build step, so there is no issue with enforcing the build of this
200            # version.
201            target_version = self.target_version
202            if not target_version:
203                assert self.skip_build, "Should have already checked this"
204                target_version = sys.version[0:3]
205            plat_specifier = ".%s-%s" % (self.plat_name, target_version)
206            build = self.get_finalized_command('build')
207            build.build_lib = os.path.join(build.build_base,
208                                           'lib' + plat_specifier)
209
210        log.info("installing to %s", self.bdist_dir)
211        install.ensure_finalized()
212
213        # avoid warning of 'install_lib' about installing
214        # into a directory not in sys.path
215        sys.path.insert(0, os.path.join(self.bdist_dir, 'PURELIB'))
216
217        install.run()
218
219        del sys.path[0]
220
221        self.mkpath(self.dist_dir)
222        fullname = self.distribution.get_fullname()
223        installer_name = self.get_installer_filename(fullname)
224        installer_name = os.path.abspath(installer_name)
225        if os.path.exists(installer_name): os.unlink(installer_name)
226
227        metadata = self.distribution.metadata
228        author = metadata.author
229        if not author:
230            author = metadata.maintainer
231        if not author:
232            author = "UNKNOWN"
233        version = metadata.get_version()
234        # ProductVersion must be strictly numeric
235        # XXX need to deal with prerelease versions
236        sversion = "%d.%d.%d" % StrictVersion(version).version
237        # Prefix ProductName with Python x.y, so that
238        # it sorts together with the other Python packages
239        # in Add-Remove-Programs (APR)
240        fullname = self.distribution.get_fullname()
241        if self.target_version:
242            product_name = "Python %s %s" % (self.target_version, fullname)
243        else:
244            product_name = "Python %s" % (fullname)
245        self.db = msilib.init_database(installer_name, schema,
246                product_name, msilib.gen_uuid(),
247                sversion, author)
248        msilib.add_tables(self.db, sequence)
249        props = [('DistVersion', version)]
250        email = metadata.author_email or metadata.maintainer_email
251        if email:
252            props.append(("ARPCONTACT", email))
253        if metadata.url:
254            props.append(("ARPURLINFOABOUT", metadata.url))
255        if props:
256            add_data(self.db, 'Property', props)
257
258        self.add_find_python()
259        self.add_files()
260        self.add_scripts()
261        self.add_ui()
262        self.db.Commit()
263
264        if hasattr(self.distribution, 'dist_files'):
265            tup = 'bdist_msi', self.target_version or 'any', fullname
266            self.distribution.dist_files.append(tup)
267
268        if not self.keep_temp:
269            remove_tree(self.bdist_dir, dry_run=self.dry_run)
270
271    def add_files(self):
272        db = self.db
273        cab = msilib.CAB("distfiles")
274        rootdir = os.path.abspath(self.bdist_dir)
275
276        root = Directory(db, cab, None, rootdir, "TARGETDIR", "SourceDir")
277        f = Feature(db, "Python", "Python", "Everything",
278                    0, 1, directory="TARGETDIR")
279
280        items = [(f, root, '')]
281        for version in self.versions + [self.other_version]:
282            target = "TARGETDIR" + version
283            name = default = "Python" + version
284            desc = "Everything"
285            if version is self.other_version:
286                title = "Python from another location"
287                level = 2
288            else:
289                title = "Python %s from registry" % version
290                level = 1
291            f = Feature(db, name, title, desc, 1, level, directory=target)
292            dir = Directory(db, cab, root, rootdir, target, default)
293            items.append((f, dir, version))
294        db.Commit()
295
296        seen = {}
297        for feature, dir, version in items:
298            todo = [dir]
299            while todo:
300                dir = todo.pop()
301                for file in os.listdir(dir.absolute):
302                    afile = os.path.join(dir.absolute, file)
303                    if os.path.isdir(afile):
304                        short = "%s|%s" % (dir.make_short(file), file)
305                        default = file + version
306                        newdir = Directory(db, cab, dir, file, default, short)
307                        todo.append(newdir)
308                    else:
309                        if not dir.component:
310                            dir.start_component(dir.logical, feature, 0)
311                        if afile not in seen:
312                            key = seen[afile] = dir.add_file(file)
313                            if file==self.install_script:
314                                if self.install_script_key:
315                                    raise DistutilsOptionError(
316                                          "Multiple files with name %s" % file)
317                                self.install_script_key = '[#%s]' % key
318                        else:
319                            key = seen[afile]
320                            add_data(self.db, "DuplicateFile",
321                                [(key + version, dir.component, key, None, dir.logical)])
322            db.Commit()
323        cab.commit(db)
324
325    def add_find_python(self):
326        """Adds code to the installer to compute the location of Python.
327
328        Properties PYTHON.MACHINE.X.Y and PYTHON.USER.X.Y will be set from the
329        registry for each version of Python.
330
331        Properties TARGETDIRX.Y will be set from PYTHON.USER.X.Y if defined,
332        else from PYTHON.MACHINE.X.Y.
333
334        Properties PYTHONX.Y will be set to TARGETDIRX.Y\\python.exe"""
335
336        start = 402
337        for ver in self.versions:
338            install_path = r"SOFTWARE\Python\PythonCore\%s\InstallPath" % ver
339            machine_reg = "python.machine." + ver
340            user_reg = "python.user." + ver
341            machine_prop = "PYTHON.MACHINE." + ver
342            user_prop = "PYTHON.USER." + ver
343            machine_action = "PythonFromMachine" + ver
344            user_action = "PythonFromUser" + ver
345            exe_action = "PythonExe" + ver
346            target_dir_prop = "TARGETDIR" + ver
347            exe_prop = "PYTHON" + ver
348            if msilib.Win64:
349                # type: msidbLocatorTypeRawValue + msidbLocatorType64bit
350                Type = 2+16
351            else:
352                Type = 2
353            add_data(self.db, "RegLocator",
354                    [(machine_reg, 2, install_path, None, Type),
355                     (user_reg, 1, install_path, None, Type)])
356            add_data(self.db, "AppSearch",
357                    [(machine_prop, machine_reg),
358                     (user_prop, user_reg)])
359            add_data(self.db, "CustomAction",
360                    [(machine_action, 51+256, target_dir_prop, "[" + machine_prop + "]"),
361                     (user_action, 51+256, target_dir_prop, "[" + user_prop + "]"),
362                     (exe_action, 51+256, exe_prop, "[" + target_dir_prop + "]\\python.exe"),
363                    ])
364            add_data(self.db, "InstallExecuteSequence",
365                    [(machine_action, machine_prop, start),
366                     (user_action, user_prop, start + 1),
367                     (exe_action, None, start + 2),
368                    ])
369            add_data(self.db, "InstallUISequence",
370                    [(machine_action, machine_prop, start),
371                     (user_action, user_prop, start + 1),
372                     (exe_action, None, start + 2),
373                    ])
374            add_data(self.db, "Condition",
375                    [("Python" + ver, 0, "NOT TARGETDIR" + ver)])
376            start += 4
377            assert start < 500
378
379    def add_scripts(self):
380        if self.install_script:
381            start = 6800
382            for ver in self.versions + [self.other_version]:
383                install_action = "install_script." + ver
384                exe_prop = "PYTHON" + ver
385                add_data(self.db, "CustomAction",
386                        [(install_action, 50, exe_prop, self.install_script_key)])
387                add_data(self.db, "InstallExecuteSequence",
388                        [(install_action, "&Python%s=3" % ver, start)])
389                start += 1
390        # XXX pre-install scripts are currently refused in finalize_options()
391        #     but if this feature is completed, it will also need to add
392        #     entries for each version as the above code does
393        if self.pre_install_script:
394            scriptfn = os.path.join(self.bdist_dir, "preinstall.bat")
395            f = open(scriptfn, "w")
396            # The batch file will be executed with [PYTHON], so that %1
397            # is the path to the Python interpreter; %0 will be the path
398            # of the batch file.
399            # rem ="""
400            # %1 %0
401            # exit
402            # """
403            # <actual script>
404            f.write('rem ="""\n%1 %0\nexit\n"""\n')
405            f.write(open(self.pre_install_script).read())
406            f.close()
407            add_data(self.db, "Binary",
408                [("PreInstall", msilib.Binary(scriptfn))
409                ])
410            add_data(self.db, "CustomAction",
411                [("PreInstall", 2, "PreInstall", None)
412                ])
413            add_data(self.db, "InstallExecuteSequence",
414                    [("PreInstall", "NOT Installed", 450)])
415
416
417    def add_ui(self):
418        db = self.db
419        x = y = 50
420        w = 370
421        h = 300
422        title = "[ProductName] Setup"
423
424        # see "Dialog Style Bits"
425        modal = 3      # visible | modal
426        modeless = 1   # visible
427
428        # UI customization properties
429        add_data(db, "Property",
430                 # See "DefaultUIFont Property"
431                 [("DefaultUIFont", "DlgFont8"),
432                  # See "ErrorDialog Style Bit"
433                  ("ErrorDialog", "ErrorDlg"),
434                  ("Progress1", "Install"),   # modified in maintenance type dlg
435                  ("Progress2", "installs"),
436                  ("MaintenanceForm_Action", "Repair"),
437                  # possible values: ALL, JUSTME
438                  ("WhichUsers", "ALL")
439                 ])
440
441        # Fonts, see "TextStyle Table"
442        add_data(db, "TextStyle",
443                 [("DlgFont8", "Tahoma", 9, None, 0),
444                  ("DlgFontBold8", "Tahoma", 8, None, 1), #bold
445                  ("VerdanaBold10", "Verdana", 10, None, 1),
446                  ("VerdanaRed9", "Verdana", 9, 255, 0),
447                 ])
448
449        # UI Sequences, see "InstallUISequence Table", "Using a Sequence Table"
450        # Numbers indicate sequence; see sequence.py for how these action integrate
451        add_data(db, "InstallUISequence",
452                 [("PrepareDlg", "Not Privileged or Windows9x or Installed", 140),
453                  ("WhichUsersDlg", "Privileged and not Windows9x and not Installed", 141),
454                  # In the user interface, assume all-users installation if privileged.
455                  ("SelectFeaturesDlg", "Not Installed", 1230),
456                  # XXX no support for resume installations yet
457                  #("ResumeDlg", "Installed AND (RESUME OR Preselected)", 1240),
458                  ("MaintenanceTypeDlg", "Installed AND NOT RESUME AND NOT Preselected", 1250),
459                  ("ProgressDlg", None, 1280)])
460
461        add_data(db, 'ActionText', text.ActionText)
462        add_data(db, 'UIText', text.UIText)
463        #####################################################################
464        # Standard dialogs: FatalError, UserExit, ExitDialog
465        fatal=PyDialog(db, "FatalError", x, y, w, h, modal, title,
466                     "Finish", "Finish", "Finish")
467        fatal.title("[ProductName] Installer ended prematurely")
468        fatal.back("< Back", "Finish", active = 0)
469        fatal.cancel("Cancel", "Back", active = 0)
470        fatal.text("Description1", 15, 70, 320, 80, 0x30003,
471                   "[ProductName] setup ended prematurely because of an error.  Your system has not been modified.  To install this program at a later time, please run the installation again.")
472        fatal.text("Description2", 15, 155, 320, 20, 0x30003,
473                   "Click the Finish button to exit the Installer.")
474        c=fatal.next("Finish", "Cancel", name="Finish")
475        c.event("EndDialog", "Exit")
476
477        user_exit=PyDialog(db, "UserExit", x, y, w, h, modal, title,
478                     "Finish", "Finish", "Finish")
479        user_exit.title("[ProductName] Installer was interrupted")
480        user_exit.back("< Back", "Finish", active = 0)
481        user_exit.cancel("Cancel", "Back", active = 0)
482        user_exit.text("Description1", 15, 70, 320, 80, 0x30003,
483                   "[ProductName] setup was interrupted.  Your system has not been modified.  "
484                   "To install this program at a later time, please run the installation again.")
485        user_exit.text("Description2", 15, 155, 320, 20, 0x30003,
486                   "Click the Finish button to exit the Installer.")
487        c = user_exit.next("Finish", "Cancel", name="Finish")
488        c.event("EndDialog", "Exit")
489
490        exit_dialog = PyDialog(db, "ExitDialog", x, y, w, h, modal, title,
491                             "Finish", "Finish", "Finish")
492        exit_dialog.title("Completing the [ProductName] Installer")
493        exit_dialog.back("< Back", "Finish", active = 0)
494        exit_dialog.cancel("Cancel", "Back", active = 0)
495        exit_dialog.text("Description", 15, 235, 320, 20, 0x30003,
496                   "Click the Finish button to exit the Installer.")
497        c = exit_dialog.next("Finish", "Cancel", name="Finish")
498        c.event("EndDialog", "Return")
499
500        #####################################################################
501        # Required dialog: FilesInUse, ErrorDlg
502        inuse = PyDialog(db, "FilesInUse",
503                         x, y, w, h,
504                         19,                # KeepModeless|Modal|Visible
505                         title,
506                         "Retry", "Retry", "Retry", bitmap=False)
507        inuse.text("Title", 15, 6, 200, 15, 0x30003,
508                   r"{\DlgFontBold8}Files in Use")
509        inuse.text("Description", 20, 23, 280, 20, 0x30003,
510               "Some files that need to be updated are currently in use.")
511        inuse.text("Text", 20, 55, 330, 50, 3,
512                   "The following applications are using files that need to be updated by this setup. Close these applications and then click Retry to continue the installation or Cancel to exit it.")
513        inuse.control("List", "ListBox", 20, 107, 330, 130, 7, "FileInUseProcess",
514                      None, None, None)
515        c=inuse.back("Exit", "Ignore", name="Exit")
516        c.event("EndDialog", "Exit")
517        c=inuse.next("Ignore", "Retry", name="Ignore")
518        c.event("EndDialog", "Ignore")
519        c=inuse.cancel("Retry", "Exit", name="Retry")
520        c.event("EndDialog","Retry")
521
522        # See "Error Dialog". See "ICE20" for the required names of the controls.
523        error = Dialog(db, "ErrorDlg",
524                       50, 10, 330, 101,
525                       65543,       # Error|Minimize|Modal|Visible
526                       title,
527                       "ErrorText", None, None)
528        error.text("ErrorText", 50,9,280,48,3, "")
529        #error.control("ErrorIcon", "Icon", 15, 9, 24, 24, 5242881, None, "py.ico", None, None)
530        error.pushbutton("N",120,72,81,21,3,"No",None).event("EndDialog","ErrorNo")
531        error.pushbutton("Y",240,72,81,21,3,"Yes",None).event("EndDialog","ErrorYes")
532        error.pushbutton("A",0,72,81,21,3,"Abort",None).event("EndDialog","ErrorAbort")
533        error.pushbutton("C",42,72,81,21,3,"Cancel",None).event("EndDialog","ErrorCancel")
534        error.pushbutton("I",81,72,81,21,3,"Ignore",None).event("EndDialog","ErrorIgnore")
535        error.pushbutton("O",159,72,81,21,3,"Ok",None).event("EndDialog","ErrorOk")
536        error.pushbutton("R",198,72,81,21,3,"Retry",None).event("EndDialog","ErrorRetry")
537
538        #####################################################################
539        # Global "Query Cancel" dialog
540        cancel = Dialog(db, "CancelDlg", 50, 10, 260, 85, 3, title,
541                        "No", "No", "No")
542        cancel.text("Text", 48, 15, 194, 30, 3,
543                    "Are you sure you want to cancel [ProductName] installation?")
544        #cancel.control("Icon", "Icon", 15, 15, 24, 24, 5242881, None,
545        #               "py.ico", None, None)
546        c=cancel.pushbutton("Yes", 72, 57, 56, 17, 3, "Yes", "No")
547        c.event("EndDialog", "Exit")
548
549        c=cancel.pushbutton("No", 132, 57, 56, 17, 3, "No", "Yes")
550        c.event("EndDialog", "Return")
551
552        #####################################################################
553        # Global "Wait for costing" dialog
554        costing = Dialog(db, "WaitForCostingDlg", 50, 10, 260, 85, modal, title,
555                         "Return", "Return", "Return")
556        costing.text("Text", 48, 15, 194, 30, 3,
557                     "Please wait while the installer finishes determining your disk space requirements.")
558        c = costing.pushbutton("Return", 102, 57, 56, 17, 3, "Return", None)
559        c.event("EndDialog", "Exit")
560
561        #####################################################################
562        # Preparation dialog: no user input except cancellation
563        prep = PyDialog(db, "PrepareDlg", x, y, w, h, modeless, title,
564                        "Cancel", "Cancel", "Cancel")
565        prep.text("Description", 15, 70, 320, 40, 0x30003,
566                  "Please wait while the Installer prepares to guide you through the installation.")
567        prep.title("Welcome to the [ProductName] Installer")
568        c=prep.text("ActionText", 15, 110, 320, 20, 0x30003, "Pondering...")
569        c.mapping("ActionText", "Text")
570        c=prep.text("ActionData", 15, 135, 320, 30, 0x30003, None)
571        c.mapping("ActionData", "Text")
572        prep.back("Back", None, active=0)
573        prep.next("Next", None, active=0)
574        c=prep.cancel("Cancel", None)
575        c.event("SpawnDialog", "CancelDlg")
576
577        #####################################################################
578        # Feature (Python directory) selection
579        seldlg = PyDialog(db, "SelectFeaturesDlg", x, y, w, h, modal, title,
580                        "Next", "Next", "Cancel")
581        seldlg.title("Select Python Installations")
582
583        seldlg.text("Hint", 15, 30, 300, 20, 3,
584                    "Select the Python locations where %s should be installed."
585                    % self.distribution.get_fullname())
586
587        seldlg.back("< Back", None, active=0)
588        c = seldlg.next("Next >", "Cancel")
589        order = 1
590        c.event("[TARGETDIR]", "[SourceDir]", ordering=order)
591        for version in self.versions + [self.other_version]:
592            order += 1
593            c.event("[TARGETDIR]", "[TARGETDIR%s]" % version,
594                    "FEATURE_SELECTED AND &Python%s=3" % version,
595                    ordering=order)
596        c.event("SpawnWaitDialog", "WaitForCostingDlg", ordering=order + 1)
597        c.event("EndDialog", "Return", ordering=order + 2)
598        c = seldlg.cancel("Cancel", "Features")
599        c.event("SpawnDialog", "CancelDlg")
600
601        c = seldlg.control("Features", "SelectionTree", 15, 60, 300, 120, 3,
602                           "FEATURE", None, "PathEdit", None)
603        c.event("[FEATURE_SELECTED]", "1")
604        ver = self.other_version
605        install_other_cond = "FEATURE_SELECTED AND &Python%s=3" % ver
606        dont_install_other_cond = "FEATURE_SELECTED AND &Python%s<>3" % ver
607
608        c = seldlg.text("Other", 15, 200, 300, 15, 3,
609                        "Provide an alternate Python location")
610        c.condition("Enable", install_other_cond)
611        c.condition("Show", install_other_cond)
612        c.condition("Disable", dont_install_other_cond)
613        c.condition("Hide", dont_install_other_cond)
614
615        c = seldlg.control("PathEdit", "PathEdit", 15, 215, 300, 16, 1,
616                           "TARGETDIR" + ver, None, "Next", None)
617        c.condition("Enable", install_other_cond)
618        c.condition("Show", install_other_cond)
619        c.condition("Disable", dont_install_other_cond)
620        c.condition("Hide", dont_install_other_cond)
621
622        #####################################################################
623        # Disk cost
624        cost = PyDialog(db, "DiskCostDlg", x, y, w, h, modal, title,
625                        "OK", "OK", "OK", bitmap=False)
626        cost.text("Title", 15, 6, 200, 15, 0x30003,
627                  "{\DlgFontBold8}Disk Space Requirements")
628        cost.text("Description", 20, 20, 280, 20, 0x30003,
629                  "The disk space required for the installation of the selected features.")
630        cost.text("Text", 20, 53, 330, 60, 3,
631                  "The highlighted volumes (if any) do not have enough disk space "
632              "available for the currently selected features.  You can either "
633              "remove some files from the highlighted volumes, or choose to "
634              "install less features onto local drive(s), or select different "
635              "destination drive(s).")
636        cost.control("VolumeList", "VolumeCostList", 20, 100, 330, 150, 393223,
637                     None, "{120}{70}{70}{70}{70}", None, None)
638        cost.xbutton("OK", "Ok", None, 0.5).event("EndDialog", "Return")
639
640        #####################################################################
641        # WhichUsers Dialog. Only available on NT, and for privileged users.
642        # This must be run before FindRelatedProducts, because that will
643        # take into account whether the previous installation was per-user
644        # or per-machine. We currently don't support going back to this
645        # dialog after "Next" was selected; to support this, we would need to
646        # find how to reset the ALLUSERS property, and how to re-run
647        # FindRelatedProducts.
648        # On Windows9x, the ALLUSERS property is ignored on the command line
649        # and in the Property table, but installer fails according to the documentation
650        # if a dialog attempts to set ALLUSERS.
651        whichusers = PyDialog(db, "WhichUsersDlg", x, y, w, h, modal, title,
652                            "AdminInstall", "Next", "Cancel")
653        whichusers.title("Select whether to install [ProductName] for all users of this computer.")
654        # A radio group with two options: allusers, justme
655        g = whichusers.radiogroup("AdminInstall", 15, 60, 260, 50, 3,
656                                  "WhichUsers", "", "Next")
657        g.add("ALL", 0, 5, 150, 20, "Install for all users")
658        g.add("JUSTME", 0, 25, 150, 20, "Install just for me")
659
660        whichusers.back("Back", None, active=0)
661
662        c = whichusers.next("Next >", "Cancel")
663        c.event("[ALLUSERS]", "1", 'WhichUsers="ALL"', 1)
664        c.event("EndDialog", "Return", ordering = 2)
665
666        c = whichusers.cancel("Cancel", "AdminInstall")
667        c.event("SpawnDialog", "CancelDlg")
668
669        #####################################################################
670        # Installation Progress dialog (modeless)
671        progress = PyDialog(db, "ProgressDlg", x, y, w, h, modeless, title,
672                            "Cancel", "Cancel", "Cancel", bitmap=False)
673        progress.text("Title", 20, 15, 200, 15, 0x30003,
674                      "{\DlgFontBold8}[Progress1] [ProductName]")
675        progress.text("Text", 35, 65, 300, 30, 3,
676                      "Please wait while the Installer [Progress2] [ProductName]. "
677                      "This may take several minutes.")
678        progress.text("StatusLabel", 35, 100, 35, 20, 3, "Status:")
679
680        c=progress.text("ActionText", 70, 100, w-70, 20, 3, "Pondering...")
681        c.mapping("ActionText", "Text")
682
683        #c=progress.text("ActionData", 35, 140, 300, 20, 3, None)
684        #c.mapping("ActionData", "Text")
685
686        c=progress.control("ProgressBar", "ProgressBar", 35, 120, 300, 10, 65537,
687                           None, "Progress done", None, None)
688        c.mapping("SetProgress", "Progress")
689
690        progress.back("< Back", "Next", active=False)
691        progress.next("Next >", "Cancel", active=False)
692        progress.cancel("Cancel", "Back").event("SpawnDialog", "CancelDlg")
693
694        ###################################################################
695        # Maintenance type: repair/uninstall
696        maint = PyDialog(db, "MaintenanceTypeDlg", x, y, w, h, modal, title,
697                         "Next", "Next", "Cancel")
698        maint.title("Welcome to the [ProductName] Setup Wizard")
699        maint.text("BodyText", 15, 63, 330, 42, 3,
700                   "Select whether you want to repair or remove [ProductName].")
701        g=maint.radiogroup("RepairRadioGroup", 15, 108, 330, 60, 3,
702                            "MaintenanceForm_Action", "", "Next")
703        #g.add("Change", 0, 0, 200, 17, "&Change [ProductName]")
704        g.add("Repair", 0, 18, 200, 17, "&Repair [ProductName]")
705        g.add("Remove", 0, 36, 200, 17, "Re&move [ProductName]")
706
707        maint.back("< Back", None, active=False)
708        c=maint.next("Finish", "Cancel")
709        # Change installation: Change progress dialog to "Change", then ask
710        # for feature selection
711        #c.event("[Progress1]", "Change", 'MaintenanceForm_Action="Change"', 1)
712        #c.event("[Progress2]", "changes", 'MaintenanceForm_Action="Change"', 2)
713
714        # Reinstall: Change progress dialog to "Repair", then invoke reinstall
715        # Also set list of reinstalled features to "ALL"
716        c.event("[REINSTALL]", "ALL", 'MaintenanceForm_Action="Repair"', 5)
717        c.event("[Progress1]", "Repairing", 'MaintenanceForm_Action="Repair"', 6)
718        c.event("[Progress2]", "repairs", 'MaintenanceForm_Action="Repair"', 7)
719        c.event("Reinstall", "ALL", 'MaintenanceForm_Action="Repair"', 8)
720
721        # Uninstall: Change progress to "Remove", then invoke uninstall
722        # Also set list of removed features to "ALL"
723        c.event("[REMOVE]", "ALL", 'MaintenanceForm_Action="Remove"', 11)
724        c.event("[Progress1]", "Removing", 'MaintenanceForm_Action="Remove"', 12)
725        c.event("[Progress2]", "removes", 'MaintenanceForm_Action="Remove"', 13)
726        c.event("Remove", "ALL", 'MaintenanceForm_Action="Remove"', 14)
727
728        # Close dialog when maintenance action scheduled
729        c.event("EndDialog", "Return", 'MaintenanceForm_Action<>"Change"', 20)
730        #c.event("NewDialog", "SelectFeaturesDlg", 'MaintenanceForm_Action="Change"', 21)
731
732        maint.cancel("Cancel", "RepairRadioGroup").event("SpawnDialog", "CancelDlg")
733
734    def get_installer_filename(self, fullname):
735        # Factored out to allow overriding in subclasses
736        if self.target_version:
737            base_name = "%s.%s-py%s.msi" % (fullname, self.plat_name,
738                                            self.target_version)
739        else:
740            base_name = "%s.%s.msi" % (fullname, self.plat_name)
741        installer_name = os.path.join(self.dist_dir, base_name)
742        return installer_name
743