Skip to content

Commit af4d1be

Browse files
author
xoviat
committed
Revise UI and increment verison.
1 parent db6e4fb commit af4d1be

File tree

3 files changed

+118
-125
lines changed

3 files changed

+118
-125
lines changed

README.md

Lines changed: 19 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
# pyinstaller_utils
2-
PyInstaller Utils allows you to build your PyInstaller executables from setup.py and create MSI installers to distribute
3-
them. PyInstaller Utils uses code from cx_Freeze to avoid reinventing the wheel. Therefore, some files are licensed
4-
under the PSF license even though the project is licensed under GPL.
2+
## What is Pyinstaller Utils?
3+
4+
PyInstaller Utils allows you to rapidly deploy your [frozen](http://docs.python-guide.org/en/latest/shipping/freezing/)
5+
Python application with minimal effort and additional code. PyInstaller utils does this by providing a simple and
6+
intuitive wrapper for PyInstaller coupled with an MSI builder. With a few lines of code and a single command, you can
7+
go directly from Python code to a compiled MSI installer. In addiiton, PyInstaller Utils does not require any
8+
non-Python dependencies beyond those required by PyInstaller, making it trivial to install.
59

610
## How do I install it?
711

@@ -17,7 +21,7 @@ from pyinstaller_utils.dist import setup
1721

1822
Then run the following command:
1923

20-
python setup.py build_exe
24+
python setup.py bdist_msi
2125

2226
That's it! PyInstaller will build all of the entry points and scripts specified in your executable.
2327

@@ -32,32 +36,19 @@ In your setup function, you can specify PyInstaller options as follows:
3236
'hiddenimports': ['requests'],
3337
'pathex': ['/my/path', '/their/path'],
3438
'icon': '/path/to/icon.png',
35-
}
36-
},
39+
},
40+
'bdist_msi': {
41+
'upgrade_code': '{66620F3A-DC3A-11E2-B341-002219E9B01E}',
42+
'shortcuts': [
43+
'ProgramMenuFolder\Hello World = my_project'
44+
],
45+
},
3746
...)
3847
```
39-
The full array of options is of course available in the PyInstaller documentation.
40-
41-
42-
## How can I build an MSI?
43-
44-
In your setup, put the following:
45-
46-
```python
47-
setup(...
48-
'bdist_msi': {
49-
'upgrade_code': '{66620F3A-DC3A-11E2-B341-002219E9B01E}',
50-
'shortcuts': [
51-
'ProgramMenuFolder\Hello World = my_project'
52-
],
53-
}
54-
...)
55-
```
48+
The full array of options for build_exe is available in the PyInstaller documentation. Providing an upgrade code is
49+
**strongly recommended** for the bdist_msi command. A license agreement will be added to the installer if there is
50+
a license text file in the same directory as setup.py.
5651

57-
Then run:
58-
59-
python setup.py bdist_msi
60-
6152
Note that PyInstaller Utils currently cannot create shortcuts that are not placed in a root system directory. In other
6253
words, you can currently have a shortcut on the desktop of in the program menu but not in a folder on the desktop or in
63-
a folder on the program menu.
54+
a folder on the program menu. This may be resolved in the future if there is greater interest.

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.1.1
1+
0.1.2

pyinstaller_utils/windist.py

Lines changed: 98 additions & 96 deletions
Original file line numberDiff line numberDiff line change
@@ -51,14 +51,15 @@ def add_config(self, fullname):
5151
msilib.add_data(self.db, 'InstallUISequence',
5252
[("PrepareDlg", None, 140),
5353
("A_SET_TARGET_DIR", 'TARGETDIR=""', 401),
54-
("LicenseDlg", "not Installed", 1230),
55-
("MaintenanceTypeDlg",
56-
"Installed and not Resume and not Preselected", 1250),
57-
("ProgressDlg", None, 1280)
54+
('LicenseDlg', 'not Installed', 1230),
55+
('MaintenanceTypeDlg',
56+
'Installed and not Resume and not Preselected', 1250),
57+
('ProgressDlg', None, 1280)
5858
])
5959

6060
# TODO: Add ability to pass arguments to executable
6161
# TODO: Validate shortcut values against known MSI table
62+
# TODO: Add ability to specify nested shortcuts
6263
for index, shortcut in enumerate(self.shortcuts):
6364
shortcut = shortcut.split('=')
6465
baseName = '{}.exe'.format(shortcut[1].strip())
@@ -76,59 +77,59 @@ def add_config(self, fullname):
7677
msilib.add_data(self.db, tableName, data)
7778

7879
def add_cancel_dialog(self):
79-
dialog = msilib.Dialog(self.db, "CancelDlg", 50, 10, 260, 85, 3,
80-
self.title, "No", "No", "No")
81-
dialog.text("Text", 48, 15, 194, 30, 3,
82-
"Are you sure you want to cancel [ProductName] installation?")
83-
button = dialog.pushbutton("Yes", 72, 57, 56, 17, 3, "Yes", "No")
84-
button.event("EndDialog", "Exit")
85-
button = dialog.pushbutton("No", 132, 57, 56, 17, 3, "No", "Yes")
86-
button.event("EndDialog", "Return")
80+
dialog = msilib.Dialog(self.db, 'CancelDlg', 50, 50, 260, 85, 3,
81+
self.title, 'No', 'No', 'No')
82+
dialog.text('Text', 48, 15, 194, 30, 3,
83+
'Are you sure you want to cancel [ProductName] installation?')
84+
button = dialog.pushbutton('Yes', 72, 57, 56, 17, 3, 'Yes', 'No')
85+
button.event('EndDialog', 'Exit')
86+
button = dialog.pushbutton('No', 132, 57, 56, 17, 3, 'No', 'Yes')
87+
button.event('EndDialog', 'Return')
8788

8889
def add_error_dialog(self):
89-
dialog = msilib.Dialog(self.db, "ErrorDlg", 50, 10, 330, 101, 65543,
90-
self.title, "ErrorText", None, None)
91-
dialog.text("ErrorText", 50, 9, 280, 48, 3, "")
92-
for text, x in [("No", 120), ("Yes", 240), ("Abort", 0),
93-
("Cancel", 42), ("Ignore", 81), ("Ok", 159), ("Retry", 198)]:
90+
dialog = msilib.Dialog(self.db, 'ErrorDlg', 50, 10, 330, 101, 65543,
91+
self.title, 'ErrorText', None, None)
92+
dialog.text('ErrorText', 50, 9, 280, 48, 3, '')
93+
for text, x in [('No', 120), ('Yes', 240), ('Abort', 0),
94+
('Cancel', 42), ('Ignore', 81), ('Ok', 159), ('Retry', 198)]:
9495
button = dialog.pushbutton(text[0], x, 72, 81, 21, 3, text, None)
95-
button.event("EndDialog", "Error%s" % text)
96+
button.event('EndDialog', 'Error%s' % text)
9697

9798
def add_exit_dialog(self):
98-
dialog = distutils.command.bdist_msi.PyDialog(self.db, "ExitDialog",
99+
dialog = distutils.command.bdist_msi.PyDialog(self.db, 'ExitDialog',
99100
self.x, self.y, self.width, self.height, self.modal,
100-
self.title, "Finish", "Finish", "Finish")
101-
dialog.title("Completing the [ProductName] installer")
102-
dialog.back("< Back", "Finish", active=False)
103-
dialog.cancel("Cancel", "Back", active=False)
104-
dialog.text("Description", 15, 235, 320, 20, 0x30003,
105-
"Click the Finish button to exit the installer.")
106-
button = dialog.next("Finish", "Cancel", name="Finish")
107-
button.event("EndDialog", "Return")
101+
self.title, 'Finish', 'Finish', 'Finish')
102+
dialog.title('Completing the [ProductName] installer')
103+
dialog.back('Back', 'Finish', active=False)
104+
dialog.cancel('Cancel', 'Back', active=False)
105+
dialog.text('Description', 15, 207, 320, 20, 0x30003,
106+
'Click the Finish button to exit the installer.')
107+
button = dialog.next('Finish', 'Cancel', name='Finish')
108+
button.event('EndDialog', 'Return')
108109

109110
def add_fatal_error_dialog(self):
110-
dialog = distutils.command.bdist_msi.PyDialog(self.db, "FatalError",
111+
dialog = distutils.command.bdist_msi.PyDialog(self.db, 'FatalError',
111112
self.x, self.y, self.width, self.height, self.modal,
112-
self.title, "Finish", "Finish", "Finish")
113-
dialog.title("[ProductName] installer ended prematurely")
114-
dialog.back("< Back", "Finish", active=False)
115-
dialog.cancel("Cancel", "Back", active=False)
116-
dialog.text("Description1", 15, 70, 320, 80, 0x30003,
117-
"[ProductName] setup ended prematurely because of an error. "
118-
"Your system has not been modified. To install this program "
119-
"at a later time, please run the installation again.")
120-
dialog.text("Description2", 15, 155, 320, 20, 0x30003,
121-
"Click the Finish button to exit the installer.")
122-
button = dialog.next("Finish", "Cancel", name="Finish")
123-
button.event("EndDialog", "Exit")
113+
self.title, 'Finish', 'Finish', 'Finish')
114+
dialog.title('[ProductName] installer ended prematurely')
115+
dialog.back('Back', 'Finish', active=False)
116+
dialog.cancel('Cancel', 'Back', active=False)
117+
dialog.text('Description1', 15, 70, 320, 80, 0x30003,
118+
'[ProductName] setup ended prematurely because of an error. '
119+
'Your system has not been modified. To install this program '
120+
'at a later time, please run the installation again.')
121+
dialog.text('Description2', 15, 155, 320, 20, 0x30003,
122+
'Click the Finish button to exit the installer.')
123+
button = dialog.next('Finish', 'Cancel', name='Finish')
124+
button.event('EndDialog', 'Exit')
124125

125126
def add_files(self):
126127
db = self.db
127-
cab = msilib.CAB("distfiles")
128-
f = msilib.Feature(db, "default", "Default Feature", "Everything", 1, directory="TARGETDIR")
128+
cab = msilib.CAB('distfiles')
129+
f = msilib.Feature(db, 'default', 'Default Feature', 'Everything', 1, directory='TARGETDIR')
129130
f.set_current()
130131
rootdir = os.path.abspath(self.bdist_dir)
131-
root = msilib.Directory(db, cab, None, rootdir, "TARGETDIR", "SourceDir")
132+
root = msilib.Directory(db, cab, None, rootdir, 'TARGETDIR', 'SourceDir')
132133
db.Commit()
133134
todo = [root]
134135
while todo:
@@ -143,26 +144,26 @@ def add_files(self):
143144
cab.commit(db)
144145

145146
def add_files_in_use_dialog(self):
146-
dialog = distutils.command.bdist_msi.PyDialog(self.db, "FilesInUse",
147+
dialog = distutils.command.bdist_msi.PyDialog(self.db, 'FilesInUse',
147148
self.x, self.y, self.width, self.height, 19, self.title,
148-
"Retry", "Retry", "Retry", bitmap=False)
149-
dialog.text("Title", 15, 6, 200, 15, 0x30003,
150-
r"{\DlgFontBold8}Files in Use")
151-
dialog.text("Description", 20, 23, 280, 20, 0x30003,
152-
"Some files that need to be updated are currently in use.")
153-
dialog.text("Text", 20, 55, 330, 50, 3,
154-
"The following applications are using files that need to be "
155-
"updated by this setup. Close these applications and then "
156-
"click Retry to continue the installation or Cancel to exit "
157-
"it.")
158-
dialog.control("List", "ListBox", 20, 107, 330, 130, 7,
159-
"FileInUseProcess", None, None, None)
160-
button = dialog.back("Exit", "Ignore", name="Exit")
161-
button.event("EndDialog", "Exit")
162-
button = dialog.next("Ignore", "Retry", name="Ignore")
163-
button.event("EndDialog", "Ignore")
164-
button = dialog.cancel("Retry", "Exit", name="Retry")
165-
button.event("EndDialog", "Retry")
149+
'Retry', 'Retry', 'Retry', bitmap=False)
150+
dialog.text('Title', 15, 6, 200, 15, 0x30003,
151+
r'{\DlgFontBold8}Files in Use')
152+
dialog.text('Description', 20, 23, 280, 20, 0x30003,
153+
'Some files that need to be updated are currently in use.')
154+
dialog.text('Text', 20, 55, 330, 50, 3,
155+
'The following applications are using files that need to be '
156+
'updated by this setup. Close these applications and then '
157+
'click Retry to continue the installation or Cancel to exit '
158+
'it.')
159+
dialog.control('List', 'ListBox', 20, 107, 330, 130, 7,
160+
'FileInUseProcess', None, None, None)
161+
button = dialog.back('Exit', 'Ignore', name='Exit')
162+
button.event('EndDialog', 'Exit')
163+
button = dialog.next('Ignore', 'Retry', name='Ignore')
164+
button.event('EndDialog', 'Ignore')
165+
button = dialog.cancel('Retry', 'Exit', name='Retry')
166+
button.event('EndDialog', 'Retry')
166167

167168
def add_maintenance_type_dialog(self):
168169
dialog = distutils.command.bdist_msi.PyDialog(self.db,
@@ -175,7 +176,7 @@ def add_maintenance_type_dialog(self):
175176
"MaintenanceForm_Action", "", "Next")
176177
group.add("Repair", 0, 18, 300, 17, "&Repair [ProductName]")
177178
group.add("Remove", 0, 36, 300, 17, "Re&move [ProductName]")
178-
dialog.back("< Back", None, active=False)
179+
dialog.back("Back", None, active=False)
179180
button = dialog.next("Finish", "Cancel")
180181
button.event("[REINSTALL]", "ALL",
181182
'MaintenanceForm_Action="Repair"', 5)
@@ -231,8 +232,8 @@ def add_progress_dialog(self):
231232
control = dialog.control("ProgressBar", "ProgressBar", 35, 120, 300,
232233
10, 65537, None, "Progress done", None, None)
233234
control.mapping("SetProgress", "Progress")
234-
dialog.back("< Back", "Next", active=False)
235-
dialog.next("Next >", "Cancel", active=False)
235+
dialog.back("Back", "Next", active=False)
236+
dialog.next("Next", "Cancel", active=False)
236237
button = dialog.cancel("Cancel", "Back")
237238
button.event("SpawnDialog", "CancelDlg")
238239

@@ -256,28 +257,22 @@ def add_properties(self):
256257
props.append(("UpgradeCode", self.upgrade_code))
257258
msilib.add_data(self.db, 'Property', props)
258259

259-
def add_select_directory_dialog(self):
260-
# TODO: Parse other types of license files
261-
for file in ['LICENSE', 'LICENSE.txt']:
262-
if os.path.isfile(file):
263-
license_file = open(file)
264-
break
265-
266-
if license_file:
260+
def add_license_dialog(self):
261+
if self.license_text:
267262
dialog = distutils.command.bdist_msi.PyDialog(self.db, 'LicenseDlg',
268263
self.x, self.y, self.width, self.height, self.modal,
269264
self.title, 'Next', 'Next', 'Cancel', bitmap=False)
270265
dialog.title('License Agreement')
271-
dialog.control('ScrollableText', 'ScrollableText', 15, 30, 340, 200, 3, None, license_text(license_file),
272-
None, None)
273-
dialog.checkbox('AcceptLicense', 15, 240, 340, 15, 3, 'LicenseAccepted',
266+
dialog.control('ScrollableText', 'ScrollableText', 20, 60, 330, 140, 7, None,
267+
self.license_text, None, None)
268+
dialog.checkbox('AcceptLicense', 20, 207, 330, 18, 3, 'LicenseAccepted',
274269
'I accept the terms in the License Agreement', None)
275-
dialog.back('< Back', 'Next', active=False)
270+
dialog.back('Back', 'Next', active=False)
276271

277-
button = dialog.next('Next >', 'Cancel', active=False)
278-
button.event('SetTargetPath', 'TARGETDIR', ordering=1)
279-
button.event('SpawnWaitDialog', 'WaitForCostingDlg', ordering=2)
280-
button.event('EndDialog', 'Return', ordering=3)
272+
button = dialog.next('Next', 'Cancel', active=False)
273+
# button.event('SetTargetPath', 'TARGETDIR', ordering=1)
274+
button.event('SpawnWaitDialog', 'WaitForCostingDlg', ordering=1)
275+
button.event('EndDialog', 'Return', ordering=2)
281276
button.condition('Enable', 'LicenseAccepted')
282277
button.condition('Disable', 'Not LicenseAccepted')
283278

@@ -302,7 +297,7 @@ def add_ui(self):
302297
self.add_files_in_use_dialog()
303298
self.add_wait_for_costing_dialog()
304299
self.add_prepare_dialog()
305-
self.add_select_directory_dialog()
300+
self.add_license_dialog()
306301
self.add_progress_dialog()
307302
self.add_maintenance_type_dialog()
308303

@@ -316,20 +311,20 @@ def add_upgrade_config(self, sversion):
316311
])
317312

318313
def add_user_exit_dialog(self):
319-
dialog = distutils.command.bdist_msi.PyDialog(self.db, "UserExit",
314+
dialog = distutils.command.bdist_msi.PyDialog(self.db, 'UserExit',
320315
self.x, self.y, self.width, self.height, self.modal,
321-
self.title, "Finish", "Finish", "Finish")
322-
dialog.title("[ProductName] installer was interrupted")
323-
dialog.back("< Back", "Finish", active=False)
324-
dialog.cancel("Cancel", "Back", active=False)
325-
dialog.text("Description1", 15, 70, 320, 80, 0x30003,
326-
"[ProductName] setup was interrupted. Your system has not "
327-
"been modified. To install this program at a later time, "
328-
"please run the installation again.")
329-
dialog.text("Description2", 15, 155, 320, 20, 0x30003,
330-
"Click the Finish button to exit the installer.")
331-
button = dialog.next("Finish", "Cancel", name="Finish")
332-
button.event("EndDialog", "Exit")
316+
self.title, 'Finish', 'Finish', 'Finish')
317+
dialog.title('[ProductName] installer was interrupted')
318+
dialog.back('Back', 'Finish', active=False)
319+
dialog.cancel('Cancel', 'Back', active=False)
320+
dialog.text('Description1', 15, 70, 320, 80, 0x30003,
321+
'[ProductName] setup was interrupted. Your system has not '
322+
'been modified. To install this program at a later time, '
323+
'please run the installation again.')
324+
dialog.text('Description2', 15, 207, 320, 20, 0x30003,
325+
'Click the Finish button to exit the installer.')
326+
button = dialog.next('Finish', 'Cancel', name='Finish')
327+
button.event('EndDialog', 'Exit')
333328

334329
def add_wait_for_costing_dialog(self):
335330
dialog = msilib.Dialog(self.db, "WaitForCostingDlg", 50, 10, 260, 85,
@@ -381,6 +376,7 @@ def finalize_options(self):
381376
raise EnvironmentError('Unable to identify build directory!')
382377

383378
self.bdist_dir = os.path.join(self.bdist_dir, build_dir())
379+
self.height = 270
384380

385381
def initialize_options(self):
386382
distutils.command.bdist_msi.bdist_msi.initialize_options(self)
@@ -393,8 +389,14 @@ def initialize_options(self):
393389
self.data = None
394390
self.shortcuts = None
395391

392+
# TODO: Parse other types of license files
393+
for file in ['LICENSE', 'LICENSE.txt']:
394+
if os.path.isfile(file):
395+
self.license_text = license_text(open(file))
396+
break
397+
396398
def run(self):
397-
self.skip_build = True
399+
# self.skip_build = True
398400
if not self.skip_build:
399401
self.run_command('build_exe')
400402
'''

0 commit comments

Comments
 (0)