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