From fba1a0142cb70329b8b077762e942bde7af91780 Mon Sep 17 00:00:00 2001 From: Jordan Date: Thu, 13 Aug 2026 13:02:30 -0700 Subject: [PATCH] Fix Linux OpenCV bundling in .mcpb and guard against silent reship MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- cli/src/lib/mcp-build.ts | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/cli/src/lib/mcp-build.ts b/cli/src/lib/mcp-build.ts index 8cd1c4a..5c1fa43 100644 --- a/cli/src/lib/mcp-build.ts +++ b/cli/src/lib/mcp-build.ts @@ -15,7 +15,7 @@ // the hand-written composite core. // ============================================================================= -import { existsSync, rmSync, cpSync, mkdtempSync, mkdirSync, readdirSync, chmodSync } from 'node:fs'; +import { existsSync, rmSync, cpSync, mkdtempSync, mkdirSync, readdirSync, chmodSync, readFileSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { tmpdir } from 'node:os'; import { spawnSync } from 'node:child_process'; @@ -84,7 +84,9 @@ function opencvLibDir(root: string): string | null { process.platform === 'darwin' ? [['Darwin', 'Release', 'macos', 'bin']] : // Must match the layout extract.ts writes: shared/opencv/Windows/x86_64/Release/bin process.platform === 'win32' ? [['Windows', 'x86_64', 'Release', 'bin'], ['Windows', 'Release', 'x64', 'bin'], ['Windows', 'x64', 'vc17', 'bin']] : - [['Linux', 'Release', 'x64', 'lib'], ['Linux', 'Release', 'aarch64', 'lib']]; + // Linux libs land flat in shared/opencv/Linux (extract.ts linuxSpec toRel); + // the Release/* paths are legacy layouts kept for older checkouts. + [['Linux'], ['Linux', 'Release', 'x64', 'lib'], ['Linux', 'Release', 'aarch64', 'lib']]; for (const rel of candidates) { const p = join(root, 'shared', 'opencv', ...rel); if (existsSync(p)) return p; @@ -92,6 +94,21 @@ function opencvLibDir(root: string): string | null { return null; } +/** + * Does the binary load OpenCV at runtime? Scans the image for the loader's + * library-name strings (ELF DT_NEEDED / Mach-O load commands / PE imports), + * which keeps this dependency-free instead of shelling out to readelf, otool + * or dumpbin. A statically linked build carries no such soname, so a miss here + * genuinely means "nothing to bundle". + */ +function linksOpenCv(bin: string): boolean { + const re = + process.platform === 'win32' ? /opencv_\w+\.dll/i : + process.platform === 'darwin' ? /libopencv_\w+\.dylib/ : + /libopencv_\w+\.so/; + return re.test(readFileSync(bin).toString('latin1')); +} + /** * Where the SDK looks for its USB/PTP transport plugins, relative to the dir * holding the executable and Cr_Core. @@ -174,11 +191,19 @@ function bundleServerBinary(root: string, stage: string): boolean { fail('No Sony SDK runtime libs found under shared/sdk/lib — the bundled binary would not run. Place the SDK ("crsdk install --zip") and retry.'); return false; } + // Never ship a bundle whose binary loads OpenCV dynamically with no OpenCV + // staged beside it. On the build machine that still runs — the binary's rpath + // falls through to the source tree — so this would otherwise only surface as + // an exit-127 loader failure on someone else's machine. + if (cvLibs === 0 && linksOpenCv(binOut)) { + fail(`The server binary loads OpenCV dynamically, but no OpenCV runtime libs were staged${cvDir ? ` (${cvDir} is empty)` : ' (no OpenCV dir found under shared/opencv)'}. The bundle would fail to start on any machine without this source tree. Place the SDK ("crsdk install --zip") and retry.`); + return false; + } codesignMac(serverOut); printKV('Binary', `server/${process.platform === 'win32' ? 'CameraWebApp.exe' : 'CameraWebApp'}`); printKV('SDK libs', `${sdkLibs} (incl. CrAdapter)`); - printKV('OpenCV libs', cvDir ? String(cvLibs) : colors.dim('not found — assuming static/linked')); + printKV('OpenCV libs', cvLibs > 0 ? String(cvLibs) : colors.dim('none needed — statically linked')); return true; }