-
Notifications
You must be signed in to change notification settings - Fork 1
Expand file tree
/
Copy pathpackage.py
More file actions
206 lines (188 loc) · 8.41 KB
/
package.py
File metadata and controls
206 lines (188 loc) · 8.41 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
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('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')
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)