use crate::core::manifest::{self, Manifest, ManifestFetcher, ManifestSource, ResolvedManifest};
use crate::orchestrator::planner::{CompatibilityContext, PlannerOptions, UpdatePlanner};
use crate::platform::registry;
use std::collections::{HashMap, HashSet};
use std::path::Path;
pub fn migrated_get_version(component_id: &str) -> String {
registry::get_version(component_id)
}
pub fn migrated_get_version_with_home(component_id: &str, home: &Path) -> String {
registry::get_version_with_home(component_id, home)
}
pub fn migrated_get_versions_batch(component_ids: &[&str]) -> Vec<registry::VersionInfo> {
registry::get_versions_batch(component_ids)
}
pub fn migrated_build_selection_plan(
manifest: &Manifest,
context: &CompatibilityContext,
options: &PlannerOptions,
) -> Result<Vec<PlannerItem>, crate::orchestrator::planner::PlannerError> {
let planner = UpdatePlanner::new();
planner.build_plan(manifest, context, options)
}
pub use crate::orchestrator::planner::PlannerItem;
pub fn build_plan_from_system(
manifest: &Manifest,
rocm_channel: &str,
include_experimental: bool,
) -> Result<Vec<PlannerItem>, crate::orchestrator::planner::PlannerError> {
let installed_ids = registry::detect_all_installed();
let mut installed_versions = HashMap::new();
let mut installed_set = HashSet::new();
for id in &installed_ids {
let v = registry::get_version(id);
installed_versions.insert(id.clone(), v);
installed_set.insert(id.clone());
}
let rocm_version = installed_versions.get("rocm").cloned().unwrap_or_default();
let context = CompatibilityContext {
rocm_version,
rocm_channel: rocm_channel.to_string(),
available_executors: HashSet::from([
crate::core::types::ExecutorKind::LegacyScript,
crate::core::types::ExecutorKind::Rust,
]),
installed_components: installed_set,
installed_versions,
gpu_architecture: String::new(),
runtime_version: env!("CARGO_PKG_VERSION").to_string(),
};
let options = PlannerOptions {
include_experimental,
..PlannerOptions::default()
};
migrated_build_selection_plan(manifest, &context, &options)
}
#[derive(Debug, Clone)]
pub struct FreshnessResult {
pub is_fresh: bool,
pub source: ManifestSource,
pub warning: Option<String>,
pub refetch_triggered: bool,
}
pub fn check_manifest_freshness(fetcher: &dyn ManifestFetcher) -> FreshnessResult {
if let Some(remote) = fetcher.fetch_remote() {
if remote.verify_trust().is_ok() {
return FreshnessResult {
is_fresh: true,
source: ManifestSource::FreshRemote,
warning: None,
refetch_triggered: false,
};
}
}
if let Some(_cached) = fetcher.load_cached() {
return FreshnessResult {
is_fresh: false,
source: ManifestSource::CachedRemote,
warning: Some(
"Using cached manifest (remote unavailable or trust check failed)".to_string(),
),
refetch_triggered: true,
};
}
FreshnessResult {
is_fresh: false,
source: ManifestSource::Baseline,
warning: Some("Using bundled baseline manifest (no remote, no cache)".to_string()),
refetch_triggered: true,
}
}
pub fn resolve_with_freshness(
fetcher: &dyn ManifestFetcher,
) -> (ResolvedManifest, FreshnessResult) {
let freshness = check_manifest_freshness(fetcher);
let resolved = manifest::resolve_manifest(fetcher);
(resolved, freshness)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ComponentInstallStatus {
Installed,
NotInstalled,
NeedsUpdate,
}
#[derive(Debug, Clone)]
pub struct DetectionResult {
pub component_id: String,
pub status: ComponentInstallStatus,
pub version: String,
pub detection_method: String,
}
pub fn migrated_detect_all() -> Vec<DetectionResult> {
let home = crate::platform::environment::resolve_user_home();
migrated_detect_all_with_home(&home)
}
pub fn migrated_detect_all_with_home(home: &Path) -> Vec<DetectionResult> {
let installed_ids = registry::detect_all_installed_with_home(home);
let mut results = Vec::new();
for info in registry::known_components() {
let version = registry::get_version_with_home(&info.id, home);
let is_installed = installed_ids.contains(&info.id);
let status = if is_installed {
ComponentInstallStatus::Installed
} else {
check_partial_installation(&info.id, home)
};
results.push(DetectionResult {
component_id: info.id.clone(),
status,
version,
detection_method: format!("{:?}", info.detection_method),
});
}
results
}
pub fn detect_component_status(component_id: &str) -> DetectionResult {
let home = crate::platform::environment::resolve_user_home();
detect_component_status_with_home(component_id, &home)
}
pub fn detect_component_status_with_home(component_id: &str, home: &Path) -> DetectionResult {
let info = registry::get_component(component_id);
let is_installed = registry::is_component_installed_with_home(component_id, home);
let version = registry::get_version_with_home(component_id, home);
let status = if is_installed {
ComponentInstallStatus::Installed
} else {
check_partial_installation(component_id, home)
};
DetectionResult {
component_id: component_id.to_string(),
status,
version,
detection_method: info
.map(|i| format!("{:?}", i.detection_method))
.unwrap_or_else(|| "Unknown".to_string()),
}
}
fn check_partial_installation(component_id: &str, home: &Path) -> ComponentInstallStatus {
match component_id {
"rocm" => {
let version_file = Path::new("/opt/rocm/.info/version").exists();
let rocminfo_works = std::process::Command::new("rocminfo")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false);
if version_file && !rocminfo_works {
ComponentInstallStatus::NeedsUpdate
} else {
ComponentInstallStatus::NotInstalled
}
}
"comfyui" | "vllm-studio" | "textgen" => {
let clone_dir = match component_id {
"comfyui" => "ComfyUI",
"vllm-studio" => "vllm-studio",
"textgen" => "text-generation-webui",
_ => return ComponentInstallStatus::NotInstalled,
};
let dir = home.join(clone_dir);
if dir.exists() && !dir.join(".git").exists() {
ComponentInstallStatus::NeedsUpdate
} else {
ComponentInstallStatus::NotInstalled
}
}
"permanent-env" => {
let env_file = home.join(".mlstack_env");
if env_file.exists() {
let content = std::fs::read_to_string(&env_file).unwrap_or_default();
if !content.contains("MLSTACK_PYTHON_BIN") {
ComponentInstallStatus::NeedsUpdate
} else {
ComponentInstallStatus::NotInstalled
}
} else {
ComponentInstallStatus::NotInstalled
}
}
_ => ComponentInstallStatus::NotInstalled,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_migrated_get_version_all_components_no_panic() {
let ids = [
"rocm",
"pytorch",
"triton",
"deepspeed",
"vllm",
"aiter",
"onnx",
"bitsandbytes",
"migraphx",
"flash-attn",
"mpi4py",
"wandb",
"comfyui",
"vllm-studio",
"textgen",
"rocm-smi",
"permanent-env",
];
for id in &ids {
let v = migrated_get_version(id);
assert!(!v.is_empty(), "version for '{id}' must not be empty");
}
}
#[test]
fn test_migrated_get_version_unknown_returns_unknown() {
assert_eq!(migrated_get_version("nonexistent"), "unknown");
assert_eq!(migrated_get_version("fake-component"), "unknown");
assert_eq!(migrated_get_version(""), "unknown");
}
#[test]
fn test_migrated_detect_all_returns_valid_results() {
let results = migrated_detect_all();
for result in &results {
assert!(
registry::is_known_component(&result.component_id),
"result component must be known: {}",
result.component_id
);
}
}
#[test]
fn test_partial_installation_detection_with_temp_dir() {
let temp_dir = tempfile::tempdir().unwrap();
let home = temp_dir.path();
let comfyui_dir = home.join("ComfyUI");
std::fs::create_dir_all(&comfyui_dir).unwrap();
let result = detect_component_status_with_home("comfyui", home);
assert_eq!(
result.status,
ComponentInstallStatus::NeedsUpdate,
"ComfyUI dir without .git must be NeedsUpdate"
);
std::fs::create_dir_all(comfyui_dir.join(".git")).unwrap();
let result = detect_component_status_with_home("comfyui", home);
assert_eq!(
result.status,
ComponentInstallStatus::Installed,
"ComfyUI with .git must be Installed"
);
}
#[test]
fn test_partial_installation_permanent_env() {
let temp_dir = tempfile::tempdir().unwrap();
let home = temp_dir.path();
let result = detect_component_status_with_home("permanent-env", home);
assert_eq!(result.status, ComponentInstallStatus::NotInstalled);
let env_file = home.join(".mlstack_env");
std::fs::write(&env_file, "# empty").unwrap();
let result = detect_component_status_with_home("permanent-env", home);
assert_eq!(
result.status,
ComponentInstallStatus::NeedsUpdate,
"env file without marker must be NeedsUpdate"
);
std::fs::write(&env_file, "export MLSTACK_PYTHON_BIN=/usr/bin/python3").unwrap();
let result = detect_component_status_with_home("permanent-env", home);
assert_eq!(
result.status,
ComponentInstallStatus::Installed,
"env file with marker must be Installed"
);
}
struct MockFetcher {
remote: Option<Manifest>,
cached: Option<Manifest>,
}
impl ManifestFetcher for MockFetcher {
fn fetch_remote(&self) -> Option<Manifest> {
self.remote.clone()
}
fn load_cached(&self) -> Option<Manifest> {
self.cached.clone()
}
}
fn make_signed_manifest() -> Manifest {
let mut m = Manifest {
schema_version: manifest::CURRENT_SCHEMA_VERSION,
sequence: 1,
generated_at: chrono::Utc::now().to_rfc3339(),
expires_at: None,
min_runtime_version: String::new(),
components: vec![],
signature: None,
};
m.signature = Some(m.compute_signature());
m
}
#[test]
fn test_freshness_check_fresh_remote() {
let fetcher = MockFetcher {
remote: Some(make_signed_manifest()),
cached: None,
};
let result = check_manifest_freshness(&fetcher);
assert!(result.is_fresh);
assert_eq!(result.source, ManifestSource::FreshRemote);
assert!(result.warning.is_none());
}
#[test]
fn test_freshness_check_offline_with_cache() {
let fetcher = MockFetcher {
remote: None,
cached: Some(make_signed_manifest()),
};
let result = check_manifest_freshness(&fetcher);
assert!(!result.is_fresh);
assert_eq!(result.source, ManifestSource::CachedRemote);
assert!(result.warning.is_some());
assert!(result.refetch_triggered);
}
#[test]
fn test_freshness_check_offline_no_cache() {
let fetcher = MockFetcher {
remote: None,
cached: None,
};
let result = check_manifest_freshness(&fetcher);
assert!(!result.is_fresh);
assert_eq!(result.source, ManifestSource::Baseline);
assert!(result.warning.is_some());
}
}