1# Copyright (C) 2005, 2006 Martin von Löwis 2# Licensed to PSF under a Contributor Agreement. 3# The bdist_wininst command proper 4# based on bdist_wininst 5""" 6Implements the bdist_msi command. 7""" 8 9import sys, os 10from distutils.core import Command 11from distutils.dir_util import remove_tree 12from distutils.sysconfig import get_python_version 13from distutils.version import StrictVersion 14from distutils.errors import DistutilsOptionError 15from distutils.util import get_platform 16from distutils import log 17import msilib 18from msilib import schema, sequence, text 19from msilib import Directory, Feature, Dialog, add_data 20 21class PyDialog(Dialog): 22 """Dialog class with a fixed layout: controls at the top, then a ruler, 23 then a list of buttons: back, next, cancel. Optionally a bitmap at the 24 left.""" 25 def __init__(self, *args, **kw): 26 """Dialog(database, name, x, y, w, h, attributes, title, first, 27 default, cancel, bitmap=true)""" 28 Dialog.__init__(self, *args) 29 ruler = self.h - 36 30 bmwidth = 152*ruler/328 31 #if kw.get("bitmap", True): 32 # self.bitmap("Bitmap", 0, 0, bmwidth, ruler, "PythonWin") 33 self.line("BottomLine", 0, ruler, self.w, 0) 34 35 def title(self, title): 36 "Set the title text of the dialog at the top." 37 # name, x, y, w, h, flags=Visible|Enabled|Transparent|NoPrefix, 38 # text, in VerdanaBold10 39 self.text("Title", 15, 10, 320, 60, 0x30003, 40 r"{\VerdanaBold10}%s" % title) 41 42 def back(self, title, next, name = "Back", active = 1): 43 """Add a back button with a given title, the tab-next button, 44 its name in the Control table, possibly initially disabled. 45 46 Return the button, so that events can be associated""" 47 if active: 48 flags = 3 # Visible|Enabled 49 else: 50 flags = 1 # Visible 51 return self.pushbutton(name, 180, self.h-27 , 56, 17, flags, title, next) 52 53 def cancel(self, title, next, name = "Cancel", active = 1): 54 """Add a cancel button with a given title, the tab-next button, 55 its name in the Control table, possibly initially disabled. 56 57 Return the button, so that events can be associated""" 58 if active: 59 flags = 3 # Visible|Enabled 60 else: 61 flags = 1 # Visible 62 return self.pushbutton(name, 304, self.h-27, 56, 17, flags, title, next) 63 64 def next(self, title, next, name = "Next", active = 1): 65 """Add a Next button with a given title, the tab-next button, 66 its name in the Control table, possibly initially disabled. 67 68 Return the button, so that events can be associated""" 69 if active: 70 flags = 3 # Visible|Enabled 71 else: 72 flags = 1 # Visible 73 return self.pushbutton(name, 236, self.h-27, 56, 17, flags, title, next) 74 75 def xbutton(self, name, title, next, xpos): 76 """Add a button with a given title, the tab-next button, 77 its name in the Control table, giving its x position; the 78 y-position is aligned with the other buttons. 79 80 Return the button, so that events can be associated""" 81 return self.pushbutton(name, int(self.w*xpos - 28), self.h-27, 56, 17, 3, title, next) 82 83class bdist_msi(Command): 84 85 description = "create a Microsoft Installer (.msi) binary distribution" 86 87 user_options = [('bdist-dir=', None, 88 "temporary directory for creating the distribution"), 89 ('plat-name=', 'p', 90 "platform name to embed in generated filenames " 91 "(default: %s)" % get_platform()), 92 ('keep-temp', 'k', 93 "keep the pseudo-installation tree around after " + 94 "creating the distribution archive"), 95 ('target-version=', None, 96 "require a specific python version" + 97 " on the target system"), 98 ('no-target-compile', 'c', 99 "do not compile .py to .pyc on the target system"), 100 ('no-target-optimize', 'o', 101 "do not compile .py to .pyo (optimized) " 102 "on the target system"), 103 ('dist-dir=', 'd', 104 "directory to put final built distributions in"), 105 ('skip-build', None, 106 "skip rebuilding everything (for testing/debugging)"), 107 ('install-script=', None, 108 "basename of installation script to be run after " 109 "installation or before deinstallation"), 110 ('pre-install-script=', None, 111 "Fully qualified filename of a script to be run before " 112 "any files are installed. This script need not be in the " 113 "distribution"), 114 ] 115 116 boolean_options = ['keep-temp', 'no-target-compile', 'no-target-optimize', 117 'skip-build'] 118 119 all_versions = ['2.0', '2.1', '2.2', '2.3', '2.4', 120 '2.5', '2.6', '2.7', '2.8', '2.9', 121 '3.0', '3.1', '3.2', '3.3', '3.4', 122 '3.5', '3.6', '3.7', '3.8', '3.9'] 123 other_version = 'X' 124 125 def 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 with open(scriptfn, "w") as f: 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 with open(self.pre_install_script) as fin: 404 f.write(fin.read()) 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