PageRenderTime 92ms CodeModel.GetById 13ms app.highlight 55ms RepoModel.GetById 2ms app.codeStats 0ms

/Lib/distutils/command/bdist_msi.py

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