225 lines
8.5 KiB
Python
225 lines
8.5 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"))
|
||
|
||
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-2. 搜集底层 GDAL/GEOS/PROJ 核心框架与二进制扩展
|
||
if os.path.isdir(CONDA_OSGEO_DLLS):
|
||
for dll_name in ["gdal.dll", "geos.dll", "geos_c.dll", "proj_9.dll"]:
|
||
dll_path = os.path.join(CONDA_OSGEO_DLLS, dll_name)
|
||
if os.path.exists(dll_path):
|
||
binaries.append((dll_path, "."))
|
||
|
||
# 映射 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'))
|
||
]
|
||
|
||
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, # ⚠️ 维持开启控制台,拒绝绝对静默崩溃
|
||
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,
|
||
) |