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