-
Notifications
You must be signed in to change notification settings - Fork 4
Expand file tree
/
Copy pathbuild_exe.py
More file actions
196 lines (164 loc) · 6.58 KB
/
Copy pathbuild_exe.py
File metadata and controls
196 lines (164 loc) · 6.58 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
#!/usr/bin/env python3
"""
build_exe.py — Build TSG Builder as a standalone executable using PyInstaller.
Usage:
python build_exe.py # Build for current platform
python build_exe.py --clean # Clean build artifacts first
The executable will be created in the dist/ folder.
"""
import argparse
import os
import platform
import shutil
import subprocess
import sys
from pathlib import Path
def get_platform_name() -> str:
"""Get a friendly platform name for the output."""
system = platform.system().lower()
if system == "darwin":
return "macos"
return system # "linux" or "windows"
def clean_build_artifacts():
"""Remove previous build artifacts."""
dirs_to_remove = ["build", "dist", "__pycache__"]
# Remove auto-generated .spec files (PyInstaller creates these)
files_to_remove = list(Path(".").glob("*.spec"))
for dir_name in dirs_to_remove:
dir_path = Path(dir_name)
if dir_path.exists():
print(f"Removing {dir_path}/")
shutil.rmtree(dir_path)
for file_path in files_to_remove:
print(f"Removing {file_path}")
file_path.unlink()
def check_pyinstaller():
"""Ensure PyInstaller is installed."""
try:
import PyInstaller
print(f"[OK] PyInstaller {PyInstaller.__version__} found")
except ImportError:
print("PyInstaller not found. Installing...")
subprocess.run([sys.executable, "-m", "pip", "install", "pyinstaller"], check=True)
print("[OK] PyInstaller installed")
def generate_build_config():
"""Generate _build_config.py with the App Insights connection string.
Reads APPINSIGHTS_CONNECTION_STRING from the environment (set in CI
via GitHub Actions secrets) and writes it into a Python module that
telemetry.py imports at runtime. If the env var is not set, a stub
config is written so the import still succeeds (telemetry disabled).
"""
conn_str = os.environ.get("APPINSIGHTS_CONNECTION_STRING", "")
config_path = Path("_build_config.py")
config_path.write_text(
f'# Auto-generated by build_exe.py — do not edit or commit\n'
f'APPINSIGHTS_CONNECTION_STRING = {repr(conn_str)}\n'
)
if conn_str:
print(f"[OK] _build_config.py generated (connection string set)")
else:
print(f"[WARN] _build_config.py generated (no connection string — telemetry disabled in binary)")
def build_executable():
"""Build the executable using PyInstaller."""
platform_name = get_platform_name()
exe_name = f"tsg-builder-{platform_name}"
# Generate telemetry build config before PyInstaller runs
generate_build_config()
# PyInstaller arguments
args = [
sys.executable, "-m", "PyInstaller",
"--name", exe_name,
"--onedir", # Folder-based (no per-launch extraction)
"--contents-directory", "_internal", # Hide bundled deps in _internal/
"--console", # Console app (needed for Flask server output)
# Add data files (templates and static assets for Flask)
"--add-data", f"templates{os.pathsep}templates",
"--add-data", f"static{os.pathsep}static",
# Hidden imports that PyInstaller might miss
"--hidden-import", "azure.identity",
"--hidden-import", "azure.ai.projects",
"--hidden-import", "azure.ai.projects.models",
"--hidden-import", "azure.ai.textanalytics",
"--hidden-import", "azure.core",
"--hidden-import", "flask",
"--hidden-import", "dotenv",
"--hidden-import", "openai",
"--hidden-import", "httpx",
"--hidden-import", "msal",
"--hidden-import", "msal_extensions",
"--hidden-import", "_build_config",
# Collect all Azure packages (they have many submodules)
"--collect-all", "azure.identity",
"--collect-all", "azure.ai.projects",
"--collect-all", "azure.ai.textanalytics",
"--collect-all", "azure.core",
"--collect-all", "msal",
# Entry point
"web_app.py",
]
print(f"\n[BUILD] Building {exe_name}...")
print(f"Command: {' '.join(args[2:])}\n")
result = subprocess.run(args, check=False)
if result.returncode != 0:
print("\n[FAILED] Build failed!")
sys.exit(1)
# Determine output path (--onedir: exe is inside a folder)
if platform_name == "windows":
exe_path = Path("dist") / exe_name / f"{exe_name}.exe"
else:
exe_path = Path("dist") / exe_name / exe_name
if exe_path.exists():
size_mb = exe_path.stat().st_size / (1024 * 1024)
folder_path = exe_path.parent
# Copy GETTING_STARTED.md into the distribution folder
getting_started = Path("GETTING_STARTED.md")
if getting_started.exists():
shutil.copy2(getting_started, folder_path / "GETTING_STARTED.md")
print(f"[OK] Included GETTING_STARTED.md in distribution")
else:
print(f"[WARN] GETTING_STARTED.md not found — skipping")
print(f"\n[OK] Build successful!")
print(f" Executable: {exe_path}")
print(f" Folder: {folder_path}")
print(f" Size: {size_mb:.1f} MB")
print(f"\n[INFO] To run:")
if platform_name == "windows":
print(f" .\\dist\\{exe_name}\\{exe_name}.exe")
else:
print(f" ./dist/{exe_name}/{exe_name}")
print(f"\n[INFO] On first run, a .env file will be created automatically.")
print(f" The setup wizard will open in your browser to configure Azure settings.")
else:
print(f"\n[FAILED] Expected output not found: {exe_path}")
sys.exit(1)
def main():
parser = argparse.ArgumentParser(
description="Build TSG Builder as a standalone executable"
)
parser.add_argument(
"--clean",
action="store_true",
help="Clean build artifacts before building",
)
parser.add_argument(
"--clean-only",
action="store_true",
help="Only clean build artifacts, don't build",
)
args = parser.parse_args()
print("=" * 60)
print("TSG Builder — Executable Build Script")
print("=" * 60)
print(f"Platform: {platform.system()} ({platform.machine()})")
print(f"Python: {sys.version}")
print("=" * 60)
if args.clean or args.clean_only:
print("\n[CLEAN] Cleaning build artifacts...")
clean_build_artifacts()
if args.clean_only:
print("[OK] Clean complete")
return
check_pyinstaller()
build_executable()
if __name__ == "__main__":
main()