# -*- mode: python ; coding: utf-8 -*- import os from pathlib import Path from PyInstaller.utils.hooks import collect_data_files, collect_submodules, collect_dynamic_libs # 获取项目根目录(spec 文件所在目录) import sys _REPO_DIR = Path(os.getcwd()).resolve() def _strip_msvc_runtime_binaries(bin_list): """ 从 ``collect_dynamic_libs`` 等额外收集的 binaries 中移除 MSVC 运行库 DLL。 chromadb / hnswlib 等 wheel 常自带 ``MSVCP140.dll`` 等;与 PyInstaller 已为 ``python.exe`` 解析到的 CRT 一并打入单文件包时,易在运行期混用多份 CRT 导致 ``MSVCP140.dll`` 内 ``0xc0000005``。统一依赖解释器自带的 CRT 集更安全。 """ skip = frozenset({ 'msvcp140.dll', 'msvcp140_1.dll', 'msvcp140_2.dll', 'msvcp140_atomic_wait.dll', 'msvcp140_codecvt_ids.dll', 'vcruntime140.dll', 'vcruntime140_1.dll', 'concrt140.dll', }) out = [] for item in bin_list: dest = item[0] base = Path(dest).name.lower() if base in skip: continue out.append(item) return out # 根目录执行:pyinstaller --clean main.spec # 收集配置文件夹和数据文件 datas = [ # 所有数据文件都不打包,从外部读取 # ('.env', '.'), # ('data/schemas', 'data/schemas'), # ('data/embeddings', 'data/embeddings'), # ('data/experiences', 'data/experiences'), ] # ChromaDB 在 Windows 单文件 exe 下如缺少 native 动态库,可能在持久化访问时直接“硬退出” # 这里显式收集 chromadb 与 hnswlib(向量索引)的动态库/二进制依赖 binaries = [] try: binaries += collect_dynamic_libs("chromadb") except Exception: pass try: binaries += collect_dynamic_libs("hnswlib") except Exception: pass # 某些 chromadb 版本会依赖包内数据文件(如 migrations/默认配置等),一并收集更稳妥 try: datas += collect_data_files("chromadb") except Exception: pass binaries = _strip_msvc_runtime_binaries(binaries) def _dedupe_msvc_runtime_in_analysis_binaries(toc): """ 对 ``Analysis`` 产出的 ``a.binaries`` 去重:同名 MSVC 运行库 DLL 只保留一份。 PyInstaller 会从 Python 安装目录、numpy、torch、chromadb 等多个依赖各自打入 同名 ``MSVCP140.dll`` 等;单文件解压到 ``_MEI*`` 后若出现多份或加载顺序混乱, 易在 ``MSVCP140.dll`` 内触发 ``0xc0000005``。优先保留 **源路径位于** ``sys.base_prefix`` 下的条目(与解释器主 CRT 一致)。 """ vc_names = frozenset({ 'msvcp140.dll', 'msvcp140_1.dll', 'msvcp140_2.dll', 'msvcp140_atomic_wait.dll', 'msvcp140_codecvt_ids.dll', 'vcruntime140.dll', 'vcruntime140_1.dll', 'concrt140.dll', }) try: base_root = str(Path(sys.base_prefix).resolve()).lower() except OSError: base_root = str(Path(sys.base_prefix)).lower() rest = [] vc_groups = {} for item in toc: dest = item[0] bname = Path(dest).name.lower() if bname not in vc_names: rest.append(item) continue vc_groups.setdefault(bname, []).append(item) picked = [] for bname, items in vc_groups.items(): if len(items) == 1: picked.append(items[0]) continue def _score(it): try: sp = str(Path(it[1]).resolve()).lower() except OSError: sp = str(it[1]).lower() under = 0 if sp.startswith(base_root) else 1 return (under, len(sp)) items_sorted = sorted(items, key=_score) picked.append(items_sorted[0]) return rest + picked # 收集所有需要的隐藏导入 hiddenimports = [ # Web 框架 'fastapi', 'fastapi.middleware.cors', 'fastapi.responses', 'uvicorn', 'uvicorn.lifespan.on', 'uvicorn.config', 'uvicorn.protocols.http.auto', 'uvicorn.protocols.websockets.auto', 'uvicorn.loops.auto', # Backend 模块(重要!) 'main', # backend/main.py 'backend.main', 'backend.nl_lite_store', 'backend.agents', 'backend.agents.orchestrator', 'backend.agents.schema_linker', 'backend.agents.sql_generator', 'backend.agents.validator', 'backend.config', 'backend.config.prompts', 'backend.config.settings', 'backend.db', 'backend.db.engine', 'backend.db.dbhub_tools', 'backend.db.dbhub_sql_parser', 'backend.db.dbhub_allowed_keywords', 'backend.llm', 'backend.llm.deepseek_client', 'backend.schema', 'backend.schema.manager', 'backend.schema.loader', 'backend.schema.indexer', 'backend.schema.models', 'backend.utils', 'backend.utils.dialog_classifier', 'backend.utils.dialog_context', 'backend.utils.embedding', 'backend.utils.fewshot_selector', 'backend.utils.question_locale', 'backend.utils.sql_parser', 'backend.utils.validators', # 数据库相关 'pymssql', 'sqlalchemy', 'sqlalchemy.dialects.mssql.pymssql', 'sqlalchemy.dialects.mysql', 'sqlalchemy.dialects.postgresql', 'sqlglot', 'sqlglot.dialects', 'sqlparse', # 向量数据库 - 使用 collect_submodules 自动收集所有子模块 # Embedding 相关 'sentencepiece', 'accelerate', 'safetensors', # LLM API 'openai', # 数据处理 'numpy', 'jieba', # Pydantic 'pydantic', 'pydantic_settings', 'dotenv', # 工具库 'tenacity', 'structlog', 'tqdm', 'camel_ai', # 标准库中可能被遗漏的 'email.mime.text', 'email.mime.multipart', ] + collect_submodules('chromadb') # 排除不必要的模块以减小体积 excludes = [ 'tkinter', 'matplotlib', 'scipy', 'pandas.tests', 'numpy.testing', 'jupyter', 'notebook', 'IPython', 'pytest', ] a = Analysis( ['api_server.py'], pathex=[str(_REPO_DIR)], binaries=binaries, datas=datas, hiddenimports=hiddenimports, hookspath=[], hooksconfig={}, runtime_hooks=[str(_REPO_DIR / 'packaging' / 'pyi_rth_crt_path.py')], excludes=excludes, noarchive=False, optimize=0, ) # 去掉 Analysis 自动收集到的重复 MSVC 运行库(比仅过滤手动 binaries 更关键) a.binaries = _dedupe_msvc_runtime_in_analysis_binaries(list(a.binaries)) pyz = PYZ(a.pure) exe = EXE( pyz, a.scripts, a.binaries, a.datas, # 单文件模式:将数据文件打包进 exe [], name='Backman_Text2SQL_Server', debug=False, bootloader_ignore_signals=False, strip=False, upx=False, # 禁用 UPX,避免兼容性问题 upx_exclude=[], runtime_tmpdir=None, console=True, disable_windowed_traceback=False, argv_emulation=False, target_arch=None, codesign_identity=None, entitlements_file=None, )