diff --git a/local-tool/server/scripts/kneron_bridge.py b/local-tool/server/scripts/kneron_bridge.py index 32357ec..bbcc85c 100644 --- a/local-tool/server/scripts/kneron_bridge.py +++ b/local-tool/server/scripts/kneron_bridge.py @@ -18,6 +18,47 @@ import io import numpy as np + +def _preload_kneron_dylibs_macos(): + """macOS 專用:用絕對路徑預先 dlopen wheel 內的 libusb + libkplus。 + + 背景: + - KneronPLUS wheel 把 libusb-1.0.0.dylib + libkplus.dylib 放在 kp/lib/。 + - macOS dyld 在載入 libkplus 時會去找它的相依 libusb-1.0.0.dylib。 + 預設搜尋路徑(/usr/local/lib、/usr/lib)在 bundled Python 環境下通常 + 找不到(我們沒有 brew libusb),於是 `import kp` 就拋 OSError → + HAS_KP=False → scan 回空陣列。 + - macOS hardened runtime 會剝掉 DYLD_LIBRARY_PATH 等環境變數,所以 + 改從 Go 端注入 env 也不保險;最穩的做法是在 Python 這端用 ctypes + 以絕對路徑先載入,後續 `import kp` 時 dyld 會重用已載入的映像。 + + Windows / Linux 不走這支 — 各自機制已在 Go 端處理(Windows 靠 PATH、 + Linux 靠 wheel 自帶的 libusb.so.1.0.0 + LD_LIBRARY_PATH)。 + """ + if sys.platform != "darwin": + return + try: + import ctypes + import importlib.util + spec = importlib.util.find_spec("kp") + if spec is None or not spec.submodule_search_locations: + return + kp_dir = spec.submodule_search_locations[0] + lib_dir = os.path.join(kp_dir, "lib") + # 載入順序:先 libusb,再 libkplus(libkplus 相依 libusb) + for name in ("libusb-1.0.0.dylib", "libkplus.dylib"): + path = os.path.join(lib_dir, name) + if os.path.isfile(path): + try: + ctypes.CDLL(path, mode=ctypes.RTLD_GLOBAL) + except OSError: + pass + except Exception: + pass + + +_preload_kneron_dylibs_macos() + try: import kp HAS_KP = True diff --git a/local-tool/visiona-local/app.go b/local-tool/visiona-local/app.go index edc8670..e7688c7 100644 --- a/local-tool/visiona-local/app.go +++ b/local-tool/visiona-local/app.go @@ -62,9 +62,9 @@ const ( type PythonMode string const ( - PythonModeAuto PythonMode = "auto" // 先試 system,失敗才走 bundled(R4 決策:M1 先 system) + PythonModeAuto PythonMode = "auto" // 先試 bundled(kp wheel 已預裝),失敗才 fallback system PythonModeBundled PythonMode = "bundled" // 策略 A:內嵌 python-build-standalone - PythonModeSystem PythonMode = "system" // 策略 B:系統 python3 + PythonModeSystem PythonMode = "system" // 策略 B:系統 python3(需使用者自行 pip install) ) // ServerStatus 回報給前端。 @@ -852,23 +852,27 @@ func (p *ServerProcess) stop() { // // M1 實作狀況: // - PythonModeSystem:完整實作(findSystemPython) -// - PythonModeAuto:先試 system,失敗才走 bundled -// - PythonModeBundled:placeholder,回錯誤(M2 才實作) +// - PythonModeAuto:先試 bundled(含預裝 kp wheel),失敗才 fallback system +// - PythonModeBundled:完整實作(ensureBundledPython) // // R5-5a 之後:python 失敗直接擋啟動(沒有模擬回退)。 +// +// 為什麼不先 system:Local-tool 的架構是整包內嵌 Python + KneronPLUS wheel, +// 使用者系統 python 絕大多數沒裝 kp,先 system 會拿到可執行但「import kp 失敗」 +// 的解譯器,導致 detector scan 空、表面卻看似啟動成功。 func (a *App) ensurePythonRuntime(mode PythonMode) (string, PythonMode, error) { if a.startupPipeline != nil { a.startupPipeline.EmitStageDetail(2, "startup.stage.2.detail.detect", 0) } switch mode { case PythonModeAuto: - if bin, err := a.findSystemPython(); err == nil { - return bin, PythonModeSystem, nil - } if bin, err := a.ensureBundledPython(); err == nil { return bin, PythonModeBundled, nil } - return "", PythonModeAuto, fmt.Errorf("no python runtime available (tried system + bundled)") + if bin, err := a.findSystemPython(); err == nil { + return bin, PythonModeSystem, nil + } + return "", PythonModeAuto, fmt.Errorf("no python runtime available (tried bundled + system)") case PythonModeSystem: bin, err := a.findSystemPython()