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>
Showing
1 changed file
with
50 additions
and
9 deletions
| ... | @@ -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'], | ... | ... |
-
Please register or sign in to post a comment