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 sha2::{Digest, Sha256};
use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;

pub struct RustAdapter;

impl EcosystemAdapter for RustAdapter {
    fn name(&self) -> &'static str {
        "rust"
    }

    fn detect_tool(&self) -> Result<String> {
        let output = Command::new("cargo").arg("--version").output().context(
            "Failed to execute `cargo --version`. Ensure `cargo` is installed and in PATH.",
        )?;

        if !output.status.success() {
            bail!(
                "`cargo --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("Cargo.lock");

        if frozen && lockfile_path.exists() {
            return Ok(lockfile_path);
        }

        let mut cmd = Command::new("cargo");
        cmd.arg("generate-lockfile").current_dir(workspace_path);

        if frozen {
            cmd.arg("--offline");
        }

        let output = cmd
            .output()
            .context("Failed to run `cargo generate-lockfile`")?;

        if !output.status.success() {
            let stderr = String::from_utf8_lossy(&output.stderr);
            bail!(
                "`cargo generate-lockfile` failed in {}: {}",
                workspace_path.display(),
                stderr
            );
        }

        if !lockfile_path.exists() {
            bail!(
                "`cargo generate-lockfile` completed but `Cargo.lock` missing at {}",
                lockfile_path.display()
            );
        }

        Ok(lockfile_path)
    }

    fn parse_lockfile(&self, lockfile_path: &Path) -> Result<Vec<GraphNode>> {
        let workspace_dir = lockfile_path.parent().unwrap_or(Path::new("."));

        // Capture workspace feature unification via `cargo metadata --format-version 1`
        let metadata_output = Command::new("cargo")
            .args(["metadata", "--format-version", "1", "--no-deps"])
            .current_dir(workspace_dir)
            .output();

        let metadata_features = if let Ok(ref output) = metadata_output {
            if output.status.success() {
                String::from_utf8_lossy(&output.stdout).to_string()
            } else {
                String::new()
            }
        } else {
            String::new()
        };

        let content = fs::read_to_string(lockfile_path)
            .with_context(|| format!("Failed to read Cargo.lock at {}", lockfile_path.display()))?;

        let parsed: toml::Value = toml::from_str(&content).with_context(|| {
            format!(
                "Failed to parse Cargo.lock TOML 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();

                // Hash includes unified feature set state if available
                let hash_input = format!("rust:{}:{}:{}", name, version, metadata_features.len());
                let content_hash = format!(
                    "sha256:{}",
                    hex::encode(Sha256::digest(hash_input.as_bytes()))
                );

                nodes.push(GraphNode {
                    ecosystem: "rust".to_string(),
                    package_name: name,
                    resolved_version: version,
                    content_hash,
                    platform_markers: None,
                    lock_ref: lockfile_path.to_string_lossy().to_string(),
                });
            }
        }

        Ok(nodes)
    }
}