# -*- coding: utf-8 -*- # ============================================================================== # 水质参数反演分析系统 - 终极纯净版 PyInstaller Spec 配置文件 # 彻底斩断 venv 交叉污染,强制 Conda 原生环境全量解析 # 用法: pyinstaller --clean water_quality_app.spec # ============================================================================== import os import sys import glob import shutil block_cipher = None # ============================================================================== # 0. 基础路径配置 (❌ 彻底移除了 VENV_SITE 变量,拒绝交叉环境污染!) # ============================================================================== PROJECT_ROOT = os.path.abspath(os.getcwd()) SRC_DIR = os.path.join(PROJECT_ROOT, "src") BUILD_DIR = os.path.join(PROJECT_ROOT, "build") DIST_DIR = os.path.join(PROJECT_ROOT, "dist") # Conda WQ_GUI 环境 DLL 路径(MSVC 运行时 + GIS 核心库原生纯净副本) CONDA_ENV_DLLS = r"D:\111\changyongruanjian\anconda\envs\WQ_GUI" CONDA_OSGEO_DLLS = os.path.join(CONDA_ENV_DLLS, "Lib", "site-packages", "osgeo") # ============================================================================== # 1. 静态资源与数据文件收集 (datas) # ============================================================================== def get_data_files(): datas = [] # 1-1. 根目录核心配置文件 (包含内置公式 CSV) single_files = [ "waterindex.csv", "requirements.txt", "README.md", "软件说明.md", ] for f in single_files: if os.path.exists(os.path.join(PROJECT_ROOT, f)): datas.append((os.path.join(PROJECT_ROOT, f), ".")) # 1-2. data/ 目录(全量递归映射,确保代码通过 get_resource_path 能读到任务栏图标) data_dir = os.path.join(PROJECT_ROOT, "data") if os.path.isdir(data_dir): datas.append((data_dir, "data")) # 1-3. src/gui/model/ 模型参数文件 model_dir = os.path.join(SRC_DIR, "gui", "model") if os.path.isdir(model_dir): datas.append((model_dir, "src/gui/model")) # 1-4. src/auth/ 离线授权验证模块 auth_dir = os.path.join(SRC_DIR, "auth") if os.path.isdir(auth_dir): datas.append((auth_dir, "src/auth")) # 1-5. 运行时环境变量与 DLL 挂载钩子 rthook_script = os.path.join(PROJECT_ROOT, "scripts", "rthook_add_dll_dirs.py") if os.path.exists(rthook_script): datas.append((rthook_script, "scripts")) # 1-6. osgeo Python 模块(__init__.py, gdal.py, ogr.py 等) # ★ 关键:PyInstaller 会把 .py 文件打入 PYZ (嵌入 EXE),但 .pyd 文件 # 作为 binaries 被放到 _internal/osgeo/ 物理目录下。当物理 osgeo/ # 目录存在但没有 __init__.py 时,Python 无法将其识别为 package, # 导致 "import osgeo" 失败 → "GDAL 未安装"。 # 解决方案:将 osgeo 的所有 .py 文件显式复制到物理 osgeo/ 目录。 if os.path.isdir(CONDA_OSGEO_DLLS): for py_file in glob.glob(os.path.join(CONDA_OSGEO_DLLS, "*.py")): datas.append((py_file, "osgeo")) return datas datas = get_data_files() # ============================================================================== # 2. 核心二进制文件搜集 (binaries) - 强制抓取纯净运行库防静默崩溃 # ============================================================================== def get_binaries(): binaries = [] # 2-1. 精准提取 Conda 环境下与编译完全匹配的 MSVC 运行库 if os.path.isdir(CONDA_ENV_DLLS): for dll_name in [ "msvcp140.dll", "msvcp140_1.dll", "msvcp140_2.dll", "msvcp140_atomic_wait.dll", "msvcp140_codecvt_ids.dll", "vcruntime140.dll", "vcruntime140_1.dll", "vcruntime140_threads.dll", "concrt140.dll", "vcomp140.dll", "vcamp140.dll", "vccorlib140.dll", ]: dll_path = os.path.join(CONDA_ENV_DLLS, dll_name) if os.path.exists(dll_path): binaries.append((dll_path, ".")) # 2-1b. Python C 扩展的运行时依赖 DLL(_ctypes → ffi.dll, _lzma → liblzma.dll 等) # 这些 DLL 位于 Conda 环境的 Library/bin,PyInstaller 分析时无法自动发现 CONDA_LIBRARY_BIN = os.path.join(CONDA_ENV_DLLS, "Library", "bin") if os.path.isdir(CONDA_LIBRARY_BIN): for dll_name in [ "ffi.dll", # _ctypes.pyd 依赖 "liblzma.dll", # _lzma.pyd 依赖 "libbz2.dll", # _bz2.pyd 依赖 "libexpat.dll", # pyexpat.pyd 依赖 "sqlite3.dll", # _sqlite3.pyd 依赖 ]: dll_path = os.path.join(CONDA_LIBRARY_BIN, dll_name) if os.path.exists(dll_path): binaries.append((dll_path, ".")) # 2-2. 搜集底层 GDAL/GEOS/PROJ 核心框架与二进制扩展 # ★ 注意:gdal.dll / geos.dll / geos_c.dll / proj_9.dll 不再显式添加, # PyInstaller 会自动将它们收集到 osgeo/ 目录(随 .pyd 依赖解析)。 # 显式添加到根目录会导致 DLL 双副本冲突 → segfault。 if os.path.isdir(CONDA_OSGEO_DLLS): # 映射 Python 3.12 对应的底层二进制接口 (C-Extension) for pyd_name in [ "_gdal.cp312-win_amd64.pyd", "_gdal_array.cp312-win_amd64.pyd", "_gdalconst.cp312-win_amd64.pyd" ]: pyd_path = os.path.join(CONDA_OSGEO_DLLS, pyd_name) if os.path.exists(pyd_path): binaries.append((pyd_path, "osgeo")) return binaries binaries = get_binaries() # ============================================================================== # 3. 隐式依赖与底层动态加载池 (hiddenimports) # ============================================================================== hidden_imports = [ "PyQt5.sip", # GDAL 空间处理核心 "osgeo", "osgeo.gdal", "osgeo.gdalconst", "osgeo.gdalnumeric", "osgeo.ogr", "osgeo.osr", # 解决 rasterio 与 fiona 二进制隐藏扩展的断层报错 "rasterio._base", "rasterio._env", "rasterio._io", "rasterio._warp", "rasterio._features", "rasterio._path", "rasterio.crs", "rasterio.env", "rasterio.features", "rasterio.warp", "rasterio.windows", "rasterio.sample", "rasterio.vrt", "fiona._env", "fiona._geometry", "fiona.ogrext", "fiona.io", "fiona.env", "fiona.features", # 几何计算与坐标转换 "shapely._geos", "shapely._geometry", "shapely.lib", "pyproj", "pyproj._datadir", "pyproj._crs", "geopandas", "geopandas._compat", "geopandas.io", # 底层科学计算库支持 "scipy.special._orthogonal", "scipy.spatial", "scipy.linalg", "sklearn.utils._index_deprecations", "sklearn.utils._testing", "sklearn._loss", "sklearn.ensemble", "sklearn.preprocessing", "sklearn.model_selection", # 报告与文档导出组件 "docx", "docx.oxml", "docx.table", "docx.text", "lxml.etree", "lxml.objectify", # 业务面板与自定义挂载 "src.gui.styles", "src.gui.components.custom_widgets", ] # ============================================================================== # 4. 运行时前置初始化钩子 (runtime_hooks) # ============================================================================== runtime_hooks = [] rthook_path = os.path.join(PROJECT_ROOT, "scripts", "rthook_add_dll_dirs.py") if os.path.exists(rthook_path): runtime_hooks.append(rthook_path) # ============================================================================== # 5. 精简排除列表 (excludes) - 严格保留标准库防级联瘫痪 # ============================================================================== excludes = [ "tkinter", "matplotlib.tests", "PyQt5.QtTest", "PyQt5.QtWebEngine", "PyQt5.QtWebEngineWidgets", "pytest", ] # ============================================================================== # 6. 核心构建流程 (Analysis -> PYZ -> EXE -> COLLECT) # ============================================================================== appname = "MegaWater" a = Analysis( ["src/gui/water_quality_gui_v2.py"], # ⚠️ 绝对关键:仅保留项目原生搜索路径,彻底切断与混合虚拟环境的联系 pathex=[PROJECT_ROOT, SRC_DIR], binaries=binaries, datas=datas, hiddenimports=hidden_imports, hookspath=["scripts"] if os.path.isdir(os.path.join(PROJECT_ROOT, "scripts")) else [], hooksconfig={}, runtime_hooks=runtime_hooks, excludes=excludes, win_no_prefer_redirects=False, cipher=block_cipher, noarchive=False, ) # 强制清洗可能由于第三方包引入导致的、存在版本冲突的残缺 MSVC 动态库 a.binaries = [ x for x in a.binaries if not x[0].lower().startswith(('msvcp140', 'vcruntime140')) ] # ★ 注意:.libs 目录中的 PROJ/GEOS/GDAL DLL 副本无需删除。 # rthook_add_dll_dirs.py 运行时钩子会在 PyQt5 之前预加载 osgeo/ 的所有 DLL, # 避免了 DLL 加载顺序导致的 segfault。各包的 .libs 副本可以安全共存。 pyz = PYZ(a.pure, a.zipped_data, cipher=block_cipher) # 生成物理启动核心 exe = EXE( pyz, a.scripts, [], exclude_binaries=True, name=appname, debug=False, bootloader_ignore_signals=False, strip=False, upx=False, # 禁用 UPX 暴力压缩,保护二进制 C++ 区段完整性 console=False, # GUI 应用,不显示控制台 disable_windowed_traceback=False, argv_emulation=False, target_arch=None, codesign_identity=None, entitlements_file=None, icon="data/icons-1/app.ico", # 锁定物理执行文件桌面正方形主图标 ) # 组装最终绿色分发目录 (_internal + EXE) - 引入全自动纯净 DLL 填坑机制 coll = COLLECT( exe, a.binaries, a.zipfiles, a.datas, # 强制将原生编译的底层运行库塞入分发根目录,终结指针断层! [ ('msvcp140.dll', os.path.join(CONDA_ENV_DLLS, 'msvcp140.dll'), 'BINARY'), ('vcruntime140.dll', os.path.join(CONDA_ENV_DLLS, 'vcruntime140.dll'), 'BINARY') ], strip=False, upx=False, upx_exclude=[], name=appname, )