//! 封面 Python 运行时定位(bundled `resources/pythonruntime/python.exe`) //! 封面脚本由服务端下发,不在此目录打包。 use std::collections::HashMap; use std::path::{Path, PathBuf}; fn exe_name() -> &'static str { if cfg!(windows) { "python.exe" } else { "python3" } } fn pythonruntime_candidates() -> Vec { let mut out = Vec::new(); // 开发源码树优先:target/debug/resources 可能是残缺副本(缺 PIL 等) out.push( PathBuf::from("src-tauri/resources/pythonruntime").join(exe_name()), ); out.push(PathBuf::from("resources/pythonruntime").join(exe_name())); if let Ok(exe) = std::env::current_exe() { if let Some(dir) = exe.parent() { out.push( dir.join("resources") .join("pythonruntime") .join(exe_name()), ); } } out } /// `python.exe` 所在目录即 PYTHONHOME(含 Lib/site-packages)。 pub fn python_home(python_exe: &Path) -> PathBuf { python_exe .parent() .map(|p| p.to_path_buf()) .unwrap_or_else(|| PathBuf::from(".")) } fn site_packages_dir(python_exe: &Path) -> PathBuf { python_home(python_exe).join("Lib").join("site-packages") } fn runtime_has_pillow(python_exe: &Path) -> bool { let site = site_packages_dir(python_exe); site.join("PIL").is_dir() || site.join("Pillow").is_dir() } /// bundled `resources/pythonruntime/python.exe` 或 `AICLIENT_PYTHON_PATH`。 pub fn locate_python() -> Option { if let Ok(p) = std::env::var("AICLIENT_PYTHON_PATH") { let pb = PathBuf::from(&p); if pb.is_file() { return Some(pb); } } for path in pythonruntime_candidates() { if path.is_file() && runtime_has_pillow(&path) { return Some(path); } } for path in pythonruntime_candidates() { if path.is_file() { return Some(path); } } None } /// 为 bundled Python 子进程设置 PYTHONHOME / PYTHONPATH(否则找不到 PIL/cv2)。 pub fn apply_python_runtime_env( env: &mut HashMap, python_exe: &Path, bundle_dir: Option<&Path>, ) { let home = python_home(python_exe); env.insert( "PYTHONHOME".into(), home.to_string_lossy().into_owned(), ); let site = site_packages_dir(python_exe); let sep = if cfg!(windows) { ";" } else { ":" }; let mut paths = vec![site.to_string_lossy().into_owned()]; if let Some(dir) = bundle_dir { paths.push(dir.to_string_lossy().into_owned()); } env.insert("PYTHONPATH".into(), paths.join(sep)); env.remove("PYTHONUSERBASE"); env.insert("PYTHONNOUSERSITE".into(), "1".into()); env.insert( "AICLIENT_PYTHON_HOME".into(), home.to_string_lossy().into_owned(), ); } /// 开发期本地脚本目录(发布构建由服务端拉取,不依赖此路径)。 pub fn locate_cover_scripts_dir() -> Option { if let Ok(p) = std::env::var("AICLIENT_COVER_SCRIPTS_DIR") { let pb = PathBuf::from(&p); if pb.join("advanced_cover_generator.py").is_file() { return Some(pb); } } let backend = PathBuf::from("../pythonbackend/scripts/cover"); if backend.join("advanced_cover_generator.py").is_file() { return backend.canonicalize().ok(); } None } /// 将 ffmpeg 所在目录 prepend 到 PATH(供 Python 子进程调用 ffmpeg)。 pub fn prepend_ffmpeg_to_path(env_path: &str) -> String { let sep = if cfg!(windows) { ";" } else { ":" }; if let Some(ffmpeg) = crate::ffmpeg::locate_ffmpeg() { if let Some(dir) = ffmpeg.parent() { return format!("{}{}{}", dir.display(), sep, env_path); } } env_path.to_string() } #[cfg(test)] mod tests { use super::*; #[test] fn pythonruntime_dev_path() { let dev = PathBuf::from("src-tauri/resources/pythonruntime/python.exe"); if dev.is_file() { assert!(locate_python().is_some()); } } }