use std::path::Path;
use tokio::process::Command;
pub const DEFAULT_PYTHON_EXECUTABLE: &str = "python3";
pub fn resolve_python_executable(configured: &str, project_root: &Path) -> String {
if configured != DEFAULT_PYTHON_EXECUTABLE {
return configured.to_string();
}
let venv_python = if cfg!(windows) {
project_root.join(".venv").join("Scripts").join("python.exe")
} else {
project_root.join(".venv").join("bin").join("python")
};
if venv_python.exists() {
venv_python.display().to_string()
} else {
configured.to_string()
}
}
pub async fn check_mecha10_rl_importable(python_executable: &str) -> Result<(), String> {
let output = Command::new(python_executable)
.args(["-c", "import mecha10_rl.mujoco.cli"])
.output()
.await;
match output {
Ok(output) if output.status.success() => Ok(()),
Ok(output) => {
let stderr = String::from_utf8_lossy(&output.stderr);
let detail = stderr.lines().last().unwrap_or("").trim();
Err(format!(
"'{python_executable} -c \"import mecha10_rl.mujoco.cli\"' failed ({}){}. \
`mecha10_rl` and/or one of its dependencies (mujoco, Pillow, redis, gymnasium) is \
not installed for this interpreter - run `pip install \
'mecha10-rl @ git+ssh://git@github.com/mecha-industries/mecha10-rl.git@main#subdirectory=rl-trainer'` \
(see mecha10-assets' rl/requirements-rl.txt.template for \
the same pattern used in scaffolded user projects), inside this resolved \
interpreter's environment - or create a project venv with `mecha10 setup` (which \
mecha10 then auto-detects and prefers) and re-run `mecha10 dev`.",
output.status,
if detail.is_empty() {
String::new()
} else {
format!(": {detail}")
}
))
}
Err(e) => Err(format!(
"failed to run '{python_executable}': {e}. Is \"{python_executable}\" (the auto-resolved \
interpreter) a valid interpreter on PATH? Install Python 3, or create a project venv \
with `mecha10 setup`, which mecha10 auto-detects and prefers over the bare system \
interpreter."
)),
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
fn unique_temp_dir(label: &str) -> std::path::PathBuf {
let dir = std::env::temp_dir().join(format!(
"cli-sim-python-env-test-{label}-{:?}",
std::thread::current().id()
));
let _ = fs::remove_dir_all(&dir);
fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn leaves_explicit_override_untouched_even_if_venv_exists() {
let root = unique_temp_dir("explicit-override");
let bin_dir = if cfg!(windows) {
root.join(".venv").join("Scripts")
} else {
root.join(".venv").join("bin")
};
fs::create_dir_all(&bin_dir).unwrap();
let venv_python = bin_dir.join(if cfg!(windows) { "python.exe" } else { "python" });
fs::write(&venv_python, b"").unwrap();
let resolved = resolve_python_executable("/usr/bin/python3.11", &root);
assert_eq!(resolved, "/usr/bin/python3.11");
fs::remove_dir_all(&root).unwrap();
}
#[test]
fn falls_back_to_configured_default_when_no_venv_exists() {
let root = unique_temp_dir("no-venv");
let resolved = resolve_python_executable(DEFAULT_PYTHON_EXECUTABLE, &root);
assert_eq!(resolved, DEFAULT_PYTHON_EXECUTABLE);
fs::remove_dir_all(&root).unwrap();
}
#[test]
fn prefers_project_venv_when_default_and_venv_exists() {
let root = unique_temp_dir("has-venv");
let bin_dir = if cfg!(windows) {
root.join(".venv").join("Scripts")
} else {
root.join(".venv").join("bin")
};
fs::create_dir_all(&bin_dir).unwrap();
let venv_python = bin_dir.join(if cfg!(windows) { "python.exe" } else { "python" });
fs::write(&venv_python, b"").unwrap();
let resolved = resolve_python_executable(DEFAULT_PYTHON_EXECUTABLE, &root);
assert_eq!(resolved, venv_python.display().to_string());
fs::remove_dir_all(&root).unwrap();
}
#[tokio::test]
async fn check_mecha10_rl_importable_reports_actionable_error_for_missing_interpreter() {
let err = check_mecha10_rl_importable("definitely-not-a-real-interpreter-xyz")
.await
.expect_err("nonexistent interpreter should fail");
assert!(err.contains("definitely-not-a-real-interpreter-xyz"));
assert!(err.contains("mecha10 setup"));
}
}