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