-
Notifications
You must be signed in to change notification settings - Fork 3
Expand file tree
/
Copy pathbuild.py
More file actions
659 lines (520 loc) · 21.5 KB
/
build.py
File metadata and controls
659 lines (520 loc) · 21.5 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
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
"""
OpenAver Windows 打包腳本
在 WSL/Linux 環境下打包出 Windows 可用的 ZIP
使用方式:
python build.py
原理:
1. 下載 Windows 嵌入式 Python
2. 用 pip download 下載 Windows wheel 檔案
3. 解壓 wheel 到 site-packages
4. 打包成 ZIP
"""
import os
import sys
import shutil
import zipfile
import urllib.request
import subprocess
import tempfile
from pathlib import Path
# ============ 配置 ============
PYTHON_VERSION = "3.12.4"
PYTHON_EMBED_URL = f"https://www.python.org/ftp/python/{PYTHON_VERSION}/python-{PYTHON_VERSION}-embed-amd64.zip"
# 專案結構
PROJECT_ROOT = Path(__file__).parent
BUILD_DIR = PROJECT_ROOT / "build"
DIST_DIR = PROJECT_ROOT / "dist"
CACHE_DIR = PROJECT_ROOT / ".build_cache" # 緩存目錄(不會被清理)
# 需要複製的專案目錄/檔案
COPY_ITEMS = [
"web",
"core",
"windows",
"locales",
"maker_mapping.json",
]
# 主要套件(會自動解析依賴)
PACKAGES = [
"fastapi",
"uvicorn[standard]",
"jinja2",
"python-multipart",
"requests",
"beautifulsoup4",
"lxml",
"curl_cffi",
"websockets",
"pillow",
"pywebview",
"httpx", # Required for FastAPI TestClient and async HTTP
]
# 打包時排除的套件(測試/開發工具,不影響運行)
EXCLUDE_PACKAGES = {
# 測試工具
'pytest', 'pytest-asyncio', 'pytest-mock', 'pytest-cov',
'pytest-playwright',
'coverage', 'pluggy', 'iniconfig',
'playwright',
# 類型檢查工具
'mypy', 'mypy-extensions', 'typed-ast', 'types-requests',
'types-beautifulsoup4', 'types-html5lib',
# 開發工具
'pip', 'setuptools', 'wheel', 'twine', 'build',
# 文檔工具
'docutils', 'pygments', 'readme-renderer',
# 未使用
'langdetect',
}
# ============ 工具函數 ============
def download_file(url: str, dest: Path) -> None:
"""下載檔案"""
print(f" 下載: {url}")
urllib.request.urlretrieve(url, dest)
print(f" 完成: {dest.name}")
def extract_zip(zip_path: Path, dest_dir: Path) -> None:
"""解壓 ZIP"""
print(f" 解壓: {zip_path.name}")
with zipfile.ZipFile(zip_path, 'r') as zf:
zf.extractall(dest_dir)
def extract_wheel(wheel_path: Path, dest_dir: Path) -> None:
"""解壓 wheel 檔案到目標目錄"""
with zipfile.ZipFile(wheel_path, 'r') as zf:
for member in zf.namelist():
# 跳過 .dist-info 以外的 metadata
if member.endswith('/'):
continue
# 解壓到目標目錄
zf.extract(member, dest_dir)
def extract_tar_gz(tar_path: Path, dest_dir: Path) -> None:
"""解壓 tar.gz 原始碼套件到目標目錄(只複製 Python 模組)"""
import tarfile
with tarfile.open(tar_path, 'r:gz') as tf:
# 找出套件目錄(通常是 package_name-version/package_name/)
members = tf.getnames()
# 找出實際的 Python 套件目錄
for member in members:
parts = member.split('/')
if len(parts) >= 2 and not parts[1].endswith('.egg-info') and not parts[1].startswith('.'):
# 可能是套件目錄
pkg_name = parts[1]
if any(m.startswith(f"{parts[0]}/{pkg_name}/") and m.endswith('.py') for m in members):
# 確認是 Python 套件
pkg_prefix = f"{parts[0]}/{pkg_name}/"
for m in tf.getmembers():
if m.name.startswith(pkg_prefix):
# 調整路徑:移除頂層目錄
m.name = m.name[len(parts[0]) + 1:]
tf.extract(m, dest_dir)
return
# ============ 打包步驟 ============
def clean_build():
"""清理舊的建置目錄"""
print("\n[1/6] 清理舊建置...")
if BUILD_DIR.exists():
shutil.rmtree(BUILD_DIR)
BUILD_DIR.mkdir(parents=True)
def download_embedded_python():
"""下載嵌入式 Python(使用緩存)"""
print("\n[2/6] 準備嵌入式 Python...")
python_dir = BUILD_DIR / "OpenAver" / "python"
python_dir.mkdir(parents=True)
# 檢查緩存
CACHE_DIR.mkdir(parents=True, exist_ok=True)
cached_zip = CACHE_DIR / f"python-{PYTHON_VERSION}-embed-amd64.zip"
if cached_zip.exists():
print(f" 使用緩存: {cached_zip.name}")
else:
print(" 下載中(首次會較慢)...")
download_file(PYTHON_EMBED_URL, cached_zip)
# 從緩存解壓
extract_zip(cached_zip, python_dir)
# 修改 _pth 檔案以啟用 site-packages
pth_files = list(python_dir.glob("python*._pth"))
if not pth_files:
raise RuntimeError("找不到 ._pth 檔案")
pth_file = pth_files[0]
pth_name = pth_file.stem # e.g., "python312"
pth_content = f"""{pth_name}.zip
.
Lib/site-packages
../app
import site
"""
pth_file.write_text(pth_content)
print(f" 已修改: {pth_file.name}")
return python_dir
def get_all_dependencies():
"""從現有 venv 獲取完整依賴列表(排除測試/開發工具)"""
result = subprocess.run(
[sys.executable, "-m", "pip", "freeze"],
capture_output=True, text=True
)
deps = []
excluded = []
for line in result.stdout.strip().split('\n'):
if '==' in line:
pkg_name = line.split('==')[0].strip()
# 標準化套件名稱(pip 用 - 和 _ 可互換)
pkg_normalized = pkg_name.lower().replace('_', '-')
if pkg_normalized in EXCLUDE_PACKAGES:
excluded.append(pkg_name)
else:
deps.append(line.strip())
if excluded:
print(f" 排除 {len(excluded)} 個測試/開發套件: {', '.join(excluded[:5])}{'...' if len(excluded) > 5 else ''}")
return deps
def download_and_install_packages(python_dir: Path):
"""下載 Windows wheel 並解壓到 site-packages(使用緩存)"""
print("\n[3/6] 準備依賴套件...")
site_packages = python_dir / "Lib" / "site-packages"
site_packages.mkdir(parents=True, exist_ok=True)
# 使用緩存目錄存放 wheel
wheels_dir = CACHE_DIR / "wheels"
wheels_dir.mkdir(parents=True, exist_ok=True)
# 從現有 venv 獲取所有已安裝的套件(帶版本號,如 "pydantic==2.11.0")
all_deps = get_all_dependencies()
# 加入 pywebview(可能不在 venv 中)
dep_names = [d.split('==')[0].lower() for d in all_deps]
if "pywebview" not in dep_names:
all_deps.append("pywebview")
# 額外依賴(手動補充 Windows 專用套件)
extra_deps = [
"bottle", "proxy-tools", "clr_loader", "pythonnet",
"win32-setctime", "colorama",
]
all_deps.extend(extra_deps)
# 建立需要的套件版本映射(套件名 → 完整 spec)
needed = {}
for dep in all_deps:
name = dep.split('==')[0].lower().replace('_', '-')
needed[name] = dep
# ⚠️ 清掉 cache 中版本不匹配的舊 wheel(避免混版)
# 這段不是多餘邏輯!PyPI 上游版本更新時,cache 裡的舊 pydantic-core
# 會跟新 pydantic 混版,導致 Windows ZIP 執行時 SystemError。
# 詳見 CLAUDE.md「Windows 打包 Wheel Cache Gotcha」
stale_count = 0
for f in list(wheels_dir.glob("*.*")):
cached_name = f.stem.split('-')[0].lower().replace('_', '-')
if cached_name in needed and '==' in needed[cached_name]:
expected_ver = needed[cached_name].split('==')[1]
# wheel 檔名格式: name-version-...
parts = f.stem.split('-')
if len(parts) >= 2 and parts[1] != expected_ver:
f.unlink()
stale_count += 1
if stale_count:
print(f" 清除 {stale_count} 個版本不匹配的舊 wheel")
# 檢查已緩存的套件(名稱級,舊版已清除所以不會混版)
cached_files = set(f.stem.split('-')[0].lower().replace('_', '-') for f in wheels_dir.glob("*.*"))
to_download = [dep for dep in all_deps if dep.split('==')[0].lower().replace('_', '-') not in cached_files]
if to_download:
print(f" 需下載 {len(to_download)} 個新套件(已緩存 {len(all_deps) - len(to_download)} 個)")
for pkg in to_download:
# 嘗試下載 Windows wheel
pip_cmd = [
sys.executable, "-m", "pip", "download",
"--dest", str(wheels_dir),
"--platform", "win_amd64",
"--python-version", "3.12",
"--only-binary", ":all:",
"--no-deps",
pkg,
]
result = subprocess.run(pip_cmd, capture_output=True, text=True)
if result.returncode != 0:
# 嘗試不限平台(純 Python 套件)
pip_cmd = [
sys.executable, "-m", "pip", "download",
"--dest", str(wheels_dir),
"--no-deps",
pkg,
]
subprocess.run(pip_cmd, capture_output=True, text=True)
else:
print(f" 全部 {len(all_deps)} 個套件已緩存")
# 解壓所有套件到 site-packages
print("\n[4/6] 安裝套件到 site-packages...")
wheel_files = list(wheels_dir.glob("*.whl"))
tar_files = list(wheels_dir.glob("*.tar.gz"))
print(f" 找到 {len(wheel_files)} 個 wheel, {len(tar_files)} 個 tar.gz")
for wheel_file in wheel_files:
pkg_name = wheel_file.stem.split('-')[0].lower().replace('_', '-')
if pkg_name in EXCLUDE_PACKAGES:
print(f" 跳過(排除): {wheel_file.name}")
continue
print(f" 安裝: {wheel_file.name}")
extract_wheel(wheel_file, site_packages)
for tar_file in tar_files:
pkg_name = tar_file.name.split('-')[0].lower().replace('_', '-')
if pkg_name in EXCLUDE_PACKAGES:
print(f" 跳過(排除): {tar_file.name}")
continue
print(f" 安裝: {tar_file.name}")
extract_tar_gz(tar_file, site_packages)
# 保留緩存(不清理 wheels_dir)
def copy_project_files():
"""複製專案檔案"""
print("\n[5/6] 複製專案檔案...")
app_dir = BUILD_DIR / "OpenAver" / "app"
app_dir.mkdir(parents=True)
for item in COPY_ITEMS:
src = PROJECT_ROOT / item
dst = app_dir / item
if src.is_dir():
print(f" 複製目錄: {item}")
shutil.copytree(src, dst, ignore=shutil.ignore_patterns(
"__pycache__", "*.pyc", ".git", ".gitignore", "config.json"
))
elif src.is_file():
print(f" 複製檔案: {item}")
shutil.copy2(src, dst)
# 複製 config.default.json(預設設定範本)
# 注意:不複製 config.json,讓目標環境保留自己的設定
config_default_src = PROJECT_ROOT / "web" / "config.default.json"
config_default_dst = app_dir / "web" / "config.default.json"
if config_default_src.exists():
shutil.copy2(config_default_src, config_default_dst)
print(" 複製檔案: config.default.json")
# 複製範例檔案到根目錄(讓用戶容易找到)
samples_src = PROJECT_ROOT / "tests" / "samples" / "basic"
samples_dst = BUILD_DIR / "OpenAver" / "教學檔案"
if samples_src.exists():
shutil.copytree(samples_src, samples_dst)
print(" 複製目錄: 教學檔案 (11 個範例)")
def create_launcher_scripts():
"""建立啟動腳本和說明檔(純英文版本,避免 Big5 編碼地雷字問題)"""
print(" 建立啟動腳本...")
root_dir = BUILD_DIR / "OpenAver"
# OpenAver.bat - 正常啟動(顯示啟動提示)
bat_content = '''@echo off
cd /d "%~dp0"
set PYTHONUTF8=1
echo ==============================
echo OpenAver Starting...
echo ==============================
echo.
start "" "python\\pythonw.exe" "app\\windows\\standalone.py"
ping -n 2 127.0.0.1 >nul
'''
# OpenAver_Debug.bat - 偵錯模式(顯示控制台)
# 使用純英文避免 Big5 地雷字問題(誌、誤、訊、回、將、以、下、置、上 等字會導致亂碼)
debug_bat_content = '''@echo off
echo ======================================
echo OpenAver Debug Mode
echo ======================================
echo.
REM Force UTF-8 encoding and detailed error output
set PYTHONUTF8=1
set PYTHONUNBUFFERED=1
set PYWEBVIEW_LOG=debug
echo [INFO] Starting OpenAver (Debug Mode)...
echo [INFO] Log location: %USERPROFILE%\\OpenAver\\logs\\debug.log
echo.
cd /d "%~dp0"
"python\\python.exe" "app\\windows\\standalone.py"
if errorlevel 1 (
echo.
echo ======================================
echo [ERROR] Startup failed!
echo ======================================
echo.
echo Please report to GitHub Issues:
echo 1. Error messages above
echo 2. Log file: %USERPROFILE%\\OpenAver\\logs\\debug.log
echo.
)
pause
'''
readme_en = '''===============================================
OpenAver Windows Setup Guide
===============================================
Option 1: One-Line Install (Recommended)
Open PowerShell (search "PowerShell" -> Enter), paste:
irm https://raw.githubusercontent.com/slive777/OpenAver/main/install.ps1 | iex
After install, double-click the OpenAver shortcut on your desktop.
===============================================
Option 2: Manual Install
!! IMPORTANT: Use 7-Zip or WinRAR to extract !!
Windows built-in extraction keeps "Mark of the Web",
which blocks the app from running.
[Step 1] Extract ZIP
- Right-click ZIP -> "Extract All..." (use 7-Zip or WinRAR for best results)
[Step 2] Open Command Prompt in the folder
- Shift + right-click inside the OpenAver folder
- Select "Open PowerShell window here" or "Open command window here"
[Step 3] Launch
OpenAver.bat
If you already extracted with Windows built-in:
Right-click the ZIP -> Properties -> check "Unblock" -> OK,
then re-extract.
[Requirements]
- Windows 10/11 64-bit
- Microsoft Edge WebView2 Runtime
https://go.microsoft.com/fwlink/p/?LinkId=2124703
- Internet connection (required on first run to fetch metadata)
[Upgrading]
- Delete %USERPROFILE%\\OpenAver\\python\\ before extracting a new version
- Your settings and logs are preserved automatically
[Startup Scripts]
OpenAver.bat — Normal launch (runs in background)
OpenAver_Debug.bat — Debug mode, logs to Command Prompt and %USERPROFILE%\\OpenAver\\logs\\debug.log
[Troubleshooting]
If the app won't start, use OpenAver_Debug.bat:
1. Double-click OpenAver_Debug.bat
2. A Command Prompt window shows live logs; also saved to %USERPROFILE%\\OpenAver\\logs\\debug.log
3. Attach the log content to your GitHub Issue
[Notes]
- First launch may take a moment
- Config: app\\web\\config.json
(Full path example: C:\\Users\\YourName\\OpenAver\\app\\web\\config.json)
- Logs: %USERPROFILE%\\OpenAver\\logs\\debug.log
[Report Issues]
GitHub: https://github.com/slive777/OpenAver/issues
Telegram: https://t.me/+J-U2l96gv0FjZTBl
'''
readme_zh = '''===============================================
OpenAver Windows 安裝指南
===============================================
方法一:一行指令安裝(推薦)
打開 PowerShell(搜尋 PowerShell → Enter),貼上:
irm https://raw.githubusercontent.com/slive777/OpenAver/main/install.ps1 | iex
安裝完成後雙擊桌面上的 OpenAver 捷徑啟動。
===============================================
方法二:手動安裝
!! 重要:請使用 7-Zip 或 WinRAR 解壓 !!
Windows 內建解壓縮會保留「網路標記」(Mark of the Web),
導致程式無法正常執行。
[步驟 1] 解壓 ZIP
- 對 ZIP 按右鍵 → 「解壓縮全部...」(建議使用 7-Zip 或 WinRAR)
[步驟 2] 在資料夾中開啟命令提示字元
- Shift + 右鍵點擊 OpenAver 資料夾內部空白處
- 選擇「在此開啟 PowerShell 視窗」或「在此開啟命令視窗」
[步驟 3] 啟動程式
OpenAver.bat
如果已經用內建解壓縮:
對 ZIP 按右鍵 → 內容 → 勾選「解除封鎖」→ 確定,
再重新解壓。
[系統需求]
- Windows 10/11 64-bit
- Microsoft Edge WebView2 Runtime
https://go.microsoft.com/fwlink/p/?LinkId=2124703
- 網路連線(首次執行需連線外部服務取得資料)
[升級注意]
- 升級前請先刪除 %USERPROFILE%\\OpenAver\\python\\ 資料夾
- 設定檔和記錄檔會自動保留
[啟動腳本說明]
OpenAver.bat — 正常啟動(程式在背景運行)
OpenAver_Debug.bat — 調試模式,命令提示字元顯示完整日誌,同時輸出到 %USERPROFILE%\\OpenAver\\logs\\debug.log
[故障排除]
如果程式無法啟動,請使用 OpenAver_Debug.bat 查看詳細日誌:
1. 雙擊 OpenAver_Debug.bat
2. 命令提示字元視窗會即時顯示日誌,同時寫入 %USERPROFILE%\\OpenAver\\logs\\debug.log
3. 將日誌內容附加到 GitHub Issue
[注意事項]
- 首次啟動可能較慢
- 設定檔:app\\web\\config.json
(完整路徑範例:C:\\Users\\你的帳號\\OpenAver\\app\\web\\config.json)
- 記錄檔:%USERPROFILE%\\OpenAver\\logs\\debug.log
[回報問題]
GitHub: https://github.com/slive777/OpenAver/issues
Telegram: https://t.me/+J-U2l96gv0FjZTBl
'''
(root_dir / "OpenAver.bat").write_text(bat_content, encoding='ascii')
(root_dir / "OpenAver_Debug.bat").write_text(debug_bat_content, encoding='ascii')
(root_dir / "README.txt").write_text(readme_en, encoding='utf-8')
(root_dir / "README_zh.txt").write_text(readme_zh, encoding='utf-8')
print(" Created: OpenAver.bat, OpenAver_Debug.bat, README.txt, README_zh.txt")
def get_directory_size(path):
"""計算目錄大小"""
total = sum(f.stat().st_size for f in Path(path).rglob('*') if f.is_file())
return total
def optimize_package():
"""優化打包體積:刪除 .dist-info、清理 __pycache__"""
print("\n[5.5/6] 優化打包體積...")
app_dir = BUILD_DIR / "OpenAver"
size_before = get_directory_size(app_dir)
# 1. 刪除 .dist-info 資料夾
dist_info_count = 0
dist_info_size = 0
for dist_info in app_dir.rglob("*.dist-info"):
if dist_info.is_dir():
size = sum(f.stat().st_size for f in dist_info.rglob('*') if f.is_file())
dist_info_size += size
shutil.rmtree(dist_info)
dist_info_count += 1
if dist_info_count > 0:
print(f" 刪除 {dist_info_count} 個 .dist-info 資料夾,節省 {dist_info_size / 1024 / 1024:.2f} MB")
# 2. 清理 __pycache__ 資料夾
pycache_count = 0
pycache_size = 0
for pycache in app_dir.rglob("__pycache__"):
if pycache.is_dir():
size = sum(f.stat().st_size for f in pycache.rglob('*') if f.is_file())
pycache_size += size
shutil.rmtree(pycache)
pycache_count += 1
if pycache_count > 0:
print(f" 刪除 {pycache_count} 個 __pycache__ 資料夾,節省 {pycache_size / 1024 / 1024:.2f} MB")
# 3. 刪除 .egg-info 資料夾
egg_info_count = 0
egg_info_size = 0
for egg_info in app_dir.rglob("*.egg-info"):
if egg_info.is_dir():
size = sum(f.stat().st_size for f in egg_info.rglob('*') if f.is_file())
egg_info_size += size
shutil.rmtree(egg_info)
egg_info_count += 1
if egg_info_count > 0:
print(f" 刪除 {egg_info_count} 個 .egg-info 資料夾,節省 {egg_info_size / 1024 / 1024:.2f} MB")
# 統計優化結果
size_after = get_directory_size(app_dir)
saved = size_before - size_after
print(f" 體積優化: {size_before / 1024 / 1024:.1f} MB → {size_after / 1024 / 1024:.1f} MB (節省 {saved / 1024 / 1024:.1f} MB)")
def create_zip_package():
"""打包成 ZIP"""
print("\n[6/6] 打包成 ZIP...")
# 讀取版本號
import sys
sys.path.insert(0, str(PROJECT_ROOT))
from core.version import VERSION
DIST_DIR.mkdir(parents=True, exist_ok=True)
zip_name = f"OpenAver-v{VERSION}-Windows-x64"
zip_path = DIST_DIR / f"{zip_name}.zip"
# 刪除舊的 ZIP
if zip_path.exists():
zip_path.unlink()
# 建立 ZIP
shutil.make_archive(
str(DIST_DIR / zip_name),
'zip',
BUILD_DIR,
"OpenAver"
)
# 計算檔案大小
size_mb = zip_path.stat().st_size / (1024 * 1024)
print(f" 完成: {zip_path.name} ({size_mb:.1f} MB)")
return zip_path
def main():
"""主程序"""
print("=" * 50)
print("OpenAver Windows 打包工具")
print("=" * 50)
try:
clean_build()
python_dir = download_embedded_python()
download_and_install_packages(python_dir)
copy_project_files()
create_launcher_scripts()
optimize_package()
zip_path = create_zip_package()
print("\n" + "=" * 50)
print("打包完成!")
print(f"輸出檔案: {zip_path}")
print("=" * 50)
except Exception as e:
print(f"\n錯誤: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == "__main__":
main()