- 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 防止减法溢出 - 面板注册表: 步骤模块分组重构 (模块一/二/三/四重划分)
252 lines
10 KiB
Python
252 lines
10 KiB
Python
# -*- 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,
|
||
) |