use crate::context::CliContext;
use anyhow::Result;
use std::process::Command;
#[derive(Debug, Clone)]
pub struct SetupArgs {
pub check_only: bool,
pub install_deps: bool,
}
pub async fn handle_setup(ctx: &mut CliContext, args: &SetupArgs) -> Result<()> {
println!();
println!("🔧 Mecha10 Environment Setup");
println!("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
println!();
let mut all_ok = true;
println!("Checking prerequisites...");
println!();
println!("1. Rust toolchain:");
let rust_ok = check_rust()?;
if rust_ok {
println!(" ✅ Rust installed");
} else {
println!(" ❌ Rust not found");
all_ok = false;
}
println!("2. Cargo:");
let cargo_ok = check_cargo()?;
if cargo_ok {
println!(" ✅ Cargo installed");
} else {
println!(" ❌ Cargo not found");
all_ok = false;
}
println!("3. Docker:");
let docker = ctx.docker();
let docker_installed = docker.check_installation().is_ok();
let docker_daemon = docker.check_daemon().is_ok();
let docker_ok = docker_installed && docker_daemon;
if docker_ok {
println!(" ✅ Docker installed and running");
} else if docker_installed {
println!(" ⚠️ Docker installed but daemon not running");
println!(" Start Docker Desktop or run: sudo systemctl start docker");
all_ok = false;
} else {
println!(" ⚠️ Docker not available");
println!(" Required for control plane services (Redis, PostgreSQL)");
all_ok = false;
}
println!("4. Redis CLI (optional):");
let redis_cli_ok = check_redis_cli()?;
if redis_cli_ok {
println!(" ✅ redis-cli installed");
} else {
println!(" ⬜ redis-cli not found (optional)");
}
println!("5. Python 3:");
let mut python_ok = check_python()?;
if python_ok {
println!(" ✅ Python 3 installed");
} else {
println!(" ⚠️ Python 3 not found (needed for AI model quantization)");
all_ok = false;
if !args.check_only && args.install_deps {
println!(" Attempting to install Python 3...");
let pinned_version = format!("{}.{}", MIN_PYTHON_VERSION.0, MIN_PYTHON_VERSION.1);
match install_python(&pinned_version).await {
Ok(true) => {
println!(" ✅ Python 3 installed");
python_ok = check_python()?;
if python_ok {
all_ok = rust_ok && cargo_ok && docker_ok && python_ok;
}
}
Ok(false) => {}
Err(e) => println!(" ❌ Failed to install Python 3: {}", e),
}
}
}
println!();
if !args.check_only && python_ok {
println!("6. Python Virtual Environment:");
match setup_venv().await {
Ok(true) => println!(" ✅ Virtual environment ready (.venv)"),
Ok(false) => println!(" ✅ Using existing virtual environment (.venv)"),
Err(e) => {
println!(" ⚠️ Failed to create virtual environment: {}", e);
println!(" Create manually with: python3 -m venv .venv");
}
}
println!();
println!("7. Python Dependencies:");
match install_python_deps(ctx).await {
Ok(true) => {
println!(" ✅ Python dependencies installed in venv");
println!();
println!(" 📝 To activate the virtual environment:");
println!(" source .venv/bin/activate");
}
Ok(false) => println!(" ⬜ No requirements.txt found (skipping)"),
Err(e) => {
println!(" ⚠️ Failed to install Python dependencies: {}", e);
println!(" Install manually with:");
println!(" source .venv/bin/activate");
println!(" pip install -r requirements.txt");
}
}
println!();
}
if !args.check_only {
println!("8. AI Models:");
let models_result = check_and_download_models(ctx).await;
match models_result {
Ok(downloaded) => {
if downloaded > 0 {
println!(" ✅ Downloaded {} recommended models", downloaded);
} else {
println!(" ⬜ Not downloaded automatically — this step no longer runs during setup");
println!(
" Run: mecha10 model pull --all (downloads this project's referenced models; \
or `mecha10 model pull <name>` per node)"
);
}
}
Err(e) => {
println!(" ⚠️ Failed to check models: {}", e);
println!(" Download this project's models with: mecha10 model pull --all");
}
}
println!();
}
if all_ok {
println!("✨ Environment setup complete!");
println!();
println!("You're ready to use Mecha10:");
println!(" • Create a project: mecha10 init");
println!(" • Download this project's AI models (not done automatically by setup): mecha10 model pull --all");
println!(" • Start services: mecha10 infrastructure start");
println!(" • Start development: mecha10 dev");
} else {
println!("⚠️ Some prerequisites are missing");
println!();
if !rust_ok || !cargo_ok {
println!("Install Rust:");
println!(" curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh");
println!();
}
if !docker_ok {
println!("Install Docker:");
println!(" https://docs.docker.com/get-docker/");
println!();
}
if !python_ok {
println!("Install Python 3:");
println!(
" macOS: brew install python@{}.{}",
MIN_PYTHON_VERSION.0, MIN_PYTHON_VERSION.1
);
println!(" Or: https://www.python.org/downloads/");
println!();
}
if !python_ok && !args.install_deps {
println!("Run with --install-deps to attempt automatic installation");
}
println!();
if args.check_only {
return Err(anyhow::anyhow!("Prerequisites check failed"));
}
}
println!();
Ok(())
}
fn check_rust() -> Result<bool> {
let output = Command::new("rustc").arg("--version").output();
Ok(output.map(|o| o.status.success()).unwrap_or(false))
}
fn check_cargo() -> Result<bool> {
let output = Command::new("cargo").arg("--version").output();
Ok(output.map(|o| o.status.success()).unwrap_or(false))
}
fn check_redis_cli() -> Result<bool> {
let output = Command::new("redis-cli").arg("--version").output();
Ok(output.map(|o| o.status.success()).unwrap_or(false))
}
const MIN_PYTHON_VERSION: (u32, u32) = (3, 12);
fn parse_python_version(version_output: &str) -> Option<(u32, u32)> {
let version_part = version_output.trim().strip_prefix("Python ")?;
let mut parts = version_part.split('.');
let major = parts.next()?.parse().ok()?;
let minor = parts.next()?.parse().ok()?;
Some((major, minor))
}
fn meets_minimum_version(found: (u32, u32), minimum: (u32, u32)) -> bool {
found >= minimum
}
fn probe_python_version(candidate: &str) -> Option<(u32, u32)> {
let output = Command::new(candidate).arg("--version").output().ok()?;
if !output.status.success() {
return None;
}
parse_python_version(&String::from_utf8_lossy(&output.stdout))
.or_else(|| parse_python_version(&String::from_utf8_lossy(&output.stderr)))
}
fn check_python() -> Result<bool> {
Ok(find_python_executable().is_ok())
}
async fn install_python(pinned_version: &str) -> Result<bool> {
let brew_available = Command::new("brew")
.arg("--version")
.output()
.map(|o| o.status.success())
.unwrap_or(false);
match python_install_strategy(std::env::consts::OS, brew_available) {
PythonInstallStrategy::Homebrew => {
println!(" Installing Python {} via Homebrew...", pinned_version);
let output = tokio::process::Command::new("brew")
.args(["install", &format!("python@{}", pinned_version)])
.output()
.await?;
if output.status.success() {
return Ok(true);
}
let stderr = String::from_utf8_lossy(&output.stderr);
println!(" Homebrew install failed: {}", stderr.trim());
Ok(false)
}
PythonInstallStrategy::ManualLinux => {
println!("Install Python 3:");
println!(" Linux: apt install python{}", pinned_version);
println!(" Or: https://www.python.org/downloads/");
Ok(false)
}
PythonInstallStrategy::ManualOther => {
println!("Install Python 3:");
println!(" macOS: brew install python@{}", pinned_version);
println!(" Or: https://www.python.org/downloads/");
Ok(false)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PythonInstallStrategy {
Homebrew,
ManualLinux,
ManualOther,
}
fn python_install_strategy(os: &str, brew_available: bool) -> PythonInstallStrategy {
match os {
"macos" if brew_available => PythonInstallStrategy::Homebrew,
"linux" => PythonInstallStrategy::ManualLinux,
_ => PythonInstallStrategy::ManualOther,
}
}
async fn setup_venv() -> Result<bool> {
let venv_path = std::path::Path::new(".venv");
if venv_path.exists() {
return Ok(false); }
let python = find_python_executable()?;
println!(" Creating virtual environment...");
let output = tokio::process::Command::new(&python)
.args(["-m", "venv", ".venv"])
.output()
.await?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("venv creation failed: {}", stderr);
}
Ok(true) }
async fn install_python_deps(_ctx: &CliContext) -> Result<bool> {
let requirements_path = std::path::Path::new("requirements.txt");
if !requirements_path.exists() {
return Ok(false);
}
let python = if std::path::Path::new(".venv/bin/python").exists() {
".venv/bin/python"
} else if std::path::Path::new(".venv/Scripts/python.exe").exists() {
".venv/Scripts/python.exe" } else {
&find_python_executable()?
};
println!(" Installing from requirements.txt...");
let output = tokio::process::Command::new(python)
.args(["-m", "pip", "install", "-r", "requirements.txt"])
.env("GIT_TERMINAL_PROMPT", "0")
.output()
.await?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
if stderr.contains("could not read Username")
|| stderr.contains("could not read Password")
|| stderr.contains("Permission denied (publickey)")
{
anyhow::bail!(
"pip install failed: {}\n\nfailed to fetch RL training dependencies via git+ssh - \
ensure your SSH key is added to GitHub (https://github.com/settings/keys) and \
`ssh -T git@github.com` succeeds.",
stderr
);
}
anyhow::bail!("pip install failed: {}", stderr);
}
Ok(true)
}
fn find_python_executable() -> Result<String> {
let versioned = format!("python{}.{}", MIN_PYTHON_VERSION.0, MIN_PYTHON_VERSION.1);
find_python_executable_among(&["python3", "python", &versioned])
}
fn find_python_executable_among(candidates: &[&str]) -> Result<String> {
let mut newest_too_old: Option<((u32, u32), &str)> = None;
for &candidate in candidates {
let Some(version) = probe_python_version(candidate) else {
continue;
};
if meets_minimum_version(version, MIN_PYTHON_VERSION) {
return Ok(candidate.to_string());
}
let should_replace = match newest_too_old {
Some((best, _)) => version > best,
None => true,
};
if should_replace {
newest_too_old = Some((version, candidate));
}
}
if let Some(((major, minor), candidate)) = newest_too_old {
anyhow::bail!(
"Found Python {major}.{minor} at `{candidate}`, but mecha10 requires Python \
{req_major}.{req_minor}+ (see mecha10-rl's rl-task-runner/pyproject.toml's \
`requires-python`). Install a newer Python 3 and make sure it comes first on PATH, \
then re-run `mecha10 setup`.",
req_major = MIN_PYTHON_VERSION.0,
req_minor = MIN_PYTHON_VERSION.1,
);
}
anyhow::bail!("Python 3 not found. Install with: brew install python3 (macOS) or apt install python3 (Linux)")
}
async fn check_and_download_models(_ctx: &CliContext) -> Result<usize> {
Ok(0)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parse_python_version_parses_standard_output() {
assert_eq!(parse_python_version("Python 3.12.1\n"), Some((3, 12)));
assert_eq!(parse_python_version("Python 3.9.2"), Some((3, 9)));
assert_eq!(parse_python_version("Python 4.0.0"), Some((4, 0)));
}
#[test]
fn parse_python_version_rejects_unparseable_output() {
assert_eq!(parse_python_version(""), None);
assert_eq!(parse_python_version("not python at all"), None);
assert_eq!(parse_python_version("Python"), None);
assert_eq!(parse_python_version("Python x.y.z"), None);
}
#[test]
fn meets_minimum_version_compares_major_and_minor() {
assert!(meets_minimum_version((3, 12), (3, 12)));
assert!(meets_minimum_version((3, 13), (3, 12)));
assert!(meets_minimum_version((4, 0), (3, 12)));
assert!(!meets_minimum_version((3, 11), (3, 12)));
assert!(!meets_minimum_version((2, 7), (3, 12)));
}
#[cfg(unix)]
fn fake_python_binary(dir: &std::path::Path, name: &str, version_output: &str) -> String {
use std::fs;
use std::os::unix::fs::PermissionsExt;
let path = dir.join(name);
fs::write(&path, format!("#!/bin/sh\necho \"{version_output}\"\n")).unwrap();
let mut perms = fs::metadata(&path).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&path, perms).unwrap();
path.display().to_string()
}
#[cfg(unix)]
#[test]
fn find_python_executable_among_skips_too_old_interpreter_for_newer_one() {
let dir = std::env::temp_dir().join(format!("mecha10-setup-test-{:?}", std::thread::current().id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let old = fake_python_binary(&dir, "old-python", "Python 3.9.2");
let new = fake_python_binary(&dir, "new-python", "Python 3.12.1");
let found = find_python_executable_among(&[old.as_str(), new.as_str()]).unwrap();
assert_eq!(found, new);
std::fs::remove_dir_all(&dir).unwrap();
}
#[cfg(unix)]
#[test]
fn find_python_executable_among_falls_back_to_versioned_binary_when_generic_names_missing() {
let dir = std::env::temp_dir().join(format!("mecha10-setup-test-kegonly-{:?}", std::thread::current().id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let missing_python3 = dir.join("definitely-missing-python3").display().to_string();
let missing_python = dir.join("definitely-missing-python").display().to_string();
let versioned = fake_python_binary(&dir, "python3.12", "Python 3.12.4");
let found =
find_python_executable_among(&[missing_python3.as_str(), missing_python.as_str(), versioned.as_str()])
.unwrap();
assert_eq!(found, versioned);
std::fs::remove_dir_all(&dir).unwrap();
}
#[cfg(unix)]
#[test]
fn find_python_executable_among_prefers_generic_name_over_versioned_fallback() {
let dir = std::env::temp_dir().join(format!(
"mecha10-setup-test-prefer-generic-{:?}",
std::thread::current().id()
));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let generic = fake_python_binary(&dir, "python3", "Python 3.13.0");
let versioned = fake_python_binary(&dir, "python3.12", "Python 3.12.4");
let found = find_python_executable_among(&[generic.as_str(), versioned.as_str()]).unwrap();
assert_eq!(found, generic);
std::fs::remove_dir_all(&dir).unwrap();
}
#[cfg(unix)]
#[test]
fn find_python_executable_among_fails_fast_with_actionable_message_when_only_too_old() {
let dir = std::env::temp_dir().join(format!("mecha10-setup-test-old-only-{:?}", std::thread::current().id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let old = fake_python_binary(&dir, "old-python", "Python 3.8.10");
let err = find_python_executable_among(&[old.as_str()]).unwrap_err();
let message = err.to_string();
assert!(
message.contains("3.8"),
"message should report the found version: {message}"
);
assert!(
message.contains("3.12"),
"message should report the required version: {message}"
);
assert!(
message.contains(&old),
"message should name the interpreter that was found: {message}"
);
std::fs::remove_dir_all(&dir).unwrap();
}
#[test]
fn find_python_executable_among_fails_when_nothing_found() {
let err = find_python_executable_among(&["definitely-not-a-real-interpreter-xyz"]).unwrap_err();
assert!(err.to_string().contains("Python 3 not found"));
}
#[test]
fn python_install_strategy_uses_homebrew_on_macos_when_available() {
assert_eq!(python_install_strategy("macos", true), PythonInstallStrategy::Homebrew);
}
#[test]
fn python_install_strategy_falls_back_to_manual_on_macos_without_brew() {
assert_eq!(
python_install_strategy("macos", false),
PythonInstallStrategy::ManualOther
);
}
#[test]
fn python_install_strategy_is_manual_apt_flavored_on_linux() {
assert_eq!(
python_install_strategy("linux", true),
PythonInstallStrategy::ManualLinux
);
assert_eq!(
python_install_strategy("linux", false),
PythonInstallStrategy::ManualLinux
);
}
#[test]
fn python_install_strategy_falls_back_to_manual_on_unknown_os() {
assert_eq!(
python_install_strategy("windows", true),
PythonInstallStrategy::ManualOther
);
assert_eq!(
python_install_strategy("freebsd", false),
PythonInstallStrategy::ManualOther
);
}
}