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