use anyhow::{Context, Result};
use std::fs;
use std::path::Path;
pub fn detect_python_prefix(project_root: &Path) -> String {
if project_root.join("uv.lock").exists() {
return "uv run python3".to_string();
}
if let Some(ref pyproject) = read_pyproject(project_root) {
if pyproject.contains("[tool.uv]") {
return "uv run python3".to_string();
}
}
if project_root.join("poetry.lock").exists() {
return "poetry run python3".to_string();
}
if let Some(ref pyproject) = read_pyproject(project_root) {
if pyproject.contains("[tool.poetry]") {
return "poetry run python3".to_string();
}
}
if project_root.join(".venv").is_dir() {
if cfg!(target_os = "windows") {
return ".venv\\Scripts\\python.exe".to_string();
}
return ".venv/bin/python3".to_string();
}
if project_root.join("Pipfile").exists() || project_root.join("Pipfile.lock").exists() {
return "pipenv run python3".to_string();
}
"python3".to_string()
}
fn read_pyproject(project_root: &Path) -> Option<String> {
fs::read_to_string(project_root.join("pyproject.toml")).ok()
}
fn cpitd_is_installed() -> bool {
std::process::Command::new("cpitd")
.arg("--version")
.output()
.is_ok_and(|o| o.status.success())
}
const CPITD_REPO_URL: &str = "https://github.com/scythia-marrow/cpitd.git";
pub(super) enum CpitdResult {
AlreadyInstalled,
InstalledFromPypi,
InstalledFromSource,
}
pub(super) fn install_cpitd(python_prefix: &str) -> Result<CpitdResult> {
if cpitd_is_installed() {
return Ok(CpitdResult::AlreadyInstalled);
}
let pypi_result = install_cpitd_from_pypi(python_prefix);
if matches!(pypi_result, Ok(true)) {
return Ok(CpitdResult::InstalledFromPypi);
}
match install_cpitd_from_source(python_prefix) {
Ok(true) => Ok(CpitdResult::InstalledFromSource),
Ok(false) => Ok(CpitdResult::AlreadyInstalled),
Err(e) => Err(e),
}
}
fn install_cpitd_from_pypi(python_prefix: &str) -> Result<bool> {
if python_prefix.starts_with("uv ") {
return run_install_command("uv", &["pip", "install", "cpitd"]);
}
if python_prefix.starts_with("poetry ") {
return run_install_command("poetry", &["add", "--group", "dev", "cpitd"]);
}
if python_prefix.starts_with(".venv/") || python_prefix.starts_with(".venv\\") {
let pip = python_prefix
.replace("python3", "pip")
.replace("python.exe", "pip.exe")
.replace("python", "pip");
return run_install_command(&pip, &["install", "cpitd"]);
}
if python_prefix.starts_with("pipenv ") {
return run_install_command("pipenv", &["install", "--dev", "cpitd"]);
}
run_install_command("python3", &["-m", "pip", "install", "cpitd"])
}
fn install_cpitd_from_source(python_prefix: &str) -> Result<bool> {
let tmp_dir = std::env::temp_dir().join("crosslink-cpitd-install");
if tmp_dir.exists() {
let _ = fs::remove_dir_all(&tmp_dir);
}
let clone_output = std::process::Command::new("git")
.args(["clone", "--depth", "1", CPITD_REPO_URL])
.arg(&tmp_dir)
.output()
.context("Failed to run git clone for cpitd")?;
if !clone_output.status.success() {
let stderr = String::from_utf8_lossy(&clone_output.stderr);
let _ = fs::remove_dir_all(&tmp_dir);
anyhow::bail!("git clone failed: {}", stderr.trim());
}
let tmp_dir_str = tmp_dir.to_string_lossy();
let result = if python_prefix.starts_with("uv ") {
run_install_command("uv", &["pip", "install", &tmp_dir_str])
} else if python_prefix.starts_with("poetry ") {
run_install_command("poetry", &["run", "pip", "install", &tmp_dir_str])
} else if python_prefix.starts_with(".venv/") || python_prefix.starts_with(".venv\\") {
let pip = python_prefix
.replace("python3", "pip")
.replace("python.exe", "pip.exe")
.replace("python", "pip");
run_install_command(&pip, &["install", &tmp_dir_str])
} else if python_prefix.starts_with("pipenv ") {
run_install_command("pipenv", &["run", "pip", "install", &tmp_dir_str])
} else {
run_install_command("python3", &["-m", "pip", "install", &tmp_dir_str])
};
let _ = fs::remove_dir_all(&tmp_dir);
result
}
fn run_install_command(program: &str, args: &[&str]) -> Result<bool> {
let output = std::process::Command::new(program)
.args(args)
.output()
.with_context(|| format!("Failed to run {} {}", program, args.join(" ")))?;
if output.status.success() {
Ok(true)
} else {
let stderr = String::from_utf8_lossy(&output.stderr);
anyhow::bail!("cpitd install failed: {}", stderr.trim());
}
}