use std::path::{Path, PathBuf};
use std::process::Command;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ExecutionProvider {
Cpu,
Cuda,
Migraphx,
}
impl ExecutionProvider {
pub fn pip_package(&self) -> &'static str {
match self {
ExecutionProvider::Cpu => "onnxruntime",
ExecutionProvider::Cuda => "onnxruntime-gpu",
ExecutionProvider::Migraphx => "onnxruntime-migraphx",
}
}
pub fn config_value(&self) -> &'static str {
match self {
ExecutionProvider::Cpu => "cpu",
ExecutionProvider::Cuda => "cuda",
ExecutionProvider::Migraphx => "migraphx",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GpuVendor {
Amd,
Nvidia,
}
#[derive(Debug, Clone)]
pub struct SetupChoices {
pub neural_enabled: bool,
pub provider: Option<ExecutionProvider>,
}
#[derive(Debug, Clone)]
pub struct SetupResult {
pub choices: SetupChoices,
pub config_path: Option<PathBuf>,
pub ort_dylib_path: Option<PathBuf>,
pub ort_version: Option<String>,
pub model_present: bool,
pub ort_installed: bool,
pub smoke_test: Option<SmokeTestResult>,
}
#[derive(Debug, Clone)]
pub struct SmokeTestResult {
pub passed: bool,
pub skipped: bool,
pub dimension: Option<usize>,
pub execution_provider: Option<String>,
pub configured_provider_label: Option<String>,
pub error: Option<String>,
pub note: Option<String>,
}
const QWEN3_EMBEDDING_DIMENSION: usize = 1024;
impl SmokeTestResult {
#[cfg_attr(not(feature = "onnx"), allow(dead_code))]
fn from_embedding_outcome(
dimension: usize,
execution_provider: Option<String>,
configured_provider_label: Option<String>,
) -> Self {
let mut error = if dimension == QWEN3_EMBEDDING_DIMENSION {
None
} else {
Some(format!(
"expected {}-dim vector, got {}-dim",
QWEN3_EMBEDDING_DIMENSION, dimension
))
};
if error.is_none() {
if let Some(configured) = configured_provider_label.as_deref() {
let active = execution_provider.as_deref();
let provider_matches = matches!(
(configured, active),
("cuda", Some("cuda"))
| ("migraphx", Some("migraphx"))
| ("rocm", Some("migraphx" | "rocm"))
);
if matches!(configured, "migraphx" | "rocm" | "cuda") && !provider_matches {
error = Some(format!(
"configured execution provider {} but worker reported {}",
configured,
active.unwrap_or("none")
));
}
}
}
Self {
passed: error.is_none(),
skipped: false,
dimension: Some(dimension),
execution_provider,
configured_provider_label,
error,
note: None,
}
}
pub fn status_line(&self) -> String {
if self.skipped {
return "embedding test: SKIP".to_string();
}
if self.passed {
match self.dimension {
Some(dim) => format!("embedding test: PASS ({}-dim vector)", dim),
None => "embedding test: PASS (dimension unavailable)".to_string(),
}
} else {
"embedding test: FAIL".to_string()
}
}
}
pub fn resolve_from_flags(
neural: bool,
no_neural: bool,
cpu: bool,
gpu: Option<GpuVendor>,
) -> Result<SetupChoices, SetupError> {
if neural && no_neural {
return Err(SetupError::Conflict {
message: "Cannot use --neural and --no-neural together. Choose one.".to_string(),
});
}
if cpu && gpu.is_some() {
return Err(SetupError::Conflict {
message: "Cannot use --cpu and --gpu together. Choose one execution provider."
.to_string(),
});
}
let effective_neural = neural || cpu || gpu.is_some();
if no_neural {
return Ok(SetupChoices {
neural_enabled: false,
provider: None,
});
}
if !effective_neural {
return Err(SetupError::NoFlags);
}
let provider = if cpu {
Some(ExecutionProvider::Cpu)
} else if let Some(vendor) = gpu {
Some(match vendor {
GpuVendor::Amd => ExecutionProvider::Migraphx,
GpuVendor::Nvidia => ExecutionProvider::Cuda,
})
} else {
Some(ExecutionProvider::Cpu)
};
Ok(SetupChoices {
neural_enabled: true,
provider,
})
}
pub fn run_interactive_flow() -> Result<SetupChoices, SetupError> {
use dialoguer::{Confirm, Select};
println!("\nLeIndex Setup\n=============\n");
println!("Neural embeddings provide semantic code search (find symbols by meaning).\n");
let want_neural = Confirm::new()
.with_prompt("Do you want neural embeddings / enhanced semantic search?")
.default(true)
.interact()
.map_err(|e| SetupError::Interactive(e.to_string()))?;
if !want_neural {
return Ok(SetupChoices {
neural_enabled: false,
provider: None,
});
}
let provider_items = vec![
"CPU (works everywhere)",
"GPU (faster, requires AMD/NVIDIA GPU)",
];
let gpu_choice = Select::new()
.with_prompt("CPU or GPU-based neural embeddings?")
.items(&provider_items)
.default(0)
.interact()
.map_err(|e| SetupError::Interactive(e.to_string()))?;
if gpu_choice == 0 {
return Ok(SetupChoices {
neural_enabled: true,
provider: Some(ExecutionProvider::Cpu),
});
}
let vendor_items = vec![
"AMD (ROCm/MIGraphX)",
"NVIDIA (CUDA)",
"N/A (no usable GPU detected)",
];
let detected_vendor = detect_gpu_vendor();
match detected_vendor {
DetectedGpu::Amd => {
println!(" (Detected AMD GPU / ROCm tooling.)");
}
DetectedGpu::Nvidia => {
println!(" (Detected NVIDIA GPU / CUDA tooling.)");
}
DetectedGpu::Unknown => {
println!(" (No AMD ROCm or NVIDIA CUDA tooling detected.)");
println!(" Recommendation: choose 'N/A' to use CPU, which works everywhere.");
println!(" If you have a GPU, install ROCm (AMD) or the CUDA toolkit (NVIDIA)");
println!(" and re-run `leindex setup`.");
}
}
let vendor_choice = Select::new()
.with_prompt("Which GPU vendor?")
.items(&vendor_items)
.default(default_gpu_vendor_index(detected_vendor))
.interact()
.map_err(|e| SetupError::Interactive(e.to_string()))?;
let provider = match vendor_choice {
0 => ExecutionProvider::Migraphx, 1 => ExecutionProvider::Cuda, _ => ExecutionProvider::Cpu, };
Ok(SetupChoices {
neural_enabled: true,
provider: Some(provider),
})
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DetectedGpu {
Amd,
Nvidia,
Unknown,
}
pub fn detect_gpu_vendor() -> DetectedGpu {
if detect_amd_gpu() {
DetectedGpu::Amd
} else if detect_nvidia_gpu() {
DetectedGpu::Nvidia
} else {
DetectedGpu::Unknown
}
}
fn default_gpu_vendor_index(detected: DetectedGpu) -> usize {
match detected {
DetectedGpu::Amd => 0,
DetectedGpu::Nvidia => 1,
DetectedGpu::Unknown => 2,
}
}
fn detect_amd_gpu() -> bool {
#[cfg(unix)]
{
let candidates = [
"/opt/rocm",
"/opt/rocm/lib/libmigraphx_c.so",
"/opt/rocm/lib/libamdhip64.so",
"/opt/rocm/bin/migraphx-driver",
"/opt/rocm/bin/rocm-smi",
];
if candidates.iter().any(|p| std::path::Path::new(p).exists()) {
return true;
}
}
if let Ok(rocm_path) = std::env::var("ROCM_PATH") {
if std::path::Path::new(&rocm_path).exists() {
return true;
}
}
false
}
fn detect_nvidia_gpu() -> bool {
#[cfg(unix)]
{
let candidates = [
"/usr/bin/nvidia-smi",
"/usr/local/cuda/bin/nvidia-smi",
"/usr/local/cuda",
"/usr/lib/x86_64-linux-gnu/libcuda.so",
"/usr/lib/x86_64-linux-gnu/libcudart.so",
];
if candidates.iter().any(|p| std::path::Path::new(p).exists()) {
return true;
}
}
#[cfg(windows)]
{
let candidates = [
"C:\\Windows\\System32\\nvidia-smi.exe",
"C:\\Program Files\\NVIDIA Corporation",
"C:\\Program Files\\NVIDIA GPU Computing Toolkit\\CUDA",
];
if candidates.iter().any(|p| std::path::Path::new(p).exists()) {
return true;
}
}
if std::env::var("CUDA_PATH").is_ok() {
return true;
}
#[cfg(unix)]
if std::process::Command::new("nvidia-smi")
.arg("--query-gpu=name")
.arg("--format=csv,noheader")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
{
return true;
}
false
}
pub fn is_interactive() -> bool {
use std::io::IsTerminal;
std::io::stdin().is_terminal()
}
pub fn execute_setup(choices: &SetupChoices) -> Result<SetupResult, SetupError> {
ensure_home_writable()?;
let ort_installed = check_ort_installed();
let desired_model_name = model_name_for_provider(choices.provider);
let model_present = check_model_present_for_name(desired_model_name);
if choices.neural_enabled {
match (ort_installed, model_present) {
(false, true) => {
println!(" -> Partial setup detected: model files present but ORT not installed.");
println!(" Installing ORT without re-downloading model...");
}
(true, false) => {
println!(" -> Partial setup detected: ORT installed but model files missing.");
println!(" Downloading model without reinstalling ORT...");
}
(false, false) => {
}
(true, true) => {
}
}
}
let (ort_dylib_path, ort_version) = if choices.neural_enabled {
let provider = choices.provider.unwrap_or(ExecutionProvider::Cpu);
let pre_existing_version = get_ort_version();
let mut upgrade_unsupported_ort = false;
if let Some(ref detected) = pre_existing_version {
match check_ort_version_compatibility(detected) {
VersionCompatibility::Unsupported {
required_min,
reason,
} => {
println!(
" -> WARNING: Detected onnxruntime {}, but LeIndex requires {} ({}).",
detected, required_min, reason
);
println!(" Upgrading to a supported version...");
upgrade_unsupported_ort = true;
}
VersionCompatibility::TooNew {
supported_max,
reason,
} => {
println!(
" -> WARNING: Detected onnxruntime {}, which is newer than the supported maximum ({}).",
detected, supported_max
);
println!(" Reason: {}.", reason);
println!(" Setup will continue, but if you hit ABI errors, pin onnxruntime to <= {}.", supported_max);
}
VersionCompatibility::Supported => {
println!(" -> onnxruntime {} detected (compatible).", detected);
}
}
}
if !ort_installed || upgrade_unsupported_ort {
install_ort(provider)?;
} else if provider != ExecutionProvider::Cpu {
if !check_provider_available(provider) {
println!(
" -> onnxruntime installed but {} not available; installing {} variant...",
provider.config_value(),
provider.pip_package()
);
install_ort(provider)?;
} else {
println!(
" -> onnxruntime with {} already available.",
provider.config_value()
);
}
} else if pre_existing_version.is_none() {
println!(" -> onnxruntime installed but unimportable; upgrading...");
install_ort(provider)?;
}
let ort_path = discover_ort_path();
let ort_ver = get_ort_version().or(pre_existing_version);
(ort_path, ort_ver)
} else {
(None, None)
};
let ort_installed_final = check_ort_installed();
let model_present = if choices.neural_enabled {
ensure_models_present(choices.provider, desired_model_name)?
} else {
model_present
};
let config = build_config(choices, ort_dylib_path.as_deref(), ort_version.as_deref());
let (config, recovery) = merge_with_existing(config);
let config_path = config
.save()
.map_err(|e| SetupError::ConfigWrite(e.to_string()))?;
if let Some(action) = recovery {
match action {
RecoveryNotice::Migrated => {
println!(" -> Existing config migrated to current schema.");
}
RecoveryNotice::RecoveredFromCorrupt(backup) => {
println!(
" -> Corrupted config detected and backed up to {}.",
backup.display()
);
}
}
}
let smoke_test = if choices.neural_enabled && ort_installed_final && model_present {
println!("\nVerifying neural search on a sample query...");
let result = run_embedding_smoke_test(choices.provider);
match &result {
SmokeTestResult {
passed: true,
dimension: Some(dim),
configured_provider_label: Some(provider),
..
} => {
println!(" -> {}.", result.status_line());
println!(" -> Configured execution provider: {}.", provider);
if let Some(active) = &result.execution_provider {
println!(" -> Active execution provider: {}.", active);
}
let _ = dim; }
SmokeTestResult {
skipped: true,
note: Some(note),
..
} => {
println!(" -> {}.", result.status_line());
println!(" {}", truncate_for_display(note, 200));
}
SmokeTestResult { passed: true, .. } => {
println!(" -> {}.", result.status_line());
}
SmokeTestResult {
passed: false,
error: Some(err),
..
} => {
println!(" -> {}.", result.status_line());
println!(" Worker error: {}", truncate_for_display(err, 200));
println!(" Actionable guidance: run `leindex setup --check` for diagnostics,");
println!(" verify ORT and model files are intact, or re-run `leindex setup`.");
}
SmokeTestResult {
passed: false,
error: None,
..
} => {
println!(" -> {}.", result.status_line());
}
}
Some(result)
} else if choices.neural_enabled {
if !ort_installed_final {
println!("\nSkipping embedding smoke test: ORT not installed.");
} else if !model_present {
println!("\nSkipping embedding smoke test: model files not present.");
}
None
} else {
None
};
Ok(SetupResult {
choices: choices.clone(),
config_path: Some(config_path),
ort_dylib_path,
ort_version,
model_present,
ort_installed: ort_installed_final,
smoke_test,
})
}
#[cfg(test)]
fn should_install_ort_for_existing_state(
ort_installed: bool,
provider: ExecutionProvider,
pre_existing_version: Option<&str>,
provider_available: bool,
) -> bool {
if !ort_installed {
return true;
}
if pre_existing_version
.map(|version| {
matches!(
check_ort_version_compatibility(version),
VersionCompatibility::Unsupported { .. }
)
})
.unwrap_or(false)
{
return true;
}
if provider != ExecutionProvider::Cpu && !provider_available {
return true;
}
provider == ExecutionProvider::Cpu && pre_existing_version.is_none()
}
fn truncate_for_display(s: &str, max_chars: usize) -> String {
if s.chars().count() <= max_chars {
return s.to_string();
}
let truncated: String = s.chars().take(max_chars).collect();
format!("{}...", truncated)
}
fn ensure_home_writable() -> Result<(), SetupError> {
let home = crate::cli::neural_config::resolve_leindex_home()
.ok_or_else(|| SetupError::Io("Cannot resolve LeIndex home directory.".to_string()))?;
let config_dir = home.join("config");
if let Err(e) = std::fs::create_dir_all(&config_dir) {
let reason = e.to_string();
if reason.to_lowercase().contains("permission")
|| reason.to_lowercase().contains("read-only")
|| e.kind() == std::io::ErrorKind::PermissionDenied
{
return Err(SetupError::PermissionDenied {
path: config_dir,
reason,
});
}
return Err(SetupError::Io(format!(
"Cannot create {}: {}",
config_dir.display(),
reason
)));
}
let sentinel = config_dir.join(".leindex-setup-probe");
if let Err(e) = std::fs::write(&sentinel, b"probe") {
let reason = e.to_string();
if reason.to_lowercase().contains("permission")
|| reason.to_lowercase().contains("read-only")
|| e.kind() == std::io::ErrorKind::PermissionDenied
{
return Err(SetupError::PermissionDenied {
path: sentinel,
reason,
});
}
return Err(SetupError::Io(format!(
"Cannot write to {}: {}",
config_dir.display(),
reason
)));
}
let _ = std::fs::remove_file(&sentinel);
Ok(())
}
fn run_embedding_smoke_test(expected_provider: Option<ExecutionProvider>) -> SmokeTestResult {
run_embedding_smoke_test_inner(expected_provider)
}
#[cfg(feature = "onnx")]
fn run_embedding_smoke_test_inner(expected_provider: Option<ExecutionProvider>) -> SmokeTestResult {
use crate::search::onnx::EmbeddingClient;
const SMOKE_TEST_TEXT: &str = "hello world";
let client = EmbeddingClient::new_pipe();
let provider_label: String = expected_provider
.map(|p| p.config_value().to_string())
.unwrap_or_else(|| "auto".to_string());
match client.embed(&[SMOKE_TEST_TEXT.to_string()], QWEN3_EMBEDDING_DIMENSION) {
Ok(response) => {
let active_provider =
client.wait_for_active_execution_provider(std::time::Duration::from_millis(500));
if response.count == 0 {
return SmokeTestResult {
passed: false,
skipped: false,
dimension: None,
execution_provider: active_provider,
configured_provider_label: Some(provider_label),
error: Some("worker returned zero embeddings".to_string()),
note: None,
};
}
SmokeTestResult::from_embedding_outcome(
response.dimension,
active_provider,
Some(provider_label),
)
}
Err(e) => {
let msg = e.to_string();
let active_provider = client.active_execution_provider();
SmokeTestResult {
passed: false,
skipped: false,
dimension: None,
execution_provider: active_provider,
configured_provider_label: Some(provider_label),
error: Some(msg),
note: None,
}
}
}
}
#[cfg(not(feature = "onnx"))]
fn run_embedding_smoke_test_inner(
_expected_provider: Option<ExecutionProvider>,
) -> SmokeTestResult {
SmokeTestResult {
passed: true, skipped: true,
dimension: None,
execution_provider: None,
configured_provider_label: None,
error: None,
note: Some(
"Binary compiled without --features onnx; smoke test skipped. \
ORT and models are loaded at runtime by the leindex-embed worker. \
To verify neural search: leindex search \"test\" --project ."
.to_string(),
),
}
}
fn merge_with_existing(
mut new_config: crate::cli::neural_config::LeIndexConfig,
) -> (
crate::cli::neural_config::LeIndexConfig,
Option<RecoveryNotice>,
) {
match crate::cli::neural_config::LeIndexConfig::load_or_recover() {
Ok((existing, action)) => {
let notice = match action {
crate::cli::neural_config::RecoveryAction::RecoveredFromCorrupt(backup) => {
Some(RecoveryNotice::RecoveredFromCorrupt(backup))
}
crate::cli::neural_config::RecoveryAction::Loaded => {
if existing.search.search_mode != new_config.search.search_mode {
new_config.search = existing.search;
}
if existing.indexing.batch_size != new_config.indexing.batch_size {
new_config.indexing = existing.indexing;
}
if let Some(ref ort_path) = existing.neural.ort_dylib_path {
if !std::path::Path::new(ort_path).exists() {
Some(RecoveryNotice::Migrated)
} else {
None
}
} else {
None
}
}
crate::cli::neural_config::RecoveryAction::CreatedDefault => None,
};
(new_config, notice)
}
Err(_) => (new_config, None),
}
}
#[derive(Debug, Clone)]
enum RecoveryNotice {
Migrated,
RecoveredFromCorrupt(PathBuf),
}
fn build_config(
choices: &SetupChoices,
ort_dylib_path: Option<&std::path::Path>,
ort_version: Option<&str>,
) -> crate::cli::neural_config::LeIndexConfig {
use crate::cli::neural_config::{IndexingConfig, NeuralConfig, SearchConfig};
let provider_str = choices.provider.map(|p| p.config_value()).unwrap_or("auto");
crate::cli::neural_config::LeIndexConfig {
neural: NeuralConfig {
enabled: choices.neural_enabled,
execution_provider: provider_str.to_string(),
ort_dylib_path: ort_dylib_path.map(|p| p.display().to_string()),
ort_version: ort_version.map(|s| s.to_string()),
model_dir: crate::cli::neural_config::model_dir_path()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "~/.leindex/models".to_string()),
model_name: model_name_for_provider(choices.provider).to_string(),
},
search: SearchConfig::default(),
indexing: IndexingConfig::default(),
}
}
const QWEN3_ONNX_REPOSITORY: &str = "zhiqing/Qwen3-Embedding-0.6B-ONNX";
const QWEN3_ONNX_REVISION: &str = "c96cc9c82d08ee7869600e2191078fc939957026";
const QWEN3_REMOTE_MODEL: &str = "model.onnx";
const QWEN3_LOCAL_MODEL: &str = crate::cli::leindex::model_download::DYNAMIC_MODEL_ONNX_FILENAME;
const QWEN3_MODEL_FILES: &[&str] = &[QWEN3_REMOTE_MODEL, "tokenizer.json", "config.json"];
#[derive(Debug, Clone, Copy)]
struct ModelDownloadProfile {
repository: &'static str,
revision: &'static str,
remote_model: &'static str,
local_model: &'static str,
files: &'static [&'static str],
}
fn model_download_profile(_provider: Option<ExecutionProvider>) -> ModelDownloadProfile {
ModelDownloadProfile {
repository: QWEN3_ONNX_REPOSITORY,
revision: QWEN3_ONNX_REVISION,
remote_model: QWEN3_REMOTE_MODEL,
local_model: QWEN3_LOCAL_MODEL,
files: QWEN3_MODEL_FILES,
}
}
fn model_name_for_provider(provider: Option<ExecutionProvider>) -> &'static str {
model_download_profile(provider)
.local_model
.strip_suffix(".onnx")
.expect("model profile local filename must end in .onnx")
}
fn check_ort_installed() -> bool {
get_ort_version().is_some()
}
pub fn get_ort_version() -> Option<String> {
let candidates = ["python3", "python"];
for cmd in &candidates {
let result = Command::new(cmd)
.arg("-c")
.arg("import onnxruntime; print(onnxruntime.__version__)")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.output();
if let Ok(out) = result {
if out.status.success() {
let v = String::from_utf8_lossy(&out.stdout).trim().to_string();
if !v.is_empty() {
return Some(v);
}
}
}
}
None
}
pub const MIN_ORT_VERSION: (u32, u32, u32) = (1, 20, 0);
pub const MAX_ORT_MAJOR: u32 = 1;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum VersionCompatibility {
Supported,
Unsupported {
required_min: String,
reason: String,
},
TooNew {
supported_max: String,
reason: String,
},
}
pub fn parse_version(s: &str) -> Option<(u32, u32, u32)> {
let core = s.split('-').next().unwrap_or(s);
let core = core.split('+').next().unwrap_or(core);
let mut parts = core.split('.');
let major = parts.next()?.parse::<u32>().ok()?;
let minor = parts.next().unwrap_or("0").parse::<u32>().ok()?;
let patch = parts.next().unwrap_or("0").parse::<u32>().ok()?;
Some((major, minor, patch))
}
pub fn check_ort_version_compatibility(detected: &str) -> VersionCompatibility {
let Some(version) = parse_version(detected) else {
return VersionCompatibility::Unsupported {
required_min: format!(
"{}.{}.{}",
MIN_ORT_VERSION.0, MIN_ORT_VERSION.1, MIN_ORT_VERSION.2
),
reason: format!(
"detected version '{}' is not a recognized onnxruntime release",
detected
),
};
};
if version.0 > MAX_ORT_MAJOR {
return VersionCompatibility::TooNew {
supported_max: format!("{}.x", MAX_ORT_MAJOR),
reason: format!(
"ORT {}.{} introduces breaking ABI changes; expected <= {}.x",
version.0, version.1, MAX_ORT_MAJOR
),
};
}
if version < MIN_ORT_VERSION {
return VersionCompatibility::Unsupported {
required_min: format!(
"{}.{}.{}",
MIN_ORT_VERSION.0, MIN_ORT_VERSION.1, MIN_ORT_VERSION.2
),
reason: "this ORT build lacks APIs the worker depends on".to_string(),
};
}
VersionCompatibility::Supported
}
fn check_provider_available(provider: ExecutionProvider) -> bool {
let provider_name = match provider {
ExecutionProvider::Migraphx => "MIGraphXExecutionProvider",
ExecutionProvider::Cuda => "CUDAExecutionProvider",
ExecutionProvider::Cpu => return true, };
let check_script = format!(
"import onnxruntime as ort; providers = ort.get_available_providers(); print('{}' in providers)",
provider_name
);
for cmd in &["python3", "python"] {
let result = Command::new(cmd)
.arg("-c")
.arg(&check_script)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.output();
if let Ok(out) = result {
if out.status.success() {
let output = String::from_utf8_lossy(&out.stdout).trim().to_string();
if output == "True" {
return true;
}
}
}
}
false
}
#[allow(dead_code)]
fn check_model_present() -> bool {
check_model_present_for_name("qwen3-embed-0.6b")
}
fn check_model_present_for_name(model_name: &str) -> bool {
let model_filename = format!("{}.onnx", model_name);
if let Some(model_dir) = crate::cli::neural_config::model_dir_path() {
if model_assets_present(&model_dir, &model_filename) {
return true;
}
}
if let Ok(exe) = std::env::current_exe() {
if let Some(parent) = exe.parent() {
if model_assets_present(&parent.join("models"), &model_filename) {
return true;
}
if let Some(gp) = parent.parent() {
if model_assets_present(&gp.join("models"), &model_filename) {
return true;
}
}
}
}
false
}
fn model_assets_present(model_dir: &Path, model_filename: &str) -> bool {
if !model_dir.join(model_filename).exists() {
return false;
}
if model_filename != crate::cli::leindex::model_download::DYNAMIC_MODEL_ONNX_FILENAME {
return true;
}
dynamic_model_assets_present(model_dir)
}
fn dynamic_model_assets_present(model_dir: &Path) -> bool {
const MIN_MODEL_BYTES: u64 = 100 * 1024 * 1024;
let model = model_dir.join(crate::cli::leindex::model_download::DYNAMIC_MODEL_ONNX_FILENAME);
if std::fs::metadata(model)
.map(|metadata| metadata.len() < MIN_MODEL_BYTES)
.unwrap_or(true)
{
return false;
}
["tokenizer.json", "config.json"].iter().all(|file| {
std::fs::metadata(model_dir.join(file))
.map(|metadata| metadata.len() > 0)
.unwrap_or(false)
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ModelChecksumStatus {
Missing,
Ok,
Unknown,
Mismatch { expected: String, actual: String },
}
#[allow(dead_code)]
pub fn model_checksum_status() -> ModelChecksumStatus {
model_checksum_status_for_name("qwen3-embed-0.6b")
}
fn model_checksum_status_for_name(model_name: &str) -> ModelChecksumStatus {
use crate::cli::leindex::model_download::{
check_file_against_manifest, parse_checksums, CheckResult, DYNAMIC_MODEL_ONNX_FILENAME,
};
let model_dir = match crate::cli::neural_config::model_dir_path() {
Some(d) => d,
None => return ModelChecksumStatus::Missing,
};
let model_filename = format!("{}.onnx", model_name);
let onnx_path = model_dir.join(&model_filename);
if !onnx_path.exists() {
return ModelChecksumStatus::Missing;
}
let manifest_path = model_dir.join("checksums.sha256");
let manifest_str = match std::fs::read_to_string(&manifest_path) {
Ok(s) => s,
Err(_) => return ModelChecksumStatus::Unknown,
};
let manifest = parse_checksums(&manifest_str);
let status = match check_file_against_manifest(&onnx_path, &manifest) {
Ok(CheckResult::Verified) => ModelChecksumStatus::Ok,
Ok(CheckResult::NoEntry) => ModelChecksumStatus::Unknown,
Ok(CheckResult::Mismatch { expected, actual }) => {
ModelChecksumStatus::Mismatch { expected, actual }
}
Ok(CheckResult::Missing) => ModelChecksumStatus::Missing,
Err(_) => ModelChecksumStatus::Unknown,
};
if model_filename == DYNAMIC_MODEL_ONNX_FILENAME && status == ModelChecksumStatus::Ok {
let metadata_verified = ["tokenizer.json", "config.json"].iter().all(|file| {
matches!(
check_file_against_manifest(&model_dir.join(file), &manifest),
Ok(CheckResult::Verified)
)
});
if !metadata_verified {
return ModelChecksumStatus::Unknown;
}
}
status
}
fn install_ort(provider: ExecutionProvider) -> Result<(), SetupError> {
let package = provider.pip_package();
let package_spec = pip_ort_package_spec(provider);
println!("Installing {} via pip...", package_spec);
let pip_cmd = find_pip().ok_or(SetupError::PipNotFound)?;
let result = Command::new(&pip_cmd.0)
.args(&pip_cmd.1)
.arg("install")
.arg(&package_spec)
.arg("--upgrade")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output();
match result {
Ok(out) if out.status.success() => {
let stdout = String::from_utf8_lossy(&out.stdout);
let stderr = String::from_utf8_lossy(&out.stderr);
let combined = if stdout.is_empty() {
stderr.to_string()
} else {
stdout.to_string()
};
if let Some(version_line) = combined
.lines()
.find(|l| l.contains("Successfully installed") && l.contains(package))
{
println!(" -> {}", version_line.trim());
} else {
println!(" -> Successfully installed {}.", package);
}
Ok(())
}
Ok(out) => {
let stderr = String::from_utf8_lossy(&out.stderr);
let stdout = String::from_utf8_lossy(&out.stdout);
let combined = format!("{}\n{}", stdout, stderr);
if is_network_error(&combined) {
return Err(SetupError::PipNetworkFailed {
package: package.to_string(),
exit_code: out.status.code().unwrap_or(-1),
output: truncate_for_error(&stderr),
});
}
Err(SetupError::PipInstallFailed {
package: package.to_string(),
exit_code: out.status.code().unwrap_or(-1),
})
}
Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
Err(SetupError::PipNotFound)
}
Err(e) => Err(SetupError::Io(format!("Failed to run pip: {}", e))),
}
}
fn pip_ort_package_spec(provider: ExecutionProvider) -> String {
format!(
"{}>={}.{}.{},<{}",
provider.pip_package(),
MIN_ORT_VERSION.0,
MIN_ORT_VERSION.1,
MIN_ORT_VERSION.2,
MAX_ORT_MAJOR + 1
)
}
fn is_network_error(output: &str) -> bool {
let lower = output.to_lowercase();
const NETWORK_HINTS: &[&str] = &[
"could not fetch url",
"connection error",
"connectionerror",
"connectionrefusederror",
"connection reset",
"connection timed out",
"connection broken",
"ssl: certificate_verify_failed",
"ssl certificate_verify_failed",
"temporary failure in name resolution",
"failed to establish a new connection",
"max retries exceeded",
"network is unreachable",
"read timed out",
"remotedisconnectederror",
"newconnectionerror",
"getaddrinfo failed",
"name or service not known",
"no such device or address",
];
NETWORK_HINTS.iter().any(|hint| lower.contains(hint))
}
fn truncate_for_error(s: &str) -> String {
const MAX_LINES: usize = 12;
let lines: Vec<&str> = s.lines().collect();
if lines.len() <= MAX_LINES {
s.trim().to_string()
} else {
format!(
"{}\n... ({} more lines truncated)",
lines[..MAX_LINES].join("\n").trim(),
lines.len() - MAX_LINES
)
}
}
fn find_pip() -> Option<(String, Vec<String>)> {
if let Ok(value) = std::env::var("PIP_BIN") {
if !value.trim().is_empty() {
if let Some((program, prefix)) = parse_pip_bin_override(&value) {
if Command::new(&program)
.args(&prefix)
.arg("--version")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
{
return Some((program, prefix));
}
eprintln!(
"warning: PIP_BIN is set to '{}' but invoking it failed; falling back to PATH discovery.",
value
);
} else {
eprintln!(
"warning: PIP_BIN is set to an unsupported command shape; use /path/to/pip or \"python3 -m pip\". Falling back to PATH discovery."
);
}
}
}
for cmd in &["pip3", "pip"] {
if Command::new(cmd)
.arg("--version")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
{
return Some((cmd.to_string(), Vec::new()));
}
}
for py in &["python3", "python"] {
if Command::new(py)
.arg("-m")
.arg("pip")
.arg("--version")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
{
return Some((py.to_string(), vec!["-m".to_string(), "pip".to_string()]));
}
}
None
}
fn parse_pip_bin_override(value: &str) -> Option<(String, Vec<String>)> {
let parts = split_pip_bin_override(value)?;
let program = parts.first()?.clone();
let args = &parts[1..];
if args.is_empty() {
return Some((program, Vec::new()));
}
if args == ["-m", "pip"] {
return Some((program, vec!["-m".to_string(), "pip".to_string()]));
}
None
}
fn split_pip_bin_override(value: &str) -> Option<Vec<String>> {
let mut parts = Vec::new();
let mut current = String::new();
let mut quote: Option<char> = None;
for ch in value.trim().chars() {
match quote {
Some(q) if ch == q => quote = None,
Some(_) => current.push(ch),
None if ch == '\'' || ch == '"' => quote = Some(ch),
None if ch.is_whitespace() => {
if !current.is_empty() {
parts.push(std::mem::take(&mut current));
}
}
None if matches!(ch, '|' | '&' | ';' | '<' | '>' | '`' | '\n' | '\r') => return None,
None => current.push(ch),
}
}
if quote.is_some() {
return None;
}
if !current.is_empty() {
parts.push(current);
}
if parts.is_empty() {
None
} else {
Some(parts)
}
}
pub(crate) fn discover_ort_path() -> Option<PathBuf> {
#[cfg(feature = "onnx")]
if let Some(outcome) = leindex_embed::ort_discovery::discover_path_only() {
return Some(outcome.path);
}
discover_ort_path_fallback()
}
fn discover_ort_path_fallback() -> Option<PathBuf> {
if let Ok(path) = std::env::var("ORT_DYLIB_PATH") {
let path = PathBuf::from(path);
if path.exists() {
return Some(path);
}
}
if let Ok(config) = crate::cli::neural_config::LeIndexConfig::load() {
if let Some(path) = config.neural.ort_dylib_path {
let path = PathBuf::from(path);
if path.exists() {
return Some(path);
}
}
}
let leindex_home = std::env::var("LEINDEX_HOME")
.map(PathBuf::from)
.or_else(|_| std::env::var("HOME").map(|home| PathBuf::from(home).join(".leindex")))
.ok();
if let Some(home) = leindex_home {
let dir = home.join("lib");
for lib_name in ort_lib_names() {
let candidate = dir.join(lib_name);
if candidate.exists() {
return Some(candidate);
}
}
if let Some(found) = scan_dir_for_ort_lib(&dir) {
return Some(found);
}
}
if let Ok(exe) = std::env::current_exe() {
if let Some(dir) = exe.parent() {
for lib_name in ort_lib_names() {
let candidate = dir.join(lib_name);
if candidate.exists() {
return Some(candidate);
}
}
if let Some(found) = scan_dir_for_ort_lib(dir) {
return Some(found);
}
}
}
for py in &["python3", "python"] {
let result = Command::new(py)
.arg("-c")
.arg("import os, onnxruntime.capi as c; print(os.path.dirname(c.__file__))")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::null())
.output();
if let Ok(out) = result {
if out.status.success() {
let capi_dir = String::from_utf8_lossy(&out.stdout).trim().to_string();
let dir = PathBuf::from(&capi_dir);
if dir.is_dir() {
for lib_name in ort_lib_names() {
let candidate = dir.join(lib_name);
if candidate.exists() {
return Some(candidate);
}
}
if let Some(path) = scan_dir_for_ort_lib(&dir) {
return Some(path);
}
}
}
}
}
for path in &["/usr/local/lib", "/usr/lib"] {
let dir = PathBuf::from(path);
for lib_name in ort_lib_names() {
let candidate = dir.join(lib_name);
if candidate.exists() {
return Some(candidate);
}
}
if let Some(found) = scan_dir_for_ort_lib(&dir) {
return Some(found);
}
}
None
}
fn scan_dir_for_ort_lib(dir: &Path) -> Option<PathBuf> {
let mut matches = std::fs::read_dir(dir)
.ok()
.into_iter()
.flat_map(|entries| entries.filter_map(Result::ok))
.map(|entry| entry.path())
.filter(|path| {
path.file_name()
.and_then(|name| name.to_str())
.map(is_ort_runtime_lib_name_for_setup)
.unwrap_or(false)
})
.collect::<Vec<_>>();
matches.sort();
matches.pop()
}
fn ort_lib_names() -> &'static [&'static str] {
#[cfg(target_os = "linux")]
{
&["libonnxruntime.so"]
}
#[cfg(target_os = "macos")]
{
&["libonnxruntime.dylib"]
}
#[cfg(target_os = "windows")]
{
&["onnxruntime.dll"]
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
{
&["libonnxruntime.so"]
}
}
#[cfg(target_os = "linux")]
fn is_ort_runtime_lib_name_for_setup(name: &str) -> bool {
name == "libonnxruntime.so" || name.starts_with("libonnxruntime.so.")
}
#[cfg(target_os = "macos")]
fn is_ort_runtime_lib_name_for_setup(name: &str) -> bool {
name == "libonnxruntime.dylib"
|| (name.starts_with("libonnxruntime.") && name.ends_with(".dylib"))
}
#[cfg(target_os = "windows")]
fn is_ort_runtime_lib_name_for_setup(name: &str) -> bool {
name.eq_ignore_ascii_case("onnxruntime.dll")
}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]
fn is_ort_runtime_lib_name_for_setup(name: &str) -> bool {
ort_lib_names().iter().any(|candidate| candidate == &name)
}
fn find_hugging_face_cli() -> Option<String> {
let mut candidates = Vec::new();
if let Ok(path) = std::env::var("HF_BIN") {
if !path.trim().is_empty() {
candidates.push(path);
}
}
candidates.extend(["hf".to_string(), "huggingface-cli".to_string()]);
candidates.into_iter().find(|program| {
Command::new(program)
.arg("download")
.arg("--help")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.status()
.map(|status| status.success())
.unwrap_or(false)
})
}
fn ensure_hugging_face_cli() -> Result<String, SetupError> {
if let Some(cli) = find_hugging_face_cli() {
return Ok(cli);
}
let package = "huggingface_hub";
let pip_cmd = find_pip().ok_or(SetupError::PipNotFound)?;
println!("Installing {} for model downloads...", package);
let output = Command::new(&pip_cmd.0)
.args(&pip_cmd.1)
.arg("install")
.arg(package)
.arg("--upgrade")
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()
.map_err(|_| SetupError::PipInstallFailed {
package: package.to_string(),
exit_code: -1,
})?;
if !output.status.success() {
let combined = format!(
"{}\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
);
if is_network_error(&combined) {
return Err(SetupError::PipNetworkFailed {
package: package.to_string(),
exit_code: output.status.code().unwrap_or(-1),
output: truncate_for_error(&combined),
});
}
return Err(SetupError::PipInstallFailed {
package: package.to_string(),
exit_code: output.status.code().unwrap_or(-1),
});
}
find_hugging_face_cli().ok_or(SetupError::HuggingFaceCliNotFound)
}
fn profile_assets_verified(model_dir: &Path, profile: ModelDownloadProfile) -> bool {
use crate::cli::leindex::model_download::{
check_file_against_manifest, parse_checksums, CheckResult,
};
if !dynamic_model_assets_present(model_dir) {
return false;
}
let Ok(contents) = std::fs::read_to_string(model_dir.join("checksums.sha256")) else {
return false;
};
let source_marker = format!("# source: {}@{}", profile.repository, profile.revision);
if !contents
.lines()
.any(|line| line.trim() == source_marker.as_str())
{
return false;
}
let manifest = parse_checksums(&contents);
[profile.local_model, "tokenizer.json", "config.json"]
.iter()
.all(|file| {
matches!(
check_file_against_manifest(&model_dir.join(file), &manifest),
Ok(CheckResult::Verified)
)
})
}
fn install_downloaded_model_file(src: &Path, dst: &Path) -> Result<(), SetupError> {
if !src.exists() {
return Err(SetupError::Io(format!(
"Hugging Face download did not produce {}",
src.display()
)));
}
if dst.exists() {
std::fs::remove_file(dst)
.map_err(|e| SetupError::Io(format!("Cannot replace {}: {}", dst.display(), e)))?;
}
if std::fs::rename(src, dst).is_err() {
std::fs::copy(src, dst).map_err(|e| {
SetupError::Io(format!(
"Cannot install downloaded model file {}: {}",
dst.display(),
e
))
})?;
}
Ok(())
}
fn generate_profile_checksum_manifest(
model_dir: &Path,
profile: ModelDownloadProfile,
) -> Result<(), SetupError> {
use crate::cli::leindex::model_download::sha256_of_file;
let mut manifest = format!("# source: {}@{}\n", profile.repository, profile.revision);
for file in [profile.local_model, "tokenizer.json", "config.json"] {
let path = model_dir.join(file);
let hash = sha256_of_file(&path)
.map_err(|e| SetupError::Io(format!("Cannot checksum {}: {}", path.display(), e)))?;
manifest.push_str(&format!("{} {}\n", hash, file));
}
std::fs::write(model_dir.join("checksums.sha256"), manifest)
.map_err(|e| SetupError::Io(format!("Cannot write model checksums: {}", e)))
}
fn ensure_hugging_face_model_present(
profile: ModelDownloadProfile,
model_dir: &Path,
) -> Result<bool, SetupError> {
if profile_assets_verified(model_dir, profile) {
println!(
" -> {} already present; all model checksums verified.",
profile.local_model
);
return Ok(true);
}
let hf = ensure_hugging_face_cli()?;
let staging = model_dir.join(format!(".hf-download-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&staging);
std::fs::create_dir_all(&staging).map_err(|e| {
SetupError::Io(format!(
"Cannot create Hugging Face staging directory {}: {}",
staging.display(),
e
))
})?;
println!(
"Downloading {} with Hugging Face CLI...",
profile.repository
);
let output = Command::new(&hf)
.arg("download")
.arg(profile.repository)
.args(profile.files)
.arg("--revision")
.arg(profile.revision)
.arg("--local-dir")
.arg(&staging)
.stdin(std::process::Stdio::null())
.stdout(std::process::Stdio::piped())
.stderr(std::process::Stdio::piped())
.output()
.map_err(|e| SetupError::Io(format!("Failed to run {}: {}", hf, e)))?;
if !output.status.success() {
let details = truncate_for_error(&format!(
"{}\n{}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
));
let _ = std::fs::remove_dir_all(&staging);
return Err(SetupError::HuggingFaceDownloadFailed {
repository: profile.repository.to_string(),
exit_code: output.status.code().unwrap_or(-1),
output: details,
});
}
install_downloaded_model_file(
&staging.join(profile.remote_model),
&model_dir.join(profile.local_model),
)?;
for file in ["tokenizer.json", "config.json"] {
install_downloaded_model_file(&staging.join(file), &model_dir.join(file))?;
}
let _ = std::fs::remove_dir_all(&staging);
if !dynamic_model_assets_present(model_dir) {
return Err(SetupError::ModelUnavailable {
model_name: profile.local_model.trim_end_matches(".onnx").to_string(),
model_dir: model_dir.to_path_buf(),
});
}
generate_profile_checksum_manifest(model_dir, profile)?;
println!(" -> Model files ready at {}", model_dir.display());
Ok(true)
}
fn ensure_models_present(
provider: Option<ExecutionProvider>,
model_name: &str,
) -> Result<bool, SetupError> {
use crate::cli::leindex::model_download::{
self, check_file_against_manifest, download_file_with_retry, iter_model_files,
parse_checksums, CheckResult, DownloadOutcome, DEFAULT_DOWNLOAD_RETRIES,
DYNAMIC_MODEL_ONNX_FILENAME,
};
let model_dir = crate::cli::neural_config::model_dir_path()
.ok_or_else(|| SetupError::Io("Cannot resolve model directory".to_string()))?;
std::fs::create_dir_all(&model_dir)
.map_err(|e| SetupError::Io(format!("Cannot create model dir: {}", e)))?;
let model_filename = format!("{}.onnx", model_name);
if model_filename == DYNAMIC_MODEL_ONNX_FILENAME {
return ensure_hugging_face_model_present(model_download_profile(provider), &model_dir);
}
let manifest_path = model_dir.join("checksums.sha256");
let model_onnx_name = model_download::MODEL_ONNX_FILENAME;
if let Some(bundled_dir) = model_download::find_bundled_models() {
copy_bundled_models(&bundled_dir, &model_dir);
}
let manifest_str = std::fs::read_to_string(&manifest_path).unwrap_or_default();
let manifest = parse_checksums(&manifest_str);
let mut downloaded_any = false;
let mut model_present_after = false;
for file in iter_model_files() {
let dest = model_dir.join(file.local);
let required = file.local != "checksums.sha256" && file.local != "LICENSE";
match check_file_against_manifest(&dest, &manifest)
.map_err(|e| SetupError::Io(format!("Cannot stat {}: {}", dest.display(), e)))?
{
CheckResult::Verified => {
println!(" -> {} already present, checksum verified.", file.local);
if file.local == model_onnx_name {
model_present_after = true;
}
continue;
}
CheckResult::NoEntry => {
if dest.exists() {
println!(
" -> {} present (no checksum entry; cannot verify).",
file.local
);
if file.local == model_onnx_name {
model_present_after = true;
}
continue;
}
}
CheckResult::Mismatch { expected, actual } => {
println!(
" -> WARNING: {} checksum mismatch (expected {}..., got {}...).",
file.local,
short_hash(&expected),
short_hash(&actual)
);
println!(" Removing corrupt file and re-downloading...");
let _ = std::fs::remove_file(&dest);
}
CheckResult::Missing => {
}
}
let outcome: DownloadOutcome = match download_file_with_retry(
file,
&model_dir,
Some(&manifest_path),
DEFAULT_DOWNLOAD_RETRIES,
) {
Ok(o) => o,
Err(e) => {
if !required {
println!(
" -> {} not available on the CDN; skipping (non-fatal).",
file.local
);
continue;
}
return Err(map_model_download_error(e));
}
};
let fresh_manifest_str =
std::fs::read_to_string(&manifest_path).unwrap_or_else(|_| manifest_str.clone());
let fresh_manifest = parse_checksums(&fresh_manifest_str);
let recheck = check_file_against_manifest(&outcome.path, &fresh_manifest)
.unwrap_or(CheckResult::Missing);
match recheck {
CheckResult::Verified => {
println!(" -> {} downloaded, checksum verified.", file.local);
}
CheckResult::NoEntry => {
println!(
" -> {} downloaded (no checksum entry in manifest; cannot verify).",
file.local
);
}
CheckResult::Mismatch { expected, actual } => {
if required {
return Err(SetupError::ModelChecksumPostDownload {
file: file.local.to_string(),
expected,
actual,
});
} else {
println!(
" -> WARNING: {} downloaded but checksum mismatch; \
keeping anyway (non-required file).",
file.local
);
}
}
CheckResult::Missing => {
return Err(SetupError::Io(format!(
"Download reported success but file is missing: {}",
outcome.path.display()
)));
}
}
if file.local == model_onnx_name {
model_present_after = true;
}
downloaded_any = true;
}
if !manifest_path.exists() {
if let Err(e) = generate_local_checksum_manifest(&model_dir) {
eprintln!(
"warning: could not generate local checksum manifest ({}); \
future runs cannot verify file integrity until checksums.sha256 \
is present in {}.",
e,
model_dir.display()
);
} else {
println!(" -> Generated local checksums.sha256 for future verification.");
}
}
if downloaded_any {
println!("\nModel files ready at {}", model_dir.display());
}
Ok(model_present_after)
}
fn generate_local_checksum_manifest(model_dir: &std::path::Path) -> std::io::Result<()> {
use crate::cli::leindex::model_download::{iter_model_files, sha256_of_file};
let manifest_path = model_dir.join("checksums.sha256");
let mut out = String::new();
for file in iter_model_files() {
if file.local == "checksums.sha256" {
continue;
}
let path = model_dir.join(file.local);
if path.exists() {
let hash = sha256_of_file(&path)?;
out.push_str(&hash);
out.push_str(" ");
out.push_str(file.local);
out.push('\n');
}
}
if out.is_empty() {
return Ok(());
}
std::fs::write(&manifest_path, out)
}
fn copy_bundled_models(bundled_dir: &std::path::Path, dest_dir: &std::path::Path) {
let skip_copy = std::env::var("LEINDEX_SKIP_MODEL_COPY")
.map(|v| !v.trim().is_empty())
.unwrap_or(false);
let mut linked_any = false;
for file in crate::cli::leindex::model_download::iter_model_files() {
let src = bundled_dir.join(file.local);
let dst = dest_dir.join(file.local);
if src.exists() && !dst.exists() {
if !linked_any {
println!(
" -> Linking bundled model files from {}...",
bundled_dir.display()
);
linked_any = true;
}
let src_resolved = std::fs::canonicalize(&src).unwrap_or_else(|_| src.clone());
let linked = try_link_model_file(&src_resolved, &dst, skip_copy);
if let Err(e) = linked {
if skip_copy {
eprintln!(
"warning: LEINDEX_SKIP_MODEL_COPY is set and symlink/hardlink failed for {} ({}); \
skipping file (will be resolved at download stage if missing).",
file.local, e
);
} else {
eprintln!(
"warning: failed to link {} from bundle ({}); will download instead.",
file.local, e
);
}
}
}
}
}
#[cfg(unix)]
fn try_symlink_model_file(src: &Path, dst: &Path) -> std::io::Result<()> {
std::os::unix::fs::symlink(src, dst)
}
#[cfg(windows)]
fn try_symlink_model_file(src: &Path, dst: &Path) -> std::io::Result<()> {
std::os::windows::fs::symlink_file(src, dst)
}
#[cfg(not(any(unix, windows)))]
fn try_symlink_model_file(_src: &Path, _dst: &Path) -> std::io::Result<()> {
Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
"symlinks are not supported on this platform",
))
}
fn try_link_model_file(
src: &std::path::Path,
dst: &std::path::Path,
skip_copy: bool,
) -> std::io::Result<()> {
if let Some(parent) = dst.parent() {
std::fs::create_dir_all(parent)?;
}
if try_symlink_model_file(src, dst).is_ok() {
return Ok(());
}
match std::fs::hard_link(src, dst) {
Ok(()) => return Ok(()),
Err(e) => {
if skip_copy {
return Err(std::io::Error::new(
std::io::ErrorKind::Unsupported,
format!(
"LEINDEX_SKIP_MODEL_COPY is set but symlink and hardlink both failed: {}",
e
),
));
}
}
}
std::fs::copy(src, dst).map(|_| ())
}
fn short_hash(hash: &str) -> String {
if hash.len() <= 12 {
hash.to_string()
} else {
format!("{}...", &hash[..12])
}
}
fn map_model_download_error(
e: crate::cli::leindex::model_download::ModelDownloadError,
) -> SetupError {
use crate::cli::leindex::model_download::ModelDownloadError as Mde;
match e {
Mde::CurlNotFound => SetupError::CurlNotFound,
Mde::Io(path, msg) => SetupError::Io(format!("{}: {}", path.display(), msg)),
Mde::ChecksumMismatch {
file,
expected,
actual,
} => SetupError::ModelDownloadFailed {
file,
url: format!("checksum expected {}, got {}", expected, actual),
exit_code: -1,
network: false,
},
Mde::DownloadFailed {
file,
url,
exit_code,
network,
} => SetupError::ModelDownloadFailed {
file,
url,
exit_code,
network,
},
}
}
pub fn run_check() -> Result<CheckResult, SetupError> {
let (config, action) = crate::cli::neural_config::LeIndexConfig::load_or_recover()
.map_err(|e| SetupError::ConfigRead(e.to_string()))?;
let ort_installed = check_ort_installed();
let live_version = get_ort_version();
let model_present = check_model_present_for_name(&config.neural.model_name);
let checksum_status = model_checksum_status_for_name(&config.neural.model_name);
let ort_path = discover_ort_path().or_else(|| {
config
.neural
.ort_dylib_path
.as_ref()
.map(PathBuf::from)
.filter(|p| p.exists())
});
let ort_version = live_version
.clone()
.or_else(|| config.neural.ort_version.clone());
let fully_configured = config.neural.enabled
&& ort_installed
&& model_present
&& !matches!(checksum_status, ModelChecksumStatus::Mismatch { .. });
println!("\nLeIndex Setup Status\n{}", "=".repeat(20));
println!();
let neural_status = if config.neural.enabled { "ON" } else { "OFF" };
println!("Neural embeddings: {}", neural_status);
println!("Execution provider: {}", config.neural.execution_provider);
let ort_status = if ort_installed {
"installed"
} else {
"not installed"
};
println!("ORT (onnxruntime): {}", ort_status);
if let Some(ref version) = ort_version {
println!("ORT version: {}", version);
match check_ort_version_compatibility(version) {
VersionCompatibility::Supported => {}
VersionCompatibility::Unsupported {
required_min,
reason,
} => {
println!(
" -> WARNING: detected {} but LeIndex requires {} ({}).",
version, required_min, reason
);
println!(" Re-run `leindex setup --neural --cpu` to upgrade.");
}
VersionCompatibility::TooNew {
supported_max,
reason,
} => {
println!(
" -> WARNING: {} is newer than the supported maximum ({}). {}",
version, supported_max, reason
);
}
}
} else if ort_installed {
println!("ORT version: (unable to determine)");
}
if let Some(ref path) = ort_path {
println!("ORT dylib path: {}", path.display());
} else if let Some(ref config_path) = config.neural.ort_dylib_path {
println!("ORT dylib (config): {} [file missing]", config_path);
} else {
println!("ORT dylib path: (not discovered)");
}
let model_status = if model_present { "present" } else { "absent" };
println!("Model files: {}", model_status);
println!("Model name: {}", config.neural.model_name);
println!("Model directory: {}", config.neural.model_dir);
match &checksum_status {
ModelChecksumStatus::Ok => {
println!("Model checksum: verified (matches checksums.sha256)");
}
ModelChecksumStatus::Unknown => {
println!("Model checksum: no manifest entry (cannot verify)");
}
ModelChecksumStatus::Mismatch { expected, actual } => {
println!(
"Model checksum: MISMATCH (expected {}..., got {}...).",
&expected[..expected.len().min(12)],
&actual[..actual.len().min(12)],
);
println!(" Re-run `leindex setup --neural --cpu` to re-download.");
}
ModelChecksumStatus::Missing => {
}
}
println!();
println!("Search mode: {}", config.search.search_mode);
println!("Neural weight: {}", config.search.neural_weight);
if let crate::cli::neural_config::RecoveryAction::RecoveredFromCorrupt(ref backup) = action {
println!();
println!(
"WARNING: Previous config was corrupted. Backed up to: {}",
backup.display()
);
}
println!();
if fully_configured {
println!("Status: Fully configured for neural search");
} else if config.neural.enabled {
println!("Status: Neural enabled but incomplete");
if !ort_installed {
println!(" -> Install ORT: leindex setup --neural --cpu");
}
if !model_present {
println!(
" -> Model files needed for {}: run leindex setup --neural",
config.neural.model_name
);
}
} else {
println!("Status: TF-IDF only (neural not configured)");
println!(" -> To enable neural search: leindex setup --neural --cpu");
}
if let Some(path) = crate::cli::neural_config::config_file_path() {
println!();
println!("Config file: {}", path.display());
}
Ok(CheckResult {
neural_enabled: config.neural.enabled,
provider: config.neural.execution_provider.clone(),
ort_installed,
ort_version,
ort_path,
model_present,
fully_configured,
})
}
#[derive(Debug, Clone)]
#[allow(dead_code)]
pub struct CheckResult {
pub neural_enabled: bool,
pub provider: String,
pub ort_installed: bool,
pub ort_version: Option<String>,
pub ort_path: Option<PathBuf>,
pub model_present: bool,
pub fully_configured: bool,
}
pub fn print_summary(result: &SetupResult) {
println!("\nSetup Summary\n{}", "-".repeat(14));
let neural_str = if result.choices.neural_enabled {
"ON"
} else {
"OFF"
};
println!("Neural: {}", neural_str);
if let Some(provider) = result.choices.provider {
println!("Provider: {}", provider.config_value());
}
let ort_str = if result.ort_installed {
"installed"
} else {
"not installed"
};
println!("ORT: {}", ort_str);
if let Some(ref version) = result.ort_version {
println!("ORT version: {}", version);
}
if let Some(ref path) = result.ort_dylib_path {
println!("ORT path: {}", path.display());
}
let model_str = if result.model_present {
"present"
} else {
"absent"
};
println!("Model: {}", model_str);
if let Some(ref path) = result.config_path {
println!("Config: {}", path.display());
}
if let Some(ref smoke) = result.smoke_test {
println!("Smoke test: {}", smoke.status_line());
if let Some(ref provider) = smoke.configured_provider_label {
println!("Configured EP: {}", provider);
}
if let Some(ref provider) = smoke.execution_provider {
println!("Active EP: {}", provider);
}
if smoke.skipped {
if let Some(ref note) = smoke.note {
println!("Note: {}", truncate_for_display(note, 200));
}
} else if let Some(ref err) = smoke.error {
if !smoke.passed {
println!("Worker error: {}", truncate_for_display(err, 200));
}
}
}
println!();
if result.choices.neural_enabled {
let fully_ready = result.model_present
&& result.ort_installed
&& result
.smoke_test
.as_ref()
.map(|s| s.passed)
.unwrap_or(false);
if fully_ready {
println!("Neural search is ready!");
} else if result.model_present && result.ort_installed {
println!("Neural search is configured but the smoke test failed.");
println!("Re-run: leindex setup --neural --cpu, or run `leindex setup --check`.");
} else {
let missing = if !result.ort_installed && !result.model_present {
"ORT and model files"
} else if !result.ort_installed {
"ORT"
} else {
"model files"
};
println!(
"Neural search is partially configured (missing: {})",
missing
);
println!("Re-run: leindex setup --neural --cpu");
}
} else {
println!("TF-IDF search is ready (neural disabled).");
println!("To enable neural search later: leindex setup");
}
}
pub fn parse_gpu_vendor(s: &str) -> Result<GpuVendor, String> {
match s.to_lowercase().as_str() {
"amd" => Ok(GpuVendor::Amd),
"nvidia" | "cuda" => Ok(GpuVendor::Nvidia),
_ => Err(format!(
"Invalid GPU vendor '{}'. Use 'amd' or 'nvidia'.",
s
)),
}
}
#[derive(Debug)]
pub enum SetupError {
Conflict { message: String },
NoFlags,
Interactive(String),
ConfigWrite(String),
ConfigRead(String),
PipNotFound,
PipInstallFailed { package: String, exit_code: i32 },
PipNetworkFailed {
package: String,
exit_code: i32,
output: String,
},
CurlNotFound,
ModelDownloadFailed {
file: String,
url: String,
exit_code: i32,
network: bool,
},
ModelChecksumPostDownload {
file: String,
expected: String,
actual: String,
},
ModelUnavailable {
model_name: String,
model_dir: PathBuf,
},
HuggingFaceCliNotFound,
HuggingFaceDownloadFailed {
repository: String,
exit_code: i32,
output: String,
},
Io(String),
PermissionDenied {
path: PathBuf,
reason: String,
},
#[allow(dead_code)]
SmokeTestCatastrophic { message: String },
}
impl std::fmt::Display for SetupError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
SetupError::Conflict { message } => write!(f, "{}", message),
SetupError::NoFlags => {
write!(f, "No setup options specified. Use --neural, --no-neural, --cpu, --gpu, or --check. Run 'leindex setup --help' for details.")
}
SetupError::Interactive(msg) => {
write!(f, "Interactive prompt failed: {}. If running in a non-interactive context, use flags like --neural --cpu.", msg)
}
SetupError::ConfigWrite(msg) => {
write!(f, "Failed to write config: {}", msg)
}
SetupError::ConfigRead(msg) => {
write!(f, "Failed to read config: {}", msg)
}
SetupError::PipNotFound => {
write!(
f,
"pip not found on PATH. Install pip first:\n \
- Debian/Ubuntu: sudo apt install python3-pip\n \
- macOS/Linux (ensurepip): python3 -m ensurepip --upgrade\n \
- Or download: https://pip.pypa.io/en/stable/installation/\n \
Alternatively, set PIP_BIN=/path/to/pip \
(or PIP_BIN=\"python3 -m pip\") to point setup at a specific pip, \
or manually install onnxruntime and set ORT_DYLIB_PATH."
)
}
SetupError::PipInstallFailed { package, exit_code } => {
write!(
f,
"Failed to install {} via pip (exit code {}). \
Check your Python environment. \
If onnxruntime is already installed in another Python, \
set PIP_BIN or ORT_DYLIB_PATH to use it.",
package, exit_code
)
}
SetupError::PipNetworkFailed {
package,
exit_code,
output,
} => {
write!(
f,
"Network failure while installing {} via pip (exit code {}). \
Check your internet connection, proxy settings, or PyPI mirror. \
pip output:\n{}",
package, exit_code, output
)
}
SetupError::CurlNotFound => {
write!(
f,
"curl not found on PATH. curl is required to download model \
files (~600 MB) for neural search. Install curl:\n \
- Debian/Ubuntu: sudo apt install curl\n \
- macOS: curl ships with macOS (verify /usr/bin/curl)\n \
- Windows 10+: curl.exe is preinstalled\n \
Alternatively, copy model files manually to \
~/.leindex/models/ and re-run `leindex setup --check`."
)
}
SetupError::ModelDownloadFailed {
file,
url,
exit_code,
network,
} => {
if *network {
write!(
f,
"Network failure downloading '{}' from {} (curl exit code {}). \
Check your internet connection, DNS, proxy settings, or \
the HuggingFace CDN status (https://status.huggingface.co). \
Re-run `leindex setup` to retry, or set LEINDEX_MODEL_PATH \
to point at an offline model directory containing '{}'.",
file, url, exit_code, file
)
} else {
write!(
f,
"Failed to download '{}' from {} (curl exit code {}). \
The file may be temporarily unavailable on the CDN, or the \
repo layout changed. Re-run `leindex setup` to retry, or \
copy the model manually to ~/.leindex/models/. If you have \
an offline copy, set LEINDEX_MODEL_PATH.",
file, url, exit_code
)
}
}
SetupError::ModelChecksumPostDownload {
file,
expected,
actual,
} => {
write!(
f,
"Checksum mismatch after downloading '{}' (expected {}, got {}). \
This usually indicates a CDN mirror returned a corrupted file or \
the model repo layout changed. Wait a few minutes and re-run \
`leindex setup`, or copy the file manually from a trusted source \
to ~/.leindex/models/{}.",
file, expected, actual, file
)
}
SetupError::ModelUnavailable {
model_name,
model_dir,
} => {
write!(
f,
"Model '{}' is not available in {}. \
LeIndex requires a validated dynamic Qwen3 ONNX model plus \
tokenizer.json and config.json. Re-run `leindex setup`, or set \
LEINDEX_MODEL_PATH to a directory containing '{}.onnx' and the metadata files.",
model_name,
model_dir.display(),
model_name
)
}
SetupError::HuggingFaceCliNotFound => {
write!(
f,
"Hugging Face CLI was not found. Install it with \
`python3 -m pip install --upgrade huggingface_hub`, ensure `hf` \
or `huggingface-cli` is on PATH, then rerun `leindex setup`. \
Set HF_BIN to use a specific executable."
)
}
SetupError::HuggingFaceDownloadFailed {
repository,
exit_code,
output,
} => {
write!(
f,
"Hugging Face CLI failed to download '{}' (exit code {}). \
Check network access, authentication, and available disk space, \
then rerun `leindex setup`. Output:\n{}",
repository, exit_code, output
)
}
SetupError::Io(msg) => write!(f, "I/O error: {}", msg),
SetupError::PermissionDenied { path, reason } => {
write!(
f,
"Permission denied writing to {}: {}. \
Check directory permissions, or set LEINDEX_HOME to a writable location \
(e.g., export LEINDEX_HOME=/tmp/leindex).",
path.display(),
reason
)
}
SetupError::SmokeTestCatastrophic { message } => {
write!(
f,
"Embedding smoke test could not run: {}. \
Check that the leindex-embed worker binary is installed and that \
ORT + model files are present. Run `leindex setup --check` for diagnostics.",
message
)
}
}
}
}
impl std::error::Error for SetupError {}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(target_os = "linux")]
#[test]
fn test_setup_ort_lib_name_accepts_versioned_linux_pip_soname() {
assert!(is_ort_runtime_lib_name_for_setup(
"libonnxruntime.so.1.25.0"
));
assert!(is_ort_runtime_lib_name_for_setup("libonnxruntime.so"));
assert!(!is_ort_runtime_lib_name_for_setup(
"libonnxruntime_providers_shared.so"
));
}
#[test]
fn test_setup_smoke_provider_label_is_configured_not_claimed_registered() {
let src = std::fs::read_to_string(file!()).expect("setup.rs should be readable");
let needle = format!("{} {}", "Execution provider", "registered");
assert!(
!src.contains(&needle),
"setup must not claim the provider is 'registered'; it only knows the configured EP"
);
}
#[test]
fn test_resolve_neural_cpu() {
let choices = resolve_from_flags(true, false, true, None).unwrap();
assert!(choices.neural_enabled);
assert_eq!(choices.provider, Some(ExecutionProvider::Cpu));
}
#[test]
fn test_resolve_neural_gpu_amd() {
let choices = resolve_from_flags(true, false, false, Some(GpuVendor::Amd)).unwrap();
assert!(choices.neural_enabled);
assert_eq!(choices.provider, Some(ExecutionProvider::Migraphx));
}
#[test]
fn test_resolve_neural_gpu_nvidia() {
let choices = resolve_from_flags(true, false, false, Some(GpuVendor::Nvidia)).unwrap();
assert!(choices.neural_enabled);
assert_eq!(choices.provider, Some(ExecutionProvider::Cuda));
}
#[test]
fn test_resolve_no_neural() {
let choices = resolve_from_flags(false, true, false, None).unwrap();
assert!(!choices.neural_enabled);
assert!(choices.provider.is_none());
}
#[test]
fn test_resolve_neural_default_cpu() {
let choices = resolve_from_flags(true, false, false, None).unwrap();
assert!(choices.neural_enabled);
assert_eq!(choices.provider, Some(ExecutionProvider::Cpu));
}
#[test]
fn test_conflict_neural_no_neural() {
let result = resolve_from_flags(true, true, false, None);
assert!(matches!(result, Err(SetupError::Conflict { .. })));
}
#[test]
fn test_conflict_cpu_gpu() {
let result = resolve_from_flags(false, false, true, Some(GpuVendor::Amd));
assert!(matches!(result, Err(SetupError::Conflict { .. })));
}
#[test]
fn test_cpu_implies_neural() {
let choices = resolve_from_flags(false, false, true, None).unwrap();
assert!(choices.neural_enabled);
assert_eq!(choices.provider, Some(ExecutionProvider::Cpu));
}
#[test]
fn test_gpu_implies_neural() {
let choices = resolve_from_flags(false, false, false, Some(GpuVendor::Amd)).unwrap();
assert!(choices.neural_enabled);
}
#[test]
fn test_no_flags_errors() {
let result = resolve_from_flags(false, false, false, None);
assert!(matches!(result, Err(SetupError::NoFlags)));
}
#[test]
fn test_parse_gpu_vendor_amd() {
assert_eq!(parse_gpu_vendor("amd").unwrap(), GpuVendor::Amd);
assert_eq!(parse_gpu_vendor("AMD").unwrap(), GpuVendor::Amd);
}
#[test]
fn test_parse_gpu_vendor_nvidia() {
assert_eq!(parse_gpu_vendor("nvidia").unwrap(), GpuVendor::Nvidia);
assert_eq!(parse_gpu_vendor("cuda").unwrap(), GpuVendor::Nvidia);
}
#[test]
fn test_parse_gpu_vendor_invalid() {
assert!(parse_gpu_vendor("intel").is_err());
}
#[test]
fn test_execution_provider_pip_package() {
assert_eq!(ExecutionProvider::Cpu.pip_package(), "onnxruntime");
assert_eq!(ExecutionProvider::Cuda.pip_package(), "onnxruntime-gpu");
assert_eq!(
ExecutionProvider::Migraphx.pip_package(),
"onnxruntime-migraphx"
);
}
#[test]
fn test_pip_ort_package_spec_is_bounded_to_supported_major() {
assert_eq!(
pip_ort_package_spec(ExecutionProvider::Cpu),
"onnxruntime>=1.20.0,<2"
);
assert_eq!(
pip_ort_package_spec(ExecutionProvider::Migraphx),
"onnxruntime-migraphx>=1.20.0,<2"
);
}
#[test]
fn test_execution_provider_config_value() {
assert_eq!(ExecutionProvider::Cpu.config_value(), "cpu");
assert_eq!(ExecutionProvider::Cuda.config_value(), "cuda");
assert_eq!(ExecutionProvider::Migraphx.config_value(), "migraphx");
}
#[test]
fn test_setup_error_display() {
let err = SetupError::Conflict {
message: "test conflict".to_string(),
};
assert!(err.to_string().contains("test conflict"));
let err = SetupError::PipNotFound;
assert!(err.to_string().contains("pip not found"));
}
#[test]
fn test_curl_not_found_error_mentions_curl() {
let err = SetupError::CurlNotFound;
let msg = err.to_string();
assert!(msg.contains("curl not found"), "{}", msg);
assert!(msg.contains("models/"), "{}", msg);
}
#[test]
fn test_model_download_network_error_mentions_connectivity() {
let err = SetupError::ModelDownloadFailed {
file: "qwen3-embed-0.6b.onnx".to_string(),
url: "https://huggingface.co/onnx-community/Qwen3-Embedding-0.6B-ONNX/resolve/main/onnx/model.onnx".to_string(),
exit_code: 28,
network: true,
};
let msg = err.to_string();
assert!(msg.contains("Network failure"), "{}", msg);
assert!(msg.contains("internet connection"), "{}", msg);
assert!(msg.contains("LEINDEX_MODEL_PATH"), "{}", msg);
assert!(msg.contains("huggingface.co"), "{}", msg);
assert!(msg.contains("exit code 28"), "{}", msg);
}
#[test]
fn test_model_download_generic_error_is_actionable() {
let err = SetupError::ModelDownloadFailed {
file: "tokenizer.json".to_string(),
url: "https://huggingface.co/onnx-community/Qwen3-Embedding-0.6B-ONNX/resolve/main/tokenizer.json".to_string(),
exit_code: 22,
network: false,
};
let msg = err.to_string();
assert!(!msg.contains("Network failure"), "{}", msg);
assert!(msg.contains("tokenizer.json"), "{}", msg);
assert!(msg.contains("Re-run"), "{}", msg);
}
#[test]
fn test_model_checksum_post_download_error_names_file_and_hashes() {
let err = SetupError::ModelChecksumPostDownload {
file: "qwen3-embed-0.6b.onnx".to_string(),
expected: "aaaa1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcd"
.to_string(),
actual: "bbbb1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcd"
.to_string(),
};
let msg = err.to_string();
assert!(msg.contains("Checksum mismatch"), "{}", msg);
assert!(msg.contains("qwen3-embed-0.6b.onnx"), "{}", msg);
assert!(msg.contains("aaaa1234567890abcdef"), "{}", msg);
assert!(msg.contains("bbbb1234567890abcdef"), "{}", msg);
assert!(msg.contains("copy the file manually"), "{}", msg);
}
#[test]
fn test_model_unavailable_error_names_dynamic_assets() {
let err = SetupError::ModelUnavailable {
model_name: "qwen3-embed-0.6b-dynamic".to_string(),
model_dir: PathBuf::from("/tmp/leindex-models"),
};
let msg = err.to_string();
assert!(msg.contains("qwen3-embed-0.6b-dynamic.onnx"), "{}", msg);
assert!(msg.contains("tokenizer.json"), "{}", msg);
assert!(msg.contains("config.json"), "{}", msg);
assert!(msg.contains("LEINDEX_MODEL_PATH"), "{}", msg);
}
#[test]
fn test_dynamic_model_assets_require_complete_single_file_download() {
let tmp = tempfile::tempdir().unwrap();
let model_path = tmp
.path()
.join(crate::cli::leindex::model_download::DYNAMIC_MODEL_ONNX_FILENAME);
let model = std::fs::File::create(model_path).unwrap();
model.set_len(100 * 1024 * 1024).unwrap();
std::fs::write(tmp.path().join("tokenizer.json"), b"{}").unwrap();
assert!(!dynamic_model_assets_present(tmp.path()));
std::fs::write(tmp.path().join("config.json"), b"{}").unwrap();
assert!(dynamic_model_assets_present(tmp.path()));
}
#[test]
fn test_model_checksum_status_missing_for_clean_dir() {
let _g = PIPE_ENV_LOCK.lock().unwrap();
let tmp = tempfile::tempdir().unwrap();
std::env::set_var("LEINDEX_HOME", tmp.path());
let status = model_checksum_status();
std::env::remove_var("LEINDEX_HOME");
assert_eq!(status, ModelChecksumStatus::Missing);
}
#[test]
fn test_pip_not_found_error_mentions_pip_bin() {
let err = SetupError::PipNotFound;
let msg = err.to_string();
assert!(
msg.contains("PIP_BIN"),
"PipNotFound must mention PIP_BIN: {}",
msg
);
assert!(msg.contains("python3-pip") || msg.contains("ensurepip"));
}
#[test]
fn test_pip_install_failed_error_mentions_package() {
let err = SetupError::PipInstallFailed {
package: "onnxruntime".to_string(),
exit_code: 1,
};
let msg = err.to_string();
assert!(msg.contains("onnxruntime"));
assert!(msg.contains("exit code 1"));
}
#[test]
fn test_pip_network_failed_error_mentions_network() {
let err = SetupError::PipNetworkFailed {
package: "onnxruntime".to_string(),
exit_code: 1,
output: "Could not fetch URL pypi.org".to_string(),
};
let msg = err.to_string();
assert!(msg.contains("Network failure"));
assert!(msg.contains("onnxruntime"));
assert!(msg.contains("internet connection"));
assert!(msg.contains("pypi.org"));
}
#[test]
fn test_parse_version_simple() {
assert_eq!(parse_version("1.25.0"), Some((1, 25, 0)));
assert_eq!(parse_version("1.20.0"), Some((1, 20, 0)));
assert_eq!(parse_version("2.0.0"), Some((2, 0, 0)));
assert_eq!(parse_version("0.9.9"), Some((0, 9, 9)));
}
#[test]
fn test_parse_version_with_prerelease() {
assert_eq!(parse_version("1.25.0-rc1"), Some((1, 25, 0)));
assert_eq!(parse_version("1.25.0+build42"), Some((1, 25, 0)));
assert_eq!(parse_version("1.25.0-rc1+meta"), Some((1, 25, 0)));
}
#[test]
fn test_parse_version_missing_patch_defaults_to_zero() {
assert_eq!(parse_version("1.25"), Some((1, 25, 0)));
assert_eq!(parse_version("1"), Some((1, 0, 0)));
}
#[test]
fn test_parse_version_invalid() {
assert_eq!(parse_version("not-a-version"), None);
assert_eq!(parse_version("v1.2.3"), None);
assert_eq!(parse_version(""), None);
}
#[test]
fn test_version_compatibility_supported() {
assert_eq!(
check_ort_version_compatibility("1.20.0"),
VersionCompatibility::Supported
);
assert_eq!(
check_ort_version_compatibility("1.25.0"),
VersionCompatibility::Supported
);
assert_eq!(
check_ort_version_compatibility("1.99.99"),
VersionCompatibility::Supported
);
}
#[test]
fn test_unsupported_cpu_ort_requests_upgrade() {
assert!(should_install_ort_for_existing_state(
true,
ExecutionProvider::Cpu,
Some("1.19.2"),
true
));
assert!(!should_install_ort_for_existing_state(
true,
ExecutionProvider::Cpu,
Some("1.25.0"),
true
));
}
#[test]
fn test_version_compatibility_too_old() {
let r = check_ort_version_compatibility("1.19.0");
match r {
VersionCompatibility::Unsupported {
required_min,
reason,
} => {
assert!(
required_min.contains("1.20.0"),
"required_min = {}",
required_min
);
assert!(!reason.is_empty());
}
other => panic!("expected Unsupported, got {:?}", other),
}
}
#[test]
fn test_version_compatibility_very_old() {
let r = check_ort_version_compatibility("0.9.9");
assert!(matches!(r, VersionCompatibility::Unsupported { .. }));
}
#[test]
fn test_version_compatibility_too_new() {
let r = check_ort_version_compatibility("2.0.0");
match r {
VersionCompatibility::TooNew {
supported_max,
reason,
} => {
assert!(
supported_max.contains("1."),
"supported_max = {}",
supported_max
);
assert!(reason.contains("ABI") || reason.contains("breaking"));
}
other => panic!("expected TooNew, got {:?}", other),
}
}
#[test]
fn test_version_compatibility_unparseable() {
let r = check_ort_version_compatibility("garbage");
assert!(matches!(r, VersionCompatibility::Unsupported { .. }));
}
#[test]
fn test_is_network_error_detects_common_failures() {
assert!(is_network_error(
"WARNING: Could not fetch URL https://pypi.org/onnxruntime"
));
assert!(is_network_error(
"ConnectionError: Failed to establish a new connection"
));
assert!(is_network_error("ReadTimeoutError: read timed out"));
assert!(is_network_error(
"WARNING: Retrying (Retry(total=4)) after connection broken"
));
assert!(is_network_error(
"HTTPSConnectionPool: Max retries exceeded with url: /simple/onnxruntime"
));
}
#[test]
fn test_is_network_error_ignores_normal_output() {
assert!(!is_network_error(
"Successfully installed onnxruntime-1.25.0"
));
assert!(!is_network_error("Requirement already satisfied: numpy"));
assert!(!is_network_error(""));
}
#[test]
fn test_truncate_for_error_short() {
let s = "line1\nline2\n";
assert_eq!(truncate_for_error(s), "line1\nline2");
}
#[test]
fn test_truncate_for_error_long() {
let long = (0..25)
.map(|i| format!("line {}", i))
.collect::<Vec<_>>()
.join("\n");
let truncated = truncate_for_error(&long);
assert!(truncated.contains("truncated"));
assert!(truncated.contains("line 0"));
assert!(!truncated.contains("line 24"));
}
#[test]
fn test_min_ort_version_constant_is_sensible() {
let _: (u32, u32, u32) = MIN_ORT_VERSION;
let _: u32 = MAX_ORT_MAJOR;
assert_eq!(MIN_ORT_VERSION.0, 1);
}
#[test]
fn test_build_config_records_ort_version() {
let choices = SetupChoices {
neural_enabled: true,
provider: Some(ExecutionProvider::Cpu),
};
let cfg = build_config(
&choices,
Some(std::path::Path::new("/usr/local/lib/libonnxruntime.so")),
Some("1.25.0"),
);
assert_eq!(cfg.neural.ort_version.as_deref(), Some("1.25.0"));
assert_eq!(
cfg.neural.ort_dylib_path.as_deref(),
Some("/usr/local/lib/libonnxruntime.so")
);
assert!(cfg.neural.enabled);
assert_eq!(cfg.neural.execution_provider, "cpu");
}
#[test]
fn test_build_config_selects_dynamic_qwen_model_for_all_local_providers() {
for provider in [
ExecutionProvider::Cpu,
ExecutionProvider::Cuda,
ExecutionProvider::Migraphx,
] {
let choices = SetupChoices {
neural_enabled: true,
provider: Some(provider),
};
let cfg = build_config(&choices, None, None);
assert_eq!(cfg.neural.model_name, "qwen3-embed-0.6b-dynamic");
}
}
#[test]
fn test_model_download_profile_uses_hugging_face_cli_assets() {
for provider in [
ExecutionProvider::Cpu,
ExecutionProvider::Cuda,
ExecutionProvider::Migraphx,
] {
let profile = model_download_profile(Some(provider));
assert_eq!(profile.repository, "zhiqing/Qwen3-Embedding-0.6B-ONNX");
assert_eq!(profile.revision, "c96cc9c82d08ee7869600e2191078fc939957026");
assert_eq!(profile.remote_model, "model.onnx");
assert_eq!(profile.local_model, "qwen3-embed-0.6b-dynamic.onnx");
assert_eq!(
profile.files,
&["model.onnx", "tokenizer.json", "config.json"]
);
}
}
#[test]
fn test_dynamic_profile_requires_all_checksums_to_match() {
let model_dir = tempfile::tempdir().unwrap();
let profile = model_download_profile(Some(ExecutionProvider::Cpu));
let model = std::fs::File::create(model_dir.path().join(profile.local_model)).unwrap();
model.set_len(100 * 1024 * 1024).unwrap();
std::fs::write(model_dir.path().join("tokenizer.json"), b"{}").unwrap();
std::fs::write(model_dir.path().join("config.json"), b"{}").unwrap();
generate_profile_checksum_manifest(model_dir.path(), profile).unwrap();
assert!(profile_assets_verified(model_dir.path(), profile));
let manifest_path = model_dir.path().join("checksums.sha256");
let unpinned_manifest = std::fs::read_to_string(&manifest_path)
.unwrap()
.lines()
.filter(|line| !line.starts_with("# source:"))
.collect::<Vec<_>>()
.join("\n");
std::fs::write(&manifest_path, unpinned_manifest).unwrap();
assert!(!profile_assets_verified(model_dir.path(), profile));
generate_profile_checksum_manifest(model_dir.path(), profile).unwrap();
std::fs::write(model_dir.path().join("config.json"), br#"{"changed":true}"#).unwrap();
assert!(!profile_assets_verified(model_dir.path(), profile));
}
#[test]
fn test_rocm_smoke_result_accepts_migraphx_runtime() {
let result = SmokeTestResult::from_embedding_outcome(
QWEN3_EMBEDDING_DIMENSION,
Some("migraphx".to_string()),
Some("rocm".to_string()),
);
assert!(result.passed);
assert!(result.error.is_none());
}
#[test]
fn test_gpu_smoke_result_fails_provider_mismatch() {
let result = SmokeTestResult::from_embedding_outcome(
1024,
Some("cpu".to_string()),
Some("migraphx".to_string()),
);
assert!(!result.passed);
assert!(result
.error
.as_deref()
.unwrap_or_default()
.contains("configured execution provider migraphx"));
}
#[test]
fn test_build_config_without_version() {
let choices = SetupChoices {
neural_enabled: true,
provider: Some(ExecutionProvider::Cpu),
};
let cfg = build_config(&choices, None, None);
assert!(cfg.neural.ort_version.is_none());
assert!(cfg.neural.ort_dylib_path.is_none());
}
use std::sync::Mutex;
static PIPE_ENV_LOCK: Mutex<()> = Mutex::new(());
#[test]
fn test_find_pip_honors_pip_bin_with_split() {
let _g = PIPE_ENV_LOCK.lock().unwrap();
std::env::set_var("PIP_BIN", "/bin/true -m pip");
let result = find_pip();
std::env::remove_var("PIP_BIN");
let (program, prefix) = result.expect("PIP_BIN should be honored");
assert_eq!(program, "/bin/true");
assert_eq!(prefix, vec!["-m".to_string(), "pip".to_string()]);
}
#[test]
fn test_parse_pip_bin_rejects_arbitrary_multi_arg_command() {
assert!(parse_pip_bin_override("/usr/bin/curl https://evil.example").is_none());
assert!(parse_pip_bin_override("python3 -m pip").is_some());
}
#[test]
fn test_find_pip_honors_pip_bin_single_token() {
let _g = PIPE_ENV_LOCK.lock().unwrap();
std::env::set_var("PIP_BIN", "/bin/true");
let result = find_pip();
std::env::remove_var("PIP_BIN");
let (program, prefix) = result.expect("PIP_BIN single token should be honored");
assert_eq!(program, "/bin/true");
assert!(prefix.is_empty());
}
#[test]
fn test_find_pip_empty_pip_bin_falls_through() {
let _g = PIPE_ENV_LOCK.lock().unwrap();
std::env::set_var("PIP_BIN", " ");
let _ = find_pip();
std::env::remove_var("PIP_BIN");
}
#[test]
fn test_smoke_test_result_pass_status_line() {
let result = SmokeTestResult {
passed: true,
skipped: false,
dimension: Some(1024),
execution_provider: None,
configured_provider_label: Some("cpu".to_string()),
error: None,
note: None,
};
let line = result.status_line();
assert!(line.contains("PASS"), "{}", line);
assert!(line.contains("1024"), "{}", line);
}
#[test]
fn test_smoke_test_result_pass_without_dimension_is_not_zero_dim() {
let result = SmokeTestResult {
passed: true,
skipped: false,
dimension: None,
execution_provider: None,
configured_provider_label: Some("cpu".to_string()),
error: None,
note: None,
};
let line = result.status_line();
assert!(line.contains("PASS"), "{}", line);
assert!(!line.contains("0-dim"), "{}", line);
assert!(line.contains("dimension unavailable"), "{}", line);
}
#[test]
fn test_smoke_test_result_fail_status_line() {
let result = SmokeTestResult {
passed: false,
skipped: false,
dimension: None,
execution_provider: None,
configured_provider_label: Some("cpu".to_string()),
error: Some("worker failed to start".to_string()),
note: None,
};
let line = result.status_line();
assert!(line.contains("FAIL"), "{}", line);
assert!(!line.contains("1024"));
}
#[test]
fn test_smoke_test_result_dimension_mismatch_is_fail() {
let result = SmokeTestResult {
passed: false,
skipped: false,
dimension: Some(768), execution_provider: None,
configured_provider_label: Some("migraphx".to_string()),
error: Some("expected 1024-dim vector, got 768-dim".to_string()),
note: None,
};
assert!(!result.passed);
assert_eq!(result.dimension, Some(768));
assert_eq!(
result.configured_provider_label.as_deref(),
Some("migraphx")
);
assert!(result.execution_provider.is_none());
}
#[test]
fn test_permission_denied_error_names_path_and_leindex_home() {
let err = SetupError::PermissionDenied {
path: PathBuf::from("/home/user/.leindex/config"),
reason: "Permission denied (os error 13)".to_string(),
};
let msg = err.to_string();
assert!(msg.contains("Permission denied"), "{}", msg);
assert!(msg.contains("/home/user/.leindex/config"), "{}", msg);
assert!(msg.contains("LEINDEX_HOME"), "{}", msg);
}
#[test]
fn test_smoke_test_catastrophic_error_is_actionable() {
let err = SetupError::SmokeTestCatastrophic {
message: "worker binary not found".to_string(),
};
let msg = err.to_string();
assert!(msg.contains("smoke test"), "{}", msg);
assert!(msg.contains("worker binary not found"), "{}", msg);
assert!(msg.contains("leindex-embed"), "{}", msg);
}
#[test]
fn test_detect_gpu_vendor_returns_enum() {
let _ = detect_gpu_vendor();
}
#[test]
fn test_default_gpu_vendor_index_matches_detection() {
assert_eq!(default_gpu_vendor_index(DetectedGpu::Amd), 0);
assert_eq!(default_gpu_vendor_index(DetectedGpu::Nvidia), 1);
assert_eq!(default_gpu_vendor_index(DetectedGpu::Unknown), 2);
}
#[test]
fn test_detected_gpu_variants_are_distinct() {
assert_ne!(DetectedGpu::Amd, DetectedGpu::Nvidia);
assert_ne!(DetectedGpu::Amd, DetectedGpu::Unknown);
assert_ne!(DetectedGpu::Nvidia, DetectedGpu::Unknown);
}
#[test]
fn test_detect_amd_gpu_no_false_positive_on_clean_system() {
let _g = PIPE_ENV_LOCK.lock().unwrap();
std::env::set_var("ROCM_PATH", "/definitely/not/a/real/path");
let _ = detect_amd_gpu();
std::env::remove_var("ROCM_PATH");
}
#[test]
fn test_detect_amd_gpu_honors_existing_rocm_path() {
let _g = PIPE_ENV_LOCK.lock().unwrap();
let tmp = tempfile::tempdir().unwrap();
std::env::set_var("ROCM_PATH", tmp.path());
assert!(detect_amd_gpu(), "existing ROCM_PATH should detect AMD");
std::env::remove_var("ROCM_PATH");
}
#[test]
fn test_detect_nvidia_gpu_with_cuda_path_env() {
let _g = PIPE_ENV_LOCK.lock().unwrap();
let tmp = tempfile::tempdir().unwrap();
std::env::set_var("CUDA_PATH", tmp.path());
assert!(
detect_nvidia_gpu(),
"existing CUDA_PATH should detect NVIDIA"
);
std::env::remove_var("CUDA_PATH");
}
#[test]
fn test_ensure_home_writable_succeeds_for_writable_leindex_home() {
let _g = PIPE_ENV_LOCK.lock().unwrap();
let tmp = tempfile::tempdir().unwrap();
std::env::set_var("LEINDEX_HOME", tmp.path());
let result = ensure_home_writable();
std::env::remove_var("LEINDEX_HOME");
assert!(
result.is_ok(),
"writable LEINDEX_HOME should pass: {:?}",
result
);
}
#[test]
fn test_ensure_home_writable_uses_leindex_home_location() {
let _g = PIPE_ENV_LOCK.lock().unwrap();
let tmp = tempfile::tempdir().unwrap();
std::env::set_var("LEINDEX_HOME", tmp.path());
let result = ensure_home_writable();
assert!(result.is_ok());
assert!(
tmp.path().join("config").is_dir(),
"config dir should be under LEINDEX_HOME"
);
std::env::remove_var("LEINDEX_HOME");
}
#[test]
fn test_ensure_home_writable_fails_for_read_only_dir() {
let _g = PIPE_ENV_LOCK.lock().unwrap();
let tmp = tempfile::tempdir().unwrap();
let base = tmp.path().to_path_buf();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
std::fs::set_permissions(&base, std::fs::Permissions::from_mode(0o555)).unwrap();
}
std::env::set_var("LEINDEX_HOME", &base);
let result = ensure_home_writable();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let _ = std::fs::set_permissions(&base, std::fs::Permissions::from_mode(0o755));
}
std::env::remove_var("LEINDEX_HOME");
#[cfg(unix)]
{
let is_root = unsafe { libc::geteuid() == 0 };
if !is_root {
match result {
Err(SetupError::PermissionDenied { path, .. }) => {
assert!(
path.starts_with(&base),
"PermissionDenied path should be under LEINDEX_HOME: {:?}",
path
);
}
other => {
assert!(
other.is_err(),
"read-only LEINDEX_HOME should fail, got {:?}",
other
);
}
}
} else {
let _ = result; }
}
#[cfg(not(unix))]
{
let _ = result; }
}
#[test]
fn test_truncate_for_display_short() {
assert_eq!(truncate_for_display("short", 100), "short");
}
#[test]
fn test_truncate_for_display_long_appends_ellipsis() {
let input = "a".repeat(250);
let result = truncate_for_display(&input, 50);
assert!(result.ends_with("..."), "{}", result);
assert_eq!(result.len(), 50 + 3);
}
#[test]
fn test_try_link_model_file_creates_symlink_on_same_filesystem() {
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("source.bin");
let dst = tmp.path().join("dest.bin");
std::fs::write(&src, b"model data").unwrap();
let result = try_link_model_file(&src, &dst, false);
assert!(
result.is_ok(),
"try_link_model_file should succeed: {:?}",
result
);
#[cfg(unix)]
{
let meta = std::fs::symlink_metadata(&dst).unwrap();
assert!(meta.file_type().is_symlink(), "dst should be a symlink");
}
let content = std::fs::read(&dst).unwrap();
assert_eq!(content, b"model data");
}
#[test]
fn test_copy_bundled_models_creates_symlinks_not_copies() {
let _g = PIPE_ENV_LOCK.lock().unwrap();
let bundled = tempfile::tempdir().unwrap();
let dest = tempfile::tempdir().unwrap();
let config_src = bundled.path().join("config.json");
std::fs::write(&config_src, b"{ \"test\": true }").unwrap();
copy_bundled_models(bundled.path(), dest.path());
let config_dst = dest.path().join("config.json");
assert!(config_dst.exists(), "config.json should exist in dest");
#[cfg(unix)]
{
let meta = std::fs::symlink_metadata(&config_dst).unwrap();
assert!(
meta.file_type().is_symlink(),
"config.json in dest should be a symlink, not a copy (resource-duplication fix)"
);
}
}
#[test]
fn test_try_link_model_file_overwrites_existing() {
let tmp = tempfile::tempdir().unwrap();
let src = tmp.path().join("source.bin");
let dst = tmp.path().join("dest.bin");
std::fs::write(&src, b"model data").unwrap();
std::fs::write(&dst, b"old data").unwrap();
let result = try_link_model_file(&src, &dst, false);
assert!(
result.is_ok(),
"copy strategy should overwrite: {:?}",
result
);
}
}