use super::check::DependencyConfig;
use crate::Result;
use anyhow::anyhow;
use std::collections::HashSet;
use std::env;
use std::io::{self, Write};
use std::process::{Command, ExitStatus, Stdio};
#[cfg(target_os = "macos")]
use std::fs;
struct InstallCommandOutput {
status: ExitStatus,
stderr: Vec<u8>,
}
impl InstallCommandOutput {
fn from_output(output: std::process::Output) -> Self {
Self {
status: output.status,
stderr: output.stderr,
}
}
}
fn skip_install_commands() -> bool {
env::var("BIOVAULT_SKIP_INSTALLS")
.ok()
.map(|v| {
let v = v.trim();
matches!(v, "1" | "true" | "TRUE" | "True")
})
.unwrap_or(false)
}
#[derive(Debug)]
enum SystemType {
GoogleColab,
MacOs,
Ubuntu,
ArchLinux,
Windows,
Unknown,
}
pub async fn execute(dependencies: Vec<String>, force: bool) -> Result<()> {
eprintln!(
"🔧 BioVault Setup: execute() called with dependencies: {:?}, force: {}",
dependencies, force
);
println!("BioVault Environment Setup");
println!("==========================\n");
let valid_deps = ["java", "docker", "nextflow", "syftbox", "uv"];
if !dependencies.is_empty() {
for dep in &dependencies {
if !valid_deps.contains(&dep.as_str()) {
return Err(anyhow!(
"Unknown dependency '{}'. Valid options are: {}",
dep,
valid_deps.join(", ")
)
.into());
}
}
println!(
"Installing specific dependencies: {}\n",
dependencies.join(", ")
);
} else {
eprintln!("🔧 No specific dependencies requested - will install all missing");
}
if force {
println!("🔄 Force mode enabled - reinstalling even if already present\n");
}
let system_type = detect_system();
eprintln!("🔧 Detected system type: {:?}", system_type);
match system_type {
SystemType::GoogleColab => {
println!("✓ Detected Google Colab environment");
eprintln!("🔧 Calling setup_google_colab()");
setup_google_colab(dependencies, force).await?;
eprintln!("✅ setup_google_colab() completed");
}
SystemType::MacOs => {
println!("✓ Detected macOS environment");
eprintln!("🔧 Calling setup_macos()");
setup_macos(dependencies, force).await?;
eprintln!("✅ setup_macos() completed");
}
SystemType::Ubuntu => {
println!("✓ Detected Ubuntu/Debian environment");
eprintln!("🔧 Calling setup_ubuntu()");
setup_ubuntu(dependencies, force).await?;
eprintln!("✅ setup_ubuntu() completed");
}
SystemType::ArchLinux => {
println!("✓ Detected Arch Linux environment");
eprintln!("🔧 Calling setup_arch()");
setup_arch(dependencies, force).await?;
eprintln!("✅ setup_arch() completed");
}
SystemType::Windows => {
println!("✓ Detected Windows environment");
eprintln!("🔧 Calling setup_windows()");
setup_windows(dependencies, force).await?;
eprintln!("✅ setup_windows() completed");
}
SystemType::Unknown => {
eprintln!("⚠️ System type is Unknown - cannot proceed with installation");
println!("ℹ️ System type not detected or not supported for automated setup");
println!(" This command currently supports:");
println!(" - Google Colab");
println!(" - macOS (Homebrew)");
println!(" - Ubuntu/Debian (apt)");
println!(" - Arch Linux (pacman)");
println!(" - Windows (WinGet)");
println!("\n For manual setup, please run: bv check");
}
}
eprintln!("✅ execute() completed successfully");
Ok(())
}
pub async fn install_single_dependency(name: &str) -> Result<Option<String>> {
eprintln!("🔧 install_single_dependency('{}') called", name);
let deps_yaml = include_str!("../../deps.yaml");
if let Ok(config) = serde_yaml::from_str::<DependencyConfig>(deps_yaml) {
let env_key = get_environment_key();
if let Some(dep) = config.dependencies.iter().find(|d| d.name == name) {
if let Some(envs) = &dep.environments {
if let Some(env_cfg) = envs.get(&env_key) {
if env_cfg.skip {
let reason = env_cfg.skip_reason.clone().unwrap_or_else(|| {
format!("{} cannot be auto-installed on this platform", name)
});
eprintln!("⏭️ {} is skipped on {}: {}", name, env_key, reason);
return Err(anyhow!("{}", reason).into());
}
}
}
}
}
execute(vec![name.to_string()], false).await?;
let path = Command::new("which")
.arg(name)
.output()
.ok()
.and_then(|output| {
if output.status.success() {
String::from_utf8(output.stdout)
.ok()
.map(|s| s.trim().to_string())
} else {
None
}
});
eprintln!(
"✅ install_single_dependency('{}') completed, path: {:?}",
name, path
);
Ok(path)
}
pub async fn install_dependencies(names: &[String]) -> Result<()> {
eprintln!("🔧 install_dependencies({:?}) called", names);
let result = execute(names.to_vec(), false).await;
if result.is_ok() {
eprintln!(
"✅ install_dependencies({:?}) completed successfully",
names
);
} else {
eprintln!("❌ install_dependencies({:?}) failed: {:?}", names, result);
}
result
}
fn detect_system() -> SystemType {
let target_os = std::env::consts::OS;
if target_os == "windows" {
return SystemType::Windows;
}
if is_google_colab() {
return SystemType::GoogleColab;
}
if target_os == "macos" {
return SystemType::MacOs;
}
if target_os == "linux" {
let has_apt = std::process::Command::new("sh")
.arg("-c")
.arg("command -v apt-get >/dev/null 2>&1")
.status()
.map(|s| s.success())
.unwrap_or(false);
if has_apt {
return SystemType::Ubuntu;
}
let has_pacman = std::process::Command::new("sh")
.arg("-c")
.arg("command -v pacman >/dev/null 2>&1")
.status()
.map(|s| s.success())
.unwrap_or(false);
if has_pacman {
return SystemType::ArchLinux;
}
}
SystemType::Unknown
}
fn get_environment_key() -> String {
match detect_system() {
SystemType::GoogleColab => "google_colab".to_string(),
SystemType::MacOs => "macos".to_string(),
SystemType::Ubuntu => "ubuntu".to_string(),
SystemType::ArchLinux => "arch".to_string(),
SystemType::Windows => "windows".to_string(),
SystemType::Unknown => "unknown".to_string(),
}
}
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
}
async fn setup_google_colab(dependencies: Vec<String>, _force: bool) -> Result<()> {
if skip_install_commands() {
println!("(test mode) Skipping Google Colab setup commands");
return Ok(());
}
println!("\nSetting up Google Colab environment...\n");
let deps_yaml = include_str!("../../deps.yaml");
let config: DependencyConfig = serde_yaml::from_str(deps_yaml)?;
let deps_to_install: Vec<_> = if dependencies.is_empty() {
let mut all_deps: Vec<_> = config.dependencies.iter().collect();
if let Some(docker_idx) = all_deps.iter().position(|d| d.name == "docker") {
let docker = all_deps.remove(docker_idx);
all_deps.push(docker);
}
all_deps
} else {
let dep_set: HashSet<_> = dependencies.iter().map(|s| s.as_str()).collect();
config
.dependencies
.iter()
.filter(|d| dep_set.contains(d.name.as_str()))
.collect()
};
let mut success_count = 0;
let mut skip_count = 0;
let mut fail_count = 0;
for dep in deps_to_install {
if let Some(environments) = &dep.environments {
if let Some(env_config) = environments.get("google_colab") {
if env_config.skip {
println!(
"⏭️ Skipping {}: {}",
dep.name,
env_config
.skip_reason
.as_ref()
.unwrap_or(&"Not needed".to_string())
);
skip_count += 1;
continue;
}
if let Some(install_commands) = &env_config.install_commands {
println!("📦 Installing {}...", dep.name);
println!(" {}", dep.description);
let mut all_succeeded = true;
for cmd in install_commands {
println!(" Running: {}", cmd);
let output = Command::new("sh").arg("-c").arg(cmd).output();
match output {
Ok(output) => {
if output.status.success() {
println!(" ✓ Command succeeded");
} else {
println!(" ❌ Command failed");
if !output.stderr.is_empty() {
println!(
" Error: {}",
String::from_utf8_lossy(&output.stderr)
);
}
all_succeeded = false;
break;
}
}
Err(e) => {
println!(" ❌ Failed to execute: {}", e);
all_succeeded = false;
break;
}
}
}
if all_succeeded {
if let Some(verify_cmd) = &env_config.verify_command {
print!(" Verifying installation... ");
let output = Command::new("sh")
.arg("-c")
.arg(verify_cmd)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output();
if let Ok(output) = output {
if output.status.success() {
println!("✓");
success_count += 1;
} else {
println!("❌ Verification failed");
fail_count += 1;
}
} else {
println!("❌ Could not verify");
fail_count += 1;
}
} else {
success_count += 1;
}
} else {
fail_count += 1;
}
println!();
}
}
}
}
println!("📝 Final setup steps for Google Colab:\n");
println!(" Add these lines to your notebook for persistence:");
println!(" ```python");
println!(" import os");
println!(" os.environ['PATH'] = f\"/usr/local/bin:{{os.environ['PATH']}}\"");
println!(" ```");
println!();
println!(" Or in a shell cell:");
println!(" ```bash");
println!(" !export PATH=\"/usr/local/bin:$PATH\"");
println!(" ```");
println!("\n==========================");
println!("Setup Summary:");
println!(" ✓ Installed: {}", success_count);
println!(" ⏭️ Skipped: {}", skip_count);
if fail_count > 0 {
println!(" ❌ Failed: {}", fail_count);
println!("\n⚠️ Some installations failed. Please check the errors above.");
} else {
println!("\n✅ Setup completed successfully!");
println!(" Run 'bv check' to verify all dependencies.");
}
Ok(())
}
async fn setup_macos(dependencies: Vec<String>, force: bool) -> Result<()> {
if skip_install_commands() {
println!("(test mode) Skipping macOS setup commands");
return Ok(());
}
use super::check::DependencyConfig;
use std::process::Command;
println!("\nSetting up macOS environment...\n");
let is_ci = env::var("CI").is_ok() || env::var("GITHUB_ACTIONS").is_ok();
let assume_yes = env::var("BIOVAULT_SETUP_ASSUME_YES")
.map(|value| {
let normalized = value.to_ascii_lowercase();
normalized == "1" || normalized == "true" || normalized == "yes" || normalized == "y"
})
.unwrap_or(false);
if assume_yes {
env::set_var("NONINTERACTIVE", "1");
env::set_var("HOMEBREW_NO_AUTO_UPDATE", "1");
env::set_var("HOMEBREW_NO_ENV_HINTS", "1");
}
if let Ok(home_dir) = env::var("HOME") {
env::set_var(
"HOMEBREW_CASK_OPTS",
format!("--appdir={}/Applications", home_dir),
);
}
let brew_in_path = which::which("brew").is_ok();
let mut brew_path = None;
if !brew_in_path {
let common_brew_paths = vec![
"/opt/homebrew/bin/brew", "/usr/local/bin/brew", ];
for path in &common_brew_paths {
if std::path::Path::new(path).exists() {
brew_path = Some(path.to_string());
println!("📦 Found Homebrew at {} (not in PATH)", path);
break;
}
}
}
if !brew_in_path && brew_path.is_none() {
println!("📦 Homebrew not found. Would you like to install it? [Y/n]: ");
if is_ci && !assume_yes {
println!(" CI mode: Skipping Homebrew installation.");
println!(" Please ensure Homebrew is pre-installed in CI environment.");
return Ok(());
}
let should_install = if assume_yes {
println!(" Auto-confirming Homebrew installation (BIOVAULT_SETUP_ASSUME_YES=1).");
true
} else {
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let answer = input.trim().to_lowercase();
answer.is_empty() || answer == "y" || answer == "yes"
};
if should_install {
println!("Installing Homebrew...");
let install_cmd = "/bin/bash -c \"$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)\"";
let status = Command::new("sh").arg("-c").arg(install_cmd).status()?;
if status.success() {
println!("✓ Homebrew installed successfully!");
if std::path::Path::new("/opt/homebrew/bin/brew").exists() {
brew_path = Some("/opt/homebrew/bin/brew".to_string());
} else if std::path::Path::new("/usr/local/bin/brew").exists() {
brew_path = Some("/usr/local/bin/brew".to_string());
}
} else {
println!("❌ Homebrew installation failed.");
println!("Please install manually from: https://brew.sh");
return Ok(());
}
} else {
println!("Skipping Homebrew installation.");
println!("Please install Homebrew manually from: https://brew.sh");
println!("Then re-run: bv setup");
return Ok(());
}
}
let brew_cmd = if brew_in_path {
"brew".to_string()
} else if let Some(ref bp) = brew_path {
bp.clone()
} else {
"brew".to_string() };
let deps_yaml = include_str!("../../deps.yaml");
let config: DependencyConfig = serde_yaml::from_str(deps_yaml)?;
let deps_to_install: Vec<_> = if dependencies.is_empty() {
let mut all_deps: Vec<_> = config.dependencies.iter().collect();
if let Some(docker_idx) = all_deps.iter().position(|d| d.name == "docker") {
let docker = all_deps.remove(docker_idx);
all_deps.push(docker);
}
all_deps
} else {
let dep_set: HashSet<_> = dependencies.iter().map(|s| s.as_str()).collect();
config
.dependencies
.iter()
.filter(|d| dep_set.contains(d.name.as_str()))
.collect()
};
let mut success_count = 0;
let mut skip_count = 0;
let mut fail_count = 0;
for dep in deps_to_install {
if let Some(environments) = &dep.environments {
if let Some(env_config) = environments.get("macos") {
if env_config.skip {
println!(
"⏭️ Skipping {}: {}",
dep.name,
env_config
.skip_reason
.as_ref()
.unwrap_or(&"Not needed on macOS".to_string())
);
skip_count += 1;
continue;
}
if let Some(install_commands) = &env_config.install_commands {
let need_install = if force {
true
} else {
let mut should_install = true;
if let Some(verify_cmd) = &env_config.verify_command {
let verified = Command::new("sh")
.arg("-c")
.arg(verify_cmd)
.env("PATH", ensure_user_space_docker_path())
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false);
if verified {
should_install = false;
}
} else {
if which::which(&dep.name).is_ok() {
should_install = false;
}
}
if dep.name == "java" {
if let Some(min_v) = dep.min_version {
if let Some(current) = java_major_version() {
if current >= min_v {
println!(" Java version {} already meets minimum requirement of {}", current, min_v);
should_install = false;
}
}
}
}
should_install
};
if !need_install {
println!("✓ {} already meets requirements. Skipping.", dep.name);
skip_count += 1;
println!();
continue;
}
println!("📦 Installing {}...", dep.name);
println!(" {}", dep.description);
let mut all_succeeded = true;
for cmd in install_commands {
if dep.name == "docker" && cmd.contains("brew install --cask") {
println!(" Installing Docker Desktop from official .dmg");
match install_docker_desktop_from_dmg() {
Ok(_) => {
if std::path::Path::new("/Applications/Docker.app").exists() {
println!(" ✓ Docker Desktop installed successfully");
println!(" ℹ️ Docker Desktop has been installed to /Applications");
println!(" ℹ️ Launch it manually to complete setup (Docker.app may not start in VMs)");
} else {
eprintln!("⚠️ Docker.app not found at /Applications/Docker.app after installation");
println!(" ❌ Docker Desktop installation could not be verified");
all_succeeded = false;
break;
}
}
Err(e) => {
println!(" ❌ Failed to install Docker Desktop: {}", e);
println!(" 💡 Fallback: You can manually download Docker Desktop from:");
println!(
" https://www.docker.com/products/docker-desktop/"
);
all_succeeded = false;
break;
}
}
continue;
}
let mut adjusted_cmd = if !brew_in_path && cmd.starts_with("brew ") {
cmd.replace("brew ", &format!("{} ", brew_cmd))
} else {
cmd.clone()
};
if force
&& adjusted_cmd.starts_with("brew install")
&& !adjusted_cmd.contains("--force")
{
if let Some(pkg_start) = adjusted_cmd.find("brew install") {
let prefix = &adjusted_cmd[..pkg_start + "brew install".len()];
let suffix = &adjusted_cmd[pkg_start + "brew install".len()..];
adjusted_cmd = format!("{} --force{}", prefix, suffix);
}
}
println!(" Running: {}", adjusted_cmd);
let output = Command::new("sh")
.arg("-c")
.arg(&adjusted_cmd)
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.output()
.map(InstallCommandOutput::from_output);
match output {
Ok(output) => {
if output.status.success() {
println!(" ✓ Command succeeded");
} else {
println!(" ❌ Command failed");
if !output.stderr.is_empty() {
println!(
" Error: {}",
String::from_utf8_lossy(&output.stderr)
);
}
all_succeeded = false;
break;
}
}
Err(e) => {
println!(" ❌ Failed to execute: {}", e);
all_succeeded = false;
break;
}
}
}
if all_succeeded {
if dep.name == "java" {
if let Some(java_path) = check_java_in_brew_not_in_path() {
let current_path = env::var("PATH").unwrap_or_default();
env::set_var("PATH", format!("{}:{}", java_path, current_path));
}
}
if dep.name == "docker" {
if !is_ci {
println!(" ⚠️ Skipping automated Docker Desktop privileged installer.");
println!(
" Please open Docker Desktop manually once to finish setup."
);
} else {
println!(" CI mode: Skipping Docker Desktop privileged setup.");
}
let updated_path = ensure_user_space_docker_path();
env::set_var("PATH", &updated_path);
}
if let Some(verify_cmd) = &env_config.verify_command {
print!(" Verifying installation... ");
let verify_result = Command::new("sh")
.arg("-c")
.arg(verify_cmd)
.env("PATH", ensure_user_space_docker_path())
.output();
match verify_result {
Ok(output) if output.status.success() => {
println!("✓");
success_count += 1;
}
Ok(output) => {
if dep.name == "docker"
&& std::path::Path::new("/Applications/Docker.app").exists()
{
println!("✓ (Docker.app installed)");
println!(" ℹ️ Docker Desktop is installed but CLI tools aren't ready yet.");
println!(" Launch Docker Desktop and complete setup to activate CLI tools.");
success_count += 1;
} else {
println!("⚠️ Verification failed (may need new shell to pick up PATH)");
if !output.stdout.is_empty() {
eprintln!(
" stdout: {}",
String::from_utf8_lossy(&output.stdout).trim_end()
);
}
if !output.stderr.is_empty() {
eprintln!(
" stderr: {}",
String::from_utf8_lossy(&output.stderr).trim_end()
);
}
println!(" ℹ️ Installation commands succeeded - try 'bv check' to verify");
success_count += 1;
}
}
Err(err) => {
if dep.name == "docker"
&& std::path::Path::new("/Applications/Docker.app").exists()
{
println!("✓ (Docker.app installed)");
println!(" ℹ️ Docker Desktop is installed but CLI tools aren't ready yet.");
println!(" Launch Docker Desktop and complete setup to activate CLI tools.");
success_count += 1;
} else {
println!(
"⚠️ Could not verify (failed to run '{}'): {}",
verify_cmd, err
);
println!(" ℹ️ Installation commands succeeded - try 'bv check' to verify");
success_count += 1;
}
}
}
} else {
success_count += 1;
}
} else {
fail_count += 1;
}
println!();
}
}
}
}
check_and_configure_java_path(is_ci).await?;
println!("\nNotes:");
println!("- If this is your first time installing Docker Desktop, open it once to finish setup and grant permissions.");
println!("\nSyftBox:");
print_syftbox_instructions();
println!("\n==========================");
println!("Setup Summary:");
println!(" ✓ Installed: {}", success_count);
println!(" ⏭️ Skipped: {}", skip_count);
if fail_count > 0 {
println!(" ❌ Failed: {}", fail_count);
println!("\n⚠️ Some installations failed. Please check the errors above.");
return Err(anyhow!("Some installations failed").into());
} else {
println!(
"\n✅ Setup completed successfully!\n Run 'bv check' to verify all dependencies."
);
}
Ok(())
}
fn print_syftbox_instructions() {
let arch = match std::env::consts::ARCH {
"aarch64" => "arm64 (Apple Silicon)",
"x86_64" => "x86_64 (Intel)",
other => other,
};
println!(
"Get the latest SyftBox for macOS ({}):\n https://github.com/OpenMined/syftbox/releases/latest",
arch
);
println!("After downloading, ensure the 'syftbox' binary is on your PATH (e.g., move to /usr/local/bin and chmod +x).");
}
#[cfg(target_os = "macos")]
fn install_docker_desktop_from_dmg() -> Result<()> {
use std::env;
eprintln!("🔧 install_docker_desktop_from_dmg() starting");
let arch = env::consts::ARCH;
let download_url = match arch {
"aarch64" => "https://desktop.docker.com/mac/main/arm64/Docker.dmg",
"x86_64" => "https://desktop.docker.com/mac/main/amd64/Docker.dmg",
_ => {
eprintln!("❌ Unsupported architecture: {}", arch);
return Err(anyhow!("Unsupported architecture: {}", arch).into());
}
};
eprintln!("🔧 Downloading Docker Desktop for architecture: {}", arch);
println!(" Downloading Docker Desktop for {}...", arch);
let temp_dir = env::temp_dir();
let dmg_path = temp_dir.join("Docker.dmg");
eprintln!("🔧 Download path: {:?}", dmg_path);
let status = Command::new("curl")
.arg("-L")
.arg("-o")
.arg(&dmg_path)
.arg(download_url)
.arg("--progress-bar")
.status()
.map_err(|e| {
eprintln!("❌ Failed to execute curl: {}", e);
anyhow!("Failed to download Docker Desktop: {}", e)
})?;
if !status.success() {
eprintln!("❌ curl command failed with status: {:?}", status.code());
return Err(anyhow!(
"Failed to download Docker Desktop (curl exit code: {:?})",
status.code()
)
.into());
}
eprintln!("✅ Download completed");
println!(" Mounting Docker.dmg...");
let output = Command::new("hdiutil")
.arg("attach")
.arg(&dmg_path)
.arg("-nobrowse")
.output()
.map_err(|e| {
eprintln!("❌ Failed to execute hdiutil: {}", e);
anyhow!("Failed to mount Docker.dmg: {}", e)
})?;
if !output.status.success() {
eprintln!(
"❌ hdiutil failed: {}",
String::from_utf8_lossy(&output.stderr)
);
return Err(anyhow!(
"Failed to mount Docker.dmg: {}",
String::from_utf8_lossy(&output.stderr)
)
.into());
}
let mount_output = String::from_utf8_lossy(&output.stdout);
eprintln!("🔧 hdiutil output: {}", mount_output);
let mount_point = mount_output
.lines()
.filter(|line| line.contains("/Volumes/"))
.next_back()
.and_then(|line| line.split_whitespace().last())
.ok_or_else(|| {
eprintln!("❌ Could not find mount point in hdiutil output");
anyhow!("Could not find mount point")
})?;
eprintln!("✅ Mounted at: {}", mount_point);
println!(" Copying Docker.app to /Applications...");
println!(" (macOS will prompt for your password)");
let docker_app_source = format!("{}/Docker.app", mount_point);
let docker_app_dest = "/Applications/Docker.app";
eprintln!("🔧 Source: {}", docker_app_source);
eprintln!("🔧 Destination: {}", docker_app_dest);
let install_script = format!(
"do shell script \"rm -rf '{}' ; cp -R '{}' '{}'\" with administrator privileges",
docker_app_dest,
docker_app_source.replace("'", "'\\''"),
docker_app_dest
);
eprintln!("🔧 Installing Docker.app with administrator privileges...");
let output = Command::new("osascript")
.arg("-e")
.arg(&install_script)
.output()
.map_err(|e| {
eprintln!("❌ Failed to execute osascript: {}", e);
anyhow!("Failed to install Docker.app: {}", e)
})?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
eprintln!("❌ osascript failed: {}", stderr);
if stderr.contains("User canceled") || stderr.contains("-128") {
return Err(anyhow!("Installation cancelled by user").into());
}
return Err(anyhow!("Failed to copy Docker.app to /Applications: {}", stderr).into());
}
eprintln!("✅ Docker.app copied successfully");
println!(" Cleaning up...");
eprintln!("🔧 Unmounting DMG...");
let _ = Command::new("hdiutil")
.arg("detach")
.arg(mount_point)
.status();
eprintln!("🔧 Removing temporary DMG file...");
let _ = fs::remove_file(&dmg_path);
println!(" Launching Docker Desktop...");
eprintln!("🔧 Launching Docker Desktop...");
let result = Command::new("open").arg(docker_app_dest).spawn();
match result {
Ok(_) => eprintln!("✅ Docker Desktop launched"),
Err(e) => eprintln!("⚠️ Could not launch Docker Desktop: {}", e),
}
eprintln!("✅ install_docker_desktop_from_dmg() completed successfully");
Ok(())
}
#[cfg(not(target_os = "macos"))]
fn install_docker_desktop_from_dmg() -> Result<()> {
Err(anyhow!("Docker Desktop .dmg installation is only supported on macOS").into())
}
fn java_major_version() -> Option<u32> {
let mut cmd = Command::new("java");
super::configure_child_process(&mut cmd);
let out = cmd.arg("-version").output().ok()?;
let text = String::from_utf8_lossy(&out.stderr);
parse_java_version(&text)
}
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(v) = stripped[..dot_pos].parse::<u32>() {
return Some(v);
}
}
} else {
let major_part = version_str.split('.').next().unwrap_or(version_str);
if let Ok(v) = major_part.parse::<u32>() {
return Some(v);
}
}
}
}
}
}
None
}
async fn setup_ubuntu(dependencies: Vec<String>, _force: bool) -> Result<()> {
if skip_install_commands() {
println!("(test mode) Skipping Ubuntu/Debian setup commands");
return Ok(());
}
use super::check::DependencyConfig;
use std::process::Command;
println!("\nSetting up Ubuntu/Debian environment...\n");
let apt_exists = Command::new("sh")
.arg("-c")
.arg("command -v apt-get >/dev/null 2>&1")
.status()
.map(|s| s.success())
.unwrap_or(false);
if !apt_exists {
println!("❌ apt-get not found. This setup targets Ubuntu/Debian-based systems.");
println!("Please ensure you're on an apt-based distribution.");
return Ok(());
}
let deps_yaml = include_str!("../../deps.yaml");
let config: DependencyConfig = serde_yaml::from_str(deps_yaml)?;
let deps_to_install: Vec<_> = if dependencies.is_empty() {
let mut all_deps: Vec<_> = config.dependencies.iter().collect();
if let Some(docker_idx) = all_deps.iter().position(|d| d.name == "docker") {
let docker = all_deps.remove(docker_idx);
all_deps.push(docker);
}
all_deps
} else {
let dep_set: HashSet<_> = dependencies.iter().map(|s| s.as_str()).collect();
config
.dependencies
.iter()
.filter(|d| dep_set.contains(d.name.as_str()))
.collect()
};
let mut success_count = 0;
let mut skip_count = 0;
let mut fail_count = 0;
for dep in deps_to_install {
if let Some(envs) = &dep.environments {
if let Some(env_cfg) = envs.get("ubuntu") {
if env_cfg.skip {
println!(
"⏭️ Skipping {}: {}",
dep.name,
env_cfg
.skip_reason
.as_ref()
.unwrap_or(&"Not needed on Ubuntu".to_string())
);
skip_count += 1;
continue;
}
if let Some(install_commands) = &env_cfg.install_commands {
let mut need_install = true;
if let Some(verify_cmd) = &env_cfg.verify_command {
let verified = Command::new("sh")
.arg("-c")
.arg(verify_cmd)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false);
if verified {
need_install = false;
}
} else if which::which(&dep.name).is_ok() {
need_install = false;
}
if dep.name == "java" {
if let Some(min_v) = dep.min_version {
if let Some(current) = java_major_version() {
if current >= min_v {
println!(" Java version {} already meets minimum requirement of {}", current, min_v);
need_install = false;
}
}
}
}
if !need_install {
println!("✓ {} already meets requirements. Skipping.", dep.name);
skip_count += 1;
println!();
continue;
}
println!("📦 Installing {}...", dep.name);
println!(" {}", dep.description);
let mut all_ok = true;
for cmd in install_commands {
println!(" Running: {}", cmd);
let cmd_to_run = if cmd.starts_with("apt-get") && !cmd.starts_with("sudo") {
format!("sudo {}", cmd)
} else {
cmd.clone()
};
let status = Command::new("sh").arg("-c").arg(&cmd_to_run).status();
match status {
Ok(s) if s.success() => println!(" ✓ Command succeeded"),
Ok(_) | Err(_) => {
println!(" ❌ Command failed");
all_ok = false;
break;
}
}
}
if all_ok {
if let Some(verify_cmd) = &env_cfg.verify_command {
print!(" Verifying installation... ");
let ok = Command::new("sh")
.arg("-c")
.arg(verify_cmd)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false);
if ok {
println!("✓");
success_count += 1;
} else {
println!("❌ Verification failed");
fail_count += 1;
}
} else {
success_count += 1;
}
} else {
fail_count += 1;
}
println!();
}
}
}
}
println!("\nNotes:");
println!("- For Docker on Ubuntu, you may need to add your user to the docker group: 'sudo usermod -aG docker $USER' and re-login.");
println!("- If Docker service is not running: 'sudo systemctl start docker'");
println!("\nSyftBox:");
println!("The installer has been invoked in setup-only mode if needed.");
println!("If you want to set up later: syftbox login; syftbox");
println!("\n==========================");
println!("Setup Summary:");
println!(" ✓ Installed: {}", success_count);
println!(" ⏭️ Skipped: {}", skip_count);
if fail_count > 0 {
println!(" ❌ Failed: {}", fail_count);
println!("\n⚠️ Some installations failed. Please check the errors above.");
return Err(anyhow!("Some installations failed").into());
} else {
println!(
"\n✅ Setup completed successfully!\n Run 'bv check' to verify all dependencies."
);
}
Ok(())
}
async fn setup_arch(dependencies: Vec<String>, _force: bool) -> Result<()> {
if skip_install_commands() {
println!("(test mode) Skipping Arch Linux setup commands");
return Ok(());
}
use super::check::DependencyConfig;
use std::process::Command;
println!("\nSetting up Arch Linux environment...\n");
let pacman_exists = Command::new("sh")
.arg("-c")
.arg("command -v pacman >/dev/null 2>&1")
.status()
.map(|s| s.success())
.unwrap_or(false);
if !pacman_exists {
println!("❌ pacman not found. This setup targets Arch Linux.");
println!("Please ensure you're on Arch/Manjaro with pacman available.");
return Ok(());
}
let deps_yaml = include_str!("../../deps.yaml");
let config: DependencyConfig = serde_yaml::from_str(deps_yaml)?;
let deps_to_install: Vec<_> = if dependencies.is_empty() {
let mut all_deps: Vec<_> = config.dependencies.iter().collect();
if let Some(docker_idx) = all_deps.iter().position(|d| d.name == "docker") {
let docker = all_deps.remove(docker_idx);
all_deps.push(docker);
}
all_deps
} else {
let dep_set: HashSet<_> = dependencies.iter().map(|s| s.as_str()).collect();
config
.dependencies
.iter()
.filter(|d| dep_set.contains(d.name.as_str()))
.collect()
};
let mut success_count = 0;
let mut skip_count = 0;
let mut fail_count = 0;
for dep in deps_to_install {
if let Some(envs) = &dep.environments {
if let Some(env_cfg) = envs.get("arch") {
if env_cfg.skip {
println!(
"⏭️ Skipping {}: {}",
dep.name,
env_cfg
.skip_reason
.as_ref()
.unwrap_or(&"Not needed on Arch".to_string())
);
skip_count += 1;
continue;
}
if let Some(install_commands) = &env_cfg.install_commands {
let mut need_install = true;
if let Some(verify_cmd) = &env_cfg.verify_command {
let verified = Command::new("sh")
.arg("-c")
.arg(verify_cmd)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false);
if verified {
need_install = false;
}
} else if which::which(&dep.name).is_ok() {
need_install = false;
}
if dep.name == "java" {
if let Some(min_v) = dep.min_version {
if let Some(current) = java_major_version() {
if current >= min_v {
println!(" Java version {} already meets minimum requirement of {}", current, min_v);
need_install = false;
}
}
}
}
if !need_install {
println!("✓ {} already meets requirements. Skipping.", dep.name);
skip_count += 1;
println!();
continue;
}
println!("📦 Installing {}...", dep.name);
println!(" {}", dep.description);
let mut all_ok = true;
for cmd in install_commands {
println!(" Running: {}", cmd);
let status = Command::new("sh").arg("-c").arg(cmd).status();
match status {
Ok(s) if s.success() => println!(" ✓ Command succeeded"),
Ok(_) | Err(_) => {
println!(" ❌ Command failed");
all_ok = false;
break;
}
}
}
if all_ok {
if let Some(verify_cmd) = &env_cfg.verify_command {
print!(" Verifying installation... ");
let ok = Command::new("sh")
.arg("-c")
.arg(verify_cmd)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false);
if ok {
println!("✓");
success_count += 1;
} else {
println!("❌ Verification failed");
fail_count += 1;
}
} else {
success_count += 1;
}
} else {
fail_count += 1;
}
println!();
}
}
}
}
println!("\nNotes:");
println!("- For Docker on Arch, you may need to enable and start the daemon: 'sudo systemctl enable --now docker' and add your user to the docker group.");
println!("\nSyftBox:");
println!("The installer has been invoked in setup-only mode if needed.");
println!("If you want to set up later: syftbox login; syftbox");
println!("\n==========================");
println!("Setup Summary:");
println!(" ✓ Installed: {}", success_count);
println!(" ⏭️ Skipped: {}", skip_count);
if fail_count > 0 {
println!(" ❌ Failed: {}", fail_count);
println!("\n⚠️ Some installations failed. Please check the errors above.");
return Err(anyhow!("Some installations failed").into());
} else {
println!(
"\n✅ Setup completed successfully!\n Run 'bv check' to verify all dependencies."
);
}
Ok(())
}
async fn setup_windows(dependencies: Vec<String>, _force: bool) -> Result<()> {
if skip_install_commands() {
println!("(test mode) Skipping Windows setup commands");
return Ok(());
}
println!("\nSetting up Windows environment...\n");
let winget_exists = {
let mut cmd = Command::new("winget");
super::configure_child_process(&mut cmd);
cmd.arg("--version")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
};
let choco_exists = {
let mut cmd = Command::new("choco");
super::configure_child_process(&mut cmd);
cmd.arg("-v")
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
};
if winget_exists {
println!("✓ WinGet found");
} else if choco_exists {
println!("❌ WinGet not found. Using Chocolatey fallback.");
} else {
println!("❌ Neither WinGet nor Chocolatey found.");
println!("Automated installation is unavailable on this system.");
println!("\nTo install WinGet:");
println!("1. Update Windows to the latest version (WinGet comes with modern Windows)");
println!("2. Or install from Microsoft Store: 'App Installer'");
println!("3. Or download from: https://github.com/microsoft/winget-cli/releases");
println!("\nAlternatively, install Chocolatey from https://chocolatey.org/install");
print_windows_manual_instructions();
return Ok(());
}
let deps_yaml = include_str!("../../deps.yaml");
let config: DependencyConfig = serde_yaml::from_str(deps_yaml)?;
let deps_to_install: Vec<_> = if dependencies.is_empty() {
let mut all_deps: Vec<_> = config.dependencies.iter().collect();
if let Some(docker_idx) = all_deps.iter().position(|d| d.name == "docker") {
let docker = all_deps.remove(docker_idx);
all_deps.push(docker);
}
all_deps
} else {
let dep_set: HashSet<_> = dependencies.iter().map(|s| s.as_str()).collect();
config
.dependencies
.iter()
.filter(|d| dep_set.contains(d.name.as_str()))
.collect()
};
let mut success_count = 0;
let mut skip_count = 0;
let mut fail_count = 0;
for dep in deps_to_install {
if let Some(envs) = &dep.environments {
if let Some(env_cfg) = envs.get("windows") {
if env_cfg.skip {
println!(
"⏭️ Skipping {}: {}",
dep.name,
env_cfg
.skip_reason
.as_ref()
.unwrap_or(&"Not needed on Windows".to_string())
);
skip_count += 1;
continue;
}
if let Some(install_commands) = &env_cfg.install_commands {
let mut need_install = true;
if let Some(verify_cmd) = &env_cfg.verify_command {
let verified = {
let mut cmd = Command::new("powershell");
super::configure_child_process(&mut cmd);
cmd.arg("-Command")
.arg(verify_cmd)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
};
if verified {
need_install = false;
}
}
if dep.name == "java" {
if let Some(min_v) = dep.min_version {
if let Some(current) = java_major_version() {
if current >= min_v {
println!(" Java version {} already meets minimum requirement of {}", current, min_v);
need_install = false;
}
}
}
}
if !need_install {
println!("✓ {} already meets requirements. Skipping.", dep.name);
skip_count += 1;
println!();
continue;
}
println!("📦 Installing {}...", dep.name);
println!(" {}", dep.description);
let mut all_ok = true;
for cmd in install_commands {
println!(" Running: {}", cmd);
let status = if cmd.starts_with("winget") {
if winget_exists {
let mut winget = Command::new("winget");
super::configure_child_process(&mut winget);
winget.args(cmd.split_whitespace().skip(1)).status()
} else {
let mut parts = cmd.split_whitespace();
let _ = parts.next(); let _ = parts.next(); let pkg = parts.next().unwrap_or("");
let choco_pkg = map_winget_pkg_to_choco(pkg);
if choco_pkg.is_empty() {
Err(std::io::Error::other("No Chocolatey mapping for package"))
} else {
let mut choco = Command::new("choco");
super::configure_child_process(&mut choco);
choco.arg("install").arg(choco_pkg).arg("-y").status()
}
}
} else {
let mut powershell = Command::new("powershell");
super::configure_child_process(&mut powershell);
powershell.arg("-Command").arg(cmd).status()
};
match status {
Ok(s) if s.success() => println!(" ✓ Command succeeded"),
Ok(_) | Err(_) => {
println!(" ❌ Command failed");
all_ok = false;
break;
}
}
}
if all_ok {
if let Some(verify_cmd) = &env_cfg.verify_command {
print!(" Verifying installation... ");
let ok = {
let mut cmd = Command::new("powershell");
super::configure_child_process(&mut cmd);
cmd.arg("-Command")
.arg(verify_cmd)
.stdout(Stdio::null())
.stderr(Stdio::null())
.status()
.map(|s| s.success())
.unwrap_or(false)
};
if ok {
println!("✓");
success_count += 1;
} else {
println!("❌ Verification failed");
fail_count += 1;
}
} else {
success_count += 1;
}
} else {
fail_count += 1;
}
println!();
}
}
}
}
println!("\nNotes:");
println!(
"- You may need to restart your terminal/PowerShell after installation to update PATH"
);
println!("- For Docker on Windows, Docker Desktop is required and may need manual setup");
print_windows_manual_instructions();
println!("\n==========================");
println!("Setup Summary:");
println!(" ✓ Installed: {}", success_count);
println!(" ⏭️ Skipped: {}", skip_count);
if fail_count > 0 {
println!(" ❌ Failed: {}", fail_count);
println!("\n⚠️ Some installations failed. Please check the errors above.");
return Err(anyhow!("Some installations failed").into());
} else {
println!(
"\n✅ Setup completed successfully!\n Run 'bv check' to verify all dependencies."
);
}
Ok(())
}
fn print_windows_manual_instructions() {
println!("\nManual Installation Options:");
println!(
"Java 17+: Download from https://openjdk.org/ or use 'winget install Microsoft.OpenJDK'"
);
println!(
"Docker: Download Docker Desktop from https://www.docker.com/products/docker-desktop/"
);
println!("Nextflow: Use WSL or Docker - no native Windows support");
println!("SyftBox: Download from https://github.com/OpenMined/syftbox/releases/latest");
println!("UV: Run 'winget install --id=astral-sh.uv -e' or use PowerShell installer from https://docs.astral.sh/uv/");
}
fn map_winget_pkg_to_choco(pkg: &str) -> &'static str {
match pkg.to_ascii_lowercase().as_str() {
"microsoft.openjdk" => "openjdk",
"astral-sh.uv" => "",
_ => "",
}
}
async fn check_and_configure_java_path(is_ci: bool) -> Result<()> {
if which::which("java").is_ok() {
return Ok(());
}
let assume_yes = env::var("BIOVAULT_SETUP_ASSUME_YES")
.map(|value| {
let normalized = value.to_ascii_lowercase();
normalized == "1" || normalized == "true" || normalized == "yes" || normalized == "y"
})
.unwrap_or(false);
let java_brew_path = check_java_in_brew_not_in_path();
if let Some(brew_path) = java_brew_path {
println!("\n⚠️ Java is installed via Homebrew but not in your PATH.");
println!(" Location: {}", brew_path);
let shell = env::var("SHELL").unwrap_or_else(|_| "/bin/zsh".to_string());
let shell_config = if shell.contains("zsh") {
format!(
"{}/.zshrc",
env::var("HOME").unwrap_or_else(|_| "~".to_string())
)
} else if shell.contains("bash") {
format!(
"{}/.bash_profile",
env::var("HOME").unwrap_or_else(|_| "~".to_string())
)
} else {
format!(
"{}/.profile",
env::var("HOME").unwrap_or_else(|_| "~".to_string())
)
};
let export_line = format!("export PATH=\"{}:$PATH\"", brew_path);
if is_ci || assume_yes {
if is_ci {
println!(" CI mode: Automatically configuring PATH...");
} else {
println!(
" Auto-confirming Java PATH configuration (BIOVAULT_SETUP_ASSUME_YES=1)."
);
}
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&shell_config)?;
writeln!(file, "\n# Added by BioVault setup")?;
writeln!(file, "{}", export_line)?;
println!(" ✓ Added to {}", shell_config);
println!(" Note: You'll need to restart your shell or run 'source {}' for changes to take effect.", shell_config);
} else {
println!("\n Would you like to automatically add Java to your PATH? [Y/n]: ");
io::stdout().flush()?;
let mut input = String::new();
io::stdin().read_line(&mut input)?;
let answer = input.trim().to_lowercase();
if answer.is_empty() || answer == "y" || answer == "yes" {
let mut file = std::fs::OpenOptions::new()
.create(true)
.append(true)
.open(&shell_config)?;
writeln!(file, "\n# Added by BioVault setup")?;
writeln!(file, "{}", export_line)?;
println!(" ✓ Added to {}", shell_config);
println!(" Note: You'll need to restart your shell or run 'source {}' for changes to take effect.", shell_config);
} else {
println!(" Skipped PATH configuration.");
println!(" To manually add Java to your PATH, run:");
println!(" echo '{}' >> {}", export_line, shell_config);
println!(" source {}", shell_config);
}
}
}
Ok(())
}
#[cfg(target_os = "macos")]
fn check_java_in_brew_not_in_path() -> Option<String> {
let brew_cmd = find_brew_command();
brew_cmd.as_ref()?;
let brew_cmd = brew_cmd.unwrap();
let output = Command::new(&brew_cmd)
.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_cmd)
.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
}
}
#[cfg(not(target_os = "macos"))]
fn check_java_in_brew_not_in_path() -> Option<String> {
None
}
#[cfg(target_os = "macos")]
fn find_brew_command() -> Option<String> {
if which::which("brew").is_ok() {
return Some("brew".to_string());
}
let common_brew_paths = vec![
"/opt/homebrew/bin/brew", "/usr/local/bin/brew", ];
for path in &common_brew_paths {
if std::path::Path::new(path).exists() {
return Some(path.to_string());
}
}
None
}
#[cfg(not(target_os = "macos"))]
#[allow(dead_code)]
fn find_brew_command() -> Option<String> {
None
}
fn ensure_user_space_docker_path() -> String {
let current_path = env::var("PATH").unwrap_or_default();
if let Ok(home) = env::var("HOME") {
let entry = format!("{}/.docker/bin", home);
if current_path.split(':').any(|segment| segment == entry) {
current_path
} else if current_path.is_empty() {
entry
} else {
format!("{}:{}", entry, current_path)
}
} else {
current_path
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::env;
struct SkipInstallGuard(Option<String>);
impl SkipInstallGuard {
fn new() -> Self {
let previous = env::var("BIOVAULT_SKIP_INSTALLS").ok();
env::set_var("BIOVAULT_SKIP_INSTALLS", "1");
Self(previous)
}
}
impl Drop for SkipInstallGuard {
fn drop(&mut self) {
if let Some(ref value) = self.0 {
env::set_var("BIOVAULT_SKIP_INSTALLS", value);
} else {
env::remove_var("BIOVAULT_SKIP_INSTALLS");
}
}
}
#[test]
#[serial_test::serial]
fn skip_install_commands_env_behavior() {
let original = env::var("BIOVAULT_SKIP_INSTALLS").ok();
env::remove_var("BIOVAULT_SKIP_INSTALLS");
assert!(
!super::skip_install_commands(),
"Expected false when env var not set"
);
env::set_var("BIOVAULT_SKIP_INSTALLS", "1");
assert!(
super::skip_install_commands(),
"Expected true when env var set to '1'"
);
env::set_var("BIOVAULT_SKIP_INSTALLS", "0");
let result = super::skip_install_commands();
let actual_value =
env::var("BIOVAULT_SKIP_INSTALLS").unwrap_or_else(|_| "NOT_SET".to_string());
assert!(
!result,
"Expected false when env var set to '0', but got true. Actual env value: '{}'",
actual_value
);
if let Some(val) = original {
env::set_var("BIOVAULT_SKIP_INSTALLS", val);
} else {
env::remove_var("BIOVAULT_SKIP_INSTALLS");
}
}
#[test]
fn java_parse_various_formats() {
let cases = [
("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\"", Some(21)),
("garbage", None),
];
for (s, want) in cases {
assert_eq!(parse_java_version(s), want);
}
}
#[test]
#[serial_test::serial]
fn google_colab_detection_via_env() {
std::env::remove_var("COLAB_RELEASE_TAG");
for (k, _) in std::env::vars() {
if k.starts_with("COLAB_") {
std::env::remove_var(k);
}
}
assert!(!is_google_colab());
std::env::set_var("COLAB_RELEASE_TAG", "test");
assert!(is_google_colab());
std::env::remove_var("COLAB_RELEASE_TAG");
}
#[test]
#[serial_test::serial]
#[cfg_attr(target_os = "windows", ignore = "Colab detection is Linux-specific")]
fn detect_system_prefers_colab_env() {
let keys: Vec<String> = std::env::vars()
.filter(|(k, _)| k.starts_with("COLAB_"))
.map(|(k, _)| k)
.collect();
for k in &keys {
std::env::remove_var(k);
}
std::env::set_var("COLAB_RELEASE_TAG", "1");
match detect_system() {
SystemType::GoogleColab => {}
other => panic!("expected GoogleColab, got {:?}", other),
}
std::env::remove_var("COLAB_RELEASE_TAG");
}
#[test]
fn print_syftbox_instructions_runs() {
super::print_syftbox_instructions();
}
#[test]
#[serial_test::serial]
fn is_google_colab_detects_prefix_env() {
std::env::remove_var("COLAB_RELEASE_TAG");
std::env::set_var("COLAB_FOO", "1");
assert!(super::is_google_colab());
std::env::remove_var("COLAB_FOO");
}
#[test]
#[serial_test::serial]
#[cfg(target_os = "macos")]
fn detect_system_reports_macos_on_macos() {
std::env::remove_var("COLAB_RELEASE_TAG");
let keys: Vec<String> = std::env::vars()
.filter(|(k, _)| k.starts_with("COLAB_"))
.map(|(k, _)| k)
.collect();
for k in keys {
std::env::remove_var(k);
}
match super::detect_system() {
super::SystemType::MacOs => {}
other => panic!("expected MacOs, got {:?}", other),
}
}
#[tokio::test]
#[serial_test::serial]
#[cfg(target_os = "macos")]
async fn setup_ubuntu_returns_ok_when_apt_missing() {
let _guard = SkipInstallGuard::new();
super::setup_ubuntu(vec![], false).await.unwrap();
}
#[tokio::test]
#[serial_test::serial]
#[cfg(target_os = "macos")]
async fn setup_arch_returns_ok_when_pacman_missing() {
let _guard = SkipInstallGuard::new();
super::setup_arch(vec![], false).await.unwrap();
}
#[test]
fn winget_to_choco_mapping() {
assert_eq!(
super::map_winget_pkg_to_choco("Microsoft.OpenJDK"),
"openjdk"
);
assert_eq!(super::map_winget_pkg_to_choco("Unknown.Package"), "");
}
#[tokio::test]
#[serial_test::serial]
async fn setup_google_colab_runs_without_panic() {
let _guard = SkipInstallGuard::new();
super::setup_google_colab(vec![], false).await.unwrap();
}
#[tokio::test]
#[serial_test::serial]
async fn setup_macos_returns_ok_without_brew() {
let _guard = SkipInstallGuard::new();
let brew_exists = std::process::Command::new("sh")
.arg("-c")
.arg("command -v brew >/dev/null 2>&1")
.status()
.map(|s| s.success())
.unwrap_or(false);
if !brew_exists {
super::setup_macos(vec![], false).await.unwrap();
}
}
#[tokio::test]
#[serial_test::serial]
#[cfg(target_os = "windows")]
#[cfg_attr(
not(feature = "e2e-tests"),
ignore = "runs installer commands; e2e-only"
)]
async fn setup_windows_returns_ok_when_tools_missing() {
let _guard = SkipInstallGuard::new();
super::setup_windows(vec![], false).await.unwrap();
}
#[tokio::test]
#[serial_test::serial]
#[cfg_attr(
target_os = "windows",
ignore = "Colab execution path installs Linux tools"
)]
async fn setup_execute_colab_branch() {
let _guard = SkipInstallGuard::new();
std::env::set_var("COLAB_RELEASE_TAG", "1");
super::execute(vec![], false).await.unwrap();
std::env::remove_var("COLAB_RELEASE_TAG");
}
#[test]
fn print_windows_manual_instructions_runs() {
super::print_windows_manual_instructions();
}
#[test]
fn test_system_type_debug() {
let s = format!("{:?}", SystemType::MacOs);
assert_eq!(s, "MacOs");
let s2 = format!("{:?}", SystemType::GoogleColab);
assert_eq!(s2, "GoogleColab");
}
#[test]
#[serial_test::serial]
fn test_detect_system_on_windows() {
if cfg!(target_os = "windows") {
match detect_system() {
SystemType::Windows => {}
_ => panic!("Expected Windows on windows platform"),
}
}
}
#[test]
#[serial_test::serial]
fn test_skip_install_commands_not_set() {
std::env::remove_var("BIOVAULT_SKIP_INSTALLS");
let _result = skip_install_commands();
}
#[test]
fn test_parse_java_version_edge_cases() {
assert_eq!(parse_java_version(""), None);
assert_eq!(parse_java_version("no version here"), None);
let _ = parse_java_version("version 999");
}
#[test]
fn test_map_winget_pkg_to_choco_all_mappings() {
assert_eq!(map_winget_pkg_to_choco("Microsoft.OpenJDK"), "openjdk");
assert_eq!(map_winget_pkg_to_choco("microsoft.openjdk"), "openjdk");
assert_eq!(map_winget_pkg_to_choco("Git.Git"), "");
assert_eq!(map_winget_pkg_to_choco("RandomPackage"), "");
}
#[test]
fn test_is_google_colab_without_env() {
let original_keys: Vec<(String, Option<String>)> = std::env::vars()
.filter(|(k, _)| k.starts_with("COLAB_"))
.map(|(k, v)| (k.clone(), Some(v.clone())))
.collect();
for (key, _) in &original_keys {
std::env::remove_var(key);
}
std::env::remove_var("COLAB_RELEASE_TAG");
let detected = is_google_colab();
for (key, value) in original_keys {
if let Some(val) = value {
std::env::set_var(key, val);
} else {
std::env::remove_var(key);
}
}
if detected {
return;
}
assert!(!detected);
}
}