use super::EcosystemAdapter;
use crate::graph::GraphNode;
use anyhow::{Context, Result, bail};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
pub struct PythonAdapter;
impl EcosystemAdapter for PythonAdapter {
fn name(&self) -> &'static str {
"python"
}
fn detect_tool(&self) -> Result<String> {
let output = Command::new("uv")
.arg("--version")
.output()
.context("Failed to execute `uv --version`. Ensure `uv` is installed and in PATH.")?;
if !output.status.success() {
bail!("`uv --version` failed with exit status: {}", output.status);
}
let version_str = String::from_utf8_lossy(&output.stdout).trim().to_string();
Ok(version_str)
}
fn resolve(&self, workspace_path: &Path, frozen: bool) -> Result<PathBuf> {
let lockfile_path = workspace_path.join("uv.lock");
if frozen && lockfile_path.exists() {
return Ok(lockfile_path);
}
let mut cmd = Command::new("uv");
cmd.arg("lock").current_dir(workspace_path);
if frozen {
cmd.arg("--frozen");
}
let output = cmd
.output()
.context("Failed to run `uv lock` in workspace")?;
if !output.status.success() {
let stderr = String::from_utf8_lossy(&output.stderr);
bail!(
"`uv lock` failed in {}: {}",
workspace_path.display(),
stderr
);
}
if !lockfile_path.exists() {
bail!(
"`uv lock` completed but `uv.lock` was not produced at {}",
lockfile_path.display()
);
}
Ok(lockfile_path)
}
fn parse_lockfile(&self, lockfile_path: &Path) -> Result<Vec<GraphNode>> {
let content = fs::read_to_string(lockfile_path).with_context(|| {
format!("Failed to read uv lockfile at {}", lockfile_path.display())
})?;
let parsed: toml::Value = toml::from_str(&content).with_context(|| {
format!(
"Failed to parse TOML in uv lockfile at {}",
lockfile_path.display()
)
})?;
let mut nodes = Vec::new();
if let Some(packages) = parsed.get("package").and_then(|p| p.as_array()) {
for pkg in packages {
let name = pkg
.get("name")
.and_then(|n| n.as_str())
.unwrap_or("unknown")
.to_string();
let version = pkg
.get("version")
.and_then(|v| v.as_str())
.unwrap_or("0.0.0")
.to_string();
let content_hash = format!(
"sha256:{}",
hex::encode(sha2::Sha256::digest(
format!("python:{}:{}", name, version).as_bytes()
))
);
nodes.push(GraphNode {
ecosystem: "python".to_string(),
package_name: name,
resolved_version: version,
content_hash,
platform_markers: pkg
.get("marker")
.and_then(|m| m.as_str())
.map(|s| s.to_string()),
lock_ref: lockfile_path.to_string_lossy().to_string(),
});
}
}
Ok(nodes)
}
}
use sha2::Digest;