Your browser sucks, JavaScript disabled! Gunnar's console needs JavaScript and WebAssembly. Git over SSH on port 2222 does not.

nordisk/nornir

crates/nornir-introspect/src/pathdeps.rs

//! Sibling **path-dep** resolution for a headless `cargo metadata` — the pure
//! (filesystem + `toml`, no network) half of nornir's security path-dep prep,
//! copied here so `depgraph::extract_with_path_deps` is self-contained.
//!
//! The network half (cloning a missing sibling repo) is NOT here — it needs git
//! and is injected by the caller as a closure, so this crate stays free of any
//! git / warehouse dependency. nornir's `security::clone_missing_path_dep_siblings`
//! is the real clone hook it passes in.

use std::path::Path;

use anyhow::{Context, Result};

/// Materialize sibling **path-dep** checkouts next to `repo` under `scan_root`,
/// so a headless `cargo metadata` (which needs the path-dep sources on disk to
/// resolve) succeeds for repos that depend on sibling crates by relative path
/// (e.g. `znippy → ../zoomies`, `nornir → ../skade`).
///
/// For every `dep = { path = "..." }` in the repo's `Cargo.toml`, resolve the
/// target's crate/repo directory name; if it isn't already reachable at the path
/// the manifest expects but *is* present under `scan_root`, drop a symlink so the
/// relative path resolves. Pure-filesystem, no cargo, no network. Best-effort and
/// idempotent. Returns the number of links created.
pub fn materialize_path_dep_siblings(repo: &Path, scan_root: &Path) -> Result<usize> {
    let manifest = repo.join("Cargo.toml");
    let text = match std::fs::read_to_string(&manifest) {
        Ok(t) => t,
        Err(_) => return Ok(0), // no manifest → nothing to do
    };
    let doc: toml::Value = toml::from_str(&text).context("parse Cargo.toml")?;
    let mut linked = 0usize;
    for table_key in ["dependencies", "dev-dependencies", "build-dependencies"] {
        let Some(deps) = doc.get(table_key).and_then(|d| d.as_table()) else {
            continue;
        };
        for (dep_name, spec) in deps {
            let Some(path) = spec
                .as_table()
                .and_then(|t| t.get("path"))
                .and_then(|p| p.as_str())
            else {
                continue;
            };
            // The location the manifest's relative `path` points at.
            let target = repo.join(path);
            if target.join("Cargo.toml").exists() {
                continue; // already resolvable in place
            }
            // The directory name the dep expects (last segment of its path).
            let leaf = Path::new(path)
                .file_name()
                .map(|s| s.to_string_lossy().into_owned())
                .unwrap_or_else(|| dep_name.clone());
            // Look for a sibling checkout under scan_root by that leaf name, then
            // by the dep's own name (repo dir often == crate name).
            let candidate = [leaf.as_str(), dep_name.as_str()]
                .into_iter()
                .map(|n| scan_root.join(n))
                .find(|c| c.join("Cargo.toml").exists());
            let Some(src) = candidate else { continue };
            if let Some(parent) = target.parent() {
                let _ = std::fs::create_dir_all(parent);
            }
            if symlink_dir(&src, &target).is_ok() {
                linked += 1;
            }
        }
    }
    Ok(linked)
}

/// List the **path-dep siblings of `repo` that are still unresolved** — i.e. the
/// manifest declares `dep = { path = "..." }` but neither the path itself nor a
/// `scan_root` sibling carries a `Cargo.toml`, so a headless `cargo metadata`
/// would fail to load them. Call this *after* materializing to turn the opaque
/// `MetadataCommand::exec` failure into a clear, named diagnostic (DR2).
///
/// Returns `(dep_name, declared_path)` pairs. Empty ⇒ every path-dep resolves.
/// Pure filesystem, never errors on a missing/garbled manifest.
pub fn unresolved_path_dep_siblings(repo: &Path, scan_root: &Path) -> Vec<(String, String)> {
    let Ok(text) = std::fs::read_to_string(repo.join("Cargo.toml")) else {
        return Vec::new();
    };
    let Ok(doc) = toml::from_str::<toml::Value>(&text) else {
        return Vec::new();
    };
    let mut unresolved = Vec::new();
    for table_key in ["dependencies", "dev-dependencies", "build-dependencies"] {
        let Some(deps) = doc.get(table_key).and_then(|d| d.as_table()) else {
            continue;
        };
        for (dep_name, spec) in deps {
            let Some(path) = spec
                .as_table()
                .and_then(|t| t.get("path"))
                .and_then(|p| p.as_str())
            else {
                continue;
            };
            // Resolvable in place (path or, post-prep, via the symlink we dropped)?
            if repo.join(path).join("Cargo.toml").exists() {
                continue;
            }
            // Still resolvable from a scan_root sibling we could have linked?
            let leaf = Path::new(path)
                .file_name()
                .map(|s| s.to_string_lossy().into_owned())
                .unwrap_or_else(|| dep_name.clone());
            let present = [leaf.as_str(), dep_name.as_str()]
                .into_iter()
                .any(|n| scan_root.join(n).join("Cargo.toml").exists());
            if !present {
                unresolved.push((dep_name.clone(), path.to_string()));
            }
        }
    }
    unresolved
}

/// Create a directory symlink `link → src`. No-op if `link` already exists.
#[cfg(unix)]
fn symlink_dir(src: &Path, link: &Path) -> std::io::Result<()> {
    if link.exists() {
        return Ok(());
    }
    std::os::unix::fs::symlink(src, link)
}

#[cfg(not(unix))]
fn symlink_dir(src: &Path, link: &Path) -> std::io::Result<()> {
    if link.exists() {
        return Ok(());
    }
    std::os::windows::fs::symlink_dir(src, link)
}