mirror of
https://github.com/webadderallorg/Recordly.git
synced 2026-09-24 14:55:37 +00:00
274 lines
7.8 KiB
TypeScript
274 lines
7.8 KiB
TypeScript
import { execFile } from "node:child_process";
|
|
import { existsSync, constants as fsConstants } from "node:fs";
|
|
import fs from "node:fs/promises";
|
|
import path from "node:path";
|
|
import { promisify } from "node:util";
|
|
import { app } from "electron";
|
|
import { nativeHelperMigrationPromise, setNativeHelperMigrationPromise } from "../state";
|
|
|
|
const execFileAsync = promisify(execFile);
|
|
|
|
/**
|
|
* Resolve a path within the app bundle, handling asar unpacking in production.
|
|
* Files listed in asarUnpack are extracted to app.asar.unpacked/ and must be
|
|
* accessed via that path instead of the asar virtual filesystem.
|
|
*/
|
|
export function resolveUnpackedAppPath(...segments: string[]): string {
|
|
const base = app.getAppPath();
|
|
const resolved = path.join(base, ...segments);
|
|
if (app.isPackaged) {
|
|
return resolved.replace(/\.asar([/\\])/, ".asar.unpacked$1");
|
|
}
|
|
return resolved;
|
|
}
|
|
|
|
export function getNativeCaptureHelperSourcePath(): string {
|
|
return resolveUnpackedAppPath("electron", "native", "ScreenCaptureKitRecorder.swift");
|
|
}
|
|
|
|
export function getNativeArchTag(platform: NodeJS.Platform = process.platform): string {
|
|
if (platform === "darwin") {
|
|
return process.arch === "arm64" ? "darwin-arm64" : "darwin-x64";
|
|
}
|
|
|
|
if (platform === "win32") {
|
|
return process.arch === "arm64" ? "win32-arm64" : "win32-x64";
|
|
}
|
|
|
|
if (platform === "linux") {
|
|
return process.arch === "arm64" ? "linux-arm64" : "linux-x64";
|
|
}
|
|
|
|
return `${platform}-${process.arch}`;
|
|
}
|
|
|
|
export function getPrebundledNativeHelperPath(
|
|
binaryName: string,
|
|
archTag = getNativeArchTag(),
|
|
): string {
|
|
return resolveUnpackedAppPath("electron", "native", "bin", archTag, binaryName);
|
|
}
|
|
|
|
export function resolvePreferredWindowsNativeHelperPath(
|
|
helperDirectory: string,
|
|
binaryName: string,
|
|
): string {
|
|
const buildOutputPath = resolveUnpackedAppPath(
|
|
"electron",
|
|
"native",
|
|
helperDirectory,
|
|
"build",
|
|
"Release",
|
|
binaryName,
|
|
);
|
|
const prebundledPath = getPrebundledNativeHelperPath(binaryName, getNativeArchTag("win32"));
|
|
|
|
if (app.isPackaged && existsSync(prebundledPath)) {
|
|
return prebundledPath;
|
|
}
|
|
|
|
// Source checkouts should run the helper staged in the branch instead of a
|
|
// stale local CMake build left over from an earlier test run.
|
|
if (existsSync(prebundledPath)) {
|
|
return prebundledPath;
|
|
}
|
|
|
|
if (existsSync(buildOutputPath)) {
|
|
return buildOutputPath;
|
|
}
|
|
|
|
return buildOutputPath;
|
|
}
|
|
|
|
export function getBundledWhisperExecutableCandidates(): string[] {
|
|
const binaryNames =
|
|
process.platform === "win32"
|
|
? ["whisper-cli.exe", "whisper-cpp.exe", "whisper.exe", "main.exe"]
|
|
: ["whisper-cli", "whisper-cpp", "whisper", "main"];
|
|
|
|
return binaryNames.map((binaryName) => getPrebundledNativeHelperPath(binaryName));
|
|
}
|
|
|
|
export function getNativeCaptureHelperBinaryPath(): string {
|
|
return path.join(app.getPath("userData"), "native-tools", "recordly-screencapturekit-helper");
|
|
}
|
|
|
|
export function getSystemCursorHelperSourcePath(): string {
|
|
return resolveUnpackedAppPath("electron", "native", "SystemCursorAssets.swift");
|
|
}
|
|
|
|
export function getSystemCursorHelperBinaryPath(): string {
|
|
return path.join(app.getPath("userData"), "native-tools", "recordly-system-cursors");
|
|
}
|
|
|
|
export function getNativeCursorMonitorSourcePath(): string {
|
|
return resolveUnpackedAppPath("electron", "native", "NativeCursorMonitor.swift");
|
|
}
|
|
|
|
export function getNativeCursorMonitorBinaryPath(): string {
|
|
return path.join(app.getPath("userData"), "native-tools", "recordly-native-cursor-monitor");
|
|
}
|
|
|
|
export function getNativeWindowListSourcePath(): string {
|
|
return resolveUnpackedAppPath("electron", "native", "ScreenCaptureKitWindowList.swift");
|
|
}
|
|
|
|
export function getNativeWindowListBinaryPath(): string {
|
|
return path.join(app.getPath("userData"), "native-tools", "recordly-window-list");
|
|
}
|
|
|
|
export function getWindowsCaptureExePath(): string {
|
|
return resolvePreferredWindowsNativeHelperPath("wgc-capture", "wgc-capture.exe");
|
|
}
|
|
|
|
export function getCursorMonitorExePath(): string {
|
|
return resolvePreferredWindowsNativeHelperPath("cursor-monitor", "cursor-monitor.exe");
|
|
}
|
|
|
|
async function migrateLegacyNativeHelperBinaries(): Promise<void> {
|
|
const legacyToCurrentPaths: Array<[string, string]> = [
|
|
[
|
|
path.join(
|
|
app.getPath("userData"),
|
|
"native-tools",
|
|
"openscreen-screencapturekit-helper",
|
|
),
|
|
getNativeCaptureHelperBinaryPath(),
|
|
],
|
|
[
|
|
path.join(app.getPath("userData"), "native-tools", "openscreen-window-list"),
|
|
getNativeWindowListBinaryPath(),
|
|
],
|
|
[
|
|
path.join(app.getPath("userData"), "native-tools", "openscreen-system-cursors"),
|
|
getSystemCursorHelperBinaryPath(),
|
|
],
|
|
[
|
|
path.join(app.getPath("userData"), "native-tools", "openscreen-native-cursor-monitor"),
|
|
getNativeCursorMonitorBinaryPath(),
|
|
],
|
|
];
|
|
|
|
for (const [legacyPath, currentPath] of legacyToCurrentPaths) {
|
|
if (legacyPath === currentPath || existsSync(currentPath) || !existsSync(legacyPath)) {
|
|
continue;
|
|
}
|
|
|
|
try {
|
|
await fs.mkdir(path.dirname(currentPath), { recursive: true });
|
|
await fs.rename(legacyPath, currentPath);
|
|
} catch (error) {
|
|
console.warn("[native-tools] Failed to migrate helper binary", {
|
|
legacyPath,
|
|
currentPath,
|
|
error,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
export async function ensureNativeHelperMigration(): Promise<void> {
|
|
if (!nativeHelperMigrationPromise) {
|
|
setNativeHelperMigrationPromise(
|
|
migrateLegacyNativeHelperBinaries().catch((error) => {
|
|
setNativeHelperMigrationPromise(null);
|
|
throw error;
|
|
}),
|
|
);
|
|
}
|
|
|
|
return nativeHelperMigrationPromise!;
|
|
}
|
|
|
|
export async function ensureSwiftHelperBinary(
|
|
sourcePath: string,
|
|
binaryPath: string,
|
|
label: string,
|
|
prebundledBinaryName?: string,
|
|
): Promise<string> {
|
|
if (prebundledBinaryName) {
|
|
const prebundledPath = getPrebundledNativeHelperPath(prebundledBinaryName);
|
|
try {
|
|
await fs.access(prebundledPath, fsConstants.X_OK);
|
|
if (app.isPackaged) {
|
|
return prebundledPath;
|
|
}
|
|
|
|
const [sourceStat, prebundledStat] = await Promise.all([
|
|
fs.stat(sourcePath),
|
|
fs.stat(prebundledPath),
|
|
]);
|
|
if (prebundledStat.mtimeMs >= sourceStat.mtimeMs) {
|
|
return prebundledPath;
|
|
}
|
|
} catch {
|
|
if (app.isPackaged) {
|
|
throw new Error(
|
|
`${label} is missing from this app build (${prebundledPath}). Reinstall or update the app.`,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
const helperDir = path.dirname(binaryPath);
|
|
await fs.mkdir(helperDir, { recursive: true });
|
|
|
|
let shouldCompile = false;
|
|
try {
|
|
const [sourceStat, binaryStat] = await Promise.all([
|
|
fs.stat(sourcePath),
|
|
fs.stat(binaryPath).catch(() => null),
|
|
]);
|
|
shouldCompile = !binaryStat || sourceStat.mtimeMs > binaryStat.mtimeMs;
|
|
} catch (error) {
|
|
throw new Error(`${label} source is unavailable: ${String(error)}`);
|
|
}
|
|
|
|
if (!shouldCompile) {
|
|
return binaryPath;
|
|
}
|
|
|
|
try {
|
|
await execFileAsync("swiftc", ["-O", sourcePath, "-o", binaryPath], {
|
|
encoding: "utf8",
|
|
timeout: 120000,
|
|
});
|
|
} catch (error) {
|
|
const err = error as NodeJS.ErrnoException & { stdout?: string; stderr?: string };
|
|
const details = [err.stderr, err.stdout].filter(Boolean).join("\n").trim();
|
|
throw new Error(details || `Failed to compile ${label}`);
|
|
}
|
|
|
|
return binaryPath;
|
|
}
|
|
|
|
export async function ensureNativeCaptureHelperBinary(): Promise<string> {
|
|
await ensureNativeHelperMigration();
|
|
return ensureSwiftHelperBinary(
|
|
getNativeCaptureHelperSourcePath(),
|
|
getNativeCaptureHelperBinaryPath(),
|
|
"native ScreenCaptureKit helper",
|
|
"recordly-screencapturekit-helper",
|
|
);
|
|
}
|
|
|
|
export async function ensureNativeWindowListBinary(): Promise<string> {
|
|
await ensureNativeHelperMigration();
|
|
return ensureSwiftHelperBinary(
|
|
getNativeWindowListSourcePath(),
|
|
getNativeWindowListBinaryPath(),
|
|
"native ScreenCaptureKit window list helper",
|
|
"recordly-window-list",
|
|
);
|
|
}
|
|
|
|
export async function ensureNativeCursorMonitorBinary(): Promise<string> {
|
|
await ensureNativeHelperMigration();
|
|
return ensureSwiftHelperBinary(
|
|
getNativeCursorMonitorSourcePath(),
|
|
getNativeCursorMonitorBinaryPath(),
|
|
"native cursor monitor helper",
|
|
"recordly-native-cursor-monitor",
|
|
);
|
|
}
|