2025年3月17日

Failed to Run PySide6 from CentOS7

近期因為一些因素,必須要把 python 的版本往上升到 3.11,而原本在使用的 PySide2 因為僅支援到 pytho-3.10,所以必須要放棄 [注1]。雪上加霜的是,因為開發的程式必須要能跑在 RHEL7 的 OS 上,然而 pip 上能安裝的 PySide6 僅能跑在 RHEL8 以上的 OS,因此開啟了一連串煩死人的支線任務。

因為升到 pytho-3.11 無可避免,因此解法主要也就三種:build from source、patchelf 或是 container。build from source 簡單明瞭,然而要 build PySide6 我還得要先弄出 QT6 + clang,考量到最後不知道要花上多少心力跟時間才能 build 出 PySide6 就放棄了。patchelf 說穿了就是想辦法抽換掉底層的 libc library,因為在 CentOS7 的 OS 上測試時看到的問題就是 libc.so 的版本太舊。container 則更簡單明了一些,反正只要把 OS 相關的環境打包起來進 container,理想上執行時就不會有 libc 太舊的問題了。

首先嘗試的是用 patchelf,因為之前有試過 patchelf 抽換 libc library,但沒用過 container,而且被指定要使用 singularity [注2],所以打算把 container 當成備案。patchelf 要做的事情就兩件

1. patch interpreter & rpath
2. pack libc library

第 1 點簡單來說 OS 一般會用 /usr/lib64 底下的 ld.so 去解析 ELF 檔,利用 % patchelf  --set-interpreter 就能把 interpreter 抽換成指定的 ld.so。因此這邊要做的事情就是先蒐集好 RHEL8 的 libc library 版本 (第二點),然後用 patchelf 改掉 python 預設使用的 ld.so 變成 RHEL8 使用的 libc library 中的 ld.so [注3, 4] 即可。修改完 interpreter 路徑後,一樣利用 patchelf 去修改 RPATH 就能讓其找到指定版本的 libc library。

singularity 的作法也不難,先指定使用了 RHEL8 的 OS 的 docker image,然後再安裝所有 PySide6 會用到的 dependent library 即可。嘗試、了解及熟悉過程雖然花了些時間,但整體上並沒有太多的意外,意外總是發生在執行階段。

這次的意外源自於執行時不論是 patchelf 的方法還是 container 的方法都會遇到類似下面的這個錯誤訊息:

ImportError(QtCore): libQt6Core.so.6: cannot open shared object file: No such file or directory

嘗試利用 LD_PRELOAD 去強制載入的話會發現依然有問題 :

ERROR: ld.so: object '.../libQt6Core.so.6' from LD_PRELOAD cannot be preloaded (cannot open shared objec t file): ignored.

分析後簡單來說就是不知道為什麼 ld.so 無法載入 libQt6Core.so.6,利用 LD_DEBUG 去查看後發現其實 ld.so 有試著要去載入,但失敗。利用 ldd 去試著載入後會發現這兩種狀況:

1. % ldd libpyside6.abi3.so.6.8
    這樣會發現無法載入 libQt6Core.so.6 

2. % ldd libQt6Core.so.6 
    這樣會發現 ldd 有列出來 dependent library,而且也都有找到

最弔詭的是利用 container 去執行也依然會有相同的錯誤。至此,能猜測問題應該是出自於 OS 這一層的相容性問題,而且因為用 container 也無法避免,所以應該是某些即便利用了 container,但依然會使用到 host OS 的東西。但因為錯誤訊息實在太難以辨認是哪一個部分不相容,最後利用了 github copilot + Claude + stack overflow 後,判定問題是出在 Linux kernel version,也就是 ABI 相容性問題。解決方法就是把 libQt6Core.so.6 裡面寫入 ABI 版本確認的 section (.note.ABI-tag) 用 strip 拿掉即可 (% strip --remove-section=.note.ABI-tag libQt6Core.so.6)。

最後列上幾個可能有助於分析這類問題的工具:

1. readelf / objdump: 解析 ELF 檔的強力工具,只是如果沒有頭緒要看甚麼東西的話也會難以利用。常用的是看 dependent library 及版本相關資訊。只是這一次的狀況是要檢查 ABI 版本,也就是 Linux kernel version 就是。

2. uname: 會列出 machine, architecture, OS kernel 等等的基礎資訊。這次的經驗告訴我 library 都檢查玩了還是有問題,那就記得要來檢查這邊有沒有問題...


最後,目前覺得 AI 對我幫助最大的部分在這種冷僻問題的答案提示。當然,沒有 AI 也能辦到,但是目前市面上的這些 AI 工具能讓我相當大程度的減少搜索答案所需要的時間跟麻煩,雖然如果問題太過冷僻的話幫助也不大就是,但也能從中找到一些提示,進而找到解答,那也是很有幫助了。


[注釋]

1. 其實 pytho-3.11 依舊可以用 pip 安裝 PySide2,但是一跑起來就 crash,所以其實也不能用。

2. 後來發現 singularity 其實也能基於 docker image 來擴展,所以其實也沒有自己想像的那麼麻煩。

3. 這邊要修改的必須要是最一開始的執行檔,也就是 python 執行檔本身,而不是有相容性問題的 PySide6 library 而已,原因在於 ld.so, libc library 是在最一開始執行時就是被使用載入的。

4. 這邊有個小細節要注意,ld.so 的路徑必須要是絕對路徑,且不能是 symbolic link。用相對路徑會導致執行路徑更換時就找不到 ld.so,symbolic link 則是無法辨識因此也不能用。

沒有留言:

張貼留言