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

nordisk/knut

crates/knut-tracing/tests/no_sibling_path_deps.rs

//! Workspace hygiene: NO manifest may point a `path = "..."` outside this repo.
//!
//! THE FAILURE THIS GUARDS AGAINST
//! ------------------------------
//! Cargo reads the manifest of every dependency, and every `[patch]` entry, at
//! *resolution* time — before features are considered, before any target is
//! selected. So a `path` that escapes the repo (`../skade/skade`,
//! `../../../draupnir`, `../../nornir`) makes `cargo metadata` / `cargo build
//! --workspace` abort outright on any checkout that does not happen to have a
//! sibling tree next to it. That is every CI runner (`actions/checkout@v4`
//! clones knut and nothing else) and every `git clone knut && cargo build`.
//!
//! Two traps that make this easy to reintroduce, both of which this test
//! deliberately covers because both were live in the tree at one point:
//!
//!   * `optional = true` does NOT protect you. An optional dependency's
//!     manifest is still read during resolution. The first bare-clone failure
//!     this repo actually hit was `draupnir`, which is optional.
//!   * A `version = "..."` sitting next to the `path` does NOT protect you
//!     either. Cargo only falls back to the registry when the `path` key is
//!     absent; with both present the path wins and must exist.
//!
//! A sibling path is also a correctness hazard beyond buildability: it couples
//! the workspace to whatever uncommitted state the neighbouring checkout is in.
//! `cargo check --workspace --all-targets` went red once because xtask's
//! `../../nornir` had dropped an import its own sibling still referenced.
//!
//! In-repo relative paths (`../graphar`, `vendor/spark-connect-arrow59`) are
//! fine and are what this test permits — vendoring in-tree is exactly the
//! escape hatch .nornir/bifrost-reasoning.md prescribes.
//!
//! This lives in `knut-tracing` because it is a workspace-wide invariant with
//! no natural crate home, and knut-tracing is the lightest member (one
//! dependency), so the guard costs almost nothing to compile and run.

use std::path::{Path, PathBuf};

/// The workspace root: `crates/knut-tracing` → up two.
fn repo_root() -> PathBuf {
    Path::new(env!("CARGO_MANIFEST_DIR"))
        .ancestors()
        .nth(2)
        .expect("crates/knut-tracing sits two levels below the workspace root")
        .to_path_buf()
}

/// Every `Cargo.toml` tracked in the repo, skipping build output and VCS dirs.
///
/// NEVER follows symlinks. This matters concretely, not theoretically: a
/// developer checkout carries `.claude/worktrees/`, a directory of symlinks to
/// *other repositories on the machine* — including one pointing back at knut
/// itself (which makes a following walk recurse forever) and others pointing at
/// repos this walk has no business reading. `entry.file_type()` reports on the
/// link itself, unlike `Path::is_dir()`, which resolves it.
fn manifests(dir: &Path, out: &mut Vec<PathBuf>) {
    let Ok(entries) = std::fs::read_dir(dir) else {
        return;
    };
    for entry in entries.flatten() {
        let Ok(file_type) = entry.file_type() else {
            continue;
        };
        if file_type.is_symlink() {
            continue;
        }
        let path = entry.path();
        let name = entry.file_name();
        let name = name.to_string_lossy();
        if file_type.is_dir() {
            // `target` is build output and `node_modules` is vendored JS; dot
            // directories are tooling state (`.git`, `.claude`), not sources.
            // Everything else, `vendor/` included, is in-repo and must obey the
            // rule.
            if name == "target" || name == "node_modules" || name.starts_with('.') {
                continue;
            }
            manifests(&path, out);
        } else if name == "Cargo.toml" {
            out.push(path);
        }
    }
}

/// Pull the value out of a `path = "..."` on a non-comment line.
///
/// Deliberately textual rather than a real TOML parse: knut-tracing has exactly
/// one dependency by design (see its Cargo.toml) and this guard is not worth
/// adding a `toml` dep for. A `path = "` occurrence in a dependency manifest is
/// unambiguous enough for the invariant being enforced.
fn path_values(line: &str) -> Vec<String> {
    let trimmed = line.trim_start();
    if trimmed.starts_with('#') {
        return Vec::new();
    }
    let mut out = Vec::new();
    let mut rest = trimmed;
    while let Some(idx) = rest.find("path") {
        rest = &rest[idx + "path".len()..];
        let after = rest.trim_start();
        let Some(after) = after.strip_prefix('=') else {
            continue;
        };
        let after = after.trim_start();
        let Some(after) = after.strip_prefix('"') else {
            continue;
        };
        let Some(end) = after.find('"') else {
            continue;
        };
        out.push(after[..end].to_string());
    }
    out
}

#[test]
fn no_manifest_path_escapes_the_repo() {
    // Walk from the *canonical* root, so the `starts_with` containment check
    // below compares like with like. Walking from the raw
    // `CARGO_MANIFEST_DIR`-derived path while comparing against the
    // canonicalized one would make every manifest look like an escape the
    // moment any component of the path is a symlink.
    let root_canon = repo_root()
        .canonicalize()
        .expect("the workspace root must be readable");

    let mut found = Vec::new();
    manifests(&root_canon, &mut found);
    assert!(
        found.len() > 5,
        "expected to walk the whole workspace, only found {found:?}"
    );

    let mut offenders = Vec::new();
    for manifest in &found {
        let text = std::fs::read_to_string(manifest).expect("manifest must be readable");
        let base = manifest.parent().expect("a manifest has a parent dir");
        for line in text.lines() {
            for value in path_values(line) {
                // Resolve textually: the target of an offending path usually
                // does not exist, so `canonicalize` is not available to us.
                let joined = base.join(&value);
                let mut normalized = PathBuf::new();
                for component in joined.components() {
                    match component {
                        std::path::Component::ParentDir => {
                            normalized.pop();
                        }
                        std::path::Component::CurDir => {}
                        other => normalized.push(other.as_os_str()),
                    }
                }
                if !normalized.starts_with(&root_canon) {
                    offenders.push(format!(
                        "{}: path = \"{}\" resolves to {}, outside the repo",
                        manifest
                            .strip_prefix(&root_canon)
                            .unwrap_or(manifest)
                            .display(),
                        value,
                        normalized.display()
                    ));
                }
            }
        }
    }

    assert!(
        offenders.is_empty(),
        "manifest paths escape the repo, so a bare clone and CI cannot resolve \
         the workspace. Use the crates.io version, or vendor the crate under \
         vendor/ as spark-connect-arrow59 is. Offenders:\n  {}",
        offenders.join("\n  ")
    );
}