mirror of
https://github.com/heygen-com/hyperframes.git
synced 2026-10-30 09:16:55 +00:00
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:
@@ -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
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user