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 只负责环境准备, ...@@ -8,16 +8,24 @@ build_mac_universal.sh / build_windows.bat 只负责环境准备,
8 8
9 修复点: 9 修复点:
10 - macOS 上 PIL/_imaging.so 依赖 @rpath/libtiff.6.dylib 等原生库, 10 - macOS 上 PIL/_imaging.so 依赖 @rpath/libtiff.6.dylib 等原生库,
11 PyInstaller 会把 @rpath 改写为 @loader_path/.. (= Contents/Frameworks/), 11 PyInstaller 把 @rpath 改写成 @loader_path/.. (= Contents/Frameworks/),
12 因此 .dylibs/*.dylib 必须平铺到 bundle 根目录,而不是保留 12 因此 .dylibs/*.dylib 必须平铺到 bundle 根目录。
13 PIL/.dylibs/ 结构 —— 否则 dlopen 在启动时失败。 13
14 策略 (三重保险):
15 1. PyInstaller 官方工具 collect_dynamic_libs(destdir='.') —— 推荐做法
16 2. 显式枚举 PIL/.dylibs/ 和 PIL/.libs/ —— 兜底
17 3. 详细打印每一步收集结果 —— 下次出错直接看日志定位
14 """ 18 """
15 import sys 19 import sys
16 from pathlib import Path 20 from pathlib import Path
21 from PyInstaller.utils.hooks import collect_dynamic_libs
17 22
18 IS_MAC = sys.platform == 'darwin' 23 IS_MAC = sys.platform == 'darwin'
19 IS_WIN = sys.platform == 'win32' 24 IS_WIN = sys.platform == 'win32'
20 25
26 print('=' * 60)
27 print(f'[spec] 平台: {sys.platform}, Python: {sys.version.split()[0]}')
28
21 # ----- 图标 ----- 29 # ----- 图标 -----
22 if IS_MAC: 30 if IS_MAC:
23 ICON = 'zb100_mac.icns' 31 ICON = 'zb100_mac.icns'
...@@ -31,21 +39,54 @@ datas = [('config.json', '.')] ...@@ -31,21 +39,54 @@ datas = [('config.json', '.')]
31 if IS_WIN: 39 if IS_WIN:
32 datas.append(('zb100_windows.ico', '.')) 40 datas.append(('zb100_windows.ico', '.'))
33 41
34 # ----- Pillow 原生库:平铺到 bundle 根 ----- 42 # ===== Pillow 原生库收集 =====
35 pil_native_libs = [] 43
44 # 策略 1: PyInstaller 官方推荐
45 try:
46 libs_std = collect_dynamic_libs('PIL', destdir='.')
47 print(f'[spec] collect_dynamic_libs("PIL", destdir=".") 返回 {len(libs_std)} 项:')
48 for src, dst in libs_std:
49 print(f'[spec] STD {Path(src).name} -> {dst}')
50 except Exception as e:
51 libs_std = []
52 print(f'[spec] collect_dynamic_libs 异常: {e}')
53
54 # 策略 2: 显式枚举 PIL 包里的原生库目录
55 libs_extra = []
56 pil_dir = None
36 try: 57 try:
37 import PIL 58 import PIL
38 pil_dir = Path(PIL.__file__).parent 59 pil_dir = Path(PIL.__file__).parent
60 print(f'[spec] PIL 安装位置: {pil_dir}')
39 for sub in ('.dylibs', '.libs'): 61 for sub in ('.dylibs', '.libs'):
40 d = pil_dir / sub 62 d = pil_dir / sub
41 if d.is_dir(): 63 if d.is_dir():
42 for lib in d.iterdir(): 64 print(f'[spec] 发现 {sub}/ 目录, 内容:')
65 for lib in sorted(d.iterdir()):
66 print(f'[spec] {lib.name} ({lib.stat().st_size} bytes)')
43 if lib.suffix.lower() in ('.dylib', '.so', '.dll'): 67 if lib.suffix.lower() in ('.dylib', '.so', '.dll'):
44 pil_native_libs.append((str(lib), '.')) 68 libs_extra.append((str(lib), '.'))
69 else:
70 print(f'[spec] {sub}/ 不存在')
45 except Exception as e: 71 except Exception as e:
46 print(f'[spec] WARN 枚举 PIL 原生库失败: {e}') 72 print(f'[spec] 枚举 PIL 失败: {e}')
73
74 # 合并去重 (按源路径)
75 seen = set()
76 pil_native_libs = []
77 for src, dst in libs_std + libs_extra:
78 if src not in seen:
79 seen.add(src)
80 pil_native_libs.append((src, dst))
81
82 print(f'[spec] 最终 Pillow 原生库数量: {len(pil_native_libs)}')
83 for src, dst in pil_native_libs:
84 print(f'[spec] FINAL {Path(src).name} -> {dst}')
85
86 if IS_MAC and len(pil_native_libs) == 0:
87 print('[spec] !!! 警告: macOS 构建但未发现 Pillow 原生库, 打包产物必然启动失败 !!!')
47 88
48 print(f'[spec] 将 {len(pil_native_libs)} 个 PIL 原生库平铺到 bundle 根目录') 89 print('=' * 60)
49 90
50 a = Analysis( 91 a = Analysis(
51 ['image_generator.py'], 92 ['image_generator.py'],
......