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 ")
);
}