forked from iBridges-for-iRODS/iBridges-GUI
-
Notifications
You must be signed in to change notification settings - Fork 0
/
pyinstaller_script.py
188 lines (161 loc) · 6.2 KB
/
pyinstaller_script.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
"""Pipeline to create pyinstaller executables.
The --onefile option is not used because Windows must upack the
executable before it can run. This makes it roughly 30 seconds
slower than the current variant.
"""
import subprocess
import utils
ICON_VAR = 'icon'
TAB_LEN = 4
TAB = ' ' * TAB_LEN
PIXMAP_STR = '.addPixmap(QtGui.QPixmap('
def run_cmd(command: str):
"""Run command in shell, stdout is printed to the screen
Parameters
----------
command : str
Input shell command line.
"""
# POSIX
if utils.utils.is_posix():
proc = subprocess.run(
command, stderr=subprocess.STDOUT, shell=True,
universal_newlines=True, executable='/bin/bash')
# Windows
else:
proc = subprocess.run(
command, stderr=subprocess.STDOUT, shell=True,
universal_newlines=True)
# Print all errors
if proc.stderr is not None:
print(f'commandline error: {proc.stderr}')
raise Exception('shell run error')
def ui_to_py(dirname: str, py_exec: str):
"""Convert the .ui files to .py files
Parameters
----------
dirname : str
Name of directory holding UI files.
py_exec : str
Name of Python executable. It should be the full path if not
in PATH.
The command-line takes the form:
python -m PyQt6.uic.pyuic -x file.ui -o file.py
"""
for uipath in sorted(utils.utils.LocalPath(dirname).glob('*.ui')):
basename = uipath.stem
pypath = utils.utils.LocalPath(dirname, f'{basename}.py')
print(f'Converting {uipath} to {pypath}')
run_cmd(f'{py_exec} -m PyQt6.uic.pyuic -x {uipath} -o {pypath}')
# Find and replace the pixelmap references. They are hardcoded.
# Also have to go up one directory for the executable, it's ugly!
inlines = pypath.read_text()
outlines = [
'"""Created by: PyQt6 UI code generator from the corresponding UI file',
'',
'WARNING: Any manual changes made to this file will be lost when pyuic6 is',
'run again. Do not edit this file unless you know what you are doing.',
'"""',
]
if PIXMAP_STR in inlines:
for inline in inlines.split('\n'):
if PIXMAP_STR not in inline:
outlines.append(inline)
else:
first, icon_path, last = inline.split('"')
icon_path = icon_path.split("..")[-1]
outlines.append(
f'{first}r"..{utils.utils.LocalPath(icon_path)}"{last}')
else:
outlines.extend(inlines.split('\n'))
pypath.write_text(
'\n'.join(line for line in outlines if not line.startswith('#')))
def remove_pyui_files(dirname: str):
"""Remove the locally stored Python versions of the UI files.
Parameters
----------
dirname : str
Name of directory holding UI files.
"""
for pypath in sorted(utils.utils.LocalPath(dirname).glob('*.py')):
if '__init__.py' not in pypath:
print(f'Removing {pypath}')
pypath.unlink()
def replace_directory(source: str, destination: str):
"""Overwrite `destination` with a copy of all contents of `source`.
Parameters
----------
source : str
Name of source directory.
destination : str
Name of destination directory.
"""
utils.utils.LocalPath(source).copy_path(destination, squash=True)
def main() -> int:
"""Main function.
Indicates (along with if __name__ == '__main__':) a Python script.
"compile" UI files into Python files so they can be imported
directly, creating a virtual environment if necessary prior to
creating a PyInstaller distribution package.
Returns
-------
int
Exit code of the program.
"""
relpath = utils.utils.LocalPath('.')
iconpath = relpath.joinpath('gui', 'icons')
uipath = relpath.joinpath('gui', 'ui_files')
venvpath = relpath.joinpath('venv')
# Step 1: Convert .ui files to .py files after first removing .py
# files that already exist. Recompiling is the best way to ensure
# they are up-to-date.
try:
remove_pyui_files(uipath)
ui_to_py(uipath, 'python')
except Exception as error:
print(f'Error converting UI files to Python: {error}')
return 1
# Step 2: Pyinstaller includes all dependencies in the environment,
# so we need to use a venv.
try:
venvpath.mkdir(exist_ok=True)
if utils.utils.is_posix():
venv_script = venvpath.joinpath('bin', 'activate')
venv_activate = f'source {venv_script}'
else:
venv_script = venvpath.joinpath("Scripts", "activate.bat")
venv_activate = str(venv_script)
# Create the venv if needed.
if not venv_script.is_file():
run_cmd(f'python -m venv {venvpath}')
run_cmd(f'{venv_activate} && python -m pip install --upgrade pip')
run_cmd(f'{venv_activate} && pip3 install -r requirements.txt')
except Exception as error:
print(f'Error finding/creating virtual environment: {error}')
return 2
# Step 3: Activate venv and run pyinstaller.
try:
filenames = f'{iconpath.joinpath("irods-iBridgesGui.ico")} irods-iBridgesGui.py'
run_cmd(f'{venv_activate} && pyinstaller --clean --noconfirm --icon {filenames}')
except Exception as error:
print(f'Error creating executable: {error}')
return 3
# Step 4: Copy icons folder to dist folder, replace if exists.
try:
dist_icons = relpath.joinpath('dist', 'icons')
replace_directory(iconpath, dist_icons)
except Exception as error:
print(f'Error copying icons: {error}')
return 4
# Optional post-installer actions.
confirmation = input("Do you want to cleanup the build environment (Y/N): ")
if confirmation[0].upper().startswith('Y'):
try:
relpath.joinpath('build').rmdir(squash=True)
relpath.joinpath('irods-iBridgesGui.spec').unlink()
except Exception as error:
print(f'Error cleaning up: {error}')
return 5
return 0
if __name__ == '__main__':
exit(main())