Files
WQ_GUI/water_quality_app.spec
duxin b609a64811 fix: 多项稳定性修复与打包优化
- PyInstaller: runtime hook 预加载 osgeo DLL 避免 Qt5 符号冲突;
  spec 纳入 osgeo .py 文件 + 运行时 DLL 依赖 (ffi/lzma/bz2/expat/sqlite3);
  移除 DLL 双副本防止 segfault
- GDAL 环境: 新增 _MEIPASS/osgeo/data/gdal 路径搜索,改善打包后 GDAL_DATA 检测
- 预览生成器: 掩膜与底图同分辨率时跳过 Warp 加速读取
- 专题图: ConvexHull 坐标中心化修复 UTM 大坐标精度退化;
  边界采样兜底防止外扩点为空;
  TIFF 已含 NaN 掩膜时跳过矢量擦除;
  形态学闭运算填充掩膜小孔洞
- NDWI: int16→float32 防止减法溢出
- 面板注册表: 步骤模块分组重构 (模块一/二/三/四重划分)
2026-07-26 20:35:50 +08:00

252 lines
10 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- 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/binPyInstaller 分析时无法自动发现
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,
)