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