Pyinstaller打包pikepdf失敗的問題排查

神王攻大人 發表於 2022-06-28

問題

最近在專案裡用到了pikepdf這個庫,用於實現pdf水印插入的一個小功能,原始碼除錯階段執行一切OK但是在出包時報瞭如下異常。

Traceback (most recent call last):
  File "pikepdf\__init__.py", line 19, in <module>
  File "PyInstaller\loader\pyimod03_importers.py", line 495, in exec_module
  File "pikepdf\_version.py", line 13, in <module>
  File "importlib\metadata.py", line 530, in version
  File "importlib\metadata.py", line 503, in distribution
  File "importlib\metadata.py", line 177, in from_name
importlib.metadata.PackageNotFoundError: pikepdf

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "main.py", line 1, in <module>
  File "PyInstaller\loader\pyimod03_importers.py", line 495, in exec_module
  File "pikepdf\__init__.py", line 21, in <module>
ImportError: Failed to determine version
[29708] Failed to execute script 'main' due to unhandled exception!

異常定位

列印了兩份堆疊的資訊,翻翻炸出來點 init.py 的21行,程式碼如下

try:
    from ._version import __version__
except ImportError as _e:  # pragma: no cover
    raise ImportError("Failed to determine version") from _e

從.version.py檔案匯入__version__失敗?看看_version.py

try:
    from importlib_metadata import version as _package_version  # type: ignore
except ImportError:
    from importlib.metadata import version as _package_version

__version__ = _package_version('pikepdf')

__all__ = ['__version__']

再看看上面的異常,也就再_package_version這個函式了。這邊可以先寫個簡單的demo.py驗證下,使用pyinstgaller編譯後執行。

# demo.py
import pikepdf

if __name__ == '__main__':
    print("Hello World")
# 輸出
Traceback (most recent call last):
  File "pikepdf\__init__.py", line 19, in <module>
  File "PyInstaller\loader\pyimod03_importers.py", line 495, in exec_module
  File "pikepdf\_version.py", line 13, in <module>
  File "importlib\metadata.py", line 530, in version
  File "importlib\metadata.py", line 503, in distribution
  File "importlib\metadata.py", line 177, in from_name
importlib.metadata.PackageNotFoundError: pikepdf

The above exception was the direct cause of the following exception:

Traceback (most recent call last):
  File "main.py", line 1, in <module>
  File "PyInstaller\loader\pyimod03_importers.py", line 495, in exec_module
  File "pikepdf\__init__.py", line 21, in <module>
ImportError: Failed to determine version
[29708] Failed to execute script 'main' due to unhandled exception!

符合預期。所以說importlib.metadata.version 無法在pyinstaller打包後執行?

問題原因

對於pkgs_to_check_at_runtime中列出的每個包,需要通過在spec檔案中使用copy_metadata(name)來收集後設資料。說白了就是pyinstaller打包後缺少對應的metadata資訊。

修復方案

1. 降低版本pikepdf的版本

這個庫以前用過,沒有出這個么蛾子。看了下舊版本的原始碼,一下是5.1.3版本之前的get_version實現,沒有使用importlib庫,自然也不會有問題。

from pkg_resources import DistributionNotFound
from pkg_resources import get_distribution as _get_distribution

try:
    __version__ = _get_distribution(__package__).version
except DistributionNotFound:  # pragma: no cover
    __version__ = "Not installed"

__all__ = ['__version__']

2. 在pyinstaller打包時指定copy-metadata

像這樣

pyinstaller -F --copy-metadata pikepdf main.py

碎碎念

我覺得完全可以像版本5.1.3之前一樣,獲取不到時賦值為"Not installed"一樣就行。然後提了個pr[https://github.com/pikepdf/pikepdf/pull/358]給作者,可惜作者認為這是importlib的鍋(不改),倒也時說得過去。