Fix Linux OpenCV bundling in .mcpb and guard against silent reship - #51
Conversation
opencvLibDir() probed shared/opencv/Linux/Release/{x64,aarch64}/lib, but
extract.ts writes Linux libs flat to shared/opencv/Linux. The lookup always
missed, so pack:mcp took the "assuming static/linked" branch and shipped a
bundle with no OpenCV in it.
This stayed invisible because the binary's RUNPATH lists $ORIGIN first and the
developer's source tree second: on the build machine the loader simply fell
through to the checkout and found the libs there. On any other machine the
three DT_NEEDED OpenCV entries fail in the loader before main(), so the server
exits 127 and every camera tool fails — not just the OSD paths.
Point the Linux lookup at the layout extract.ts actually writes, and refuse to
pack when the binary loads OpenCV dynamically with nothing staged beside it,
so this cannot silently reship. The check scans the image for loader library
names rather than shelling out to readelf/otool/dumpbin.
Fixes #33
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
This supersedes a duplicate fix I had in #54 — I'd written the same change before this PR existed, and dropped my commit in favour of this one. Yours is verified on the platform that actually matters (clean-machine simulation, One refinement worth considering, from having written the same guard:
Comparing by module name closes it, and is a small change to what you already have: /** "libopencv_core.so.408" | "opencv_world4100.dll" | "libopencv_core.4.8.dylib" → "core" | "world". */
function opencvModule(file: string): string | null {
const m = /opencv_([a-z]+)/i.exec(file);
return m ? m[1].toLowerCase() : null;
}
/** OpenCV modules the binary loads dynamically, e.g. ["core","highgui","imgcodecs","imgproc"]. */
function opencvDynamicDeps(bin: string): string[] {
const pattern =
process.platform === 'darwin' ? /libopencv_[a-z0-9]+(?:\.\d+)*\.dylib/gi :
process.platform === 'win32' ? /opencv_[a-z0-9]+\.dll/gi :
/libopencv_[a-z0-9]+\.so(?:\.\d+)*/gi;
const found = readFileSync(bin).toString('latin1').match(pattern) ?? [];
return [...new Set(found.map(opencvModule).filter((m): m is string => m !== null))].sort();
}Then the guard compares needed against staged, and names what is missing: const needed = opencvDynamicDeps(binOut);
const staged = new Set(readdirSync(serverOut).map(opencvModule).filter(Boolean));
const missing = needed.filter((m) => !staged.has(m));
if (missing.length > 0) { fail(`… not in the bundle: ${missing.join(', ')}`); return false; }Matching on the module name rather than the full filename keeps library versioning from causing a false failure — Verified on macOS: this reports exactly Entirely your call whether it's worth the extra code — the boolean form already fixes the shipped bug, and partial staging is the rarer failure. Happy to send it as a follow-up PR on top of this one if you'd rather keep this merge small. |
Fixes #33.
The bug
opencvLibDir()probedshared/opencv/Linux/Release/{x64,aarch64}/lib, butextract.ts(linuxSpec,toRel: 'Linux') writes Linux libs flat toshared/opencv/Linux. The lookup always returnednull,pack:mcptook the silent "assuming static/linked" branch, and the shipped.mcpbcontained zero OpenCV libs.macOS and Windows agree by construction — only Linux was mismatched.
Why it went unnoticed
The binary's RUNPATH is
$ORIGIN:<repo>/shared/sdk/lib:<repo>/shared/opencv/Linux. On the build machine$ORIGINhas no OpenCV, so the loader falls through to the developer's source tree and finds it there. The bundle appears to work, and every MCP tool passes.On any machine without that checkout, the three
libopencv_*DT_NEEDEDentries fail in the loader beforemain():So the symptom is not "OSD rendering breaks" — the server never starts and every camera tool fails.
Changes
Both in
cli/src/lib/mcp-build.ts:opencvLibDir()— probeshared/opencv/Linuxfirst, keeping the twoRelease/*paths as legacy fallbacks (mirroring how Windows already carries fallbacks).linksOpenCv()guard —pack:mcpnow fails instead of shipping when the binary loads OpenCV dynamically and nothing was staged beside it. It scans the image for loader library names (libopencv_*.so/*.dylib/opencv_*.dll) rather than depending onreadelf/otool/dumpbin, so it works on all three platforms. The "assuming static/linked" wording became misleading and is now "none needed — statically linked", only reachable when the binary genuinely doesn't link OpenCV.Deliberately not included: stripping the absolute dev paths from RUNPATH. That's the masking bug rather than the bundling bug, and it deserves its own issue —
$ORIGINis already first, so this fix is correct without it.Test plan
Verified on linux-arm64 (Raspberry Pi), SDK V2.02.00, ILCE-7M5 over USB.
crsdk pack:mcpreportsOpenCV libs 4(was silently0); bundle contains all fourlibopencv_*.so.408$ORIGIN. All deps resolve,0unresolved. This is the exact setup that previously exited 127.1024x680, OSD composite720x480rendered correctly with full camera UI./proc/<pid>/mapson the running process confirmslibopencv_{core,imgcodecs,imgproc}mapped from the bundle, with 0 libs resolved from the source tree.pack:mcpabort with a clear message and exit1(CI-safe), instead of shipping a broken bundle. The previously good bundle is left untouched on failure.Note: the
terminate called without an active exceptionseen at shutdown is pre-existing #42 and unrelated to this change.