use crate::manifest_lookup::resolve_plugin_ref;
use r2x_config::Config;
use r2x_logger as logger;
use r2x_manifest::types::Manifest;
use r2x_python::utils::resolve_site_package_path;
use std::collections::HashSet;
use std::path::{Path, PathBuf};
use std::process::{Command, Stdio};
#[derive(Debug, Clone, PartialEq)]
pub enum VerificationResult {
Valid,
Missing(Vec<String>),
}
#[derive(Debug)]
pub enum VerificationError {
VenvNotFound(PathBuf),
VerificationFailed(String),
ReinstallFailed(String),
}
impl std::fmt::Display for VerificationError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
VerificationError::VenvNotFound(path) => {
write!(f, "Virtual environment not found at: {}", path.display())
}
VerificationError::VerificationFailed(msg) => {
write!(f, "Package verification failed: {}", msg)
}
VerificationError::ReinstallFailed(msg) => {
write!(f, "Package reinstallation failed: {}", msg)
}
}
}
}
impl std::error::Error for VerificationError {}
pub fn verify_plugin_packages(
manifest: &Manifest,
plugin_key: &str,
) -> Result<VerificationResult, VerificationError> {
logger::debug(&format!("Verifying packages for plugin: {}", plugin_key));
let resolved = resolve_plugin_ref(manifest, plugin_key)
.map_err(|e| VerificationError::VerificationFailed(e.to_string()))?;
let package_name = resolved.package.name.to_string();
let config = Config::load().map_err(|e| {
VerificationError::VerificationFailed(format!("Failed to load config: {}", e))
})?;
let venv_path = PathBuf::from(config.get_venv_path());
if !venv_path.exists() {
return Err(VerificationError::VenvNotFound(venv_path));
}
let missing_packages = check_packages_installed(&venv_path, &[&package_name])?;
if missing_packages.is_empty() {
logger::debug(&format!("Package '{}' verified successfully", package_name));
Ok(VerificationResult::Valid)
} else {
logger::debug(&format!("Missing packages: {:?}", missing_packages));
Ok(VerificationResult::Missing(missing_packages))
}
}
fn check_packages_installed(
venv_path: &Path,
packages: &[&str],
) -> Result<Vec<String>, VerificationError> {
let site_packages = get_site_packages_dir(venv_path)?;
let mut missing = Vec::new();
for package in packages {
let package_dir_name = package.replace('-', "_");
let package_dir = site_packages.join(&package_dir_name);
let dist_info_pattern = format!("{}-*.dist-info", package_dir_name);
let package_exists =
package_dir.exists() || dist_info_exists(&site_packages, &dist_info_pattern);
if package_exists {
logger::debug(&format!("Package '{}' found in site-packages", package));
} else {
logger::debug(&format!("Package '{}' not found in site-packages", package));
missing.push((*package).to_string());
}
}
Ok(missing)
}
fn get_site_packages_dir(venv_path: &Path) -> Result<PathBuf, VerificationError> {
logger::debug(&format!(
"Getting site-packages directory for venv: {}",
venv_path.display()
));
resolve_site_package_path(venv_path).map_err(|e| match e {
r2x_python::errors::BridgeError::VenvNotFound(path) => {
VerificationError::VenvNotFound(path)
}
_ => VerificationError::VerificationFailed(format!("{}", e)),
})
}
fn dist_info_exists(site_packages: &Path, pattern: &str) -> bool {
let pattern_prefix = pattern.split('-').next().unwrap_or("");
if let Ok(entries) = std::fs::read_dir(site_packages) {
for entry in entries.flatten() {
let name = entry.file_name();
let name_str = name.to_string_lossy();
if name_str.starts_with(pattern_prefix) && name_str.ends_with(".dist-info") {
return true;
}
}
}
false
}
pub fn ensure_packages(packages: Vec<String>, config: &Config) -> Result<(), VerificationError> {
if packages.is_empty() {
return Ok(());
}
logger::info(&format!(
"Installing missing packages: {}",
packages.join(", ")
));
let uv_path = config
.uv_path
.as_ref()
.ok_or_else(|| VerificationError::ReinstallFailed("uv not configured".to_string()))?;
let python_exe = config.get_venv_python_path();
let mut cmd = Command::new(uv_path);
cmd.arg("pip")
.arg("install")
.arg("--python")
.arg(&python_exe)
.arg("--prerelease=allow")
.arg("--no-progress");
for package in &packages {
cmd.arg(package);
}
logger::debug(&format!("Running: {:?}", cmd));
let status = cmd
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.status()
.map_err(|e| VerificationError::ReinstallFailed(format!("Failed to execute uv: {}", e)))?;
if !status.success() {
return Err(VerificationError::ReinstallFailed(format!(
"uv pip install failed: exit code {}",
status.code().unwrap_or(-1)
)));
}
logger::success(&format!(
"Successfully installed {} packages",
packages.len()
));
Ok(())
}
pub fn verify_and_ensure_plugin(
manifest: &Manifest,
plugin_key: &str,
) -> Result<(), VerificationError> {
logger::debug(&format!("Verifying and ensuring plugin: {}", plugin_key));
match verify_plugin_packages(manifest, plugin_key)? {
VerificationResult::Valid => {
logger::debug("All packages verified successfully");
Ok(())
}
VerificationResult::Missing(packages) => {
logger::info(&format!(
"Missing {} package(s), reinstalling...",
packages.len()
));
let config = Config::load().map_err(|e| {
VerificationError::ReinstallFailed(format!("Failed to load config: {}", e))
})?;
ensure_packages(packages, &config)?;
logger::success("Packages verified and installed");
Ok(())
}
}
}
pub fn verify_all_packages(manifest: &Manifest) -> Result<HashSet<String>, VerificationError> {
let mut missing_packages = HashSet::new();
let config = Config::load().map_err(|e| {
VerificationError::VerificationFailed(format!("Failed to load config: {}", e))
})?;
let venv_path = PathBuf::from(config.get_venv_path());
if !venv_path.exists() {
return Err(VerificationError::VenvNotFound(venv_path));
}
let mut all_packages: HashSet<String> = HashSet::new();
for pkg in &manifest.packages {
all_packages.insert(pkg.name.to_string());
}
let packages_vec: Vec<String> = all_packages.into_iter().collect();
let packages_refs: Vec<&str> = packages_vec.iter().map(|s| s.as_str()).collect();
let missing = check_packages_installed(&venv_path, &packages_refs)?;
for package in missing {
missing_packages.insert(package);
}
Ok(missing_packages)
}
#[cfg(test)]
mod tests {
use crate::package_verification::*;
#[test]
fn test_verification_result_valid() {
let result = VerificationResult::Valid;
assert_eq!(result, VerificationResult::Valid);
}
#[test]
fn test_verification_result_missing() {
let packages = vec!["r2x-reeds".to_string(), "r2x-core".to_string()];
let result = VerificationResult::Missing(packages.clone());
match result {
VerificationResult::Missing(p) => assert_eq!(p, packages),
VerificationResult::Valid => unreachable!("Expected Missing variant"),
}
}
#[test]
fn test_package_name_conversion() {
let package = "r2x-reeds";
let converted = package.replace('-', "_");
assert_eq!(converted, "r2x_reeds");
}
#[test]
fn test_verification_error_display() {
let err = VerificationError::VerificationFailed("test error".to_string());
assert_eq!(err.to_string(), "Package verification failed: test error");
}
#[test]
fn test_dist_info_pattern() {
let pattern = "r2x_reeds-*.dist-info";
let pattern_prefix = pattern.split('-').next().unwrap_or("");
assert_eq!(pattern_prefix, "r2x_reeds");
let example_dist_info = "r2x_reeds-1.2.3.dist-info";
assert!(example_dist_info.starts_with(pattern_prefix));
assert!(example_dist_info.ends_with(".dist-info"));
}
#[test]
fn test_verification_workflow() {
let valid_result = VerificationResult::Valid;
let missing_result = VerificationResult::Missing(vec!["r2x-reeds".to_string()]);
assert!(matches!(missing_result, VerificationResult::Missing(_)));
assert!(matches!(valid_result, VerificationResult::Valid));
}
}