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