-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathpackage.py
More file actions
258 lines (240 loc) · 11.7 KB
/
Copy pathpackage.py
File metadata and controls
258 lines (240 loc) · 11.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
import os
import sys
import shutil
import importlib.util as _importlib_util
import PyInstaller.__main__
def _add_data_arg(src: str, dest: str) -> str:
"""Return a PyInstaller --add-data argument with the correct separator.
PyInstaller expects --add-data=SRC:DEST on POSIX and --add-data=SRC;DEST
on Windows.
"""
return f"--add-data={src}{os.pathsep}{dest}"
def build(gpu: bool = False):
app_name = "AmicoScript-GPU" if gpu else "AmicoScript"
# Detect OS
is_windows = sys.platform.startswith('win')
is_macos = sys.platform == 'darwin'
# Define paths
root = os.path.dirname(os.path.abspath(__file__))
dist = os.path.join(root, "dist")
build_dir = os.path.join(root, "build")
# Clean up previous builds
for d in [dist, build_dir]:
if os.path.exists(d):
print(f"Cleaning {d}...")
shutil.rmtree(d)
# PyInstaller arguments
args = [
'run.py', # Entry point
f'--name={app_name}', # Output name
'--onedir', # Better for large apps (faster launch/debug)
'--paths=backend', # Make backend modules importable during analysis/runtime
_add_data_arg('frontend', 'frontend'), # Include frontend files
_add_data_arg('scripts', 'scripts'), # Include scripts (e.g. meeting_watcher/setup.bat)
_add_data_arg('VERSION', '.'), # Include VERSION at bundle root
_add_data_arg('CHANGELOG.md', '.'), # Include changelog
'--hidden-import=main', # backend/main.py imported dynamically in run.py
'--hidden-import=ffmpeg_helper', # backend/ffmpeg_helper.py imported dynamically in run.py
'--hidden-import=sse_starlette.sse',
]
# Exclude known heavy/optional modules so PyInstaller doesn't accidentally
# pull them into the bundle when building from a minimal venv.
excludes = [
'torchcodec',
'tensorboard',
'torch.utils.tensorboard',
'uvicorn.streaming',
]
for ex in excludes:
args.append(f"--exclude-module={ex}")
# Only collect package data for optional heavy packages if they are
# actually installed in the build environment (keeps minimal venv builds
# quiet and small). Mirror the logic used in package_interactive.py so
# minimal venv builds remain minimal.
try:
if _importlib_util.find_spec('faster_whisper') is not None:
args.append('--hidden-import=faster_whisper')
args.append('--collect-data=faster_whisper')
if _importlib_util.find_spec('pyannote.audio') is not None:
args.append('--hidden-import=pyannote.audio')
args.append('--collect-data=pyannote.audio')
if _importlib_util.find_spec('huggingface_hub') is not None:
# Imported dynamically via importlib in backend/resource_downloader.py
args.append('--hidden-import=huggingface_hub')
# Native desktop shell. Without pywebview the bundle still builds and
# runs, it just falls back to opening a system browser tab (run.py).
if _importlib_util.find_spec('webview') is not None:
# collect-all: pywebview ships JS shims under webview/js/ and,
# on Windows, the WebView2 interop DLLs under webview/lib/.
args.append('--collect-all=webview')
if is_macos:
# The Cocoa backend reaches pyobjc frameworks lazily; static
# analysis doesn't see them.
for mod in ('objc', 'Foundation', 'AppKit', 'WebKit', 'Quartz'):
args.append(f'--hidden-import={mod}')
elif is_windows:
# EdgeChromium backend goes through pythonnet -> clr_loader.
for pkg in ('clr_loader', 'pythonnet'):
if _importlib_util.find_spec(pkg) is not None:
args.append(f'--collect-all={pkg}')
args.append('--hidden-import=clr')
else:
print('WARNING: pywebview not installed — the build will open the UI '
'in a system browser instead of a native window. Run '
'"pip install -r requirements-pyinstaller.txt" first.')
# Embedded meeting watcher (Windows native build only): bundle the
# watcher module + its Windows-only audio deps if they're installed in
# the build env. Minimal/Docker builds skip this and stay lean.
if is_windows and _importlib_util.find_spec('pyaudiowpatch') is not None:
args.append('--paths=scripts/meeting_watcher')
args.append('--hidden-import=watcher')
args.append('--hidden-import=pyaudiowpatch')
if _importlib_util.find_spec('pycaw') is not None:
args.append('--collect-submodules=pycaw')
if _importlib_util.find_spec('comtypes') is not None:
# Submodules, not just the package: pycaw reaches into
# comtypes.client / comtypes.automation lazily at runtime, which
# PyInstaller's static analysis doesn't see.
args.append('--collect-submodules=comtypes')
if _importlib_util.find_spec('winotify') is not None:
args.append('--collect-all=winotify')
# Tray icon: the embedded watcher's only always-visible recording
# indicator once the browser tab is closed.
if _importlib_util.find_spec('pystray') is not None:
args.append('--collect-submodules=pystray')
if _importlib_util.find_spec('PIL') is not None:
args.append('--hidden-import=PIL.Image')
args.append('--hidden-import=PIL.ImageDraw')
elif is_windows:
# Loud, because the failure is silent otherwise: the app builds and
# runs fine, meeting auto-capture just never works. Install
# scripts/meeting_watcher/requirements.txt before building.
print('WARNING: pyaudiowpatch not installed — the Windows build will '
'NOT include the embedded meeting watcher. Run '
'"pip install -r scripts/meeting_watcher/requirements.txt" first.')
except Exception:
# Fall back to not collecting heavy package data in minimal environments
pass
# Platform-specific UI flags
if is_macos:
# Create a macOS .app bundle. Provide a bundle identifier and optional icon.
args.append('--windowed')
# Set a bundle identifier (change to your reverse-domain identifier if desired)
args.append('--osx-bundle-identifier=org.amico.AmicoScript')
# Choose an .icns icon. Prefer images/AmicoScript.icns, otherwise pick any .icns in images/.
icon_default = os.path.join(root, 'images', 'AmicoScript.icns')
if os.path.exists(icon_default):
icon_path = icon_default
else:
images_dir = os.path.join(root, 'images')
icon_candidates = []
if os.path.isdir(images_dir):
for fn in os.listdir(images_dir):
if fn.lower().endswith('.icns'):
icon_candidates.append(os.path.join(images_dir, fn))
icon_path = icon_candidates[0] if icon_candidates else None
if icon_path:
args.append(f'--icon={icon_path}')
elif is_windows:
# On Windows, avoid a console window and embed an .ico icon
args.append('--noconsole')
# Prefer images/AmicoScript.ico or the first .ico found in images/
icon_default = os.path.join(root, 'images', 'AmicoScript.ico')
if os.path.exists(icon_default):
icon_path = icon_default
else:
images_dir = os.path.join(root, 'images')
icon_candidates = []
if os.path.isdir(images_dir):
for fn in os.listdir(images_dir):
if fn.lower().endswith('.ico'):
icon_candidates.append(os.path.join(images_dir, fn))
icon_path = icon_candidates[0] if icon_candidates else None
if icon_path:
args.append(f'--icon={icon_path}')
if is_windows:
version_file_path = None
try:
root_version = os.path.join(root, 'VERSION')
if os.path.exists(root_version):
ver_text = open(root_version, 'r', encoding='utf-8').read().strip()
else:
ver_text = '0.0.0'
ver_nums = ver_text.split('.')
while len(ver_nums) < 3:
ver_nums.append('0')
filevers = tuple(int(x) if x.isdigit() else 0 for x in (ver_nums + ['0'])[:4])
build_meta_dir = os.path.join(root, 'buildmeta')
os.makedirs(build_meta_dir, exist_ok=True)
version_file_path = os.path.join(build_meta_dir, 'version_info.txt')
with open(version_file_path, 'w', encoding='utf-8') as vf:
vf.write('''# UTF-8
VSVersionInfo(
ffi=FixedFileInfo(
filevers=%s,
prodvers=%s,
mask=0x3f,
flags=0x0,
OS=0x40004,
fileType=0x1,
subtype=0x0,
date=(0, 0)
),
kids=[
StringFileInfo(
[
StringTable(
'040904B0',
[
StringStruct('CompanyName', ''),
StringStruct('FileDescription', '%s'),
StringStruct('FileVersion', '%s'),
StringStruct('InternalName', '%s'),
StringStruct('LegalCopyright', ''),
StringStruct('OriginalFilename', '%s'),
StringStruct('ProductName', '%s'),
StringStruct('ProductVersion', '%s')
]
)
]
),
VarFileInfo([VarStruct('Translation', [1033, 1200])])
]
)
''' % (str(filevers), str(filevers), app_name, ver_text, app_name, f'{app_name}.exe', app_name, ver_text))
args.append('--version-file=%s' % version_file_path)
except Exception:
pass
print("Starting build with PyInstaller...")
PyInstaller.__main__.run(args)
print("\nDraft build complete!")
if is_macos:
app_path = os.path.join(dist, 'AmicoScript.app')
print(f"Output available in: {app_path}")
# Ensure executables inside the .app are executable (fixes Finder 'prohibitory' icon)
contents_mac_os = os.path.join(app_path, 'Contents', 'MacOS')
if os.path.isdir(contents_mac_os):
for fname in os.listdir(contents_mac_os):
fpath = os.path.join(contents_mac_os, fname)
try:
# make file executable
os.chmod(fpath, os.stat(fpath).st_mode | 0o111)
except Exception:
pass
# also mark ffmpeg or other bundled binaries if placed in Contents/MacOS
# (PyInstaller may put binaries in Resources or MacOS depending on spec)
resources_dir = os.path.join(app_path, 'Contents', 'Resources')
if os.path.isdir(resources_dir):
for root_dir, dirs, files in os.walk(resources_dir):
for fn in files:
if fn.lower().startswith('ffmpeg') or fn.endswith('.so') or fn.endswith('.dylib'):
fpath = os.path.join(root_dir, fn)
try:
os.chmod(fpath, os.stat(fpath).st_mode | 0o111)
except Exception:
pass
else:
print(f"Output available in: {dist}/AmicoScript")
print("\nNote: You may need to manually bundle ffmpeg binaries in the dist folder if not in system path.")
if __name__ == "__main__":
build(gpu='--gpu' in sys.argv)