-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathbuild_release.py
More file actions
429 lines (359 loc) · 14.7 KB
/
build_release.py
File metadata and controls
429 lines (359 loc) · 14.7 KB
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
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
#!/usr/bin/env python3
"""
Protector ISAPI Manager - Build Release Script
Protector Sistemas
Script automatizado para gerar build de release:
1. Atualiza build_info.py com timestamp
2. Gera file_version_info.txt (metadados do .exe no Windows)
3. Executa PyInstaller para gerar o .exe
4. (Opcional) Executa Inno Setup para gerar instalador
5. (Opcional) Cria release no GitHub com upload do instalador
USO:
python build_release.py # Build apenas
python build_release.py --installer # Build + Instalador Inno Setup
python build_release.py --github # Build + Instalador + GitHub Release
python build_release.py --clean # Limpar dist/ e build/
"""
import os
import sys
import time
import shutil
import argparse
import subprocess
from datetime import datetime
# Diretório do projeto
PROJECT_DIR = os.path.dirname(os.path.abspath(__file__))
os.chdir(PROJECT_DIR)
# Importar versão
sys.path.insert(0, PROJECT_DIR)
from version import (VERSION, VERSION_STATUS, APP_NAME, APP_PUBLISHER,
GITHUB_OWNER, GITHUB_REPO)
def log(msg, level="INFO"):
ts = datetime.now().strftime("%H:%M:%S")
prefix = {"INFO": "ℹ", "OK": "✓", "WARN": "⚠", "ERROR": "✗", "BUILD": "🔨"}.get(level, "•")
print(f" [{ts}] {prefix} {msg}")
def _force_remove_readonly(func, path, exc_info):
"""Handler para rmtree no Windows: remove read-only e tenta de novo."""
import stat
try:
os.chmod(path, stat.S_IWRITE)
func(path)
except Exception:
pass # ignorar arquivos que não conseguir remover
def clean():
"""Remove diretórios de build anteriores."""
for d in ["build", "dist", "__pycache__"]:
path = os.path.join(PROJECT_DIR, d)
if os.path.isdir(path):
try:
shutil.rmtree(path, onexc=_force_remove_readonly)
log(f"Removido: {d}/", "OK")
except Exception:
# Fallback: comando do sistema
try:
import platform as plat
if plat.system() == "Windows":
subprocess.run(["cmd", "/c", "rd", "/s", "/q", path],
capture_output=True, timeout=30)
else:
subprocess.run(["rm", "-rf", path],
capture_output=True, timeout=30)
log(f"Removido (via shell): {d}/", "OK")
except Exception as e:
log(f"Não conseguiu remover {d}/ — {e}. Continuando...", "WARN")
def update_build_info():
"""Atualiza build_info.py com timestamp atual."""
stamp = datetime.now().strftime("%Y-%m-%d %H:%M")
path = os.path.join(PROJECT_DIR, "build_info.py")
with open(path, "w", encoding="utf-8") as f:
f.write(f'BUILD_TIMESTAMP = "{stamp}"\n')
log(f"Build stamp: {stamp}", "OK")
return stamp
def generate_version_info():
"""Gera file_version_info.txt para metadados do .exe no Windows."""
parts = VERSION.split(".")
major = int(parts[0]) if len(parts) > 0 else 0
minor = int(parts[1]) if len(parts) > 1 else 0
patch = int(parts[2]) if len(parts) > 2 else 0
content = f"""# UTF-8
VSVersionInfo(
ffi=FixedFileInfo(
filevers=({major}, {minor}, {patch}, 0),
prodvers=({major}, {minor}, {patch}, 0),
mask=0x3f,
flags=0x0,
OS=0x40004,
fileType=0x1,
subtype=0x0,
date=(0, 0)
),
kids=[
StringFileInfo(
[
StringTable(
u'041604B0',
[
StringStruct(u'CompanyName', u'{APP_PUBLISHER}'),
StringStruct(u'FileDescription', u'{APP_NAME}'),
StringStruct(u'FileVersion', u'{VERSION}'),
StringStruct(u'InternalName', u'Protector_ISAPI_Manager'),
StringStruct(u'LegalCopyright', u'© {datetime.now().year} {APP_PUBLISHER}'),
StringStruct(u'OriginalFilename', u'Protector_ISAPI_Manager.exe'),
StringStruct(u'ProductName', u'{APP_NAME}'),
StringStruct(u'ProductVersion', u'{VERSION}'),
]
)
]
),
VarFileInfo([VarStruct(u'Translation', [0x0416, 1200])])
]
)
"""
path = os.path.join(PROJECT_DIR, "file_version_info.txt")
with open(path, "w", encoding="utf-8") as f:
f.write(content)
log("file_version_info.txt gerado", "OK")
def build_exe():
"""Executa PyInstaller para gerar executável (Windows .exe ou macOS .app)."""
import platform as plat
is_mac = plat.system() == "Darwin"
log(f"Iniciando PyInstaller build ({'macOS' if is_mac else 'Windows'})...", "BUILD")
spec = os.path.join(PROJECT_DIR, "build.spec")
cmd = [sys.executable, "-m", "PyInstaller", spec, "--clean", "--noconfirm"]
log(f"Comando: {' '.join(cmd)}")
start = time.time()
result = subprocess.run(cmd, cwd=PROJECT_DIR, capture_output=False)
elapsed = time.time() - start
if result.returncode != 0:
log(f"PyInstaller FALHOU (código {result.returncode})", "ERROR")
return False
if is_mac:
app_path = os.path.join(PROJECT_DIR, "dist", "Protector ISAPI Manager.app")
exe_path = os.path.join(PROJECT_DIR, "dist", "Protector_ISAPI_Manager")
found = os.path.isdir(app_path) or os.path.isfile(exe_path)
else:
exe_path = os.path.join(PROJECT_DIR, "dist", "Protector_ISAPI_Manager.exe")
found = os.path.isfile(exe_path)
if found:
if is_mac and os.path.isdir(app_path):
log(f"Build OK: {app_path} em {elapsed:.0f}s", "OK")
else:
size_mb = os.path.getsize(exe_path) / (1024 * 1024)
log(f"Build OK: {exe_path} ({size_mb:.1f} MB) em {elapsed:.0f}s", "OK")
return True
else:
log("Executável não encontrado após build!", "ERROR")
return False
def build_dmg():
"""Cria DMG para macOS a partir do .app gerado pelo PyInstaller."""
import platform as plat
if plat.system() != "Darwin":
log("DMG só pode ser gerado no macOS", "WARN")
return False
app_path = os.path.join(PROJECT_DIR, "dist", "Protector ISAPI Manager.app")
if not os.path.isdir(app_path):
log(".app não encontrado! Execute build_exe() primeiro.", "ERROR")
return False
dmg_name = f"Protector_ISAPI_Manager_v{VERSION}_macOS.dmg"
dmg_path = os.path.join(PROJECT_DIR, "dist", "installer", dmg_name)
os.makedirs(os.path.dirname(dmg_path), exist_ok=True)
log("Criando DMG...", "BUILD")
# Tentar create-dmg (brew install create-dmg) ou hdiutil
try:
result = subprocess.run(["which", "create-dmg"], capture_output=True)
if result.returncode == 0:
cmd = [
"create-dmg",
"--volname", "Protector ISAPI Manager",
"--window-size", "600", "400",
"--icon-size", "100",
"--app-drop-link", "400", "150",
"--icon", "Protector ISAPI Manager.app", "200", "150",
dmg_path, app_path
]
result = subprocess.run(cmd, cwd=PROJECT_DIR)
else:
# Fallback: hdiutil (nativo macOS)
cmd = [
"hdiutil", "create", "-volname", "Protector ISAPI Manager",
"-srcfolder", app_path, "-ov", "-format", "UDZO", dmg_path
]
result = subprocess.run(cmd)
if os.path.isfile(dmg_path):
size_mb = os.path.getsize(dmg_path) / (1024 * 1024)
log(f"DMG OK: {dmg_name} ({size_mb:.1f} MB)", "OK")
return True
else:
log("DMG não encontrado após build!", "ERROR")
return False
except Exception as e:
log(f"Erro ao criar DMG: {e}", "ERROR")
return False
def build_installer():
"""Executa Inno Setup para gerar o instalador."""
iss_path = os.path.join(PROJECT_DIR, "installer.iss")
if not os.path.isfile(iss_path):
log("installer.iss não encontrado!", "ERROR")
return False
# Tentar localizar o ISCC.exe (Inno Setup Compiler)
iscc_paths = [
r"C:\Program Files (x86)\Inno Setup 6\ISCC.exe",
r"C:\Program Files\Inno Setup 6\ISCC.exe",
r"C:\Program Files (x86)\Inno Setup 5\ISCC.exe",
]
iscc = None
for p in iscc_paths:
if os.path.isfile(p):
iscc = p
break
# Tentar via PATH
if not iscc:
try:
result = subprocess.run(["where", "ISCC"], capture_output=True, text=True)
if result.returncode == 0:
iscc = result.stdout.strip().split("\n")[0]
except Exception:
pass
if not iscc:
log("Inno Setup (ISCC.exe) não encontrado!", "WARN")
log("Instale: https://jrsoftware.org/isdl.php", "INFO")
log("Após instalar, execute: ISCC installer.iss", "INFO")
return False
log("Gerando instalador com Inno Setup...", "BUILD")
# Criar diretório de output
os.makedirs(os.path.join(PROJECT_DIR, "dist", "installer"), exist_ok=True)
cmd = [iscc, iss_path]
result = subprocess.run(cmd, cwd=PROJECT_DIR, capture_output=False)
if result.returncode != 0:
log("Inno Setup FALHOU!", "ERROR")
return False
installer_name = f"Protector_ISAPI_Manager_v{VERSION}_Setup.exe"
installer_path = os.path.join(PROJECT_DIR, "dist", "installer", installer_name)
if os.path.isfile(installer_path):
size_mb = os.path.getsize(installer_path) / (1024 * 1024)
log(f"Instalador OK: {installer_name} ({size_mb:.1f} MB)", "OK")
return True
log("Instalador não encontrado após build!", "ERROR")
return False
def create_github_release(installer_path=None):
"""Cria release no GitHub e faz upload do instalador."""
tag = f"v{VERSION}"
# Verificar se gh CLI está disponível
try:
subprocess.run(["gh", "--version"], capture_output=True, check=True)
except (FileNotFoundError, subprocess.CalledProcessError):
log("GitHub CLI (gh) não instalado. Instale: https://cli.github.com", "WARN")
log(f"Crie a release manualmente: https://github.com/{GITHUB_OWNER}/{GITHUB_REPO}/releases/new", "INFO")
return False
log(f"Criando release {tag} no GitHub...", "BUILD")
# Release notes
notes = f"""## Protector ISAPI Manager v{VERSION}
### Novidades v4.0
- Backup completo ZIP com manifest SHA256 e restore
- Import/Export de terminais via Excel
- Sistema de sessões com rastreabilidade
- Página de eventos com busca por período
- Configuração NTP com timezone correto (POSIX)
- Auto-update via GitHub Releases
- Instalador profissional Windows (Inno Setup)
### Correções
- Timezone NTP: corrigido sinal invertido POSIX
- Eventos: timezone obrigatório no startTime ISAPI
- Foto: retry com sessão fresca quando cache expira
- Restore backup: botão funcional na tela de backup
### Requisitos
- Windows 10/11 (64-bit)
- Rede com acesso aos terminais Hikvision
"""
cmd = [
"gh", "release", "create", tag,
"--repo", f"{GITHUB_OWNER}/{GITHUB_REPO}",
"--title", f"Protector ISAPI Manager v{VERSION}",
"--notes", notes,
]
if VERSION_STATUS != "stable":
cmd.append("--prerelease")
# Adicionar arquivo do instalador
if installer_path and os.path.isfile(installer_path):
cmd.append(installer_path)
result = subprocess.run(cmd, capture_output=True, text=True)
if result.returncode == 0:
log(f"Release {tag} criada: {result.stdout.strip()}", "OK")
return True
else:
log(f"Erro: {result.stderr.strip()}", "ERROR")
return False
def main():
parser = argparse.ArgumentParser(description="Build Release - Protector ISAPI Manager")
parser.add_argument("--clean", action="store_true", help="Limpar build anterior")
parser.add_argument("--installer", action="store_true", help="Gerar instalador Inno Setup")
parser.add_argument("--github", action="store_true", help="Criar release no GitHub")
parser.add_argument("--skip-build", action="store_true", help="Pular PyInstaller build")
args = parser.parse_args()
print()
print(f" ╔══════════════════════════════════════════════╗")
print(f" ║ Protector ISAPI Manager - Build Release ║")
print(f" ║ Versão: {VERSION:<10s} Status: {VERSION_STATUS:<12s} ║")
print(f" ╚══════════════════════════════════════════════╝")
print()
start_total = time.time()
# 1. Clean
if args.clean:
log("Limpando build anterior...", "BUILD")
clean()
import platform as plat
is_mac = plat.system() == "Darwin"
# 2. Atualizar build info
update_build_info()
# 3. Gerar version info (Windows only)
if not is_mac:
generate_version_info()
else:
log("macOS: pulando file_version_info.txt (Windows-only)", "INFO")
# 4. Build executável
if not args.skip_build:
if not build_exe():
log("BUILD ABORTADO!", "ERROR")
sys.exit(1)
else:
log("PyInstaller build pulado (--skip-build)", "WARN")
# 5. Instalador (plataforma-específico)
installer_path = None
if args.installer or args.github:
if is_mac:
if build_dmg():
installer_path = os.path.join(
PROJECT_DIR, "dist", "installer",
f"Protector_ISAPI_Manager_v{VERSION}_macOS.dmg")
else:
log("DMG não gerado — continuando sem ele", "WARN")
else:
if build_installer():
installer_path = os.path.join(
PROJECT_DIR, "dist", "installer",
f"Protector_ISAPI_Manager_v{VERSION}_Setup.exe")
else:
log("Instalador não gerado — continuando sem ele", "WARN")
# 6. GitHub Release
if args.github:
create_github_release(installer_path)
elapsed = time.time() - start_total
print()
log(f"Build completo em {elapsed:.0f}s", "OK")
print()
# Resumo
if is_mac:
app_path = os.path.join(PROJECT_DIR, "dist", "Protector ISAPI Manager.app")
if os.path.isdir(app_path):
print(f" 📦 App: dist/Protector ISAPI Manager.app")
if installer_path and os.path.isfile(installer_path):
print(f" 📦 DMG: {installer_path}")
else:
exe_path = os.path.join(PROJECT_DIR, "dist", "Protector_ISAPI_Manager.exe")
if os.path.isfile(exe_path):
print(f" 📦 Executável: dist/Protector_ISAPI_Manager.exe")
if installer_path and os.path.isfile(installer_path):
print(f" 📦 Instalador: {installer_path}")
print()
if __name__ == "__main__":
main()