Skip to content

Commit 6467141

Browse files
committed
fix(build): harden macOS ffmpeg dylib staging
1 parent 57b141a commit 6467141

3 files changed

Lines changed: 52 additions & 15 deletions

File tree

‎scripts/before-pack.cjs‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,12 @@ const MAC_REQUIRED = [
105105
breaks: "the compositor addon cannot be loaded at all (dyld error at require())",
106106
fix: FIX_MAC,
107107
})),
108+
{
109+
match: (name) => /^libavdevice\.\d+\.dylib$/.test(name),
110+
what: "the LGPL libavdevice dylib the ffmpeg CLI links",
111+
breaks: "ffmpeg dies in dyld before main(), so waveform and STT extraction cannot start",
112+
fix: FIX_MAC,
113+
},
108114
{
109115
match: (name) => name === "whisper-stt-server",
110116
what: "the whisper.cpp STT helper",

‎scripts/before-pack.test.mjs‎

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -311,6 +311,7 @@ describe("MAC_REQUIRED", () => {
311311
"whisper-stt-server",
312312
"libggml-base.dylib",
313313
"openscreen-screencapturekit-helper",
314+
"libavdevice.62.dylib",
314315
...["avcodec", "avformat", "avutil", "swresample", "swscale", "avfilter"].map(
315316
(lib, i) => `lib${lib}.${62 - i}.dylib`,
316317
),
@@ -328,4 +329,31 @@ describe("MAC_REQUIRED", () => {
328329
).toThrow(/ffmpeg CLI/);
329330
});
330331
});
332+
333+
it("refuses a payload whose ffmpeg CLI is missing libavdevice", () => {
334+
const { MAC_REQUIRED, checkNativePayload } = testing();
335+
const files = Object.fromEntries(
336+
[
337+
"compositor_view.node",
338+
"ffmpeg",
339+
"whisper-stt-server",
340+
"libggml-base.dylib",
341+
"openscreen-screencapturekit-helper",
342+
...["avcodec", "avformat", "avutil", "swresample", "swscale", "avfilter"].map(
343+
(lib, i) => `lib${lib}.${62 - i}.dylib`,
344+
),
345+
].map((name) => [name, Buffer.from("x")]),
346+
);
347+
withPayload(files, (dir) => {
348+
expect(() =>
349+
checkNativePayload({
350+
dir,
351+
required: MAC_REQUIRED,
352+
osLabel: "macOS",
353+
bundleNoun: "the .app",
354+
emptyDirFix: "unused",
355+
}),
356+
).toThrow(/libavdevice/);
357+
});
358+
});
331359
});

‎scripts/build-macos-compositor-addon.mjs‎

Lines changed: 18 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -270,24 +270,25 @@ function stageFfmpegBinary(outDir, ffmpegDir) {
270270
throw new Error(`${to} links no ffmpeg dylib — nothing to rewrite, which is wrong.`);
271271
}
272272
// The CLI links more of the SDK than the addon does (libavdevice, libpostproc, …).
273-
// `vendorFfmpegDylibs` staged only what the addon needs, so copy anything still
274-
// missing — with the same `@rpath/<name>` id and inter-library rewrites, so the
275-
// chain resolves the same way the addon's set does.
276-
const copied = [];
277-
for (const dep of deps) {
273+
// Refresh every direct dependency, including files vendorFfmpegDylibs already staged.
274+
// The output directory persists between local builds, so keeping an existing file could
275+
// mix a new CLI with an old SDK dylib or preserve a half-rewritten file from an interrupted
276+
// run. Replacing the complete set also gives every file the same atomic-install guarantee
277+
// as the addon and CLI.
278+
const stagedLibraries = [];
279+
for (const dep of new Set(deps)) {
278280
const name = path.basename(dep);
279281
const staged = path.join(outDir, name);
280-
if (fs.existsSync(staged)) continue;
281282
const sdkLib = path.join(ffmpegDir, "lib", name);
282283
if (!fs.existsSync(sdkLib)) {
283284
throw new Error(`Missing ${sdkLib}; the binary links it but the SDK does not ship it.`);
284285
}
285-
fs.copyFileSync(sdkLib, staged);
286+
installAtomically(sdkLib, staged);
286287
fs.chmodSync(staged, 0o755);
287288
execFileSync("install_name_tool", ["-id", `@rpath/${name}`, staged]);
288-
copied.push(staged);
289+
stagedLibraries.push(staged);
289290
}
290-
for (const file of [...copied, to]) {
291+
for (const file of [...stagedLibraries, to]) {
291292
const fileDeps = execFileSync("otool", ["-L", file], { encoding: "utf8" })
292293
.split("\n")
293294
.map((line) => line.trim().split(" ")[0])
@@ -300,12 +301,14 @@ function stageFfmpegBinary(outDir, ffmpegDir) {
300301
execFileSync("codesign", ["--force", "--sign", "-", file]);
301302
}
302303

303-
const remaining = absolutePaths(to);
304-
if (remaining.length > 0) {
305-
throw new Error(
306-
`${path.basename(to)} still references build-machine paths after rewriting: ` +
307-
remaining.join(", "),
308-
);
304+
for (const file of [...stagedLibraries, to]) {
305+
const remaining = absolutePaths(file);
306+
if (remaining.length > 0) {
307+
throw new Error(
308+
`${path.basename(file)} still references build-machine paths after rewriting: ` +
309+
remaining.join(", "),
310+
);
311+
}
309312
}
310313
console.log(`Staged ffmpeg binary at ${to} (rewritten to @rpath beside the vendored dylibs)`);
311314
}

0 commit comments

Comments
 (0)