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