• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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 """
6 Implements the bdist_msi command.
7 """
8 
9 import os
10 import sys
11 import warnings
12 from distutils.core import Command
13 from distutils.dir_util import remove_tree
14 from distutils.sysconfig import get_python_version
15 from distutils.version import StrictVersion
16 from distutils.errors import DistutilsOptionError
17 from distutils.util import get_platform
18 from distutils import log
19 import msilib
20 from msilib import schema, sequence, text
21 from msilib import Directory, Feature, Dialog, add_data
22 
23 class PyDialog(Dialog):
24     """Dialog class with a fixed layout: controls at the top, then a ruler,
25     then a list of buttons: back, next, cancel. Optionally a bitmap at the
26     left."""
27     def __init__(self, *args, **kw):
28         """Dialog(database, name, x, y, w, h, attributes, title, first,
29         default, cancel, bitmap=true)"""
30         Dialog.__init__(self, *args)
31         ruler = self.h - 36
32         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 
85 class 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