use crate::config::Config;
use crate::Result;
use anyhow::anyhow;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::env;
use std::path::{Path, PathBuf};
use std::process::Command;
#[derive(Debug, Serialize, Deserialize)]
pub struct DependencyConfig {
pub dependencies: Vec<Dependency>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct Dependency {
pub name: String,
pub check_running: bool,
#[serde(default)]
pub min_version: Option<u32>,
pub install_instructions: String,
pub description: String,
#[serde(default)]
pub website: Option<String>,
#[serde(default)]
pub environments: Option<HashMap<String, EnvironmentConfig>>,
#[serde(default)]
pub package_manager: bool,
#[serde(default)]
pub os_filter: Option<Vec<String>>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct EnvironmentConfig {
#[serde(default)]
pub install_commands: Option<Vec<String>>,
#[serde(default)]
pub verify_command: Option<String>,
#[serde(default)]
pub skip: bool,
#[serde(default)]
pub skip_reason: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DependencyCheckResult {
pub dependencies: Vec<DependencyResult>,
pub all_satisfied: bool,
}
#[derive(Debug, Serialize, Deserialize)]
pub struct DependencyResult {
pub name: String,
pub found: bool,
pub path: Option<String>,
pub version: Option<String>,
pub running: Option<bool>,
pub skipped: bool,
pub skip_reason: Option<String>,
pub description: Option<String>,
pub website: Option<String>,
pub install_instructions: Option<String>,
}
pub fn check_single_dependency(
name: &str,
custom_path: Option<String>,
) -> Result<DependencyResult> {
let deps_yaml = include_str!("../../deps.yaml");
let config: DependencyConfig = serde_yaml::from_str(deps_yaml)?;
let dep = config
.dependencies
.iter()
.find(|d| d.name == name)
.ok_or_else(|| anyhow!("Dependency '{}' not found", name))?;
let is_colab = is_google_colab();
if is_colab {
if let Some(environments) = &dep.environments {
if let Some(colab_config) = environments.get("google_colab") {
if colab_config.skip {
return Ok(DependencyResult {
name: dep.name.clone(),
found: false,
path: None,
version: None,
running: None,
skipped: true,
skip_reason: colab_config.skip_reason.clone(),
description: Some(dep.description.clone()),
website: dep.website.clone(),
install_instructions: Some(dep.install_instructions.clone()),
});
}
}
}
}
let mut fallback_path: Option<String> = custom_path.clone();
let direct_path = if let Some(custom) = custom_path.as_ref() {
if Path::new(custom).exists() {
let is_valid = match dep.name.as_str() {
"java" => {
Command::new(custom)
.arg("-version")
.output()
.map(|output| {
let version_str = String::from_utf8_lossy(&output.stderr);
version_str.contains("version")
&& (version_str.contains("java") || version_str.contains("openjdk"))
})
.unwrap_or(false)
}
"docker" => Command::new(custom)
.arg("--version")
.output()
.map(|output| {
let version_str = String::from_utf8_lossy(&output.stdout);
version_str.contains("Docker")
})
.unwrap_or(false),
"nextflow" => Command::new(custom)
.arg("-version")
.output()
.map(|output| {
let version_str = String::from_utf8_lossy(&output.stdout);
version_str.contains("nextflow") || version_str.contains("version")
})
.unwrap_or(false),
"syftbox" => Command::new(custom)
.arg("--version")
.output()
.map(|output| {
let version_str = String::from_utf8_lossy(&output.stdout);
version_str.contains("syftbox") || version_str.contains("version")
})
.unwrap_or(false),
_ => true,
};
if is_valid {
Some(custom.clone())
} else {
None
}
} else {
None
}
} else {
let bv_config = Config::load().ok();
if let Some(config_path) = bv_config
.as_ref()
.and_then(|cfg| cfg.get_binary_path(&dep.name))
{
fallback_path = Some(config_path.clone());
if Path::new(&config_path).exists() {
Some(config_path)
} else {
None
}
} else {
None
}
};
let binary_path = direct_path
.or_else(|| {
which::which(&dep.name)
.ok()
.map(|p| p.display().to_string())
})
.or_else(|| find_in_well_known_locations(&dep.name));
let mut java_brew_path: Option<String> = None;
if binary_path.is_none() && dep.name == "java" && std::env::consts::OS == "macos" {
java_brew_path = check_java_in_brew_not_in_path();
}
#[cfg(target_os = "macos")]
let docker_desktop_installed = dep.name == "docker" && is_docker_desktop_installed();
#[cfg(not(target_os = "macos"))]
let docker_desktop_installed = false;
let mut syftbox_sbenv_path: Option<String> = None;
if binary_path.is_none() && dep.name == "syftbox" {
syftbox_sbenv_path = check_syftbox_in_sbenv();
}
if binary_path.is_none()
&& java_brew_path.is_none()
&& !docker_desktop_installed
&& syftbox_sbenv_path.is_none()
{
return Ok(DependencyResult {
name: dep.name.clone(),
found: false,
path: fallback_path,
version: None,
running: None,
skipped: false,
skip_reason: None,
description: Some(dep.description.clone()),
website: dep.website.clone(),
install_instructions: Some(dep.install_instructions.clone()),
});
} else if let Some(brew_path) = java_brew_path {
return Ok(DependencyResult {
name: dep.name.clone(),
found: true,
path: Some(format!("{}/java", brew_path)),
version: None,
running: None,
skipped: false,
skip_reason: None,
description: Some(dep.description.clone()),
website: dep.website.clone(),
install_instructions: Some(dep.install_instructions.clone()),
});
} else if docker_desktop_installed && binary_path.is_none() {
return Ok(DependencyResult {
name: dep.name.clone(),
found: true,
path: Some("/Applications/Docker.app".to_string()),
version: None,
running: Some(false),
skipped: false,
skip_reason: None,
description: Some(dep.description.clone()),
website: dep.website.clone(),
install_instructions: Some("Docker Desktop is installed but not running. Please start Docker Desktop from your Applications folder.".to_string()),
});
} else if let Some(sbenv_path) = syftbox_sbenv_path {
let version = Command::new(&sbenv_path)
.arg("--version")
.output()
.ok()
.filter(|output| output.status.success())
.and_then(|output| {
let version_str = String::from_utf8_lossy(&output.stdout);
version_str.split_whitespace().nth(2).map(|v| v.to_string())
});
return Ok(DependencyResult {
name: dep.name.clone(),
found: true,
path: Some(sbenv_path),
version,
running: None,
skipped: false,
skip_reason: None,
description: Some(dep.description.clone()),
website: dep.website.clone(),
install_instructions: Some(dep.install_instructions.clone()),
});
} else if let Some(path) = binary_path {
let (version_ok, version_str) = if let Some(min_version) = dep.min_version {
check_version_with_info(&dep.name, min_version)
} else {
(true, get_version_string(&dep.name))
};
let is_running = if dep.check_running {
Some(check_if_running(&dep.name))
} else {
None
};
return Ok(DependencyResult {
name: dep.name.clone(),
found: version_ok,
path: Some(path),
version: version_str,
running: is_running,
skipped: false,
skip_reason: None,
description: Some(dep.description.clone()),
website: dep.website.clone(),
install_instructions: Some(dep.install_instructions.clone()),
});
}
Ok(DependencyResult {
name: dep.name.clone(),
found: false,
path: None,
version: None,
running: None,
skipped: false,
skip_reason: None,
description: Some(dep.description.clone()),
website: dep.website.clone(),
install_instructions: Some(dep.install_instructions.clone()),
})
}
pub fn check_dependencies_result() -> Result<DependencyCheckResult> {
let deps_yaml = include_str!("../../deps.yaml");
let config: DependencyConfig = serde_yaml::from_str(deps_yaml)?;
let is_colab = is_google_colab();
let bv_config = Config::load().ok();
let mut all_found = true;
let mut all_running = true;
let mut results: Vec<DependencyResult> = Vec::new();
let current_os = std::env::consts::OS;
for dep in &config.dependencies {
if dep.package_manager {
continue;
}
if let Some(os_filter) = &dep.os_filter {
if !os_filter.contains(¤t_os.to_string()) {
continue;
}
}
if is_colab {
if let Some(environments) = &dep.environments {
if let Some(colab_config) = environments.get("google_colab") {
if colab_config.skip {
results.push(DependencyResult {
name: dep.name.clone(),
found: false,
path: None,
version: None,
running: None,
skipped: true,
skip_reason: colab_config.skip_reason.clone(),
description: Some(dep.description.clone()),
website: dep.website.clone(),
install_instructions: Some(dep.install_instructions.clone()),
});
continue;
}
}
}
}
let custom_path = bv_config
.as_ref()
.and_then(|cfg| cfg.get_binary_path(&dep.name));
let binary_path = if custom_path
.as_ref()
.map(|p| Path::new(p).exists())
.unwrap_or(false)
{
custom_path.clone()
} else {
which::which(&dep.name)
.ok()
.map(|p| p.display().to_string())
}
.or_else(|| find_in_well_known_locations(&dep.name));
let mut java_brew_path: Option<String> = None;
if binary_path.is_none() && dep.name == "java" && std::env::consts::OS == "macos" {
java_brew_path = check_java_in_brew_not_in_path();
}
#[cfg(target_os = "macos")]
let docker_desktop_installed = dep.name == "docker" && is_docker_desktop_installed();
#[cfg(not(target_os = "macos"))]
let docker_desktop_installed = false;
let mut syftbox_sbenv_path: Option<String> = None;
if binary_path.is_none() && dep.name == "syftbox" {
syftbox_sbenv_path = check_syftbox_in_sbenv();
}
if binary_path.is_none()
&& java_brew_path.is_none()
&& !docker_desktop_installed
&& syftbox_sbenv_path.is_none()
{
all_found = false;
results.push(DependencyResult {
name: dep.name.clone(),
found: false,
path: custom_path.clone(),
version: None,
running: None,
skipped: false,
skip_reason: None,
description: Some(dep.description.clone()),
website: dep.website.clone(),
install_instructions: Some(dep.install_instructions.clone()),
});
} else if docker_desktop_installed && binary_path.is_none() {
all_running = false; results.push(DependencyResult {
name: dep.name.clone(),
found: true,
path: Some("/Applications/Docker.app".to_string()),
version: None,
running: Some(false),
skipped: false,
skip_reason: None,
description: Some(dep.description.clone()),
website: dep.website.clone(),
install_instructions: Some("Docker Desktop is installed but not running. Please start Docker Desktop from your Applications folder.".to_string()),
});
} else if let Some(brew_path) = java_brew_path {
results.push(DependencyResult {
name: dep.name.clone(),
found: true,
path: Some(format!("{}/java", brew_path)),
version: None,
running: None,
skipped: false,
skip_reason: None,
description: Some(dep.description.clone()),
website: dep.website.clone(),
install_instructions: Some(dep.install_instructions.clone()),
});
} else if let Some(sbenv_path) = syftbox_sbenv_path {
let version = Command::new(&sbenv_path)
.arg("--version")
.output()
.ok()
.filter(|output| output.status.success())
.and_then(|output| {
let version_str = String::from_utf8_lossy(&output.stdout);
version_str.split_whitespace().nth(2).map(|v| v.to_string())
});
results.push(DependencyResult {
name: dep.name.clone(),
found: true,
path: Some(sbenv_path),
version,
running: None,
skipped: false,
skip_reason: None,
description: Some(dep.description.clone()),
website: dep.website.clone(),
install_instructions: Some(dep.install_instructions.clone()),
});
} else if let Some(path) = binary_path {
let (version_ok, version_str) = if let Some(min_version) = dep.min_version {
check_version_with_info(&dep.name, min_version)
} else {
(true, get_version_string(&dep.name))
};
if !version_ok {
all_found = false;
}
let is_running = if dep.check_running {
Some(check_if_running(&dep.name))
} else {
None
};
if let Some(running) = is_running {
if !running {
all_running = false;
}
}
results.push(DependencyResult {
name: dep.name.clone(),
found: version_ok,
path: Some(path),
version: version_str,
running: is_running,
skipped: false,
skip_reason: None,
description: Some(dep.description.clone()),
website: dep.website.clone(),
install_instructions: Some(dep.install_instructions.clone()),
});
}
}
Ok(DependencyCheckResult {
dependencies: results,
all_satisfied: all_found && all_running,
})
}
fn find_in_well_known_locations(executable: &str) -> Option<String> {
let mut search_dirs: Vec<PathBuf> = Vec::new();
#[cfg(target_os = "macos")]
{
search_dirs.extend([
PathBuf::from("/opt/homebrew/bin"),
PathBuf::from("/opt/homebrew/sbin"),
PathBuf::from("/usr/local/bin"),
PathBuf::from("/usr/local/sbin"),
PathBuf::from("/usr/bin"),
PathBuf::from("/usr/sbin"),
PathBuf::from("/bin"),
PathBuf::from("/sbin"),
]);
if executable == "docker" {
search_dirs.extend([
PathBuf::from("/Applications/Docker.app/Contents/Resources/bin"),
PathBuf::from("/usr/local/bin"), PathBuf::from("/opt/homebrew/bin"), ]);
}
}
#[cfg(target_os = "linux")]
{
search_dirs.extend([
PathBuf::from("/usr/local/bin"),
PathBuf::from("/usr/bin"),
PathBuf::from("/bin"),
PathBuf::from("/usr/local/sbin"),
PathBuf::from("/usr/sbin"),
PathBuf::from("/sbin"),
PathBuf::from("/snap/bin"),
PathBuf::from("/var/lib/snapd/snap/bin"),
]);
}
#[cfg(target_os = "windows")]
{
search_dirs.extend([
PathBuf::from("C:/Program Files"),
PathBuf::from("C:/Program Files (x86)"),
]);
if executable == "docker" {
search_dirs.push(PathBuf::from(
"C:/Program Files/Docker/Docker/resources/bin",
));
}
}
if let Some(home) = dirs::home_dir() {
search_dirs.push(home.join(".local/bin"));
search_dirs.push(home.join(".cargo/bin"));
search_dirs.push(home.join("bin"));
}
let binary_name = if cfg!(target_os = "windows") {
format!("{}.exe", executable)
} else {
executable.to_string()
};
for dir in search_dirs {
let candidate = dir.join(&binary_name);
if candidate.is_file() {
return Some(candidate.display().to_string());
}
}
None
}
pub async fn execute(json: bool) -> Result<()> {
let deps_yaml = include_str!("../../deps.yaml");
let config: DependencyConfig = serde_yaml::from_str(deps_yaml)?;
if !json {
println!("BioVault Dependency Check");
println!("=========================\n");
}
let is_colab = is_google_colab();
if is_colab && !json {
println!("ℹ️ Google Colab environment detected\n");
}
let is_ci = env::var("CI").is_ok() || env::var("GITHUB_ACTIONS").is_ok();
let bv_config = Config::load().ok();
let mut all_found = true;
let mut all_running = true;
let mut results: Vec<DependencyResult> = Vec::new();
let current_os = std::env::consts::OS;
for dep in &config.dependencies {
if dep.package_manager {
continue;
}
if let Some(os_filter) = &dep.os_filter {
if !os_filter.contains(¤t_os.to_string()) {
continue;
}
}
if is_colab {
if let Some(environments) = &dep.environments {
if let Some(colab_config) = environments.get("google_colab") {
if colab_config.skip {
if !json {
println!("Checking {}... ⏭️ SKIPPED", dep.name);
println!(
" Reason: {}",
colab_config
.skip_reason
.as_ref()
.unwrap_or(&"Not available in Colab".to_string())
);
println!();
}
results.push(DependencyResult {
name: dep.name.clone(),
found: false,
path: None,
version: None,
running: None,
skipped: true,
skip_reason: colab_config.skip_reason.clone(),
description: Some(dep.description.clone()),
website: dep.website.clone(),
install_instructions: Some(dep.install_instructions.clone()),
});
continue;
}
}
}
}
if !json {
print!("Checking {}... ", dep.name);
}
let custom_path = bv_config
.as_ref()
.and_then(|cfg| cfg.binary_paths.as_ref())
.and_then(|bp| match dep.name.as_str() {
"java" => bp.java.clone(),
"docker" => bp.docker.clone(),
"nextflow" => bp.nextflow.clone(),
_ => None,
});
let binary_path = if let Some(custom) = custom_path {
if std::path::Path::new(&custom).exists() {
Some(custom)
} else {
None
}
} else {
which::which(&dep.name)
.ok()
.map(|p| p.display().to_string())
};
let mut java_brew_path: Option<String> = None;
if binary_path.is_none() && dep.name == "java" && std::env::consts::OS == "macos" {
java_brew_path = check_java_in_brew_not_in_path();
}
let mut uv_windows_path: Option<String> = None;
if binary_path.is_none() && dep.name == "uv" && std::env::consts::OS == "windows" {
uv_windows_path = check_uv_in_windows_not_in_path();
}
#[cfg(target_os = "macos")]
let docker_desktop_installed = dep.name == "docker" && is_docker_desktop_installed();
#[cfg(not(target_os = "macos"))]
let docker_desktop_installed = false;
let mut syftbox_sbenv_path: Option<String> = None;
if binary_path.is_none() && dep.name == "syftbox" {
syftbox_sbenv_path = check_syftbox_in_sbenv();
}
if binary_path.is_none()
&& java_brew_path.is_none()
&& uv_windows_path.is_none()
&& !docker_desktop_installed
&& syftbox_sbenv_path.is_none()
{
all_found = false;
if !json {
println!("❌ NOT FOUND");
println!(" Description: {}", dep.description);
println!(" Installation instructions:");
for line in dep.install_instructions.lines() {
if !line.trim().is_empty() {
println!(" {}", line);
}
}
println!();
}
results.push(DependencyResult {
name: dep.name.clone(),
found: false,
path: None,
version: None,
running: None,
skipped: false,
skip_reason: None,
description: Some(dep.description.clone()),
website: dep.website.clone(),
install_instructions: Some(dep.install_instructions.clone()),
});
} else if docker_desktop_installed && binary_path.is_none() {
all_running = false;
if !json {
println!("⚠️ Found (Docker Desktop installed but not running)");
println!(" Docker Desktop is installed on your system.");
println!(" To start Docker, open Docker Desktop from your Applications folder.");
println!(" Location: /Applications/Docker.app");
println!();
}
results.push(DependencyResult {
name: dep.name.clone(),
found: true,
path: Some("/Applications/Docker.app".to_string()),
version: None,
running: Some(false),
skipped: false,
skip_reason: None,
description: Some(dep.description.clone()),
website: dep.website.clone(),
install_instructions: Some("Docker Desktop is installed but not running. Please start Docker Desktop from your Applications folder.".to_string()),
});
} else if let Some(brew_path) = java_brew_path {
if !json {
println!("⚠️ Found (not in PATH)");
println!(" Java is installed via Homebrew at: {}", brew_path);
println!(" But it's not available in your PATH.");
println!(" To fix this, add the following to your shell config:");
let shell = env::var("SHELL").unwrap_or_else(|_| "/bin/zsh".to_string());
let shell_config = if shell.contains("zsh") {
"~/.zshrc"
} else if shell.contains("bash") {
"~/.bash_profile"
} else {
"your shell config file"
};
println!(
" echo 'export PATH=\"{}:$PATH\"' >> {}",
brew_path, shell_config
);
println!(" source {}", shell_config);
if !is_ci {
println!(" Or run 'bv setup' to configure this automatically.");
}
println!();
}
results.push(DependencyResult {
name: dep.name.clone(),
found: true,
path: Some(format!("{}/java", brew_path)),
version: None,
running: None,
skipped: false,
skip_reason: None,
description: Some(dep.description.clone()),
website: dep.website.clone(),
install_instructions: Some(dep.install_instructions.clone()),
});
} else if let Some(uv_path) = uv_windows_path {
if !json {
println!("⚠️ Found (not in PATH)");
println!(" UV is installed at: {}", uv_path);
println!(" But it's not available in your PATH.");
println!(" To make it available:");
println!(" Option 1: Open a new Command Prompt or PowerShell window");
println!(" Option 2: Manually refresh your PATH:");
println!(" PowerShell: $env:Path = [System.Environment]::GetEnvironmentVariable(\"Path\",\"User\")");
if !is_ci {
println!(" Option 3: Run 'bv setup' again after restarting your terminal");
}
println!();
}
results.push(DependencyResult {
name: dep.name.clone(),
found: true,
path: Some(uv_path.clone()),
version: get_uv_version(&uv_path),
running: None,
skipped: false,
skip_reason: None,
description: Some(dep.description.clone()),
website: dep.website.clone(),
install_instructions: Some(dep.install_instructions.clone()),
});
} else if let Some(sbenv_path) = syftbox_sbenv_path {
let version = Command::new(&sbenv_path)
.arg("--version")
.output()
.ok()
.filter(|output| output.status.success())
.and_then(|output| {
let version_str = String::from_utf8_lossy(&output.stdout);
version_str.split_whitespace().nth(2).map(|v| v.to_string())
});
if !json {
if let Some(ref ver) = version {
println!("✓ Found (version {})", ver);
} else {
println!("✓ Found");
}
println!(" Path: {}", sbenv_path);
println!(" Note: Found in ~/.sbenv/binaries");
}
results.push(DependencyResult {
name: dep.name.clone(),
found: true,
path: Some(sbenv_path),
version,
running: None,
skipped: false,
skip_reason: None,
description: Some(dep.description.clone()),
website: dep.website.clone(),
install_instructions: Some(dep.install_instructions.clone()),
});
} else if let Some(path) = binary_path {
let (version_ok, version_str) = if let Some(min_version) = dep.min_version {
check_version_with_info(&dep.name, min_version)
} else {
(true, get_version_string(&dep.name))
};
if !version_ok {
all_found = false;
if !json {
let min_ver = dep.min_version.unwrap_or(0);
println!("❌ Version too old (requires {} or higher)", min_ver);
println!(" Description: {}", dep.description);
println!(" Installation instructions:");
for line in dep.install_instructions.lines() {
if !line.trim().is_empty() {
println!(" {}", line);
}
}
println!();
}
results.push(DependencyResult {
name: dep.name.clone(),
found: true,
path: Some(path),
version: version_str,
running: None,
skipped: false,
skip_reason: None,
description: Some(dep.description.clone()),
website: dep.website.clone(),
install_instructions: Some(dep.install_instructions.clone()),
});
} else {
let is_running = if dep.check_running {
Some(check_if_running(&dep.name))
} else {
None
};
if let Some(running) = is_running {
if !running {
all_running = false;
}
}
if !json {
if let Some(ref ver) = version_str {
print!("✓ Found (version {})", ver);
} else {
print!("✓ Found");
}
if let Some(running) = is_running {
if running {
println!(" (running)");
} else {
println!(" (NOT RUNNING)");
println!(
" To start {}, run: {}",
dep.name,
get_start_command(&dep.name)
);
}
} else {
println!();
}
println!(" Path: {}", path);
}
results.push(DependencyResult {
name: dep.name.clone(),
found: true,
path: Some(path),
version: version_str,
running: is_running,
skipped: false,
skip_reason: None,
description: Some(dep.description.clone()),
website: dep.website.clone(),
install_instructions: Some(dep.install_instructions.clone()),
});
}
}
}
if json {
let result = DependencyCheckResult {
dependencies: results,
all_satisfied: all_found && all_running,
};
println!("{}", serde_json::to_string_pretty(&result)?);
if all_found && all_running {
Ok(())
} else if !all_found {
Err(anyhow!("Dependencies missing").into())
} else {
Err(anyhow!("Services not running").into())
}
} else {
println!("\n=========================");
if all_found && all_running {
println!("✓ All dependencies satisfied!");
Ok(())
} else if !all_found {
println!(
"⚠️ Some dependencies are missing. Please install them using the instructions above."
);
Err(anyhow!("Dependencies missing").into())
} else {
println!(
"⚠️ Some services are not running. Please start them using the commands above."
);
Err(anyhow!("Services not running").into())
}
}
}
fn check_if_running(service: &str) -> bool {
match service {
"docker" => {
Command::new("docker")
.arg("info")
.output()
.map(|output| output.status.success())
.unwrap_or(false)
}
_ => false,
}
}
#[cfg(target_os = "macos")]
fn is_docker_desktop_installed() -> bool {
let docker_app = Path::new("/Applications/Docker.app");
if docker_app.exists() {
return true;
}
if let Some(home) = dirs::home_dir() {
let user_docker_app = home.join("Applications/Docker.app");
if user_docker_app.exists() {
return true;
}
}
false
}
#[cfg(not(target_os = "macos"))]
#[allow(dead_code)]
fn is_docker_desktop_installed() -> bool {
false
}
fn check_syftbox_in_sbenv() -> Option<String> {
let home = dirs::home_dir()?;
let sbenv_binaries = home.join(".sbenv").join("binaries");
if !sbenv_binaries.exists() {
return None;
}
let mut syftbox_binaries = Vec::new();
if let Ok(entries) = std::fs::read_dir(&sbenv_binaries) {
for entry in entries.flatten() {
let path = entry.path();
if path.is_dir() {
let syftbox_path = path.join("syftbox");
if syftbox_path.is_file() {
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
if let Ok(metadata) = syftbox_path.metadata() {
let permissions = metadata.permissions();
if permissions.mode() & 0o111 != 0 {
syftbox_binaries.push(syftbox_path);
}
}
}
#[cfg(not(unix))]
{
syftbox_binaries.push(syftbox_path);
}
}
}
}
}
if syftbox_binaries.is_empty() {
return None;
}
syftbox_binaries.sort_by(|a, b| {
let a_parent = a
.parent()
.and_then(|p| p.file_name())
.map(|n| n.to_string_lossy());
let b_parent = b
.parent()
.and_then(|p| p.file_name())
.map(|n| n.to_string_lossy());
b_parent.cmp(&a_parent)
});
syftbox_binaries.first().map(|p| p.display().to_string())
}
fn get_start_command(service: &str) -> String {
match service {
"docker" => {
if std::env::consts::OS == "macos" {
"Open Docker Desktop from your Applications folder (/Applications/Docker.app)"
.to_string()
} else if std::env::consts::OS == "windows" {
"Open Docker Desktop from your Start menu".to_string()
} else {
"Start Docker daemon with 'sudo systemctl start docker' or open Docker Desktop"
.to_string()
}
}
_ => format!("Start {}", service),
}
}
#[allow(dead_code)]
fn check_version(tool: &str, min_version: u32) -> bool {
match tool {
"java" => check_java_version(min_version),
_ => true, }
}
#[allow(dead_code)]
fn check_java_version(min_version: u32) -> bool {
let output = Command::new("java").arg("-version").output();
match output {
Ok(output) => {
let version_str = String::from_utf8_lossy(&output.stderr);
if let Some(version) = parse_java_version(&version_str) {
if version >= min_version {
print!(" (version {})", version);
true
} else {
false
}
} else {
false
}
}
Err(_) => false,
}
}
fn parse_java_version(output: &str) -> Option<u32> {
for line in output.lines() {
if line.contains("version") {
if let Some(start) = line.find('"') {
if let Some(end) = line[start + 1..].find('"') {
let version_str = &line[start + 1..start + 1 + end];
if let Some(stripped) = version_str.strip_prefix("1.") {
if let Some(dot_pos) = stripped.find('.') {
if let Ok(version) = stripped[..dot_pos].parse::<u32>() {
return Some(version);
}
}
} else {
let major_part = version_str.split('.').next().unwrap_or(version_str);
if let Ok(version) = major_part.parse::<u32>() {
return Some(version);
}
}
}
}
}
}
None
}
fn check_version_with_info(tool: &str, min_version: u32) -> (bool, Option<String>) {
match tool {
"java" => check_java_version_with_info(min_version),
_ => (true, get_version_string(tool)),
}
}
fn get_version_string(tool: &str) -> Option<String> {
match tool {
"docker" => get_docker_version(),
"syftbox" => get_syftbox_version(),
"nextflow" => get_nextflow_version(),
"uv" => get_uv_version(tool),
_ => None,
}
}
fn get_docker_version() -> Option<String> {
let output = Command::new("docker").arg("--version").output().ok()?;
if !output.status.success() {
return None;
}
let version_str = String::from_utf8_lossy(&output.stdout);
version_str
.split_whitespace()
.nth(2)
.map(|v| v.trim_end_matches(',').to_string())
}
fn get_syftbox_version() -> Option<String> {
let output = Command::new("syftbox").arg("--version").output().ok()?;
if !output.status.success() {
return None;
}
let version_str = String::from_utf8_lossy(&output.stdout);
version_str.split_whitespace().nth(2).map(|v| v.to_string())
}
fn get_nextflow_version() -> Option<String> {
let output = Command::new("nextflow").arg("-version").output().ok()?;
if !output.status.success() {
return None;
}
let version_str = String::from_utf8_lossy(&output.stdout);
for line in version_str.lines() {
if line.trim().starts_with("version ") {
return line.split_whitespace().nth(1).map(|v| v.to_string());
}
}
None
}
fn check_java_version_with_info(min_version: u32) -> (bool, Option<String>) {
let output = Command::new("java").arg("-version").output();
match output {
Ok(output) => {
let version_str = String::from_utf8_lossy(&output.stderr);
if let Some(version) = parse_java_version(&version_str) {
(version >= min_version, Some(version.to_string()))
} else {
(false, None)
}
}
Err(_) => (false, None),
}
}
fn is_google_colab() -> bool {
if env::var("COLAB_RELEASE_TAG").is_ok() {
return true;
}
for (key, _) in env::vars() {
if key.starts_with("COLAB_") {
return true;
}
}
false
}
fn check_java_in_brew_not_in_path() -> Option<String> {
if which::which("brew").is_err() {
return None;
}
let output = Command::new("brew")
.args(["list", "--formula"])
.output()
.ok()?;
let installed_packages = String::from_utf8_lossy(&output.stdout);
let mut found_java_package = None;
for line in installed_packages.lines() {
if line.starts_with("openjdk") {
found_java_package = Some(line.to_string());
break;
}
}
found_java_package.as_ref()?;
let pkg = found_java_package.unwrap();
let prefix_output = Command::new("brew")
.args(["--prefix", &pkg])
.output()
.ok()?;
if !prefix_output.status.success() {
return None;
}
let brew_prefix = String::from_utf8_lossy(&prefix_output.stdout)
.trim()
.to_string();
let java_bin_path = format!("{}/bin", brew_prefix);
if std::path::Path::new(&format!("{}/java", java_bin_path)).exists() {
Some(java_bin_path)
} else {
None
}
}
fn check_uv_in_windows_not_in_path() -> Option<String> {
let possible_locations = [
env::var("LOCALAPPDATA")
.ok()
.map(|p| format!("{}\\Programs\\uv\\uv.exe", p)),
env::var("LOCALAPPDATA")
.ok()
.map(|p| format!("{}\\uv\\bin\\uv.exe", p)),
env::var("PROGRAMFILES")
.ok()
.map(|p| format!("{}\\uv\\uv.exe", p)),
env::var("USERPROFILE")
.ok()
.map(|p| format!("{}\\.cargo\\bin\\uv.exe", p)),
env::var("USERPROFILE")
.ok()
.map(|p| format!("{}\\.local\\bin\\uv.exe", p)),
env::var("LOCALAPPDATA")
.ok()
.map(|p| format!("{}\\Microsoft\\WinGet\\Packages\\astral-sh.uv_Microsoft.Winget.Source_8wekyb3d8bbwe\\uv.exe", p)),
];
for location in possible_locations.iter().flatten() {
if std::path::Path::new(location).exists() {
return Some(location.clone());
}
}
None
}
fn get_uv_version(uv_path: &str) -> Option<String> {
let output = Command::new(uv_path).arg("--version").output().ok()?;
if !output.status.success() {
return None;
}
let version_str = String::from_utf8_lossy(&output.stdout);
version_str.split_whitespace().nth(1).map(|v| v.to_string())
}
pub fn check_brew_installed() -> Result<bool> {
#[cfg(not(target_os = "macos"))]
{
Ok(true) }
#[cfg(target_os = "macos")]
{
let brew_check = which::which("brew").is_ok();
Ok(brew_check)
}
}
pub fn install_brew() -> Result<String> {
#[cfg(not(target_os = "macos"))]
{
Err(anyhow!("Homebrew installation is only supported on macOS").into())
}
#[cfg(target_os = "macos")]
{
use std::process::Stdio;
let install_script = r#"/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)""#;
let mut child = Command::new("/bin/bash")
.arg("-c")
.arg(install_script)
.stdin(Stdio::inherit())
.stdout(Stdio::inherit())
.stderr(Stdio::inherit())
.spawn()
.map_err(|e| anyhow!("Failed to start Homebrew installation: {}", e))?;
let status = child
.wait()
.map_err(|e| anyhow!("Failed to wait for Homebrew installation: {}", e))?;
if status.success() {
let brew_path = which::which("brew")
.ok()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "/opt/homebrew/bin/brew".to_string());
Ok(brew_path)
} else {
Err(anyhow!("Homebrew installation failed").into())
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn java_version_parsing_various_formats() {
let samples = [
("openjdk version \"17.0.2\" 2022-01-18", Some(17)),
("java version \"1.8.0_321\"", Some(8)),
("openjdk version \"11.0.14\" 2022-01-18", Some(11)),
("java version \"21\" 2023-09-19", Some(21)),
("no version here", None),
];
for (out, expected) in samples {
assert_eq!(parse_java_version(out), expected);
}
}
#[test]
fn start_command_and_running_checks() {
let docker_cmd = get_start_command("docker");
if std::env::consts::OS == "macos" {
assert_eq!(
docker_cmd,
"Open Docker Desktop from your Applications folder (/Applications/Docker.app)"
);
} else if std::env::consts::OS == "windows" {
assert_eq!(docker_cmd, "Open Docker Desktop from your Start menu");
} else {
assert_eq!(
docker_cmd,
"Start Docker daemon with 'sudo systemctl start docker' or open Docker Desktop"
);
}
assert_eq!(get_start_command("xyz"), "Start xyz");
assert!(!check_if_running("xyz"));
}
#[test]
fn check_version_non_java_defaults_true() {
assert!(check_version("not-java", 9999));
}
#[test]
fn test_parse_java_version_edge_cases() {
assert_eq!(parse_java_version(""), None);
assert_eq!(parse_java_version("version"), None);
assert_eq!(parse_java_version("version \"\""), None);
assert_eq!(parse_java_version("version \"not a number\""), None);
assert_eq!(parse_java_version("java 17"), None);
assert_eq!(parse_java_version("version \"1.\""), None);
assert_eq!(parse_java_version("version \"1.x.0\""), None);
}
#[test]
fn test_parse_java_version_modern_formats() {
assert_eq!(
parse_java_version("openjdk version \"17\" 2021-09-14"),
Some(17)
);
assert_eq!(
parse_java_version("openjdk version \"21.0.1\" 2023-10-17 LTS"),
Some(21)
);
assert_eq!(
parse_java_version("java version \"19.0.2\" 2023-01-17"),
Some(19)
);
}
#[test]
fn test_parse_java_version_legacy_formats() {
assert_eq!(parse_java_version("java version \"1.7.0_80\""), Some(7));
assert_eq!(parse_java_version("java version \"1.6.0_45\""), Some(6));
assert_eq!(parse_java_version("java version \"1.8.0_351\""), Some(8));
}
#[test]
#[serial_test::serial]
fn test_google_colab_detection() {
let was_set = env::var("COLAB_RELEASE_TAG").is_ok();
let old_value = env::var("COLAB_RELEASE_TAG").ok();
env::set_var("COLAB_RELEASE_TAG", "release-123");
assert!(is_google_colab());
if was_set {
if let Some(val) = old_value {
env::set_var("COLAB_RELEASE_TAG", val);
}
} else {
env::remove_var("COLAB_RELEASE_TAG");
}
}
#[test]
#[serial_test::serial]
fn test_google_colab_detection_prefix() {
let was_set = env::var("COLAB_TEST_VAR").is_ok();
env::set_var("COLAB_TEST_VAR", "test");
assert!(is_google_colab());
if !was_set {
env::remove_var("COLAB_TEST_VAR");
}
}
#[test]
#[serial_test::serial]
fn execute_reports_missing_in_clean_env() {
let old_path = std::env::var("PATH").unwrap_or_default();
std::env::set_var("PATH", "");
let rt = tokio::runtime::Runtime::new().unwrap();
let res = rt.block_on(execute(false));
assert!(res.is_err());
std::env::set_var("PATH", old_path);
}
#[test]
#[serial_test::serial]
#[cfg_attr(
not(feature = "slow-tests"),
ignore = "env-dependent; covered in slow/integration"
)]
#[cfg_attr(windows, ignore = "Windows shell semantics; covered in integration CI")]
fn execute_with_all_tools_available_returns_ok() {
let dir = TempDir::new().unwrap();
let make_exec = |name: &str, body: &str| {
let p = dir.path().join(name);
fs::write(&p, format!("#!/bin/sh\n{}\n", body)).unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perm = fs::metadata(&p).unwrap().permissions();
perm.set_mode(0o755);
fs::set_permissions(&p, perm).unwrap();
}
p
};
make_exec("java", "echo 'openjdk version \"21\" 2024-01-01' 1>&2");
make_exec("docker", "[ \"$1\" = \"info\" ] && exit 0; exit 0");
make_exec("nextflow", "exit 0");
make_exec("syftbox", "exit 0");
make_exec("uv", "exit 0");
let old_path = std::env::var("PATH").unwrap_or_default();
let new_path = format!("{}:{}", dir.path().display(), old_path);
std::env::set_var("PATH", &new_path);
let rt = tokio::runtime::Runtime::new().unwrap();
let res = rt.block_on(execute(false));
assert!(res.is_ok());
std::env::set_var("PATH", old_path);
}
#[test]
#[serial_test::serial]
fn execute_with_docker_not_running_reports_warning() {
let dir = TempDir::new().unwrap();
let make_exec = |name: &str, body: &str| {
let p = dir.path().join(name);
fs::write(&p, format!("#!/bin/sh\n{}\n", body)).unwrap();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mut perm = fs::metadata(&p).unwrap().permissions();
perm.set_mode(0o755);
fs::set_permissions(&p, perm).unwrap();
}
p
};
make_exec("java", "echo 'openjdk version \"21\"' 1>&2");
make_exec("nextflow", "exit 0");
make_exec("syftbox", "exit 0");
make_exec("uv", "exit 0");
make_exec("docker", "[ \"$1\" = \"info\" ] && exit 1; exit 0");
let old_path = std::env::var("PATH").unwrap_or_default();
let new_path = format!("{}:{}", dir.path().display(), old_path);
std::env::set_var("PATH", &new_path);
let rt = tokio::runtime::Runtime::new().unwrap();
let res = rt.block_on(execute(false));
assert!(res.is_err());
std::env::set_var("PATH", old_path);
}
}