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