mirror of
https://github.com/nimbold/Firelink.git
synced 2026-08-29 03:57:11 +00:00
fix(macos): unify bundled yt-dlp detection
This commit is contained in:
@@ -179,7 +179,7 @@ console.log('\n─── 6. yt-dlp packaging ───');
|
|||||||
// ───── Check 7, 8 & 9: Engine version self-tests ─────
|
// ───── Check 7, 8 & 9: Engine version self-tests ─────
|
||||||
console.log('\n─── 7 & 8 & 9. Engine version self-tests ───');
|
console.log('\n─── 7 & 8 & 9. Engine version self-tests ───');
|
||||||
|
|
||||||
function runEngine(label, engine, args) {
|
function runEngine(label, engine, args, timeout = 30000) {
|
||||||
const p = binPath(engine);
|
const p = binPath(engine);
|
||||||
if (!fs.existsSync(p)) {
|
if (!fs.existsSync(p)) {
|
||||||
fail(`${label} binary not found at ${p}`);
|
fail(`${label} binary not found at ${p}`);
|
||||||
@@ -189,7 +189,7 @@ function runEngine(label, engine, args) {
|
|||||||
try {
|
try {
|
||||||
const stdout = execFileSync(p, args, {
|
const stdout = execFileSync(p, args, {
|
||||||
encoding: 'utf-8',
|
encoding: 'utf-8',
|
||||||
timeout: 30000,
|
timeout,
|
||||||
stdio: ['ignore', 'pipe', 'pipe'],
|
stdio: ['ignore', 'pipe', 'pipe'],
|
||||||
});
|
});
|
||||||
const firstLine = stdout.trim().split('\n')[0];
|
const firstLine = stdout.trim().split('\n')[0];
|
||||||
@@ -209,7 +209,7 @@ function runEngine(label, engine, args) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
runEngine('yt-dlp', 'yt-dlp', ['--version']);
|
runEngine('yt-dlp', 'yt-dlp', ['--version'], 45000);
|
||||||
runEngine('ffmpeg', 'ffmpeg', ['-version']);
|
runEngine('ffmpeg', 'ffmpeg', ['-version']);
|
||||||
runEngine('deno', 'deno', ['--version']);
|
runEngine('deno', 'deno', ['--version']);
|
||||||
runEngine('aria2c', 'aria2c', ['--version']);
|
runEngine('aria2c', 'aria2c', ['--version']);
|
||||||
|
|||||||
+166
-56
@@ -257,7 +257,9 @@ async fn fetch_media_metadata(app_handle: tauri::AppHandle, url: String, cookie_
|
|||||||
let path_env = format!("{}:/usr/bin:/bin", bin_dir_str);
|
let path_env = format!("{}:/usr/bin:/bin", bin_dir_str);
|
||||||
|
|
||||||
use tauri_plugin_shell::ShellExt;
|
use tauri_plugin_shell::ShellExt;
|
||||||
let mut cmd = app_handle.shell().sidecar("yt-dlp").map_err(|e| format!("Failed to create sidecar yt-dlp: {}", e))?;
|
let ytdlp_path = resolve_bundled_binary_path(&app_handle, "yt-dlp")
|
||||||
|
.map_err(|e| format!("failed to find bundled yt-dlp: {e}"))?;
|
||||||
|
let mut cmd = app_handle.shell().command(&ytdlp_path);
|
||||||
cmd = cmd.env("PATH", &path_env)
|
cmd = cmd.env("PATH", &path_env)
|
||||||
.arg("--dump-json")
|
.arg("--dump-json")
|
||||||
.arg("--no-warnings")
|
.arg("--no-warnings")
|
||||||
@@ -330,27 +332,11 @@ async fn fetch_media_metadata(app_handle: tauri::AppHandle, url: String, cookie_
|
|||||||
#[tauri::command]
|
#[tauri::command]
|
||||||
async fn test_ytdlp(app_handle: tauri::AppHandle) -> Result<String, String> {
|
async fn test_ytdlp(app_handle: tauri::AppHandle) -> Result<String, String> {
|
||||||
println!("test_ytdlp called!");
|
println!("test_ytdlp called!");
|
||||||
use tauri_plugin_shell::ShellExt;
|
let (version, error, _) = run_sidecar_version(&app_handle, "yt-dlp", &["--version"]).await;
|
||||||
|
match (version, error) {
|
||||||
let output = app_handle.shell().sidecar("yt-dlp")
|
(Some(version), None) => Ok(version),
|
||||||
.map_err(|e| e.to_string())?
|
(_, Some(error)) => Err(error),
|
||||||
.arg("--version")
|
_ => Err("yt-dlp returned no version output".to_string()),
|
||||||
.output()
|
|
||||||
.await
|
|
||||||
.map_err(|e| {
|
|
||||||
println!("Failed to execute: {}", e);
|
|
||||||
format!("Failed to execute yt-dlp: {}", e)
|
|
||||||
})?;
|
|
||||||
|
|
||||||
println!("yt-dlp execution finished with status: {:?}", output.status);
|
|
||||||
if output.status.success() {
|
|
||||||
let text = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
|
||||||
println!("yt-dlp output: {}", text);
|
|
||||||
Ok(text)
|
|
||||||
} else {
|
|
||||||
let err = String::from_utf8_lossy(&output.stderr);
|
|
||||||
println!("yt-dlp error output: {}", err);
|
|
||||||
Err(format!("yt-dlp error: {}", err))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -359,8 +345,8 @@ async fn test_ffmpeg(app_handle: tauri::AppHandle) -> Result<String, String> {
|
|||||||
println!("test_ffmpeg called!");
|
println!("test_ffmpeg called!");
|
||||||
use tauri_plugin_shell::ShellExt;
|
use tauri_plugin_shell::ShellExt;
|
||||||
|
|
||||||
let output = app_handle.shell().sidecar("ffmpeg")
|
let binary_path = resolve_bundled_binary_path(&app_handle, "ffmpeg")?;
|
||||||
.map_err(|e| e.to_string())?
|
let output = app_handle.shell().command(&binary_path)
|
||||||
.arg("-version")
|
.arg("-version")
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
@@ -395,8 +381,8 @@ async fn test_deno(app_handle: tauri::AppHandle) -> Result<String, String> {
|
|||||||
println!("test_deno called!");
|
println!("test_deno called!");
|
||||||
use tauri_plugin_shell::ShellExt;
|
use tauri_plugin_shell::ShellExt;
|
||||||
|
|
||||||
let output = app_handle.shell().sidecar("deno")
|
let binary_path = resolve_bundled_binary_path(&app_handle, "deno")?;
|
||||||
.map_err(|e| e.to_string())?
|
let output = app_handle.shell().command(&binary_path)
|
||||||
.arg("--version")
|
.arg("--version")
|
||||||
.output()
|
.output()
|
||||||
.await
|
.await
|
||||||
@@ -737,28 +723,64 @@ async fn run_sidecar_version(
|
|||||||
) -> (Option<String>, Option<String>, Option<String>) {
|
) -> (Option<String>, Option<String>, Option<String>) {
|
||||||
let binary_path = match resolve_bundled_binary_path(app_handle, sidecar_name) {
|
let binary_path = match resolve_bundled_binary_path(app_handle, sidecar_name) {
|
||||||
Ok(p) => p,
|
Ok(p) => p,
|
||||||
Err(e) => return (None, Some(format!("Cannot find '{}': {}", sidecar_name, e)), None),
|
Err(e) => return (None, Some(format!("Missing bundled binary '{}': {}", sidecar_name, e)), None),
|
||||||
};
|
};
|
||||||
|
|
||||||
let bin = binary_path.clone();
|
if let Err(error) = validate_bundled_binary(&binary_path) {
|
||||||
let arg_owned: Vec<String> = args.iter().map(|s| s.to_string()).collect();
|
return (None, Some(error), None);
|
||||||
|
}
|
||||||
|
|
||||||
let result = tokio::time::timeout(std::time::Duration::from_secs(5), async {
|
let cache_key = version_cache_key(&binary_path, args);
|
||||||
tokio::process::Command::new(&bin)
|
if let Ok(cache) = version_check_cache().lock() {
|
||||||
.args(&arg_owned)
|
if let Some(cached) = cache.get(&cache_key) {
|
||||||
.output().await
|
return cached.clone();
|
||||||
}).await;
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut command = tokio::process::Command::new(&binary_path);
|
||||||
|
command.args(args).kill_on_drop(true);
|
||||||
|
let output_future = command.output();
|
||||||
|
tokio::pin!(output_future);
|
||||||
|
|
||||||
|
const NORMAL_VERSION_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(5);
|
||||||
|
const YTDLP_FIRST_RUN_GRACE: std::time::Duration = std::time::Duration::from_secs(40);
|
||||||
|
|
||||||
|
let result = match tokio::time::timeout(NORMAL_VERSION_TIMEOUT, &mut output_future).await {
|
||||||
|
Ok(result) => Ok(result),
|
||||||
|
Err(_) if sidecar_name == "yt-dlp" && !ytdlp_expects_internal_dir(&binary_path) => {
|
||||||
|
log::info!(
|
||||||
|
"Bundled yt-dlp version check exceeded {} seconds; allowing {} seconds for standalone first-run initialization",
|
||||||
|
NORMAL_VERSION_TIMEOUT.as_secs(),
|
||||||
|
YTDLP_FIRST_RUN_GRACE.as_secs()
|
||||||
|
);
|
||||||
|
tokio::time::timeout(YTDLP_FIRST_RUN_GRACE, &mut output_future)
|
||||||
|
.await
|
||||||
|
.map_err(|_| NORMAL_VERSION_TIMEOUT + YTDLP_FIRST_RUN_GRACE)
|
||||||
|
}
|
||||||
|
Err(_) => Err(NORMAL_VERSION_TIMEOUT),
|
||||||
|
};
|
||||||
|
|
||||||
let output = match result {
|
let output = match result {
|
||||||
Ok(Ok(o)) => o,
|
Ok(Ok(output)) => output,
|
||||||
Ok(Err(e)) => return (None, Some(format!("Failed to execute '{}': {}", sidecar_name, e)), None),
|
Ok(Err(e)) => return (None, Some(format!("Failed to execute '{}': {}", sidecar_name, e)), None),
|
||||||
Err(_) => return (None, Some(format!("'{}' version check timed out after 5 seconds", sidecar_name)), None),
|
Err(timeout) => {
|
||||||
|
return (
|
||||||
|
None,
|
||||||
|
Some(format!(
|
||||||
|
"'{}' version check timed out after {} seconds at '{}'",
|
||||||
|
sidecar_name,
|
||||||
|
timeout.as_secs(),
|
||||||
|
binary_path.display()
|
||||||
|
)),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string();
|
||||||
let stderr_tail = if stderr.is_empty() { None } else { Some(stderr.clone()) };
|
let stderr_tail = if stderr.is_empty() { None } else { Some(stderr.clone()) };
|
||||||
|
|
||||||
if output.status.success() {
|
let result = if output.status.success() {
|
||||||
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string();
|
||||||
(Some(stdout), None, stderr_tail)
|
(Some(stdout), None, stderr_tail)
|
||||||
} else {
|
} else {
|
||||||
@@ -769,7 +791,95 @@ async fn run_sidecar_version(
|
|||||||
format!("Exited with code {:?}", output.status.code())
|
format!("Exited with code {:?}", output.status.code())
|
||||||
};
|
};
|
||||||
(if stdout.is_empty() { None } else { Some(stdout) }, Some(err), stderr_tail)
|
(if stdout.is_empty() { None } else { Some(stdout) }, Some(err), stderr_tail)
|
||||||
|
};
|
||||||
|
|
||||||
|
if result.1.is_none() {
|
||||||
|
if let Ok(mut cache) = version_check_cache().lock() {
|
||||||
|
cache.insert(cache_key, result.clone());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
type VersionCheckResult = (Option<String>, Option<String>, Option<String>);
|
||||||
|
|
||||||
|
fn version_check_cache(
|
||||||
|
) -> &'static std::sync::Mutex<std::collections::HashMap<String, VersionCheckResult>> {
|
||||||
|
static CACHE: std::sync::OnceLock<
|
||||||
|
std::sync::Mutex<std::collections::HashMap<String, VersionCheckResult>>,
|
||||||
|
> = std::sync::OnceLock::new();
|
||||||
|
CACHE.get_or_init(|| std::sync::Mutex::new(std::collections::HashMap::new()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn version_cache_key(binary_path: &std::path::Path, args: &[&str]) -> String {
|
||||||
|
let modified = std::fs::metadata(binary_path)
|
||||||
|
.and_then(|metadata| metadata.modified())
|
||||||
|
.ok()
|
||||||
|
.and_then(|modified| modified.duration_since(std::time::UNIX_EPOCH).ok())
|
||||||
|
.map(|duration| duration.as_nanos())
|
||||||
|
.unwrap_or_default();
|
||||||
|
format!("{}:{modified}:{}", binary_path.display(), args.join("\u{1f}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ytdlp_expects_internal_dir(binary_path: &std::path::Path) -> bool {
|
||||||
|
binary_path
|
||||||
|
.parent()
|
||||||
|
.is_some_and(|parent| parent.join("_internal").is_dir())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn validate_bundled_binary(binary_path: &std::path::Path) -> Result<(), String> {
|
||||||
|
let metadata = std::fs::metadata(binary_path).map_err(|error| {
|
||||||
|
format!(
|
||||||
|
"Missing bundled binary at '{}': {error}",
|
||||||
|
binary_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
if !metadata.is_file() {
|
||||||
|
return Err(format!(
|
||||||
|
"Bundled binary path is not a file: '{}'",
|
||||||
|
binary_path.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(unix)]
|
||||||
|
{
|
||||||
|
use std::os::unix::fs::PermissionsExt;
|
||||||
|
if metadata.permissions().mode() & 0o111 == 0 {
|
||||||
|
return Err(format!(
|
||||||
|
"Bundled binary is not executable: '{}'",
|
||||||
|
binary_path.display()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(target_os = "macos")]
|
||||||
|
{
|
||||||
|
let expected_arch = if cfg!(target_arch = "aarch64") {
|
||||||
|
"arm64"
|
||||||
|
} else {
|
||||||
|
"x86_64"
|
||||||
|
};
|
||||||
|
if let Ok(output) = std::process::Command::new("/usr/bin/lipo")
|
||||||
|
.arg("-archs")
|
||||||
|
.arg(binary_path)
|
||||||
|
.output()
|
||||||
|
{
|
||||||
|
if output.status.success() {
|
||||||
|
let archs = String::from_utf8_lossy(&output.stdout);
|
||||||
|
if !archs.split_whitespace().any(|arch| arch == expected_arch) {
|
||||||
|
return Err(format!(
|
||||||
|
"Wrong architecture for '{}': expected {}, found {}",
|
||||||
|
binary_path.display(),
|
||||||
|
expected_arch,
|
||||||
|
archs.trim()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn generate_remediation_hint(error: &str, _kind: &str) -> Option<String> {
|
fn generate_remediation_hint(error: &str, _kind: &str) -> Option<String> {
|
||||||
@@ -896,9 +1006,9 @@ async fn check_ytdlp(app_handle: &tauri::AppHandle) -> EngineStatusItem {
|
|||||||
daemon_alive: None,
|
daemon_alive: None,
|
||||||
rpc_ready: None,
|
rpc_ready: None,
|
||||||
last_stderr_tail: None,
|
last_stderr_tail: None,
|
||||||
expects_internal_dir: Some(has_internal_dir),
|
expects_internal_dir: has_internal_dir.then_some(true),
|
||||||
has_internal_dir: Some(has_internal_dir),
|
has_internal_dir: has_internal_dir.then_some(true),
|
||||||
has_python_framework: Some(has_python_framework),
|
has_python_framework: has_internal_dir.then_some(has_python_framework),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1009,10 +1119,12 @@ fn resolve_bundled_binary_path(app_handle: &tauri::AppHandle, binary_name: &str)
|
|||||||
if cfg!(target_arch = "aarch64") { "aarch64" } else { "x86_64" },
|
if cfg!(target_arch = "aarch64") { "aarch64" } else { "x86_64" },
|
||||||
);
|
);
|
||||||
|
|
||||||
// Production: sidecar sits next to the main executable inside the .app bundle
|
// Production: use the resource copy as the canonical engine location.
|
||||||
if let Ok(exe_path) = std::env::current_exe() {
|
if let Ok(resource_dir) = app_handle.path().resource_dir() {
|
||||||
if let Some(exe_dir) = exe_path.parent() {
|
for candidate in [
|
||||||
let candidate = exe_dir.join(&full_name);
|
resource_dir.join("binaries").join(&full_name),
|
||||||
|
resource_dir.join(&full_name),
|
||||||
|
] {
|
||||||
if candidate.is_file() {
|
if candidate.is_file() {
|
||||||
log::info!("Resolved bundled '{}' at: {:?}", binary_name, candidate);
|
log::info!("Resolved bundled '{}' at: {:?}", binary_name, candidate);
|
||||||
return Ok(candidate);
|
return Ok(candidate);
|
||||||
@@ -1038,17 +1150,14 @@ fn resolve_bundled_binary_path(app_handle: &tauri::AppHandle, binary_name: &str)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback: Tauri resource directory (some configs place sidecars there)
|
// Compatibility fallback for older bundles produced with externalBin.
|
||||||
if let Ok(resource_dir) = app_handle.path().resource_dir() {
|
if let Ok(exe_path) = std::env::current_exe() {
|
||||||
let candidate = resource_dir.join(&full_name);
|
if let Some(exe_dir) = exe_path.parent() {
|
||||||
if candidate.is_file() {
|
let candidate = exe_dir.join(binary_name);
|
||||||
log::info!("Resolved bundled '{}' at: {:?}", binary_name, candidate);
|
if candidate.is_file() {
|
||||||
return Ok(candidate);
|
log::info!("Resolved legacy bundled '{}' at: {:?}", binary_name, candidate);
|
||||||
}
|
return Ok(candidate);
|
||||||
let candidate2 = resource_dir.join("binaries").join(&full_name);
|
}
|
||||||
if candidate2.is_file() {
|
|
||||||
log::info!("Resolved bundled '{}' at: {:?}", binary_name, candidate2);
|
|
||||||
return Ok(candidate2);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1179,7 +1288,8 @@ pub(crate) async fn start_media_download_internal(
|
|||||||
let mut processing_started = false;
|
let mut processing_started = false;
|
||||||
|
|
||||||
while strike <= MAX_RETRIES {
|
while strike <= MAX_RETRIES {
|
||||||
let mut cmd = app_handle.shell().sidecar("yt-dlp").map_err(|e| e.to_string())?
|
let ytdlp_path = resolve_bundled_binary_path(&app_handle, "yt-dlp")?;
|
||||||
|
let mut cmd = app_handle.shell().command(&ytdlp_path)
|
||||||
.arg("--newline")
|
.arg("--newline")
|
||||||
.arg("--no-check-formats")
|
.arg("--no-check-formats")
|
||||||
.arg("--socket-timeout").arg("20")
|
.arg("--socket-timeout").arg("20")
|
||||||
|
|||||||
@@ -38,12 +38,6 @@
|
|||||||
"bundle": {
|
"bundle": {
|
||||||
"active": true,
|
"active": true,
|
||||||
"targets": "all",
|
"targets": "all",
|
||||||
"externalBin": [
|
|
||||||
"binaries/yt-dlp",
|
|
||||||
"binaries/aria2c",
|
|
||||||
"binaries/ffmpeg",
|
|
||||||
"binaries/deno"
|
|
||||||
],
|
|
||||||
"icon": [
|
"icon": [
|
||||||
"icons/32x32.png",
|
"icons/32x32.png",
|
||||||
"icons/128x128.png",
|
"icons/128x128.png",
|
||||||
|
|||||||
@@ -112,8 +112,8 @@ export default function SettingsView() {
|
|||||||
{item.rpc_ready != null && <p>RPC ready: {String(item.rpc_ready)}</p>}
|
{item.rpc_ready != null && <p>RPC ready: {String(item.rpc_ready)}</p>}
|
||||||
{item.rpc_port != null && <p>RPC port: {item.rpc_port}</p>}
|
{item.rpc_port != null && <p>RPC port: {item.rpc_port}</p>}
|
||||||
{item.last_stderr_tail && <details><summary className="cursor-pointer text-text-muted">daemon stderr</summary><pre className="mt-1 whitespace-pre-wrap">{item.last_stderr_tail}</pre></details>}
|
{item.last_stderr_tail && <details><summary className="cursor-pointer text-text-muted">daemon stderr</summary><pre className="mt-1 whitespace-pre-wrap">{item.last_stderr_tail}</pre></details>}
|
||||||
{item.expects_internal_dir != null && <p>Expects _internal layout: {String(item.expects_internal_dir)}</p>}
|
{item.expects_internal_dir === true && <p>Packaging: PyInstaller onedir (_internal required)</p>}
|
||||||
{item.has_internal_dir != null && <p>_internal directory found: {String(item.has_internal_dir)}</p>}
|
{item.has_internal_dir === true && <p>_internal directory found: true</p>}
|
||||||
{item.has_python_framework != null && <p>Python runtime found: {String(item.has_python_framework)}</p>}
|
{item.has_python_framework != null && <p>Python runtime found: {String(item.has_python_framework)}</p>}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
Reference in New Issue
Block a user