| # Copyright (C) 2005, 2006 Martin von Löwis |
| # Licensed to PSF under a Contributor Agreement. |
| # The bdist_wininst command proper |
| # based on bdist_wininst |
| """ |
| Implements the bdist_msi command. |
| """ |
| |
| import os |
| import sys |
| import warnings |
| from distutils.core import Command |
| from distutils.dir_util import remove_tree |
| from distutils.sysconfig import get_python_version |
| from distutils.version import StrictVersion |
| from distutils.errors import DistutilsOptionError |
| from distutils.util import get_platform |
| from distutils import log |
| import msilib |
| from msilib import schema, sequence, text |
| from msilib import Directory, Feature, Dialog, add_data |
| |
| |
| class PyDialog(Dialog): |
| """Dialog class with a fixed layout: controls at the top, then a ruler, |
| then a list of buttons: back, next, cancel. Optionally a bitmap at the |
| left.""" |
| |
| def __init__(self, *args, **kw): |
| """Dialog(database, name, x, y, w, h, attributes, title, first, |
| default, cancel, bitmap=true)""" |
| super().__init__(*args) |
| ruler = self.h - 36 |
| bmwidth = 152 * ruler / 328 |
| # if kw.get("bitmap", True): |
| # self.bitmap("Bitmap", 0, 0, bmwidth, ruler, "PythonWin") |
| self.line("BottomLine", 0, ruler, self.w, 0) |
| |
| def title(self, title): |
| "Set the title text of the dialog at the top." |
| # name, x, y, w, h, flags=Visible|Enabled|Transparent|NoPrefix, |
| # text, in VerdanaBold10 |
| self.text("Title", 15, 10, 320, 60, 0x30003, r"{\VerdanaBold10}%s" % title) |
| |
| def back(self, title, next, name="Back", active=1): |
| """Add a back button with a given title, the tab-next button, |
| its name in the Control table, possibly initially disabled. |
| |
| Return the button, so that events can be associated""" |
| if active: |
| flags = 3 # Visible|Enabled |
| else: |
| flags = 1 # Visible |
| return self.pushbutton(name, 180, self.h - 27, 56, 17, flags, title, next) |
| |
| def cancel(self, title, next, name="Cancel", active=1): |
| """Add a cancel button with a given title, the tab-next button, |
| its name in the Control table, possibly initially disabled. |
| |
| Return the button, so that events can be associated""" |
| if active: |
| flags = 3 # Visible|Enabled |
| else: |
| flags = 1 # Visible |
| return self.pushbutton(name, 304, self.h - 27, 56, 17, flags, title, next) |
| |
| def next(self, title, next, name="Next", active=1): |
| """Add a Next button with a given title, the tab-next button, |
| its name in the Control table, possibly initially disabled. |
| |
| Return the button, so that events can be associated""" |
| if active: |
| flags = 3 # Visible|Enabled |
| else: |
| flags = 1 # Visible |
| return self.pushbutton(name, 236, self.h - 27, 56, 17, flags, title, next) |
| |
| def xbutton(self, name, title, next, xpos): |
| """Add a button with a given title, the tab-next button, |
| its name in the Control table, giving its x position; the |
| y-position is aligned with the other buttons. |
| |
| Return the button, so that events can be associated""" |
| return self.pushbutton( |
| name, int(self.w * xpos - 28), self.h - 27, 56, 17, 3, title, next |
| ) |
| |
| |
| class bdist_msi(Command): |
| |
| description = "create a Microsoft Installer (.msi) binary distribution" |
| |
| user_options = [ |
| ('bdist-dir=', None, "temporary directory for creating the distribution"), |
| ( |
| 'plat-name=', |
| 'p', |
| "platform name to embed in generated filenames " |
| "(default: %s)" % get_platform(), |
| ), |
| ( |
| 'keep-temp', |
| 'k', |
| "keep the pseudo-installation tree around after " |
| + "creating the distribution archive", |
| ), |
| ( |
| 'target-version=', |
| None, |
| "require a specific python version" + " on the target system", |
| ), |
| ('no-target-compile', 'c', "do not compile .py to .pyc on the target system"), |
| ( |
| 'no-target-optimize', |
| 'o', |
| "do not compile .py to .pyo (optimized) " "on the target system", |
| ), |
| ('dist-dir=', 'd', "directory to put final built distributions in"), |
| ('skip-build', None, "skip rebuilding everything (for testing/debugging)"), |
| ( |
| 'install-script=', |
| None, |
| "basename of installation script to be run after " |
| "installation or before deinstallation", |
| ), |
| ( |
| 'pre-install-script=', |
| None, |
| "Fully qualified filename of a script to be run before " |
| "any files are installed. This script need not be in the " |
| "distribution", |
| ), |
| ] |
| |
| boolean_options = [ |
| 'keep-temp', |
| 'no-target-compile', |
| 'no-target-optimize', |
| 'skip-build', |
| ] |
| |
| all_versions = [ |
| '2.0', |
| '2.1', |
| '2.2', |
| '2.3', |
| '2.4', |
| '2.5', |
| '2.6', |
| '2.7', |
| '2.8', |
| '2.9', |
| '3.0', |
| '3.1', |
| '3.2', |
| '3.3', |
| '3.4', |
| '3.5', |
| '3.6', |
| '3.7', |
| '3.8', |
| '3.9', |
| ] |
| other_version = 'X' |
| |
| def __init__(self, *args, **kw): |
| super().__init__(*args, **kw) |
| warnings.warn( |
| "bdist_msi command is deprecated since Python 3.9, " |
| "use bdist_wheel (wheel packages) instead", |
| DeprecationWarning, |
| 2, |
| ) |
| |
| def initialize_options(self): |
| self.bdist_dir = None |
| self.plat_name = None |
| self.keep_temp = 0 |
| self.no_target_compile = 0 |
| self.no_target_optimize = 0 |
| self.target_version = None |
| self.dist_dir = None |
| self.skip_build = None |
| self.install_script = None |
| self.pre_install_script = None |
| self.versions = None |
| |
| def finalize_options(self): |
| self.set_undefined_options('bdist', ('skip_build', 'skip_build')) |
| |
| if self.bdist_dir is None: |
| bdist_base = self.get_finalized_command('bdist').bdist_base |
| self.bdist_dir = os.path.join(bdist_base, 'msi') |
| |
| short_version = get_python_version() |
| if (not self.target_version) and self.distribution.has_ext_modules(): |
| self.target_version = short_version |
| |
| if self.target_version: |
| self.versions = [self.target_version] |
| if ( |
| not self.skip_build |
| and self.distribution.has_ext_modules() |
| and self.target_version != short_version |
| ): |
| raise DistutilsOptionError( |
| "target version can only be %s, or the '--skip-build'" |
| " option must be specified" % (short_version,) |
| ) |
| else: |
| self.versions = list(self.all_versions) |
| |
| self.set_undefined_options( |
| 'bdist', |
| ('dist_dir', 'dist_dir'), |
| ('plat_name', 'plat_name'), |
| ) |
| |
| if self.pre_install_script: |
| raise DistutilsOptionError( |
| "the pre-install-script feature is not yet implemented" |
| ) |
| |
| if self.install_script: |
| for script in self.distribution.scripts: |
| if self.install_script == os.path.basename(script): |
| break |
| else: |
| raise DistutilsOptionError( |
| "install_script '%s' not found in scripts" % self.install_script |
| ) |
| self.install_script_key = None |
| |
| def run(self): |
| if not self.skip_build: |
| self.run_command('build') |
| |
| install = self.reinitialize_command('install', reinit_subcommands=1) |
| install.prefix = self.bdist_dir |
| install.skip_build = self.skip_build |
| install.warn_dir = 0 |
| |
| install_lib = self.reinitialize_command('install_lib') |
| # we do not want to include pyc or pyo files |
| install_lib.compile = 0 |
| install_lib.optimize = 0 |
| |
| if self.distribution.has_ext_modules(): |
| # If we are building an installer for a Python version other |
| # than the one we are currently running, then we need to ensure |
| # our build_lib reflects the other Python version rather than ours. |
| # Note that for target_version!=sys.version, we must have skipped the |
| # build step, so there is no issue with enforcing the build of this |
| # version. |
| target_version = self.target_version |
| if not target_version: |
| assert self.skip_build, "Should have already checked this" |
| target_version = '%d.%d' % sys.version_info[:2] |
| plat_specifier = ".%s-%s" % (self.plat_name, target_version) |
| build = self.get_finalized_command('build') |
| build.build_lib = os.path.join(build.build_base, 'lib' + plat_specifier) |
| |
| log.info("installing to %s", self.bdist_dir) |
| install.ensure_finalized() |
| |
| # avoid warning of 'install_lib' about installing |
| # into a directory not in sys.path |
| sys.path.insert(0, os.path.join(self.bdist_dir, 'PURELIB')) |
| |
| install.run() |
| |
| del sys.path[0] |
| |
| self.mkpath(self.dist_dir) |
| fullname = self.distribution.get_fullname() |
| installer_name = self.get_installer_filename(fullname) |
| installer_name = os.path.abspath(installer_name) |
| if os.path.exists(installer_name): |
| os.unlink(installer_name) |
| |
| metadata = self.distribution.metadata |
| author = metadata.author or metadata.maintainer |
| version = metadata.get_version() |
| # ProductVersion must be strictly numeric |
| # XXX need to deal with prerelease versions |
| sversion = "%d.%d.%d" % StrictVersion(version).version |
| # Prefix ProductName with Python x.y, so that |
| # it sorts together with the other Python packages |
| # in Add-Remove-Programs (APR) |
| fullname = self.distribution.get_fullname() |
| if self.target_version: |
| product_name = "Python %s %s" % (self.target_version, fullname) |
| else: |
| product_name = "Python %s" % (fullname) |
| self.db = msilib.init_database( |
| installer_name, schema, product_name, msilib.gen_uuid(), sversion, author |
| ) |
| msilib.add_tables(self.db, sequence) |
| props = [('DistVersion', version)] |
| email = metadata.author_email or metadata.maintainer_email |
| if email: |
| props.append(("ARPCONTACT", email)) |
| if metadata.url: |
| props.append(("ARPURLINFOABOUT", metadata.url)) |
| if props: |
| add_data(self.db, 'Property', props) |
| |
| self.add_find_python() |
| self.add_files() |
| self.add_scripts() |
| self.add_ui() |
| self.db.Commit() |
| |
| if hasattr(self.distribution, 'dist_files'): |
| tup = 'bdist_msi', self.target_version or 'any', fullname |
| self.distribution.dist_files.append(tup) |
| |
| if not self.keep_temp: |
| remove_tree(self.bdist_dir, dry_run=self.dry_run) |
| |
| def add_files(self): |
| db = self.db |
| cab = msilib.CAB("distfiles") |
| rootdir = os.path.abspath(self.bdist_dir) |
| |
| root = Directory(db, cab, None, rootdir, "TARGETDIR", "SourceDir") |
| f = Feature(db, "Python", "Python", "Everything", 0, 1, directory="TARGETDIR") |
| |
| items = [(f, root, '')] |
| for version in self.versions + [self.other_version]: |
| target = "TARGETDIR" + version |
| name = default = "Python" + version |
| desc = "Everything" |
| if version is self.other_version: |
| title = "Python from another location" |
| level = 2 |
| else: |
| title = "Python %s from registry" % version |
| level = 1 |
| f = Feature(db, name, title, desc, 1, level, directory=target) |
| dir = Directory(db, cab, root, rootdir, target, default) |
| items.append((f, dir, version)) |
| db.Commit() |
| |
| seen = {} |
| for feature, dir, version in items: |
| todo = [dir] |
| while todo: |
| dir = todo.pop() |
| for file in os.listdir(dir.absolute): |
| afile = os.path.join(dir.absolute, file) |
| if os.path.isdir(afile): |
| short = "%s|%s" % (dir.make_short(file), file) |
| default = file + version |
| newdir = Directory(db, cab, dir, file, default, short) |
| todo.append(newdir) |
| else: |
| if not dir.component: |
| dir.start_component(dir.logical, feature, 0) |
| if afile not in seen: |
| key = seen[afile] = dir.add_file(file) |
| if file == self.install_script: |
| if self.install_script_key: |
| raise DistutilsOptionError( |
| "Multiple files with name %s" % file |
| ) |
| self.install_script_key = '[#%s]' % key |
| else: |
| key = seen[afile] |
| add_data( |
| self.db, |
| "DuplicateFile", |
| [ |
| ( |
| key + version, |
| dir.component, |
| key, |
| None, |
| dir.logical, |
| ) |
| ], |
| ) |
| db.Commit() |
| cab.commit(db) |
| |
| def add_find_python(self): |
| """Adds code to the installer to compute the location of Python. |
| |
| Properties PYTHON.MACHINE.X.Y and PYTHON.USER.X.Y will be set from the |
| registry for each version of Python. |
| |
| Properties TARGETDIRX.Y will be set from PYTHON.USER.X.Y if defined, |
| else from PYTHON.MACHINE.X.Y. |
| |
| Properties PYTHONX.Y will be set to TARGETDIRX.Y\\python.exe""" |
| |
| start = 402 |
| for ver in self.versions: |
| install_path = r"SOFTWARE\Python\PythonCore\%s\InstallPath" % ver |
| machine_reg = "python.machine." + ver |
| user_reg = "python.user." + ver |
| machine_prop = "PYTHON.MACHINE." + ver |
| user_prop = "PYTHON.USER." + ver |
| machine_action = "PythonFromMachine" + ver |
| user_action = "PythonFromUser" + ver |
| exe_action = "PythonExe" + ver |
| target_dir_prop = "TARGETDIR" + ver |
| exe_prop = "PYTHON" + ver |
| if msilib.Win64: |
| # type: msidbLocatorTypeRawValue + msidbLocatorType64bit |
| Type = 2 + 16 |
| else: |
| Type = 2 |
| add_data( |
| self.db, |
| "RegLocator", |
| [ |
| (machine_reg, 2, install_path, None, Type), |
| (user_reg, 1, install_path, None, Type), |
| ], |
| ) |
| add_data( |
| self.db, |
| "AppSearch", |
| [(machine_prop, machine_reg), (user_prop, user_reg)], |
| ) |
| add_data( |
| self.db, |
| "CustomAction", |
| [ |
| ( |
| machine_action, |
| 51 + 256, |
| target_dir_prop, |
| "[" + machine_prop + "]", |
| ), |
| (user_action, 51 + 256, target_dir_prop, "[" + user_prop + "]"), |
| ( |
| exe_action, |
| 51 + 256, |
| exe_prop, |
| "[" + target_dir_prop + "]\\python.exe", |
| ), |
| ], |
| ) |
| add_data( |
| self.db, |
| "InstallExecuteSequence", |
| [ |
| (machine_action, machine_prop, start), |
| (user_action, user_prop, start + 1), |
| (exe_action, None, start + 2), |
| ], |
| ) |
| add_data( |
| self.db, |
| "InstallUISequence", |
| [ |
| (machine_action, machine_prop, start), |
| (user_action, user_prop, start + 1), |
| (exe_action, None, start + 2), |
| ], |
| ) |
| add_data(self.db, "Condition", [("Python" + ver, 0, "NOT TARGETDIR" + ver)]) |
| start += 4 |
| assert start < 500 |
| |
| def add_scripts(self): |
| if self.install_script: |
| start = 6800 |
| for ver in self.versions + [self.other_version]: |
| install_action = "install_script." + ver |
| exe_prop = "PYTHON" + ver |
| add_data( |
| self.db, |
| "CustomAction", |
| [(install_action, 50, exe_prop, self.install_script_key)], |
| ) |
| add_data( |
| self.db, |
| "InstallExecuteSequence", |
| [(install_action, "&Python%s=3" % ver, start)], |
| ) |
| start += 1 |
| # XXX pre-install scripts are currently refused in finalize_options() |
| # but if this feature is completed, it will also need to add |
| # entries for each version as the above code does |
| if self.pre_install_script: |
| scriptfn = os.path.join(self.bdist_dir, "preinstall.bat") |
| with open(scriptfn, "w") as f: |
| # The batch file will be executed with [PYTHON], so that %1 |
| # is the path to the Python interpreter; %0 will be the path |
| # of the batch file. |
| # rem =""" |
| # %1 %0 |
| # exit |
| # """ |
| # <actual script> |
| f.write('rem ="""\n%1 %0\nexit\n"""\n') |
| with open(self.pre_install_script) as fin: |
| f.write(fin.read()) |
| add_data(self.db, "Binary", [("PreInstall", msilib.Binary(scriptfn))]) |
| add_data(self.db, "CustomAction", [("PreInstall", 2, "PreInstall", None)]) |
| add_data( |
| self.db, |
| "InstallExecuteSequence", |
| [("PreInstall", "NOT Installed", 450)], |
| ) |
| |
| def add_ui(self): |
| db = self.db |
| x = y = 50 |
| w = 370 |
| h = 300 |
| title = "[ProductName] Setup" |
| |
| # see "Dialog Style Bits" |
| modal = 3 # visible | modal |
| modeless = 1 # visible |
| track_disk_space = 32 |
| |
| # UI customization properties |
| add_data( |
| db, |
| "Property", |
| # See "DefaultUIFont Property" |
| [ |
| ("DefaultUIFont", "DlgFont8"), |
| # See "ErrorDialog Style Bit" |
| ("ErrorDialog", "ErrorDlg"), |
| ("Progress1", "Install"), # modified in maintenance type dlg |
| ("Progress2", "installs"), |
| ("MaintenanceForm_Action", "Repair"), |
| # possible values: ALL, JUSTME |
| ("WhichUsers", "ALL"), |
| ], |
| ) |
| |
| # Fonts, see "TextStyle Table" |
| add_data( |
| db, |
| "TextStyle", |
| [ |
| ("DlgFont8", "Tahoma", 9, None, 0), |
| ("DlgFontBold8", "Tahoma", 8, None, 1), # bold |
| ("VerdanaBold10", "Verdana", 10, None, 1), |
| ("VerdanaRed9", "Verdana", 9, 255, 0), |
| ], |
| ) |
| |
| # UI Sequences, see "InstallUISequence Table", "Using a Sequence Table" |
| # Numbers indicate sequence; see sequence.py for how these action integrate |
| add_data( |
| db, |
| "InstallUISequence", |
| [ |
| ("PrepareDlg", "Not Privileged or Windows9x or Installed", 140), |
| ( |
| "WhichUsersDlg", |
| "Privileged and not Windows9x and not Installed", |
| 141, |
| ), |
| # In the user interface, assume all-users installation if privileged. |
| ("SelectFeaturesDlg", "Not Installed", 1230), |
| # XXX no support for resume installations yet |
| # ("ResumeDlg", "Installed AND (RESUME OR Preselected)", 1240), |
| ( |
| "MaintenanceTypeDlg", |
| "Installed AND NOT RESUME AND NOT Preselected", |
| 1250, |
| ), |
| ("ProgressDlg", None, 1280), |
| ], |
| ) |
| |
| add_data(db, 'ActionText', text.ActionText) |
| add_data(db, 'UIText', text.UIText) |
| ##################################################################### |
| # Standard dialogs: FatalError, UserExit, ExitDialog |
| fatal = PyDialog( |
| db, "FatalError", x, y, w, h, modal, title, "Finish", "Finish", "Finish" |
| ) |
| fatal.title("[ProductName] Installer ended prematurely") |
| fatal.back("< Back", "Finish", active=0) |
| fatal.cancel("Cancel", "Back", active=0) |
| fatal.text( |
| "Description1", |
| 15, |
| 70, |
| 320, |
| 80, |
| 0x30003, |
| "[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.", |
| ) |
| fatal.text( |
| "Description2", |
| 15, |
| 155, |
| 320, |
| 20, |
| 0x30003, |
| "Click the Finish button to exit the Installer.", |
| ) |
| c = fatal.next("Finish", "Cancel", name="Finish") |
| c.event("EndDialog", "Exit") |
| |
| user_exit = PyDialog( |
| db, "UserExit", x, y, w, h, modal, title, "Finish", "Finish", "Finish" |
| ) |
| user_exit.title("[ProductName] Installer was interrupted") |
| user_exit.back("< Back", "Finish", active=0) |
| user_exit.cancel("Cancel", "Back", active=0) |
| user_exit.text( |
| "Description1", |
| 15, |
| 70, |
| 320, |
| 80, |
| 0x30003, |
| "[ProductName] setup was interrupted. Your system has not been modified. " |
| "To install this program at a later time, please run the installation again.", |
| ) |
| user_exit.text( |
| "Description2", |
| 15, |
| 155, |
| 320, |
| 20, |
| 0x30003, |
| "Click the Finish button to exit the Installer.", |
| ) |
| c = user_exit.next("Finish", "Cancel", name="Finish") |
| c.event("EndDialog", "Exit") |
| |
| exit_dialog = PyDialog( |
| db, "ExitDialog", x, y, w, h, modal, title, "Finish", "Finish", "Finish" |
| ) |
| exit_dialog.title("Completing the [ProductName] Installer") |
| exit_dialog.back("< Back", "Finish", active=0) |
| exit_dialog.cancel("Cancel", "Back", active=0) |
| exit_dialog.text( |
| "Description", |
| 15, |
| 235, |
| 320, |
| 20, |
| 0x30003, |
| "Click the Finish button to exit the Installer.", |
| ) |
| c = exit_dialog.next("Finish", "Cancel", name="Finish") |
| c.event("EndDialog", "Return") |
| |
| ##################################################################### |
| # Required dialog: FilesInUse, ErrorDlg |
| inuse = PyDialog( |
| db, |
| "FilesInUse", |
| x, |
| y, |
| w, |
| h, |
| 19, # KeepModeless|Modal|Visible |
| title, |
| "Retry", |
| "Retry", |
| "Retry", |
| bitmap=False, |
| ) |
| inuse.text("Title", 15, 6, 200, 15, 0x30003, r"{\DlgFontBold8}Files in Use") |
| inuse.text( |
| "Description", |
| 20, |
| 23, |
| 280, |
| 20, |
| 0x30003, |
| "Some files that need to be updated are currently in use.", |
| ) |
| inuse.text( |
| "Text", |
| 20, |
| 55, |
| 330, |
| 50, |
| 3, |
| "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.", |
| ) |
| inuse.control( |
| "List", |
| "ListBox", |
| 20, |
| 107, |
| 330, |
| 130, |
| 7, |
| "FileInUseProcess", |
| None, |
| None, |
| None, |
| ) |
| c = inuse.back("Exit", "Ignore", name="Exit") |
| c.event("EndDialog", "Exit") |
| c = inuse.next("Ignore", "Retry", name="Ignore") |
| c.event("EndDialog", "Ignore") |
| c = inuse.cancel("Retry", "Exit", name="Retry") |
| c.event("EndDialog", "Retry") |
| |
| # See "Error Dialog". See "ICE20" for the required names of the controls. |
| error = Dialog( |
| db, |
| "ErrorDlg", |
| 50, |
| 10, |
| 330, |
| 101, |
| 65543, # Error|Minimize|Modal|Visible |
| title, |
| "ErrorText", |
| None, |
| None, |
| ) |
| error.text("ErrorText", 50, 9, 280, 48, 3, "") |
| # error.control("ErrorIcon", "Icon", 15, 9, 24, 24, 5242881, None, "py.ico", None, None) |
| error.pushbutton("N", 120, 72, 81, 21, 3, "No", None).event( |
| "EndDialog", "ErrorNo" |
| ) |
| error.pushbutton("Y", 240, 72, 81, 21, 3, "Yes", None).event( |
| "EndDialog", "ErrorYes" |
| ) |
| error.pushbutton("A", 0, 72, 81, 21, 3, "Abort", None).event( |
| "EndDialog", "ErrorAbort" |
| ) |
| error.pushbutton("C", 42, 72, 81, 21, 3, "Cancel", None).event( |
| "EndDialog", "ErrorCancel" |
| ) |
| error.pushbutton("I", 81, 72, 81, 21, 3, "Ignore", None).event( |
| "EndDialog", "ErrorIgnore" |
| ) |
| error.pushbutton("O", 159, 72, 81, 21, 3, "Ok", None).event( |
| "EndDialog", "ErrorOk" |
| ) |
| error.pushbutton("R", 198, 72, 81, 21, 3, "Retry", None).event( |
| "EndDialog", "ErrorRetry" |
| ) |
| |
| ##################################################################### |
| # Global "Query Cancel" dialog |
| cancel = Dialog(db, "CancelDlg", 50, 10, 260, 85, 3, title, "No", "No", "No") |
| cancel.text( |
| "Text", |
| 48, |
| 15, |
| 194, |
| 30, |
| 3, |
| "Are you sure you want to cancel [ProductName] installation?", |
| ) |
| # cancel.control("Icon", "Icon", 15, 15, 24, 24, 5242881, None, |
| # "py.ico", None, None) |
| c = cancel.pushbutton("Yes", 72, 57, 56, 17, 3, "Yes", "No") |
| c.event("EndDialog", "Exit") |
| |
| c = cancel.pushbutton("No", 132, 57, 56, 17, 3, "No", "Yes") |
| c.event("EndDialog", "Return") |
| |
| ##################################################################### |
| # Global "Wait for costing" dialog |
| costing = Dialog( |
| db, |
| "WaitForCostingDlg", |
| 50, |
| 10, |
| 260, |
| 85, |
| modal, |
| title, |
| "Return", |
| "Return", |
| "Return", |
| ) |
| costing.text( |
| "Text", |
| 48, |
| 15, |
| 194, |
| 30, |
| 3, |
| "Please wait while the installer finishes determining your disk space requirements.", |
| ) |
| c = costing.pushbutton("Return", 102, 57, 56, 17, 3, "Return", None) |
| c.event("EndDialog", "Exit") |
| |
| ##################################################################### |
| # Preparation dialog: no user input except cancellation |
| prep = PyDialog( |
| db, "PrepareDlg", x, y, w, h, modeless, title, "Cancel", "Cancel", "Cancel" |
| ) |
| prep.text( |
| "Description", |
| 15, |
| 70, |
| 320, |
| 40, |
| 0x30003, |
| "Please wait while the Installer prepares to guide you through the installation.", |
| ) |
| prep.title("Welcome to the [ProductName] Installer") |
| c = prep.text("ActionText", 15, 110, 320, 20, 0x30003, "Pondering...") |
| c.mapping("ActionText", "Text") |
| c = prep.text("ActionData", 15, 135, 320, 30, 0x30003, None) |
| c.mapping("ActionData", "Text") |
| prep.back("Back", None, active=0) |
| prep.next("Next", None, active=0) |
| c = prep.cancel("Cancel", None) |
| c.event("SpawnDialog", "CancelDlg") |
| |
| ##################################################################### |
| # Feature (Python directory) selection |
| seldlg = PyDialog( |
| db, "SelectFeaturesDlg", x, y, w, h, modal, title, "Next", "Next", "Cancel" |
| ) |
| seldlg.title("Select Python Installations") |
| |
| seldlg.text( |
| "Hint", |
| 15, |
| 30, |
| 300, |
| 20, |
| 3, |
| "Select the Python locations where %s should be installed." |
| % self.distribution.get_fullname(), |
| ) |
| |
| seldlg.back("< Back", None, active=0) |
| c = seldlg.next("Next >", "Cancel") |
| order = 1 |
| c.event("[TARGETDIR]", "[SourceDir]", ordering=order) |
| for version in self.versions + [self.other_version]: |
| order += 1 |
| c.event( |
| "[TARGETDIR]", |
| "[TARGETDIR%s]" % version, |
| "FEATURE_SELECTED AND &Python%s=3" % version, |
| ordering=order, |
| ) |
| c.event("SpawnWaitDialog", "WaitForCostingDlg", ordering=order + 1) |
| c.event("EndDialog", "Return", ordering=order + 2) |
| c = seldlg.cancel("Cancel", "Features") |
| c.event("SpawnDialog", "CancelDlg") |
| |
| c = seldlg.control( |
| "Features", |
| "SelectionTree", |
| 15, |
| 60, |
| 300, |
| 120, |
| 3, |
| "FEATURE", |
| None, |
| "PathEdit", |
| None, |
| ) |
| c.event("[FEATURE_SELECTED]", "1") |
| ver = self.other_version |
| install_other_cond = "FEATURE_SELECTED AND &Python%s=3" % ver |
| dont_install_other_cond = "FEATURE_SELECTED AND &Python%s<>3" % ver |
| |
| c = seldlg.text( |
| "Other", 15, 200, 300, 15, 3, "Provide an alternate Python location" |
| ) |
| c.condition("Enable", install_other_cond) |
| c.condition("Show", install_other_cond) |
| c.condition("Disable", dont_install_other_cond) |
| c.condition("Hide", dont_install_other_cond) |
| |
| c = seldlg.control( |
| "PathEdit", |
| "PathEdit", |
| 15, |
| 215, |
| 300, |
| 16, |
| 1, |
| "TARGETDIR" + ver, |
| None, |
| "Next", |
| None, |
| ) |
| c.condition("Enable", install_other_cond) |
| c.condition("Show", install_other_cond) |
| c.condition("Disable", dont_install_other_cond) |
| c.condition("Hide", dont_install_other_cond) |
| |
| ##################################################################### |
| # Disk cost |
| cost = PyDialog( |
| db, "DiskCostDlg", x, y, w, h, modal, title, "OK", "OK", "OK", bitmap=False |
| ) |
| cost.text( |
| "Title", 15, 6, 200, 15, 0x30003, r"{\DlgFontBold8}Disk Space Requirements" |
| ) |
| cost.text( |
| "Description", |
| 20, |
| 20, |
| 280, |
| 20, |
| 0x30003, |
| "The disk space required for the installation of the selected features.", |
| ) |
| cost.text( |
| "Text", |
| 20, |
| 53, |
| 330, |
| 60, |
| 3, |
| "The highlighted volumes (if any) do not have enough disk space " |
| "available for the currently selected features. You can either " |
| "remove some files from the highlighted volumes, or choose to " |
| "install less features onto local drive(s), or select different " |
| "destination drive(s).", |
| ) |
| cost.control( |
| "VolumeList", |
| "VolumeCostList", |
| 20, |
| 100, |
| 330, |
| 150, |
| 393223, |
| None, |
| "{120}{70}{70}{70}{70}", |
| None, |
| None, |
| ) |
| cost.xbutton("OK", "Ok", None, 0.5).event("EndDialog", "Return") |
| |
| ##################################################################### |
| # WhichUsers Dialog. Only available on NT, and for privileged users. |
| # This must be run before FindRelatedProducts, because that will |
| # take into account whether the previous installation was per-user |
| # or per-machine. We currently don't support going back to this |
| # dialog after "Next" was selected; to support this, we would need to |
| # find how to reset the ALLUSERS property, and how to re-run |
| # FindRelatedProducts. |
| # On Windows9x, the ALLUSERS property is ignored on the command line |
| # and in the Property table, but installer fails according to the documentation |
| # if a dialog attempts to set ALLUSERS. |
| whichusers = PyDialog( |
| db, |
| "WhichUsersDlg", |
| x, |
| y, |
| w, |
| h, |
| modal, |
| title, |
| "AdminInstall", |
| "Next", |
| "Cancel", |
| ) |
| whichusers.title( |
| "Select whether to install [ProductName] for all users of this computer." |
| ) |
| # A radio group with two options: allusers, justme |
| g = whichusers.radiogroup( |
| "AdminInstall", 15, 60, 260, 50, 3, "WhichUsers", "", "Next" |
| ) |
| g.add("ALL", 0, 5, 150, 20, "Install for all users") |
| g.add("JUSTME", 0, 25, 150, 20, "Install just for me") |
| |
| whichusers.back("Back", None, active=0) |
| |
| c = whichusers.next("Next >", "Cancel") |
| c.event("[ALLUSERS]", "1", 'WhichUsers="ALL"', 1) |
| c.event("EndDialog", "Return", ordering=2) |
| |
| c = whichusers.cancel("Cancel", "AdminInstall") |
| c.event("SpawnDialog", "CancelDlg") |
| |
| ##################################################################### |
| # Installation Progress dialog (modeless) |
| progress = PyDialog( |
| db, |
| "ProgressDlg", |
| x, |
| y, |
| w, |
| h, |
| modeless, |
| title, |
| "Cancel", |
| "Cancel", |
| "Cancel", |
| bitmap=False, |
| ) |
| progress.text( |
| "Title", |
| 20, |
| 15, |
| 200, |
| 15, |
| 0x30003, |
| r"{\DlgFontBold8}[Progress1] [ProductName]", |
| ) |
| progress.text( |
| "Text", |
| 35, |
| 65, |
| 300, |
| 30, |
| 3, |
| "Please wait while the Installer [Progress2] [ProductName]. " |
| "This may take several minutes.", |
| ) |
| progress.text("StatusLabel", 35, 100, 35, 20, 3, "Status:") |
| |
| c = progress.text("ActionText", 70, 100, w - 70, 20, 3, "Pondering...") |
| c.mapping("ActionText", "Text") |
| |
| # c=progress.text("ActionData", 35, 140, 300, 20, 3, None) |
| # c.mapping("ActionData", "Text") |
| |
| c = progress.control( |
| "ProgressBar", |
| "ProgressBar", |
| 35, |
| 120, |
| 300, |
| 10, |
| 65537, |
| None, |
| "Progress done", |
| None, |
| None, |
| ) |
| c.mapping("SetProgress", "Progress") |
| |
| progress.back("< Back", "Next", active=False) |
| progress.next("Next >", "Cancel", active=False) |
| progress.cancel("Cancel", "Back").event("SpawnDialog", "CancelDlg") |
| |
| ################################################################### |
| # Maintenance type: repair/uninstall |
| maint = PyDialog( |
| db, "MaintenanceTypeDlg", x, y, w, h, modal, title, "Next", "Next", "Cancel" |
| ) |
| maint.title("Welcome to the [ProductName] Setup Wizard") |
| maint.text( |
| "BodyText", |
| 15, |
| 63, |
| 330, |
| 42, |
| 3, |
| "Select whether you want to repair or remove [ProductName].", |
| ) |
| g = maint.radiogroup( |
| "RepairRadioGroup", |
| 15, |
| 108, |
| 330, |
| 60, |
| 3, |
| "MaintenanceForm_Action", |
| "", |
| "Next", |
| ) |
| # g.add("Change", 0, 0, 200, 17, "&Change [ProductName]") |
| g.add("Repair", 0, 18, 200, 17, "&Repair [ProductName]") |
| g.add("Remove", 0, 36, 200, 17, "Re&move [ProductName]") |
| |
| maint.back("< Back", None, active=False) |
| c = maint.next("Finish", "Cancel") |
| # Change installation: Change progress dialog to "Change", then ask |
| # for feature selection |
| # c.event("[Progress1]", "Change", 'MaintenanceForm_Action="Change"', 1) |
| # c.event("[Progress2]", "changes", 'MaintenanceForm_Action="Change"', 2) |
| |
| # Reinstall: Change progress dialog to "Repair", then invoke reinstall |
| # Also set list of reinstalled features to "ALL" |
| c.event("[REINSTALL]", "ALL", 'MaintenanceForm_Action="Repair"', 5) |
| c.event("[Progress1]", "Repairing", 'MaintenanceForm_Action="Repair"', 6) |
| c.event("[Progress2]", "repairs", 'MaintenanceForm_Action="Repair"', 7) |
| c.event("Reinstall", "ALL", 'MaintenanceForm_Action="Repair"', 8) |
| |
| # Uninstall: Change progress to "Remove", then invoke uninstall |
| # Also set list of removed features to "ALL" |
| c.event("[REMOVE]", "ALL", 'MaintenanceForm_Action="Remove"', 11) |
| c.event("[Progress1]", "Removing", 'MaintenanceForm_Action="Remove"', 12) |
| c.event("[Progress2]", "removes", 'MaintenanceForm_Action="Remove"', 13) |
| c.event("Remove", "ALL", 'MaintenanceForm_Action="Remove"', 14) |
| |
| # Close dialog when maintenance action scheduled |
| c.event("EndDialog", "Return", 'MaintenanceForm_Action<>"Change"', 20) |
| # c.event("NewDialog", "SelectFeaturesDlg", 'MaintenanceForm_Action="Change"', 21) |
| |
| maint.cancel("Cancel", "RepairRadioGroup").event("SpawnDialog", "CancelDlg") |
| |
| def get_installer_filename(self, fullname): |
| # Factored out to allow overriding in subclasses |
| if self.target_version: |
| base_name = "%s.%s-py%s.msi" % ( |
| fullname, |
| self.plat_name, |
| self.target_version, |
| ) |
| else: |
| base_name = "%s.%s.msi" % (fullname, self.plat_name) |
| installer_name = os.path.join(self.dist_dir, base_name) |
| return installer_name |