8af666ed by 柴进

:mag: spec 加详细诊断输出 + 引入 collect_dynamic_libs 双重保险

上一版 spec (4097b529) 在 macOS 26 构建出的包仍崩在同一位置:
  dlopen: Library not loaded: @rpath/libtiff.6.dylib

修改:
- 引入 PyInstaller 官方 collect_dynamic_libs('PIL', destdir='.')
  作为主策略, 显式枚举 .dylibs/.libs 作为兜底
- 每步打印详细信息: PIL 安装路径、找到的文件列表、
  最终 binaries 合并结果
- 最后如果 len == 0 直接打印警告, 免得构建成功但运行时才崩

下次构建输出里找 [spec] 开头的行, 就能看清是哪步出了问题:
  - collect_dynamic_libs 返回空? -> PIL 没 bundled dylibs
  - .dylibs/ 不存在? -> Pillow 装的不是 wheel
  - 有文件但 bundle 里没有? -> PyInstaller 传入后续处理的问题

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 4097b529
......@@ -8,16 +8,24 @@ build_mac_universal.sh / build_windows.bat 只负责环境准备,
修复点:
- macOS 上 PIL/_imaging.so 依赖 @rpath/libtiff.6.dylib 等原生库,
PyInstaller 会把 @rpath 改写为 @loader_path/.. (= Contents/Frameworks/),
因此 .dylibs/*.dylib 必须平铺到 bundle 根目录,而不是保留
PIL/.dylibs/ 结构 —— 否则 dlopen 在启动时失败。
PyInstaller 把 @rpath 改写成 @loader_path/.. (= Contents/Frameworks/),
因此 .dylibs/*.dylib 必须平铺到 bundle 根目录。
策略 (三重保险):
1. PyInstaller 官方工具 collect_dynamic_libs(destdir='.') —— 推荐做法
2. 显式枚举 PIL/.dylibs/ 和 PIL/.libs/ —— 兜底
3. 详细打印每一步收集结果 —— 下次出错直接看日志定位
"""
import sys
from pathlib import Path
from PyInstaller.utils.hooks import collect_dynamic_libs
IS_MAC = sys.platform == 'darwin'
IS_WIN = sys.platform == 'win32'
print('=' * 60)
print(f'[spec] 平台: {sys.platform}, Python: {sys.version.split()[0]}')
# ----- 图标 -----
if IS_MAC:
ICON = 'zb100_mac.icns'
......@@ -31,21 +39,54 @@ datas = [('config.json', '.')]
if IS_WIN:
datas.append(('zb100_windows.ico', '.'))
# ----- Pillow 原生库:平铺到 bundle 根 -----
pil_native_libs = []
# ===== Pillow 原生库收集 =====
# 策略 1: PyInstaller 官方推荐
try:
libs_std = collect_dynamic_libs('PIL', destdir='.')
print(f'[spec] collect_dynamic_libs("PIL", destdir=".") 返回 {len(libs_std)} 项:')
for src, dst in libs_std:
print(f'[spec] STD {Path(src).name} -> {dst}')
except Exception as e:
libs_std = []
print(f'[spec] collect_dynamic_libs 异常: {e}')
# 策略 2: 显式枚举 PIL 包里的原生库目录
libs_extra = []
pil_dir = None
try:
import PIL
pil_dir = Path(PIL.__file__).parent
print(f'[spec] PIL 安装位置: {pil_dir}')
for sub in ('.dylibs', '.libs'):
d = pil_dir / sub
if d.is_dir():
for lib in d.iterdir():
print(f'[spec] 发现 {sub}/ 目录, 内容:')
for lib in sorted(d.iterdir()):
print(f'[spec] {lib.name} ({lib.stat().st_size} bytes)')
if lib.suffix.lower() in ('.dylib', '.so', '.dll'):
pil_native_libs.append((str(lib), '.'))
libs_extra.append((str(lib), '.'))
else:
print(f'[spec] {sub}/ 不存在')
except Exception as e:
print(f'[spec] WARN 枚举 PIL 原生库失败: {e}')
print(f'[spec] 枚举 PIL 失败: {e}')
# 合并去重 (按源路径)
seen = set()
pil_native_libs = []
for src, dst in libs_std + libs_extra:
if src not in seen:
seen.add(src)
pil_native_libs.append((src, dst))
print(f'[spec] 最终 Pillow 原生库数量: {len(pil_native_libs)}')
for src, dst in pil_native_libs:
print(f'[spec] FINAL {Path(src).name} -> {dst}')
if IS_MAC and len(pil_native_libs) == 0:
print('[spec] !!! 警告: macOS 构建但未发现 Pillow 原生库, 打包产物必然启动失败 !!!')
print(f'[spec] 将 {len(pil_native_libs)} 个 PIL 原生库平铺到 bundle 根目录')
print('=' * 60)
a = Analysis(
['image_generator.py'],
......