|
| 1 | +# -*- mode: python ; coding: utf-8 -*- |
| 2 | +""" |
| 3 | +PyInstaller 打包配方:把 viralens 连同一个真正的 Python 解释器冻进一个文件夹, |
| 4 | +用户无需自己装 Python / pip 依赖,双击即用。三平台(Win/Mac/Linux)同一份 spec。 |
| 5 | +
|
| 6 | + 本地构建(Windows): py -m PyInstaller --noconfirm --clean packaging/viralens.spec |
| 7 | + 产物: dist/viralens/viralens(.exe) ← onedir,整个文件夹打包分发 |
| 8 | +
|
| 9 | +为什么用 onedir 而不是 onefile:流水线一次运行会让 app 自己重新拉起 ~6 次 |
| 10 | +(fetch → 各分析步骤)。onefile 每次启动都要把上百 MB 解压到临时目录,会非常慢; |
| 11 | +onedir 直接就地运行,子步骤秒起。 |
| 12 | +""" |
| 13 | +import os |
| 14 | +import sys |
| 15 | +from PyInstaller.utils.hooks import collect_all |
| 16 | + |
| 17 | +REPO = os.path.dirname(SPECPATH) # SPECPATH 由 PyInstaller 注入 = packaging/ |
| 18 | +SCRIPTS = os.path.join(REPO, "scripts") |
| 19 | +ENTRY = os.path.join(SPECPATH, "viralens_app.py") |
| 20 | +ICON = os.path.join(SPECPATH, "icon.ico") # 可选;不存在就不用 |
| 21 | + |
| 22 | +datas, binaries, hiddenimports = [], [], [] |
| 23 | + |
| 24 | +# —— 重依赖:连子模块 + 数据文件(jieba 词典、bilibili_api 资源等)一起收 —— |
| 25 | +for pkg in ("bilibili_api", "aiohttp", "jieba"): |
| 26 | + d, b, h = collect_all(pkg) |
| 27 | + datas += d |
| 28 | + binaries += b |
| 29 | + hiddenimports += h |
| 30 | + |
| 31 | +# matplotlib / numpy / Pillow 自带 PyInstaller hook,会自动带数据;这里补 Agg 后端保险 |
| 32 | +hiddenimports += ["matplotlib.backends.backend_agg", "numpy", "PIL"] |
| 33 | + |
| 34 | +# 原生窗口后端(可选):装了 pywebview 就连同其平台后端一起收进来,实现「双击弹原生窗口」; |
| 35 | +# 没装(比如 Linux 不打包 webview)则打成「开浏览器」回退版 —— 入口 viralens_app.py 会自动判断。 |
| 36 | +try: |
| 37 | + import webview # noqa: F401 |
| 38 | + _d, _b, _h = collect_all("webview") |
| 39 | + datas += _d |
| 40 | + binaries += _b |
| 41 | + hiddenimports += _h |
| 42 | +except Exception: |
| 43 | + pass |
| 44 | + |
| 45 | +# —— 项目自己的脚本 —— |
| 46 | +# app 懒加载它们、viralens 通过 `--vl-exec <模块名>` 用 runpy 跑它们, |
| 47 | +# 静态分析有可能看不全,这里全部显式声明,确保都被冻进去。 |
| 48 | +hiddenimports += [ |
| 49 | + "runtime", "app", "viralens", |
| 50 | + "fetch_multi", "fetch_bilibili", "fetch_youtube", |
| 51 | + "compare_form", "creator_profile", "scan_signals", "charts", "export_data", "build_report", |
| 52 | + "diagnose", "analyze_video", "import_private", |
| 53 | + "creators", "features", "benchmarks", |
| 54 | + "classify_and_stats", "comments", "compare_meme", "fetch_covers", |
| 55 | + "fetch_videos", "resolve_creators", "subtitle", |
| 56 | +] |
| 57 | + |
| 58 | +# —— 只读资源:网页界面 + 配置模板 —— 放进打包根目录,app.py 用 runtime.ASSET_DIR 找它们 —— |
| 59 | +datas += [ |
| 60 | + (os.path.join(SCRIPTS, "gui.html"), "."), |
| 61 | + (os.path.join(SCRIPTS, "diagnose.html"), "."), |
| 62 | + (os.path.join(SCRIPTS, "config_local.example.py"), "."), |
| 63 | +] |
| 64 | + |
| 65 | +a = Analysis( |
| 66 | + [ENTRY], |
| 67 | + pathex=[SCRIPTS], # 让 import runtime / app / 各脚本 找得到 |
| 68 | + binaries=binaries, |
| 69 | + datas=datas, |
| 70 | + hiddenimports=hiddenimports, |
| 71 | + hookspath=[], |
| 72 | + runtime_hooks=[], |
| 73 | + # yt_dlp 体积巨大且只服务于「下 YouTube 视频开头」这个可选功能(缺了会优雅降级); |
| 74 | + # tkinter 是 GUI 工具包,我们用 matplotlib 的 Agg 后端、用不到它。 |
| 75 | + # 注意:不要排 unittest/test —— matplotlib→pyparsing.testing 会在导入时 import unittest。 |
| 76 | + excludes=["yt_dlp", "tkinter"], |
| 77 | + noarchive=False, |
| 78 | +) |
| 79 | + |
| 80 | +pyz = PYZ(a.pure) |
| 81 | + |
| 82 | +exe = EXE( |
| 83 | + pyz, |
| 84 | + a.scripts, |
| 85 | + [], |
| 86 | + exclude_binaries=True, |
| 87 | + name="viralens", |
| 88 | + debug=False, |
| 89 | + bootloader_ignore_signals=False, |
| 90 | + strip=False, |
| 91 | + upx=False, |
| 92 | + console=False, # 窗口模式:不弹终端。子进程 stdout 由 viralens_app._ensure_std() 接回管道 |
| 93 | + icon=(ICON if os.path.exists(ICON) else None), |
| 94 | +) |
| 95 | + |
| 96 | +coll = COLLECT( |
| 97 | + exe, |
| 98 | + a.binaries, |
| 99 | + a.datas, |
| 100 | + strip=False, |
| 101 | + upx=False, |
| 102 | + name="viralens", |
| 103 | +) |
| 104 | + |
| 105 | +# macOS:在 onedir 外再包一层 .app,双击即用(无终端窗口)。 |
| 106 | +if sys.platform == "darwin": |
| 107 | + app_bundle = BUNDLE( |
| 108 | + coll, |
| 109 | + name="viralens.app", |
| 110 | + icon=(ICON if os.path.exists(ICON) else None), |
| 111 | + bundle_identifier="dev.harryxin.viralens", |
| 112 | + info_plist={ |
| 113 | + "CFBundleName": "viralens", |
| 114 | + "CFBundleDisplayName": "viralens", |
| 115 | + "NSHighResolutionCapable": True, |
| 116 | + "LSBackgroundOnly": False, |
| 117 | + }, |
| 118 | + ) |
0 commit comments