fix(cli): Apple Silicon skips an ffprobe or ffmpeg that cannot run and names it (#6760)

* fix(cli): Apple Silicon Macs without ffmpeg get working arm64 ffprobe and ffmpeg

* fix(cli): drop the bundled ffmpeg installers, name the x86-only binary it skipped
This commit is contained in:
Miguel Ángel
2026-10-29 23:20:11 +00:00
committed by GitHub
parent 3e462f4277
commit d81099b1ff
3 changed files with 95 additions and 6 deletions
+7 -1
View File
@@ -1,5 +1,5 @@
import { getCachedGpuEncoder } from "@hyperframes/engine";
import { findFfBinary } from "@hyperframes/parsers/ff-binaries";
import { findFfBinary, getSkippedX86Binary } from "@hyperframes/parsers/ff-binaries";
import { detectLinuxDistro, ffmpegInstallCommand } from "./linuxDeps.js";
import { runCancellableProcess } from "../utils/cancellableProcess.js";
@@ -96,6 +96,12 @@ export function getFFmpegInstallCommand(): string | undefined {
}
export function getFFmpegInstallHint(): string {
const skipped = getSkippedX86Binary();
const base = installHint();
return skipped ? `${base} (ignored ${skipped}: x86_64 only, it cannot run on this Mac)` : base;
}
function installHint(): string {
const command = getFFmpegInstallCommand();
// Guarding on `command`, not the platform alone: the function above is the
// sole owner of platform-to-command, so the day win32 stops returning one
+47 -2
View File
@@ -184,7 +184,7 @@ describe("findFfBinary", () => {
function installerProject(
name: "ffmpeg" | "ffprobe",
{ viaInstaller = false, mode = 0o755 } = {},
{ viaInstaller = false, mode = 0o755, content = "" as string | Buffer } = {},
) {
const project = realpathSync(mkdtempSync(join(tmpdir(), "hf-ff-installer-")));
const platformPackage = `@${name}-installer/${process.platform}-${process.arch}`;
@@ -205,7 +205,7 @@ describe("findFfBinary", () => {
);
}
const binary = join(packageDir, process.platform === "win32" ? `${name}.exe` : name);
writeFileSync(binary, "");
writeFileSync(binary, content);
chmodSync(binary, mode);
return { project, binary };
}
@@ -279,6 +279,29 @@ describe("findFfBinary", () => {
},
);
describe.skipIf(process.platform === "win32")("on Apple Silicon without Rosetta", () => {
const machO = (cpu: number) => Buffer.from([0xcf, 0xfa, 0xed, 0xfe, cpu, 0x00, 0x00, 0x01]);
function asAppleSilicon() {
Object.defineProperty(process, "platform", { value: "darwin", configurable: true });
vi.spyOn(process, "arch", "get").mockReturnValue("arm64");
}
it("skips an x86_64 installer binary", async () => {
asAppleSilicon();
const { project } = installerProject("ffprobe", { content: machO(0x07) });
expect(await findInProjectDir(project, "ffprobe")).toBeUndefined();
});
it("keeps an arm64 installer binary", async () => {
asAppleSilicon();
const { project, binary } = installerProject("ffprobe", { content: machO(0x0c) });
expect(await findInProjectDir(project, "ffprobe")).toBe(binary);
});
});
it("returns undefined when the binary is nowhere, and caches the miss until cleared", async () => {
delete process.env.HYPERFRAMES_FFMPEG_PATH;
Object.defineProperty(process, "platform", { value: "linux", configurable: true });
@@ -345,3 +368,25 @@ describe("findFfBinary", () => {
expect(execFileSync).toHaveBeenCalledTimes(2);
});
});
describe("isX86OnlyMachO", () => {
function fixture(bytes: number[]): string {
const file = join(realpathSync(mkdtempSync(join(tmpdir(), "hf-macho-"))), "ffprobe");
writeFileSync(file, Buffer.from(bytes));
onTestFinished(() => rmSync(file, { force: true }));
return file;
}
it("flags a 64-bit x86_64 Mach-O (the ffprobe-static darwin/arm64 payload)", async () => {
const { isX86OnlyMachO } = await importFresh();
expect(isX86OnlyMachO(fixture([0xcf, 0xfa, 0xed, 0xfe, 0x07, 0x00, 0x00, 0x01]))).toBe(true);
});
it("passes an arm64 Mach-O, a universal binary, a script and a missing file", async () => {
const { isX86OnlyMachO } = await importFresh();
expect(isX86OnlyMachO(fixture([0xcf, 0xfa, 0xed, 0xfe, 0x0c, 0x00, 0x00, 0x01]))).toBe(false);
expect(isX86OnlyMachO(fixture([0xca, 0xfe, 0xba, 0xbe, 0x00, 0x00, 0x00, 0x02]))).toBe(false);
expect(isX86OnlyMachO(fixture([0x23, 0x21, 0x2f, 0x62, 0x69, 0x6e, 0x2f, 0x73]))).toBe(false);
expect(isX86OnlyMachO("/nonexistent/ffprobe")).toBe(false);
});
});
+41 -3
View File
@@ -1,5 +1,5 @@
import { execFileSync } from "node:child_process";
import { accessSync, constants, existsSync } from "node:fs";
import { accessSync, closeSync, constants, existsSync, openSync, readSync } from "node:fs";
import { createRequire } from "node:module";
import { delimiter, dirname, join, resolve } from "node:path";
@@ -41,11 +41,48 @@ function chooseBestPathCandidate(
);
}
const MACHO_CPU_X86_64 = 0x01000007;
const ROSETTA_RUNTIME = "/Library/Apple/usr/libexec/oah/libRosettaRuntime";
// A Mach-O built only for x86_64 (some npm packages ship one under darwin/arm64). A universal
// binary or an unreadable file is not one.
export function isX86OnlyMachO(file: string): boolean {
const header = Buffer.alloc(8);
try {
const fd = openSync(file, "r");
try {
readSync(fd, header, 0, 8, 0);
} finally {
closeSync(fd);
}
} catch {
return false;
}
return header.readUInt32LE(0) === 0xfeedfacf && header.readUInt32LE(4) === MACHO_CPU_X86_64;
}
// Without Rosetta an Apple Silicon Mac cannot run an x86_64 binary, so it must not be chosen.
function runsOnThisMac(candidate: string): boolean {
if (process.platform !== "darwin" || process.arch !== "arm64" || existsSync(ROSETTA_RUNTIME)) {
return true;
}
if (!isX86OnlyMachO(candidate)) return true;
skippedX86Binary ??= candidate;
return false;
}
let skippedX86Binary: string | undefined;
/** The first x86_64-only binary skipped on this Mac, for naming in the "not found" hint. */
export function getSkippedX86Binary(): string | undefined {
return skippedX86Binary;
}
function isExecutablePathCandidate(candidate: string): boolean {
if (process.platform === "win32") return existsSync(candidate);
try {
accessSync(candidate, constants.X_OK);
return true;
return runsOnThisMac(candidate);
} catch {
return false;
}
@@ -95,7 +132,7 @@ const COMMON_BIN_DIRS =
function findInCommonDirsOrInstallerPackage(name: FfBinaryName): string | undefined {
for (const dir of COMMON_BIN_DIRS) {
const candidate = `${dir}/${name}`;
if (existsSync(candidate)) return candidate;
if (existsSync(candidate) && runsOnThisMac(candidate)) return candidate;
}
return findInInstallerPackage(name);
}
@@ -205,4 +242,5 @@ export function findFfBinary(
export function clearFfBinaryLookupCache(): void {
pathLookupCache.clear();
lastMissAt.clear();
skippedX86Binary = undefined;
}