136 lines
4.0 KiB
Rust
136 lines
4.0 KiB
Rust
//! 封面 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<PathBuf> {
|
||
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<PathBuf> {
|
||
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<String, String>,
|
||
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<PathBuf> {
|
||
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());
|
||
}
|
||
}
|
||
}
|