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 = 0
    135         self.install_script = None
    136         self.pre_install_script = None
    137         self.versions = None
    138 
    139     def finalize_options (self):
    140         if self.bdist_dir is None:
    141             bdist_base = self.get_finalized_command('bdist').bdist_base
    142             self.bdist_dir = os.path.join(bdist_base, 'msi')
    143         short_version = get_python_version()
    144         if (not self.target_version) and self.distribution.has_ext_modules():
    145             self.target_version = short_version
    146         if self.target_version:
    147             self.versions = [self.target_version]
    148             if not self.skip_build and self.distribution.has_ext_modules()\
    149                and self.target_version != short_version:
    150                 raise DistutilsOptionError, \
    151                       "target version can only be %s, or the '--skip-build'" \
    152                       " option must be specified" % (short_version,)
    153         else:
    154             self.versions = list(self.all_versions)
    155 
    156         self.set_undefined_options('bdist',
    157                                    ('dist_dir', 'dist_dir'),
    158                                    ('plat_name', 'plat_name'),
    159                                    )
    160 
    161         if self.pre_install_script:
    162             raise DistutilsOptionError, "the pre-install-script feature is not yet implemented"
    163 
    164         if self.install_script:
    165             for script in self.distribution.scripts:
    166                 if self.install_script == os.path.basename(script):
    167                     break
    168             else:
    169                 raise DistutilsOptionError, \
    170                       "install_script '%s' not found in scripts" % \
    171                       self.install_script
    172         self.install_script_key = None
    173     # finalize_options()

    174 
    175 
    176     def run (self):
    177         if not self.skip_build:
    178             self.run_command('build')
    179 
    180         install = self.reinitialize_command('install', reinit_subcommands=1)
    181         install.prefix = self.bdist_dir
    182         install.skip_build = self.skip_build
    183         install.warn_dir = 0
    184 
    185         install_lib = self.reinitialize_command('install_lib')
    186         # we do not want to include pyc or pyo files

    187         install_lib.compile = 0
    188         install_lib.optimize = 0
    189 
    190         if self.distribution.has_ext_modules():
    191             # If we are building an installer for a Python version other

    192             # than the one we are currently running, then we need to ensure

    193             # our build_lib reflects the other Python version rather than ours.

    194             # Note that for target_version!=sys.version, we must have skipped the

    195             # build step, so there is no issue with enforcing the build of this

    196             # version.

    197             target_version = self.target_version
    198             if not target_version:
    199                 assert self.skip_build, "Should have already checked this"
    200                 target_version = sys.version[0:3]
    201             plat_specifier = ".%s-%s" % (self.plat_name, target_version)
    202             build = self.get_finalized_command('build')
    203             build.build_lib = os.path.join(build.build_base,
    204                                            'lib' + plat_specifier)
    205 
    206         log.info("installing to %s", self.bdist_dir)
    207         install.ensure_finalized()
    208 
    209         # avoid warning of 'install_lib' about installing

    210         # into a directory not in sys.path

    211         sys.path.insert(0, os.path.join(self.bdist_dir, 'PURELIB'))
    212 
    213         install.run()
    214 
    215         del sys.path[0]
    216 
    217         self.mkpath(self.dist_dir)
    218         fullname = self.distribution.get_fullname()
    219         installer_name = self.get_installer_filename(fullname)
    220         installer_name = os.path.abspath(installer_name)
    221         if os.path.exists(installer_name): os.unlink(installer_name)
    222 
    223         metadata = self.distribution.metadata
    224         author = metadata.author
    225         if not author:
    226             author = metadata.maintainer
    227         if not author:
    228             author = "UNKNOWN"
    229         version = metadata.get_version()
    230         # ProductVersion must be strictly numeric

    231         # XXX need to deal with prerelease versions

    232         sversion = "%d.%d.%d" % StrictVersion(version).version
    233         # Prefix ProductName with Python x.y, so that

    234         # it sorts together with the other Python packages

    235         # in Add-Remove-Programs (APR)

    236         fullname = self.distribution.get_fullname()
    237         if self.target_version:
    238             product_name = "Python %s %s" % (self.target_version, fullname)
    239         else:
    240             product_name = "Python %s" % (fullname)
    241         self.db = msilib.init_database(installer_name, schema,
    242                 product_name, msilib.gen_uuid(),
    243                 sversion, author)
    244         msilib.add_tables(self.db, sequence)
    245         props = [('DistVersion', version)]
    246         email = metadata.author_email or metadata.maintainer_email
    247         if email:
    248             props.append(("ARPCONTACT", email))
    249         if metadata.url:
    250             props.append(("ARPURLINFOABOUT", metadata.url))
    251         if props:
    252             add_data(self.db, 'Property', props)
    253 
    254         self.add_find_python()
    255         self.add_files()
    256         self.add_scripts()
    257         self.add_ui()
    258         self.db.Commit()
    259 
    260         if hasattr(self.distribution, 'dist_files'):
    261             tup = 'bdist_msi', self.target_version or 'any', fullname
    262             self.distribution.dist_files.append(tup)
    263 
    264         if not self.keep_temp:
    265             remove_tree(self.bdist_dir, dry_run=self.dry_run)
    266 
    267     def add_files(self):
    268         db = self.db
    269         cab = msilib.CAB("distfiles")
    270         rootdir = os.path.abspath(self.bdist_dir)
    271 
    272         root = Directory(db, cab, None, rootdir, "TARGETDIR", "SourceDir")
    273         f = Feature(db, "Python", "Python", "Everything",
    274                     0, 1, directory="TARGETDIR")
    275 
    276         items = [(f, root, '')]
    277         for version in self.versions + [self.other_version]:
    278             target = "TARGETDIR" + version
    279             name = default = "Python" + version
    280             desc = "Everything"
    281             if version is self.other_version:
    282                 title = "Python from another location"
    283                 level = 2
    284             else:
    285                 title = "Python %s from registry" % version
    286                 level = 1
    287             f = Feature(db, name, title, desc, 1, level, directory=target)
    288             dir = Directory(db, cab, root, rootdir, target, default)
    289             items.append((f, dir, version))
    290         db.Commit()
    291 
    292         seen = {}
    293         for feature, dir, version in items:
    294             todo = [dir]
    295             while todo:
    296                 dir = todo.pop()
    297                 for file in os.listdir(dir.absolute):
    298                     afile = os.path.join(dir.absolute, file)
    299                     if os.path.isdir(afile):
    300                         short = "%s|%s" % (dir.make_short(file), file)
    301                         default = file + version
    302                         newdir = Directory(db, cab, dir, file, default, short)
    303                         todo.append(newdir)
    304                     else:
    305                         if not dir.component:
    306                             dir.start_component(dir.logical, feature, 0)
    307                         if afile not in seen:
    308                             key = seen[afile] = dir.add_file(file)
    309                             if file==self.install_script:
    310                                 if self.install_script_key:
    311                                     raise DistutilsOptionError(
    312                                           "Multiple files with name %s" % file)
    313                                 self.install_script_key = '[#%s]' % key
    314                         else:
    315                             key = seen[afile]
    316                             add_data(self.db, "DuplicateFile",
    317                                 [(key + version, dir.component, key, None, dir.logical)])
    318             db.Commit()
    319         cab.commit(db)
    320 
    321     def add_find_python(self):
    322         """Adds code to the installer to compute the location of Python.
    323 
    324         Properties PYTHON.MACHINE.X.Y and PYTHON.USER.X.Y will be set from the
    325         registry for each version of Python.
    326 
    327         Properties TARGETDIRX.Y will be set from PYTHON.USER.X.Y if defined,
    328         else from PYTHON.MACHINE.X.Y.
    329 
    330         Properties PYTHONX.Y will be set to TARGETDIRX.Y\\python.exe"""
    331 
    332         start = 402
    333         for ver in self.versions:
    334             install_path = r"SOFTWARE\Python\PythonCore\%s\InstallPath" % ver
    335             machine_reg = "python.machine." + ver
    336             user_reg = "python.user." + ver
    337             machine_prop = "PYTHON.MACHINE." + ver
    338             user_prop = "PYTHON.USER." + ver
    339             machine_action = "PythonFromMachine" + ver
    340             user_action = "PythonFromUser" + ver
    341             exe_action = "PythonExe" + ver
    342             target_dir_prop = "TARGETDIR" + ver
    343             exe_prop = "PYTHON" + ver
    344             if msilib.Win64:
    345                 # type: msidbLocatorTypeRawValue + msidbLocatorType64bit

    346                 Type = 2+16
    347             else:
    348                 Type = 2
    349             add_data(self.db, "RegLocator",
    350                     [(machine_reg, 2, install_path, None, Type),
    351                      (user_reg, 1, install_path, None, Type)])
    352             add_data(self.db, "AppSearch",
    353                     [(machine_prop, machine_reg),
    354                      (user_prop, user_reg)])
    355             add_data(self.db, "CustomAction",
    356                     [(machine_action, 51+256, target_dir_prop, "[" + machine_prop + "]"),
    357                      (user_action, 51+256, target_dir_prop, "[" + user_prop + "]"),
    358                      (exe_action, 51+256, exe_prop, "[" + target_dir_prop + "]\\python.exe"),
    359                     ])
    360             add_data(self.db, "InstallExecuteSequence",
    361                     [(machine_action, machine_prop, start),
    362                      (user_action, user_prop, start + 1),
    363                      (exe_action, None, start + 2),
    364                     ])
    365             add_data(self.db, "InstallUISequence",
    366                     [(machine_action, machine_prop, start),
    367                      (user_action, user_prop, start + 1),
    368                      (exe_action, None, start + 2),
    369                     ])
    370             add_data(self.db, "Condition",
    371                     [("Python" + ver, 0, "NOT TARGETDIR" + ver)])
    372             start += 4
    373             assert start < 500
    374 
    375     def add_scripts(self):
    376         if self.install_script:
    377             start = 6800
    378             for ver in self.versions + [self.other_version]:
    379                 install_action = "install_script." + ver
    380                 exe_prop = "PYTHON" + ver
    381                 add_data(self.db, "CustomAction",
    382                         [(install_action, 50, exe_prop, self.install_script_key)])
    383                 add_data(self.db, "InstallExecuteSequence",
    384                         [(install_action, "&Python%s=3" % ver, start)])
    385                 start += 1
    386         # XXX pre-install scripts are currently refused in finalize_options()

    387         #     but if this feature is completed, it will also need to add

    388         #     entries for each version as the above code does

    389         if self.pre_install_script:
    390             scriptfn = os.path.join(self.bdist_dir, "preinstall.bat")
    391             f = open(scriptfn, "w")
    392             # The batch file will be executed with [PYTHON], so that %1

    393             # is the path to the Python interpreter; %0 will be the path

    394             # of the batch file.

    395             # rem ="""

    396             # %1 %0

    397             # exit

    398             # """

    399             # <actual script>

    400             f.write('rem ="""\n%1 %0\nexit\n"""\n')
    401             f.write(open(self.pre_install_script).read())
    402             f.close()
    403             add_data(self.db, "Binary",
    404                 [("PreInstall", msilib.Binary(scriptfn))
    405                 ])
    406             add_data(self.db, "CustomAction",
    407                 [("PreInstall", 2, "PreInstall", None)
    408                 ])
    409             add_data(self.db, "InstallExecuteSequence",
    410                     [("PreInstall", "NOT Installed", 450)])
    411 
    412 
    413     def add_ui(self):
    414         db = self.db
    415         x = y = 50
    416         w = 370
    417         h = 300
    418         title = "[ProductName] Setup"
    419 
    420         # see "Dialog Style Bits"

    421         modal = 3      # visible | modal

    422         modeless = 1   # visible

    423 
    424         # UI customization properties

    425         add_data(db, "Property",
    426                  # See "DefaultUIFont Property"

    427                  [("DefaultUIFont", "DlgFont8"),
    428                   # See "ErrorDialog Style Bit"

    429                   ("ErrorDialog", "ErrorDlg"),
    430                   ("Progress1", "Install"),   # modified in maintenance type dlg

    431                   ("Progress2", "installs"),
    432                   ("MaintenanceForm_Action", "Repair"),
    433                   # possible values: ALL, JUSTME

    434                   ("WhichUsers", "ALL")
    435                  ])
    436 
    437         # Fonts, see "TextStyle Table"

    438         add_data(db, "TextStyle",
    439                  [("DlgFont8", "Tahoma", 9, None, 0),
    440                   ("DlgFontBold8", "Tahoma", 8, None, 1), #bold

    441                   ("VerdanaBold10", "Verdana", 10, None, 1),
    442                   ("VerdanaRed9", "Verdana", 9, 255, 0),
    443                  ])
    444 
    445         # UI Sequences, see "InstallUISequence Table", "Using a Sequence Table"

    446         # Numbers indicate sequence; see sequence.py for how these action integrate

    447         add_data(db, "InstallUISequence",
    448                  [("PrepareDlg", "Not Privileged or Windows9x or Installed", 140),
    449                   ("WhichUsersDlg", "Privileged and not Windows9x and not Installed", 141),
    450                   # In the user interface, assume all-users installation if privileged.

    451                   ("SelectFeaturesDlg", "Not Installed", 1230),
    452                   # XXX no support for resume installations yet

    453                   #("ResumeDlg", "Installed AND (RESUME OR Preselected)", 1240),

    454                   ("MaintenanceTypeDlg", "Installed AND NOT RESUME AND NOT Preselected", 1250),
    455                   ("ProgressDlg", None, 1280)])
    456 
    457         add_data(db, 'ActionText', text.ActionText)
    458         add_data(db, 'UIText', text.UIText)
    459         #####################################################################

    460         # Standard dialogs: FatalError, UserExit, ExitDialog

    461         fatal=PyDialog(db, "FatalError", x, y, w, h, modal, title,
    462                      "Finish", "Finish", "Finish")
    463         fatal.title("[ProductName] Installer ended prematurely")
    464         fatal.back("< Back", "Finish", active = 0)
    465         fatal.cancel("Cancel", "Back", active = 0)
    466         fatal.text("Description1", 15, 70, 320, 80, 0x30003,
    467                    "[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.")
    468         fatal.text("Description2", 15, 155, 320, 20, 0x30003,
    469                    "Click the Finish button to exit the Installer.")
    470         c=fatal.next("Finish", "Cancel", name="Finish")
    471         c.event("EndDialog", "Exit")
    472 
    473         user_exit=PyDialog(db, "UserExit", x, y, w, h, modal, title,
    474                      "Finish", "Finish", "Finish")
    475         user_exit.title("[ProductName] Installer was interrupted")
    476         user_exit.back("< Back", "Finish", active = 0)
    477         user_exit.cancel("Cancel", "Back", active = 0)
    478         user_exit.text("Description1", 15, 70, 320, 80, 0x30003,
    479                    "[ProductName] setup was interrupted.  Your system has not been modified.  "
    480                    "To install this program at a later time, please run the installation again.")
    481         user_exit.text("Description2", 15, 155, 320, 20, 0x30003,
    482                    "Click the Finish button to exit the Installer.")
    483         c = user_exit.next("Finish", "Cancel", name="Finish")
    484         c.event("EndDialog", "Exit")
    485 
    486         exit_dialog = PyDialog(db, "ExitDialog", x, y, w, h, modal, title,
    487                              "Finish", "Finish", "Finish")
    488         exit_dialog.title("Completing the [ProductName] Installer")
    489         exit_dialog.back("< Back", "Finish", active = 0)
    490         exit_dialog.cancel("Cancel", "Back", active = 0)
    491         exit_dialog.text("Description", 15, 235, 320, 20, 0x30003,
    492                    "Click the Finish button to exit the Installer.")
    493         c = exit_dialog.next("Finish", "Cancel", name="Finish")
    494         c.event("EndDialog", "Return")
    495 
    496         #####################################################################

    497         # Required dialog: FilesInUse, ErrorDlg

    498         inuse = PyDialog(db, "FilesInUse",
    499                          x, y, w, h,
    500                          19,                # KeepModeless|Modal|Visible

    501                          title,
    502                          "Retry", "Retry", "Retry", bitmap=False)
    503         inuse.text("Title", 15, 6, 200, 15, 0x30003,
    504                    r"{\DlgFontBold8}Files in Use")
    505         inuse.text("Description", 20, 23, 280, 20, 0x30003,
    506                "Some files that need to be updated are currently in use.")
    507         inuse.text("Text", 20, 55, 330, 50, 3,
    508                    "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.")
    509         inuse.control("List", "ListBox", 20, 107, 330, 130, 7, "FileInUseProcess",
    510                       None, None, None)
    511         c=inuse.back("Exit", "Ignore", name="Exit")
    512         c.event("EndDialog", "Exit")
    513         c=inuse.next("Ignore", "Retry", name="Ignore")
    514         c.event("EndDialog", "Ignore")
    515         c=inuse.cancel("Retry", "Exit", name="Retry")
    516         c.event("EndDialog","Retry")
    517 
    518         # See "Error Dialog". See "ICE20" for the required names of the controls.

    519         error = Dialog(db, "ErrorDlg",
    520                        50, 10, 330, 101,
    521                        65543,       # Error|Minimize|Modal|Visible

    522                        title,
    523                        "ErrorText", None, None)
    524         error.text("ErrorText", 50,9,280,48,3, "")
    525         #error.control("ErrorIcon", "Icon", 15, 9, 24, 24, 5242881, None, "py.ico", None, None)

    526         error.pushbutton("N",120,72,81,21,3,"No",None).event("EndDialog","ErrorNo")
    527         error.pushbutton("Y",240,72,81,21,3,"Yes",None).event("EndDialog","ErrorYes")
    528         error.pushbutton("A",0,72,81,21,3,"Abort",None).event("EndDialog","ErrorAbort")
    529         error.pushbutton("C",42,72,81,21,3,"Cancel",None).event("EndDialog","ErrorCancel")
    530         error.pushbutton("I",81,72,81,21,3,"Ignore",None).event("EndDialog","ErrorIgnore")
    531         error.pushbutton("O",159,72,81,21,3,"Ok",None).event("EndDialog","ErrorOk")
    532         error.pushbutton("R",198,72,81,21,3,"Retry",None).event("EndDialog","ErrorRetry")
    533 
    534         #####################################################################

    535         # Global "Query Cancel" dialog

    536         cancel = Dialog(db, "CancelDlg", 50, 10, 260, 85, 3, title,
    537                         "No", "No", "No")
    538         cancel.text("Text", 48, 15, 194, 30, 3,
    539                     "Are you sure you want to cancel [ProductName] installation?")
    540         #cancel.control("Icon", "Icon", 15, 15, 24, 24, 5242881, None,

    541         #               "py.ico", None, None)

    542         c=cancel.pushbutton("Yes", 72, 57, 56, 17, 3, "Yes", "No")
    543         c.event("EndDialog", "Exit")
    544 
    545         c=cancel.pushbutton("No", 132, 57, 56, 17, 3, "No", "Yes")
    546         c.event("EndDialog", "Return")
    547 
    548         #####################################################################

    549         # Global "Wait for costing" dialog

    550         costing = Dialog(db, "WaitForCostingDlg", 50, 10, 260, 85, modal, title,
    551                          "Return", "Return", "Return")
    552         costing.text("Text", 48, 15, 194, 30, 3,
    553                      "Please wait while the installer finishes determining your disk space requirements.")
    554         c = costing.pushbutton("Return", 102, 57, 56, 17, 3, "Return", None)
    555         c.event("EndDialog", "Exit")
    556 
    557         #####################################################################

    558         # Preparation dialog: no user input except cancellation

    559         prep = PyDialog(db, "PrepareDlg", x, y, w, h, modeless, title,
    560                         "Cancel", "Cancel", "Cancel")
    561         prep.text("Description", 15, 70, 320, 40, 0x30003,
    562                   "Please wait while the Installer prepares to guide you through the installation.")
    563         prep.title("Welcome to the [ProductName] Installer")
    564         c=prep.text("ActionText", 15, 110, 320, 20, 0x30003, "Pondering...")
    565         c.mapping("ActionText", "Text")
    566         c=prep.text("ActionData", 15, 135, 320, 30, 0x30003, None)
    567         c.mapping("ActionData", "Text")
    568         prep.back("Back", None, active=0)
    569         prep.next("Next", None, active=0)
    570         c=prep.cancel("Cancel", None)
    571         c.event("SpawnDialog", "CancelDlg")
    572 
    573         #####################################################################

    574         # Feature (Python directory) selection

    575         seldlg = PyDialog(db, "SelectFeaturesDlg", x, y, w, h, modal, title,
    576                         "Next", "Next", "Cancel")
    577         seldlg.title("Select Python Installations")
    578 
    579         seldlg.text("Hint", 15, 30, 300, 20, 3,
    580                     "Select the Python locations where %s should be installed."
    581                     % self.distribution.get_fullname())
    582 
    583         seldlg.back("< Back", None, active=0)
    584         c = seldlg.next("Next >", "Cancel")
    585         order = 1
    586         c.event("[TARGETDIR]", "[SourceDir]", ordering=order)
    587         for version in self.versions + [self.other_version]:
    588             order += 1
    589             c.event("[TARGETDIR]", "[TARGETDIR%s]" % version,
    590                     "FEATURE_SELECTED AND &Python%s=3" % version,
    591                     ordering=order)
    592         c.event("SpawnWaitDialog", "WaitForCostingDlg", ordering=order + 1)
    593         c.event("EndDialog", "Return", ordering=order + 2)
    594         c = seldlg.cancel("Cancel", "Features")
    595         c.event("SpawnDialog", "CancelDlg")
    596 
    597         c = seldlg.control("Features", "SelectionTree", 15, 60, 300, 120, 3,
    598                            "FEATURE", None, "PathEdit", None)
    599         c.event("[FEATURE_SELECTED]", "1")
    600         ver = self.other_version
    601         install_other_cond = "FEATURE_SELECTED AND &Python%s=3" % ver
    602         dont_install_other_cond = "FEATURE_SELECTED AND &Python%s<>3" % ver
    603 
    604         c = seldlg.text("Other", 15, 200, 300, 15, 3,
    605                         "Provide an alternate Python location")
    606         c.condition("Enable", install_other_cond)
    607         c.condition("Show", install_other_cond)
    608         c.condition("Disable", dont_install_other_cond)
    609         c.condition("Hide", dont_install_other_cond)
    610 
    611         c = seldlg.control("PathEdit", "PathEdit", 15, 215, 300, 16, 1,
    612                            "TARGETDIR" + ver, None, "Next", None)
    613         c.condition("Enable", install_other_cond)
    614         c.condition("Show", install_other_cond)
    615         c.condition("Disable", dont_install_other_cond)
    616         c.condition("Hide", dont_install_other_cond)
    617 
    618         #####################################################################

    619         # Disk cost

    620         cost = PyDialog(db, "DiskCostDlg", x, y, w, h, modal, title,
    621                         "OK", "OK", "OK", bitmap=False)
    622         cost.text("Title", 15, 6, 200, 15, 0x30003,
    623                   "{\DlgFontBold8}Disk Space Requirements")
    624         cost.text("Description", 20, 20, 280, 20, 0x30003,
    625                   "The disk space required for the installation of the selected features.")
    626         cost.text("Text", 20, 53, 330, 60, 3,
    627                   "The highlighted volumes (if any) do not have enough disk space "
    628               "available for the currently selected features.  You can either "
    629               "remove some files from the highlighted volumes, or choose to "
    630               "install less features onto local drive(s), or select different "
    631               "destination drive(s).")
    632         cost.control("VolumeList", "VolumeCostList", 20, 100, 330, 150, 393223,
    633                      None, "{120}{70}{70}{70}{70}", None, None)
    634         cost.xbutton("OK", "Ok", None, 0.5).event("EndDialog", "Return")
    635 
    636         #####################################################################

    637         # WhichUsers Dialog. Only available on NT, and for privileged users.

    638         # This must be run before FindRelatedProducts, because that will

    639         # take into account whether the previous installation was per-user

    640         # or per-machine. We currently don't support going back to this

    641         # dialog after "Next" was selected; to support this, we would need to

    642         # find how to reset the ALLUSERS property, and how to re-run

    643         # FindRelatedProducts.

    644         # On Windows9x, the ALLUSERS property is ignored on the command line

    645         # and in the Property table, but installer fails according to the documentation

    646         # if a dialog attempts to set ALLUSERS.

    647         whichusers = PyDialog(db, "WhichUsersDlg", x, y, w, h, modal, title,
    648                             "AdminInstall", "Next", "Cancel")
    649         whichusers.title("Select whether to install [ProductName] for all users of this computer.")
    650         # A radio group with two options: allusers, justme

    651         g = whichusers.radiogroup("AdminInstall", 15, 60, 260, 50, 3,
    652                                   "WhichUsers", "", "Next")
    653         g.add("ALL", 0, 5, 150, 20, "Install for all users")
    654         g.add("JUSTME", 0, 25, 150, 20, "Install just for me")
    655 
    656         whichusers.back("Back", None, active=0)
    657 
    658         c = whichusers.next("Next >", "Cancel")
    659         c.event("[ALLUSERS]", "1", 'WhichUsers="ALL"', 1)
    660         c.event("EndDialog", "Return", ordering = 2)
    661 
    662         c = whichusers.cancel("Cancel", "AdminInstall")
    663         c.event("SpawnDialog", "CancelDlg")
    664 
    665         #####################################################################

    666         # Installation Progress dialog (modeless)

    667         progress = PyDialog(db, "ProgressDlg", x, y, w, h, modeless, title,
    668                             "Cancel", "Cancel", "Cancel", bitmap=False)
    669         progress.text("Title", 20, 15, 200, 15, 0x30003,
    670                       "{\DlgFontBold8}[Progress1] [ProductName]")
    671         progress.text("Text", 35, 65, 300, 30, 3,
    672                       "Please wait while the Installer [Progress2] [ProductName]. "
    673                       "This may take several minutes.")
    674         progress.text("StatusLabel", 35, 100, 35, 20, 3, "Status:")
    675 
    676         c=progress.text("ActionText", 70, 100, w-70, 20, 3, "Pondering...")
    677         c.mapping("ActionText", "Text")
    678 
    679         #c=progress.text("ActionData", 35, 140, 300, 20, 3, None)

    680         #c.mapping("ActionData", "Text")

    681 
    682         c=progress.control("ProgressBar", "ProgressBar", 35, 120, 300, 10, 65537,
    683                            None, "Progress done", None, None)
    684         c.mapping("SetProgress", "Progress")
    685 
    686         progress.back("< Back", "Next", active=False)
    687         progress.next("Next >", "Cancel", active=False)
    688         progress.cancel("Cancel", "Back").event("SpawnDialog", "CancelDlg")
    689 
    690         ###################################################################

    691         # Maintenance type: repair/uninstall

    692         maint = PyDialog(db, "MaintenanceTypeDlg", x, y, w, h, modal, title,
    693                          "Next", "Next", "Cancel")
    694         maint.title("Welcome to the [ProductName] Setup Wizard")
    695         maint.text("BodyText", 15, 63, 330, 42, 3,
    696                    "Select whether you want to repair or remove [ProductName].")
    697         g=maint.radiogroup("RepairRadioGroup", 15, 108, 330, 60, 3,
    698                             "MaintenanceForm_Action", "", "Next")
    699         #g.add("Change", 0, 0, 200, 17, "&Change [ProductName]")

    700         g.add("Repair", 0, 18, 200, 17, "&Repair [ProductName]")
    701         g.add("Remove", 0, 36, 200, 17, "Re&move [ProductName]")
    702 
    703         maint.back("< Back", None, active=False)
    704         c=maint.next("Finish", "Cancel")
    705         # Change installation: Change progress dialog to "Change", then ask

    706         # for feature selection

    707         #c.event("[Progress1]", "Change", 'MaintenanceForm_Action="Change"', 1)

    708         #c.event("[Progress2]", "changes", 'MaintenanceForm_Action="Change"', 2)

    709 
    710         # Reinstall: Change progress dialog to "Repair", then invoke reinstall

    711         # Also set list of reinstalled features to "ALL"

    712         c.event("[REINSTALL]", "ALL", 'MaintenanceForm_Action="Repair"', 5)
    713         c.event("[Progress1]", "Repairing", 'MaintenanceForm_Action="Repair"', 6)
    714         c.event("[Progress2]", "repairs", 'MaintenanceForm_Action="Repair"', 7)
    715         c.event("Reinstall", "ALL", 'MaintenanceForm_Action="Repair"', 8)
    716 
    717         # Uninstall: Change progress to "Remove", then invoke uninstall

    718         # Also set list of removed features to "ALL"

    719         c.event("[REMOVE]", "ALL", 'MaintenanceForm_Action="Remove"', 11)
    720         c.event("[Progress1]", "Removing", 'MaintenanceForm_Action="Remove"', 12)
    721         c.event("[Progress2]", "removes", 'MaintenanceForm_Action="Remove"', 13)
    722         c.event("Remove", "ALL", 'MaintenanceForm_Action="Remove"', 14)
    723 
    724         # Close dialog when maintenance action scheduled

    725         c.event("EndDialog", "Return", 'MaintenanceForm_Action<>"Change"', 20)
    726         #c.event("NewDialog", "SelectFeaturesDlg", 'MaintenanceForm_Action="Change"', 21)

    727 
    728         maint.cancel("Cancel", "RepairRadioGroup").event("SpawnDialog", "CancelDlg")
    729 
    730     def get_installer_filename(self, fullname):
    731         # Factored out to allow overriding in subclasses

    732         if self.target_version:
    733             base_name = "%s.%s-py%s.msi" % (fullname, self.plat_name,
    734                                             self.target_version)
    735         else:
    736             base_name = "%s.%s.msi" % (fullname, self.plat_name)
    737         installer_name = os.path.join(self.dist_dir, base_name)
    738         return installer_name
    739