Files
Firelink/scripts/verify-binaries.js
T
NimBold a8b792074f fix(media): harden YouTube metadata loading
Bundle the self-contained yt-dlp onedir runtime to avoid per-request extraction latency, explicitly use bundled Deno, and guard the packaging layout. Also fix Add window menu/reset behavior and harden diagnostics and toast lifecycle handling.
2026-06-20 23:57:13 +03:30

334 lines
9.2 KiB
JavaScript

#!/usr/bin/env node
import fs from 'node:fs';
import path from 'node:path';
import os from 'node:os';
import { execFileSync, spawn } from 'node:child_process';
import { fileURLToPath } from 'node:url';
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const archMap = { x64: 'x86_64', arm64: 'aarch64' };
const platformMap = {
darwin: 'apple-darwin',
win32: 'pc-windows-msvc',
linux: 'unknown-linux-gnu',
};
const currentArch = archMap[os.arch()];
const currentPlatform = platformMap[os.platform()];
if (!currentArch || !currentPlatform) {
console.error(`Unsupported architecture or platform: ${os.arch()} / ${os.platform()}`);
process.exit(1);
}
const targetTriple = `${currentArch}-${currentPlatform}`;
const isWindows = os.platform() === 'win32';
const isMacOS = os.platform() === 'darwin';
const ext = isWindows ? '.exe' : '';
const suffix = `-${targetTriple}${ext}`;
const scriptsDir = __dirname;
const binariesDir = path.join(scriptsDir, '..', 'src-tauri', 'binaries');
const requiredEngines = ['yt-dlp', 'aria2c', 'ffmpeg', 'deno'];
const FORBIDDEN_OTOOL_PATHS = ['/opt/homebrew', '/usr/local/Cellar'];
const FORBIDDEN_STDERR = [
'Failed to load Python shared library',
'Library not loaded',
'image not found',
'Connection refused',
];
let exitCode = 0;
function fail(msg) {
console.error(`[FAIL] ${msg}`);
exitCode = 1;
}
function ok(msg) {
console.log(`[OK] ${msg}`);
}
function binName(engine) {
return `${engine}${suffix}`;
}
function binPath(engine) {
return path.join(binariesDir, binName(engine));
}
// ───── Check 1: Sidecar existence ─────
console.log(`\n─── 1. Sidecar existence (${targetTriple}) ───`);
for (const eng of requiredEngines) {
const p = binPath(eng);
if (fs.existsSync(p)) {
ok(`Found ${binName(eng)}`);
} else {
fail(`Missing ${binName(eng)}`);
}
}
if (exitCode !== 0) {
console.error('\nAborting: missing required sidecars.');
process.exit(1);
}
// ───── Check 2: Executable permission ─────
console.log('\n─── 2. Executable permission ───');
for (const eng of requiredEngines) {
try {
fs.accessSync(binPath(eng), fs.constants.X_OK);
ok(`Executable ${binName(eng)}`);
} catch {
fail(`Not executable ${binName(eng)}`);
}
}
// ───── Check 3: file(1) identification ─────
console.log('\n─── 3. file(1) identification ───');
for (const eng of requiredEngines) {
try {
const out = execFileSync('file', ['--brief', binPath(eng)], {
encoding: 'utf-8',
timeout: 5000,
}).trim();
ok(`${binName(eng)}: ${out}`);
} catch (e) {
fail(`file ${binName(eng)}: ${e.message}`);
}
}
// ───── Check 4 & 5: otool -L linkage (macOS only) ─────
if (isMacOS) {
console.log('\n─── 4 & 5. otool -L linkage check ───');
for (const eng of requiredEngines) {
const p = binPath(eng);
try {
const out = execFileSync('otool', ['-L', p], {
encoding: 'utf-8',
timeout: 10000,
});
const lines = out
.split('\n')
.slice(1)
.map((l) => l.trim())
.filter(Boolean);
let hasBad = false;
for (const fp of FORBIDDEN_OTOOL_PATHS) {
for (const line of lines) {
if (line.includes(fp)) {
fail(`${binName(eng)} links to '${fp}': ${line}`);
hasBad = true;
}
}
}
if (!hasBad) {
ok(`${binName(eng)}: no local-only dylib paths`);
}
} catch (e) {
fail(`otool -L ${binName(eng)}: ${e.message}`);
}
}
}
// ───── Check 6: yt-dlp packaging ─────
console.log('\n─── 6. yt-dlp packaging ───');
{
const yt = binPath('yt-dlp');
if (!fs.existsSync(yt)) {
fail('yt-dlp binary not found, cannot verify packaging');
} else {
const internalDir = path.join(binariesDir, '_internal');
const hasInternal = fs.existsSync(internalDir) && fs.statSync(internalDir).isDirectory();
if (hasInternal) {
console.log(' Detected PyInstaller onedir layout (_internal/ present)');
let entries;
try {
entries = fs.readdirSync(internalDir);
ok(`_internal/ directory exists with ${entries.length} entries`);
} catch (e) {
fail(`Cannot read _internal/: ${e.message}`);
}
if (entries) {
let broken = 0;
for (const entry of entries) {
const ep = path.join(internalDir, entry);
if (fs.lstatSync(ep).isSymbolicLink()) {
try {
fs.accessSync(ep);
} catch {
fail(`Broken symlink in _internal/: ${entry}`);
broken++;
}
}
}
if (broken === 0) {
ok('_internal/ symlinks valid');
}
}
const requiredRuntimeFiles = [
path.join(internalDir, 'Python'),
path.join(internalDir, 'yt_dlp_ejs', 'yt', 'solver', 'core.min.js'),
path.join(internalDir, 'yt_dlp_ejs', 'yt', 'solver', 'lib.min.js'),
];
for (const required of requiredRuntimeFiles) {
if (fs.existsSync(required)) {
ok(`yt-dlp runtime component: ${path.relative(binariesDir, required)}`);
} else {
fail(`Missing yt-dlp runtime component: ${path.relative(binariesDir, required)}`);
}
}
} else {
fail('yt-dlp must use the self-contained onedir distribution; onefile adds ~17 seconds to every launch');
}
}
}
// ───── Check 7, 8 & 9: Engine version self-tests ─────
console.log('\n─── 7 & 8 & 9. Engine version self-tests ───');
function runEngine(label, engine, args, timeout = 30000) {
const p = binPath(engine);
if (!fs.existsSync(p)) {
fail(`${label} binary not found at ${p}`);
return;
}
let stderr = '';
try {
const stdout = execFileSync(p, args, {
encoding: 'utf-8',
timeout,
stdio: ['ignore', 'pipe', 'pipe'],
});
const firstLine = stdout.trim().split('\n')[0];
ok(`${label} version: ${firstLine}`);
} catch (e) {
stderr = e.stderr || '';
const stdout = e.stdout || '';
const detail = stdout.trim().split('\n')[0] || e.message;
fail(`${label} execution failed: ${detail}`);
}
if (stderr) {
for (const pattern of FORBIDDEN_STDERR) {
if (stderr.includes(pattern)) {
fail(`${label} stderr contains '${pattern}'`);
}
}
}
}
runEngine('yt-dlp cold start', 'yt-dlp', ['--version'], 20000);
runEngine('yt-dlp warm start', 'yt-dlp', ['--version'], 8000);
runEngine('ffmpeg', 'ffmpeg', ['-version']);
runEngine('deno', 'deno', ['--version']);
runEngine('aria2c', 'aria2c', ['--version']);
// ───── aria2 RPC smoke test (macOS only) ─────
if (isMacOS) {
console.log('\n─── aria2 RPC smoke test ───');
await (async function testAria2Rpc() {
const p = binPath('aria2c');
if (!fs.existsSync(p)) {
fail('aria2c binary not found, cannot run RPC test');
return;
}
const port = 16801 + (process.pid % 1000);
const proc = spawn(p, [
'--enable-rpc',
`--rpc-listen-port=${port}`,
'--rpc-max-request-size=1K',
'--quiet',
'--console-log-level=error',
'--rpc-listen-all=false',
], {
stdio: ['ignore', 'pipe', 'pipe'],
timeout: 15000,
});
let rpcStderr = '';
proc.stderr.on('data', (d) => {
rpcStderr += d.toString();
});
const body = JSON.stringify({
jsonrpc: '2.0',
id: 'firelink-verify',
method: 'aria2.getVersion',
params: [],
});
const result = await new Promise((resolve) => {
const maxAttempts = 20;
let attempts = 0;
function tryFetch() {
attempts++;
fetch(`http://127.0.0.1:${port}/jsonrpc`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body,
})
.then(async (res) => {
resolve({ ok: true, data: await res.text() });
})
.catch(() => {
if (attempts >= maxAttempts) {
resolve({ ok: false, error: `RPC not ready after ${maxAttempts} attempts` });
return;
}
setTimeout(tryFetch, 300);
});
}
tryFetch();
});
// Clean up
proc.kill('SIGTERM');
setTimeout(() => {
try {
proc.kill('SIGKILL');
} catch {}
}, 2000);
if (result.ok) {
try {
const resp = JSON.parse(result.data);
if (resp?.result?.version) {
ok(`aria2 RPC version: ${resp.result.version}`);
} else {
fail(`aria2 RPC unexpected response: ${result.data}`);
}
} catch (e) {
fail(`aria2 RPC parse error: ${e.message}`);
}
} else {
fail(result.error);
}
if (rpcStderr) {
for (const pattern of FORBIDDEN_STDERR) {
if (rpcStderr.includes(pattern)) {
fail(`aria2 RPC stderr contains '${pattern}'`);
}
}
}
})();
}
// ───── Result ─────
console.log('');
if (exitCode !== 0) {
console.error(`[FAIL] ${exitCode} engine verification check(s) failed.`);
process.exit(1);
} else {
console.log('[PASS] All engine verification checks passed.');
process.exit(0);
}