rivox 1.0.0

Universal polyglot build coordination layer for Python, Rust, and Node monorepos
Documentation
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();

                // Calculate deterministic hash for node
                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;