forked from AndresCidoncha/VisualHardwareAlarm
-
Notifications
You must be signed in to change notification settings - Fork 0
/
build_release.py
291 lines (223 loc) · 9.12 KB
/
build_release.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
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
import os
import re
import shutil
import logging
import subprocess
from zipfile import ZipFile, ZIP_DEFLATED
from string import Template
import PyInstaller.__main__
RUN_SCRIPT = 'run.py'
RELEASES_PATH = 'releases'
BUILD_VERSION_TEMPLATE_PATH = 'build_version_template.txt'
SETUP_SCRIPT_TEMPLATE_PATH = 'setup_script_template.nsi'
ICON_PATH = 'resources/icon/icon.f0.ico'
RELEASE_NAME = 'RGBHardwareMonitor'
VERSION_TEMPLATE = '{version_major}.{version_minor}.{version_build}{version_revision}'
PORTABLE_RELEASE_NAME_TEMPLATE = '{release_name}-{version}-portable.zip'
SETUP_RELEASE_NAME_TEMPLATE = '{release_name}-{version}-setup.exe'
MAKENSIS_PATH = r'C:\Program Files (x86)\NSIS\makensis.exe'
logger = logging.getLogger(__name__)
def abort(msg=None):
if msg:
logger.info(msg)
logger.info('Build process aborted')
exit(1)
def yes_no_prompt(msg):
return input(f'{msg} [y/N]: ').lower() in ('y', 'yes')
def run_process(args, **kwargs):
if isinstance(args, str):
args = args.split(' ')
run_kwargs = dict(capture_output=True, check=True, text=True)
run_kwargs.update(kwargs)
return subprocess.run(args, **run_kwargs)
def combined_std_out_err(process_result):
return process_result.stdout + process_result.stderr.strip()
def create_readable_version(major, minor, build, revision, commit, long=False):
bsplit = build.split('-', 1)
if len(bsplit) > 1:
build, v_type = bsplit
else:
v_type = None
v = '.'.join((major, minor, build))
if revision and revision not in (0, '0'):
v += '.' + revision
if v_type:
v += '-' + v_type
if long:
v += f' ({commit})'
return v
def ver_num_clean(v):
return re.sub(r'[^\d]+', '', v, re.I)
class TemplateFile:
def __init__(self, template_path):
with open(template_path, mode='r', encoding='utf8') as fp:
self.template = Template(fp.read())
def format(self, *args, **kwargs):
return self.template.safe_substitute(*args, **kwargs)
def write(self, output_path, *args, **kwargs):
with open(output_path, mode='w', encoding='utf8') as fp:
fp.write(self.format(*args, **kwargs))
def copy_asset(src, dest=None):
if dest is None:
dest = src
dest = os.path.join(build_dist_path, dest)
if os.path.exists(dest):
logger.debug(f'Removing previous existing asset "{dest}"')
if os.path.isdir(dest):
shutil.rmtree(dest)
else:
os.remove(dest)
logger.info(f'Copying asset "{src}" to "{dest}"')
if os.path.isdir(src):
shutil.copytree(src, dest)
else:
shutil.copyfile(src, dest)
def remove_prefix(text, prefix):
if text.startswith(prefix):
return text[len(prefix):]
return text
def zip_dir(zip_path, dir_path):
with ZipFile(zip_path, 'w', ZIP_DEFLATED) as zipf:
for root, dirs, files in os.walk(dir_path):
for file in files:
file_path = os.path.join(root, file)
zipf.write(file_path, arcname=remove_prefix(file_path, dir_path))
def generate_setup_files_instructions(base_path, is_uninstall=False):
prefix = "$INSTDIR"
def emit_dir_instr(rel_path):
nonlocal instructions
dest_path = os.path.join(prefix, rel_path).rstrip('\\/')
instructions.append(f'SetOutPath "{dest_path}"' if not is_uninstall else f'RmDir "{dest_path}"')
def emit_file_instr(file_path):
nonlocal instructions
abs_path = os.path.abspath(file_path)
rel_path = remove_prefix(file_path, base_path).lstrip('\\/')
dest_path = os.path.join(prefix, rel_path)
instructions.append(f'File "{abs_path}"' if not is_uninstall else f'Delete "{dest_path}"')
instructions = []
for abs_root, dirs, files in os.walk(base_path, topdown=not is_uninstall):
rel_root = remove_prefix(abs_root, base_path).lstrip('\\/')
if not is_uninstall:
emit_dir_instr(rel_root)
for file in files:
emit_file_instr(os.path.join(abs_root, file))
if is_uninstall and rel_root:
emit_dir_instr(rel_root)
return '\n'.join(instructions)
#
# ----- RELEASE BUILD START ----- #
#
# -- Check git status is clean
logger.info('Checking git status')
git_status = run_process('git status -s')
if git_status.stdout.strip():
logger.warning(f'Git status unclean:\n{combined_std_out_err(git_status)}')
if not yes_no_prompt('There are uncommitted/untracked files. Continue building?'):
abort()
# -- Create version number from tags/commit
logger.info('Creating release version number')
git_describe = run_process('git describe --tags --long')
version_match = re.match(r'v(\d+.*)\.(\d+.*)\.(\d+.*)-(\d+)-(.+)$', git_describe.stdout.strip(), flags=re.I)
if not version_match:
abort(f'Couldn\'t extract version from git describe. Output was:\n{combined_std_out_err(git_describe)}')
ver_major = version_match.group(1)
ver_minor = version_match.group(2)
ver_build = version_match.group(3)
ver_revision = version_match.group(4)
ver_commit = version_match.group(5)
version = create_readable_version(ver_major, ver_minor, ver_build, ver_revision, ver_commit)
version_long = create_readable_version(ver_major, ver_minor, ver_build, ver_revision, ver_commit, long=True)
version_numeric = '.'.join(ver_num_clean(v) for v in (ver_major, ver_minor, ver_build, ver_revision))
# -- Create release paths
logger.info('Making release paths')
release_path = os.path.join(RELEASES_PATH, version)
build_dist_path = os.path.join(release_path, 'dist')
build_work_path = os.path.join(release_path, 'temp')
build_version_path = os.path.join(release_path, 'build_version.txt')
setup_script_path = os.path.join(release_path, 'setup_script.nsi')
executable_path = os.path.join(build_dist_path, f'{RELEASE_NAME}.exe')
portable_release_path = os.path.join(release_path, PORTABLE_RELEASE_NAME_TEMPLATE.format(release_name=RELEASE_NAME,
version=version))
setup_release_path = os.path.join(release_path, SETUP_RELEASE_NAME_TEMPLATE.format(release_name=RELEASE_NAME,
version=version))
if os.path.exists(release_path):
if not yes_no_prompt('Release directory already exists. Delete and continue building?'):
abort()
else:
shutil.rmtree(release_path)
os.makedirs(release_path, exist_ok=True)
# -- Compiling build_version file
logger.info('Compiling build_version file')
build_version_template = TemplateFile(BUILD_VERSION_TEMPLATE_PATH)
build_version_namespace = dict(
release_name=RELEASE_NAME,
version_major=ver_num_clean(ver_major),
version_minor=ver_num_clean(ver_minor),
version_build=ver_num_clean(ver_build),
version_revision=ver_num_clean(ver_revision),
version=version,
version_commit=ver_commit,
)
build_version_template.write(build_version_path, build_version_namespace)
# -- Create dist build
logger.info('Building dist')
PyInstaller.__main__.run([
'--clean',
'--noconfirm',
f'--distpath={build_dist_path}',
f'--workpath={build_work_path}',
'--onefile',
'--add-data=resources;resources',
'--add-data=modules;modules',
'--hidden-import=pkg_resources',
'--hidden-import=pkg_resources.py2_warn',
'--windowed',
f'--icon={ICON_PATH}',
f'--version-file={build_version_path}',
# '-d', 'all',
'--win-private-assemblies',
f'--name={RELEASE_NAME}',
RUN_SCRIPT
])
# -- Copying additional assets
logger.info('Copying additional assets')
copy_asset('config.ini')
copy_asset('arduino')
copy_asset('LICENSE')
# -- Package portable release
logger.info('Packaging portable release')
zip_dir(portable_release_path, build_dist_path)
# -- Compiling setup_script file
# TODO: Uninstall / cleanup before install
# TODO: Keep configs and user-added files
# TODO: Move config and arduino sketches somewhere else?
# TODO: Add OpenHardwareMonitor download and unpack (inside code folder)
# + automatically set OHM path in config and close_ohm_on_exit
# + check licensing
# TODO: Remove autorun before uninstall (only if not upgrading)
logger.info('Compiling setup_script file')
setup_script_template = TemplateFile(SETUP_SCRIPT_TEMPLATE_PATH)
setup_script_install_instructions = generate_setup_files_instructions(build_dist_path)
setup_script_uninstall_instructions = generate_setup_files_instructions(build_dist_path, is_uninstall=True)
setup_script_namespace = dict(
t_release_name=RELEASE_NAME,
t_version=version_numeric,
t_root_abspath=os.path.abspath('.'),
t_setup_abspath=os.path.abspath(setup_release_path),
t_install_instructions=setup_script_install_instructions,
t_uninstall_instructions=setup_script_uninstall_instructions,
)
setup_script_template.write(setup_script_path, setup_script_namespace)
# -- Build setup file
logger.info('Packaging setup release')
run_process([MAKENSIS_PATH, setup_script_path])
# -- Clean up
logger.info('Cleaning up temp files')
os.remove(f'{RELEASE_NAME}.spec')
os.remove(build_version_path)
os.remove(setup_script_path)
shutil.rmtree(build_work_path)
logger.info('Done.')
#
# ----- RELEASE BUILD END ----- #
#