340 lines
12 KiB
Python
340 lines
12 KiB
Python
"""
|
||
检测系统依赖工具
|
||
用于检测 PDF 生成所需的系统依赖
|
||
"""
|
||
import os
|
||
import sys
|
||
import platform
|
||
from pathlib import Path
|
||
from loguru import logger
|
||
from ctypes import util as ctypes_util
|
||
|
||
BOX_CONTENT_WIDTH = 62
|
||
|
||
|
||
def _box_line(text: str = "") -> str:
|
||
"""Render a single line inside the 66-char help box."""
|
||
return f"║ {text:<{BOX_CONTENT_WIDTH}}║\n"
|
||
|
||
|
||
def _get_platform_specific_instructions():
|
||
"""
|
||
获取针对当前平台的安装说明
|
||
|
||
Returns:
|
||
str: 平台特定的安装说明
|
||
"""
|
||
system = platform.system()
|
||
|
||
def _box_lines(lines):
|
||
"""批量将多行文本包装成带边框的提示块"""
|
||
return "".join(_box_line(line) for line in lines)
|
||
|
||
if system == "Darwin": # macOS
|
||
return _box_lines(
|
||
[
|
||
"🍎 macOS 系统解决方案:",
|
||
"",
|
||
"步骤 1: 安装依赖(宿主机执行)",
|
||
" brew install pango gdk-pixbuf libffi",
|
||
"",
|
||
"步骤 2: 设置 DYLD_LIBRARY_PATH(必做)",
|
||
" Apple Silicon:",
|
||
" export DYLD_LIBRARY_PATH=/opt/homebrew/lib:$DYLD_LIBRARY_PATH",
|
||
" Intel:",
|
||
" export DYLD_LIBRARY_PATH=/usr/local/lib:$DYLD_LIBRARY_PATH",
|
||
"",
|
||
"步骤 3: 永久生效(推荐)",
|
||
" 将 export DYLD_LIBRARY_PATH=... 追加到 ~/.zshrc",
|
||
" Apple 用 /opt/homebrew/lib,Intel 用 /usr/local/lib",
|
||
" 执行 source ~/.zshrc 后再打开新终端",
|
||
"",
|
||
"步骤 4: 新开终端执行验证",
|
||
" python -m ReportEngine.utils.dependency_check",
|
||
" 输出含 “✓ Pango 依赖检测通过” 即配置正确",
|
||
]
|
||
)
|
||
elif system == "Linux":
|
||
return _box_lines(
|
||
[
|
||
"🐧 Linux 系统解决方案:",
|
||
"",
|
||
"Ubuntu/Debian(宿主机执行):",
|
||
" sudo apt-get update",
|
||
" sudo apt-get install -y \\",
|
||
" libpango-1.0-0 libpangoft2-1.0-0 libffi-dev libcairo2",
|
||
" libgdk-pixbuf-2.0-0(缺失时改为 libgdk-pixbuf2.0-0)",
|
||
"",
|
||
"CentOS/RHEL:",
|
||
" sudo yum install -y pango gdk-pixbuf2 libffi-devel cairo",
|
||
"",
|
||
"Docker 部署无需额外安装,镜像已包含依赖",
|
||
]
|
||
)
|
||
elif system == "Windows":
|
||
return _box_lines(
|
||
[
|
||
"🪟 Windows 系统解决方案:",
|
||
"",
|
||
"步骤 1: 安装 GTK3 Runtime(宿主机执行)",
|
||
" 下载页: README 中的 GTK3 Runtime 链接(建议默认路径)",
|
||
"",
|
||
"步骤 2: 将 GTK 安装目录下的 bin 加入 PATH(需新终端)",
|
||
" set PATH=C:\\Program Files\\GTK3-Runtime Win64\\bin;%PATH%",
|
||
" 自定义路径请替换,或设置环境变量 GTK_BIN_PATH",
|
||
" 可选: 永久添加 PATH 示例:",
|
||
" setx PATH \"C:\\Program Files\\GTK3-Runtime Win64\\bin;%PATH%\"",
|
||
"",
|
||
"步骤 3: 验证(新终端执行)",
|
||
" python -m ReportEngine.utils.dependency_check",
|
||
" 输出含 “✓ Pango 依赖检测通过” 即配置正确",
|
||
]
|
||
)
|
||
else:
|
||
return _box_lines(["请查看 PDF 导出 README 了解您系统的安装方法"])
|
||
|
||
|
||
def _ensure_windows_gtk_paths():
|
||
"""
|
||
为 Windows 自动补充 GTK/Pango 运行时搜索路径,解决 DLL 未找到问题。
|
||
|
||
Returns:
|
||
str | None: 成功添加的路径(没有命中则为 None)
|
||
"""
|
||
if platform.system() != "Windows":
|
||
return None
|
||
|
||
candidates = []
|
||
seen = set()
|
||
|
||
def _add_candidate(path_like):
|
||
"""收集可能的GTK安装路径,避免重复并兼容用户自定义目录"""
|
||
if not path_like:
|
||
return
|
||
p = Path(path_like)
|
||
# 如果传入的是安装根目录,尝试拼接 bin
|
||
if p.is_dir() and p.name.lower() == "bin":
|
||
key = str(p.resolve()).lower()
|
||
if key not in seen:
|
||
seen.add(key)
|
||
candidates.append(p)
|
||
else:
|
||
for maybe in (p, p / "bin"):
|
||
key = str(maybe.resolve()).lower()
|
||
if maybe.exists() and key not in seen:
|
||
seen.add(key)
|
||
candidates.append(maybe)
|
||
|
||
# 用户自定义提示优先
|
||
for env_var in ("GTK3_RUNTIME_PATH", "GTK_RUNTIME_PATH", "GTK_BIN_PATH", "GTK_BIN_DIR", "GTK_PATH"):
|
||
_add_candidate(os.environ.get(env_var))
|
||
|
||
program_files = os.environ.get("ProgramFiles", r"C:\\Program Files")
|
||
program_files_x86 = os.environ.get("ProgramFiles(x86)", r"C:\\Program Files (x86)")
|
||
default_dirs = [
|
||
Path(program_files) / "GTK3-Runtime Win64",
|
||
Path(program_files_x86) / "GTK3-Runtime Win64",
|
||
Path(program_files) / "GTK3-Runtime Win32",
|
||
Path(program_files_x86) / "GTK3-Runtime Win32",
|
||
Path(program_files) / "GTK3-Runtime",
|
||
Path(program_files_x86) / "GTK3-Runtime",
|
||
]
|
||
|
||
# 常见自定义安装位置(其他盘符 / DevelopSoftware 目录)
|
||
common_drives = ["C", "D", "E", "F"]
|
||
common_names = ["GTK3-Runtime Win64", "GTK3-Runtime Win32", "GTK3-Runtime"]
|
||
for drive in common_drives:
|
||
root = Path(f"{drive}:/")
|
||
for name in common_names:
|
||
default_dirs.append(root / name)
|
||
default_dirs.append(root / "DevelopSoftware" / name)
|
||
|
||
# 扫描 Program Files 下所有以 GTK 开头的目录,适配自定义安装目录名
|
||
for root in (program_files, program_files_x86):
|
||
root_path = Path(root)
|
||
if root_path.exists():
|
||
for child in root_path.glob("GTK*"):
|
||
default_dirs.append(child)
|
||
|
||
for d in default_dirs:
|
||
_add_candidate(d)
|
||
|
||
# 如果用户已把自定义路径加入 PATH,也尝试识别
|
||
path_entries = os.environ.get("PATH", "").split(os.pathsep)
|
||
for entry in path_entries:
|
||
if not entry:
|
||
continue
|
||
# 粗筛包含 gtk 或 pango 的目录
|
||
if "gtk" in entry.lower() or "pango" in entry.lower():
|
||
_add_candidate(entry)
|
||
|
||
for path in candidates:
|
||
if not path or not path.exists():
|
||
continue
|
||
if not any(path.glob("pango*-1.0-*.dll")) and not (path / "pango-1.0-0.dll").exists():
|
||
continue
|
||
|
||
try:
|
||
if hasattr(os, "add_dll_directory"):
|
||
os.add_dll_directory(str(path))
|
||
except Exception:
|
||
# 如果添加失败,继续尝试 PATH 方式
|
||
pass
|
||
|
||
current_path = os.environ.get("PATH", "")
|
||
if str(path) not in current_path.split(";"):
|
||
os.environ["PATH"] = f"{path};{current_path}"
|
||
|
||
return str(path)
|
||
|
||
return None
|
||
|
||
|
||
def prepare_pango_environment():
|
||
"""
|
||
初始化运行所需的本地依赖搜索路径(当前主要针对 Windows 和 macOS)。
|
||
|
||
Returns:
|
||
str | None: 成功添加的路径(没有命中则为 None)
|
||
"""
|
||
system = platform.system()
|
||
if system == "Windows":
|
||
return _ensure_windows_gtk_paths()
|
||
if system == "Darwin":
|
||
# 自动补全 DYLD_LIBRARY_PATH,兼容 Apple Silicon 与 Intel
|
||
candidates = [Path("/opt/homebrew/lib"), Path("/usr/local/lib")]
|
||
current = os.environ.get("DYLD_LIBRARY_PATH", "")
|
||
added = []
|
||
for c in candidates:
|
||
if c.exists() and str(c) not in current.split(":"):
|
||
added.append(str(c))
|
||
if added:
|
||
os.environ["DYLD_LIBRARY_PATH"] = ":".join(added + ([current] if current else []))
|
||
return os.environ["DYLD_LIBRARY_PATH"]
|
||
return None
|
||
|
||
|
||
def _probe_native_libs():
|
||
"""
|
||
使用 ctypes 查找关键原生库,帮助定位缺失组件。
|
||
|
||
Returns:
|
||
list[str]: 未找到的库标识
|
||
"""
|
||
system = platform.system()
|
||
targets = []
|
||
|
||
if system == "Windows":
|
||
targets = [
|
||
("pango", ["pango-1.0-0"]),
|
||
("gobject", ["gobject-2.0-0"]),
|
||
("gdk-pixbuf", ["gdk_pixbuf-2.0-0"]),
|
||
("cairo", ["cairo-2"]),
|
||
]
|
||
else:
|
||
targets = [
|
||
("pango", ["pango-1.0"]),
|
||
("gobject", ["gobject-2.0"]),
|
||
("gdk-pixbuf", ["gdk_pixbuf-2.0"]),
|
||
("cairo", ["cairo", "cairo-2"]),
|
||
]
|
||
|
||
missing = []
|
||
for key, variants in targets:
|
||
found = any(ctypes_util.find_library(v) for v in variants)
|
||
if not found:
|
||
missing.append(key)
|
||
return missing
|
||
|
||
|
||
def check_pango_available():
|
||
"""
|
||
检测 Pango 库是否可用
|
||
|
||
Returns:
|
||
tuple: (is_available: bool, message: str)
|
||
"""
|
||
added_path = prepare_pango_environment()
|
||
missing_native = _probe_native_libs()
|
||
|
||
try:
|
||
# 尝试导入 weasyprint 并初始化 Pango
|
||
from weasyprint import HTML
|
||
from weasyprint.text.ffi import ffi, pango
|
||
|
||
# 尝试调用 Pango 函数来确认库可用
|
||
pango.pango_version()
|
||
|
||
return True, "✓ Pango 依赖检测通过,PDF 导出功能可用"
|
||
except OSError as e:
|
||
# Pango 库未安装或无法加载
|
||
error_msg = str(e)
|
||
platform_instructions = _get_platform_specific_instructions()
|
||
windows_hint = ""
|
||
if platform.system() == "Windows":
|
||
prefix = "已尝试自动添加 GTK 路径: "
|
||
max_path_len = BOX_CONTENT_WIDTH - len(prefix)
|
||
path_display = added_path or "未找到默认路径"
|
||
if len(path_display) > max_path_len:
|
||
path_display = path_display[: max_path_len - 3] + "..."
|
||
windows_hint = _box_line(prefix + path_display)
|
||
arch_note = _box_line("🔍 若已安装仍报错:确认 Python 与 GTK 位数一致后重开终端")
|
||
else:
|
||
arch_note = ""
|
||
|
||
missing_note = ""
|
||
if missing_native:
|
||
missing_str = ", ".join(missing_native)
|
||
missing_note = _box_line(f"未识别到的依赖: {missing_str}")
|
||
|
||
if 'gobject' in error_msg.lower() or 'pango' in error_msg.lower() or 'gdk' in error_msg.lower():
|
||
box_top = "╔" + "═" * 64 + "╗\n"
|
||
box_bottom = "╚" + "═" * 64 + "╝"
|
||
return False, (
|
||
box_top
|
||
+ _box_line("⚠️ PDF 导出依赖缺失")
|
||
+ _box_line()
|
||
+ _box_line("📄 PDF 导出功能将不可用(其他功能不受影响)")
|
||
+ _box_line()
|
||
+ windows_hint
|
||
+ arch_note
|
||
+ missing_note
|
||
+ platform_instructions
|
||
+ _box_line()
|
||
+ _box_line("📖 文档:static/Partial README for PDF Exporting/README.md")
|
||
+ box_bottom
|
||
)
|
||
return False, f"⚠ PDF 依赖加载失败: {error_msg};缺失/未识别: {', '.join(missing_native) if missing_native else '未知'}"
|
||
except ImportError as e:
|
||
# weasyprint 未安装
|
||
return False, (
|
||
"⚠ WeasyPrint 未安装\n"
|
||
"解决方法: pip install weasyprint"
|
||
)
|
||
except Exception as e:
|
||
# 其他未知错误
|
||
return False, f"⚠ PDF 依赖检测失败: {e}"
|
||
|
||
|
||
def log_dependency_status():
|
||
"""
|
||
记录系统依赖状态到日志
|
||
"""
|
||
is_available, message = check_pango_available()
|
||
|
||
if is_available:
|
||
logger.success(message)
|
||
else:
|
||
logger.warning(message)
|
||
logger.info("💡 提示:PDF 导出功能需要 Pango 库支持,但不影响系统其他功能的正常使用")
|
||
logger.info("📚 安装说明请参考:static/Partial README for PDF Exporting/README.md")
|
||
|
||
return is_available
|
||
|
||
|
||
if __name__ == "__main__":
|
||
# 用于独立测试
|
||
is_available, message = check_pango_available()
|
||
print(message)
|
||
sys.exit(0 if is_available else 1)
|