Repository navigation
Expand file tree
/
Copy pathbuild_pyinstaller.py
More file actions
191 lines (153 loc) · 5.95 KB
/
Copy pathbuild_pyinstaller.py
File metadata and controls
191 lines (153 loc) · 5.95 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
"""
PyInstaller build script for SessionPrep.
Builds standalone executables using the shared build configuration.
Usage:
uv run python build_pyinstaller.py [cli|gui|all] [--onefile] [--clean]
"""
import argparse
import os
import shutil
import subprocess
import sys
import platform
from build_conf import TARGETS, BASE_DIR, DIST_PYINSTALLER, MACOS_APP_NAME
DIST_DIR = os.path.join(BASE_DIR, DIST_PYINSTALLER)
def clean():
"""Remove previous build artifacts."""
if os.path.isdir(DIST_DIR):
print(f"Removing {DIST_DIR}")
shutil.rmtree(DIST_DIR)
def _check_imports(target_key: str) -> list[str]:
"""Check that hidden-import packages are installed."""
from importlib.util import find_spec
seen = set()
missing = []
# Check hidden imports defined in build_conf
imports = TARGETS[target_key].get("pyinstaller_hidden_imports", [])
for mod_name in imports:
top = mod_name.split(".")[0]
if top in seen:
continue
seen.add(top)
if find_spec(top) is None:
missing.append(top)
return missing
def build(target_key: str, onefile: bool = False):
"""Run PyInstaller to create one executable."""
target = TARGETS[target_key]
# Name from build_conf includes .exe extension on Windows, strip it for PyInstaller --name
app_name_ext = target["name"]
app_name = os.path.splitext(app_name_ext)[0]
entry_point = os.path.join(BASE_DIR, target["script"])
print(f"\n{'=' * 60}")
print(f"Building: {app_name}")
print(f"{'=' * 60}")
missing = _check_imports(target_key)
if missing:
print(f"\nERROR: Required packages not installed: {', '.join(missing)}")
if target_key == "gui":
print("Install GUI dependencies first: uv sync --extra gui")
print()
return False
# Define consistent paths matching Nuitka structure
# Work path: dist_pyinstaller/sessionprep-linux-x64.build
work_path = os.path.join(DIST_DIR, f"{app_name}.build")
cmd = [
sys.executable, "-m", "PyInstaller",
"--name", app_name,
"--noconfirm",
"--distpath", DIST_DIR,
"--workpath", work_path,
"--specpath", DIST_DIR,
"--collect-all", "sessionpreplib",
"--collect-all", "sessionprepgui",
"--collect-all", "soundfile",
"--hidden-import", "numpy",
]
for imp in target.get("pyinstaller_hidden_imports", []):
cmd.extend(["--hidden-import", imp])
icon_path = target.get("icon")
if icon_path and os.path.isfile(icon_path):
cmd.extend(["--icon", icon_path])
is_macos = platform.system() == "Darwin"
windowed = target.get("pyinstaller_windowed", False)
macos_app = is_macos and windowed
# On macOS GUI, override --name so the .app bundle and CFBundleName
# use the display name (e.g. "SessionPrep") instead of the platform-
# suffixed executable name.
if macos_app:
cmd[cmd.index(app_name)] = MACOS_APP_NAME
if windowed:
cmd.append("--windowed")
else:
cmd.append("--console")
# macOS: --onefile + --windowed is deprecated.
if onefile and not macos_app:
cmd.append("--onefile")
else:
if onefile and macos_app:
print("Note: macOS GUI always builds as onedir (.app bundle — DMG created by workflow)")
cmd.append("--onedir")
cmd.append(entry_point)
print(f"Running: {' '.join(cmd)}")
print()
result = subprocess.run(cmd, cwd=BASE_DIR)
if result.returncode != 0:
print(f"\nBuild failed for {app_name} with exit code {result.returncode}", file=sys.stderr)
return False
# Check for output
if macos_app:
# PyInstaller creates MACOS_APP_NAME.app inside DIST_DIR
bundle_path = os.path.join(DIST_DIR, f"{MACOS_APP_NAME}.app")
if os.path.isdir(bundle_path):
print(f"\nBuild successful: {bundle_path}")
else:
print(f"\nBuild completed but .app bundle not found at: {bundle_path}")
elif onefile:
exe_path = os.path.join(DIST_DIR, app_name_ext)
if os.path.isfile(exe_path):
size_mb = os.path.getsize(exe_path) / (1024 * 1024)
print(f"\nBuild successful: {exe_path}")
print(f"Size: {size_mb:.1f} MB")
else:
print(f"\nBuild completed but executable not found at expected path: {exe_path}")
else:
# Onedir puts it in dist/APP_NAME/APP_NAME_EXT
exe_path = os.path.join(DIST_DIR, app_name, app_name_ext)
if os.path.isfile(exe_path):
size_mb = os.path.getsize(exe_path) / (1024 * 1024)
print(f"\nBuild successful: {exe_path}")
print(f"Size: {size_mb:.1f} MB")
else:
print(f"\nBuild completed but executable not found at expected path: {exe_path}")
return True
def main():
parser = argparse.ArgumentParser(description="Build SessionPrep with PyInstaller")
# Positional argument to match build_nuitka.py style (optional)
parser.add_argument("target", choices=["cli", "gui", "all"], default="all", nargs="?",
help="Which target to build (default: all)")
parser.add_argument("--onefile", action="store_true",
help="Build single executables")
parser.add_argument("--clean", action="store_true",
help="Clean build artifacts before building")
args = parser.parse_args()
if args.clean:
clean()
targets_to_build = []
if args.target == "all":
targets_to_build = ["cli", "gui"]
else:
targets_to_build = [args.target]
failed = []
for t in targets_to_build:
if not build(t, onefile=args.onefile):
failed.append(t)
print(f"\n{'=' * 60}")
if failed:
print(f"Done. Failed: {', '.join(failed)}")
sys.exit(1)
else:
print(f"Done. Built: {', '.join(targets_to_build)}")
print(f"Output: {DIST_DIR}")
if __name__ == "__main__":
main()