use std::ffi::OsStr;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct Project {
pub name: Option<String>,
pub runnable: bool,
}
pub fn discover(dir: &Path) -> Option<(Project, PathBuf)> {
let mut nearest: Option<(PathBuf, String)> = None;
let mut workspace: Option<(PathBuf, String)> = None;
for candidate in dir.ancestors() {
let Ok(text) = std::fs::read_to_string(candidate.join("Cargo.toml")) else {
continue;
};
if declares_workspace(&text) {
workspace = Some((candidate.to_path_buf(), text.clone()));
}
nearest.get_or_insert_with(|| (candidate.to_path_buf(), text));
}
let (here, here_text) = nearest?;
let (root, root_text) = workspace.unwrap_or((here.clone(), here_text));
Some((read(&root_text, &here), root))
}
fn declares_workspace(text: &str) -> bool {
text.lines().any(|line| {
let line = line.trim();
line == "[workspace]" || line.starts_with("[workspace.")
})
}
fn read(text: &str, runs_in: &Path) -> Project {
let mut name = None;
let mut in_package = false;
for line in text.lines() {
let line = line.trim();
if line.starts_with('[') {
in_package = line == "[package]";
continue;
}
if in_package && name.is_none() {
if let Some(value) = line.strip_prefix("name") {
name = value
.trim_start()
.strip_prefix('=')
.map(|value| value.trim().trim_matches(['"', '\'']).to_owned())
.filter(|value| !value.is_empty());
}
}
}
let runnable = runs_in.join("src/main.rs").exists() || runs_in.join("src/bin").is_dir();
Project { name, runnable }
}
pub struct Command {
pub name: &'static str,
pub args: &'static [&'static str],
pub description: &'static str,
pub group: &'static str,
}
pub fn commands(project: &Project) -> Vec<&'static Command> {
const ALL: &[Command] = &[
Command {
name: "run",
args: &["run"],
description: "Build and run the binary",
group: "Development",
},
Command {
name: "build",
args: &["build", "--release", "--workspace"],
description: "Build in release mode",
group: "Build",
},
Command {
name: "check",
args: &["check", "--all-targets", "--workspace"],
description: "Type-check without building",
group: "Build",
},
Command {
name: "test",
args: &["test", "--all-features", "--workspace"],
description: "Run the test suite",
group: "Quality",
},
Command {
name: "clippy",
args: &[
"clippy",
"--all-targets",
"--all-features",
"--workspace",
"--",
"-D",
"warnings",
],
description: "Lint with warnings denied",
group: "Quality",
},
Command {
name: "fmt",
args: &["fmt", "--all"],
description: "Format the source",
group: "Quality",
},
Command {
name: "doc",
args: &["doc", "--no-deps", "--open"],
description: "Build and open the documentation",
group: "Quality",
},
];
ALL.iter()
.filter(|command| command.name != "run" || project.runnable)
.collect()
}
pub fn has_subcommand(name: &str) -> bool {
std::env::var_os("PATH").is_some_and(|path| lists(&path, name))
}
fn lists(path: &OsStr, name: &str) -> bool {
let file = format!("cargo-{name}");
std::env::split_paths(path).any(|dir| dir.join(&file).is_file())
}
pub const CHECKS: &[(&str, &[&str])] = &[
("Format", &["fmt", "--check", "--all"]),
(
"Lint",
&[
"clippy",
"--all-targets",
"--all-features",
"--workspace",
"--",
"-D",
"warnings",
],
),
("Docs", &["rustdoc", "--", "-D", "warnings"]),
("Tests", &["test", "--all-features", "--workspace"]),
];
pub const FEATURE_CHECKS: &[(&str, &[&str])] = &[(
"Docs, all features",
&["rustdoc", "--all-features", "--", "-D", "warnings"],
)];
pub fn virtual_members(root: &Path) -> Vec<String> {
let virtual_root = std::fs::read_to_string(root.join("Cargo.toml"))
.is_ok_and(|text| declares_workspace(&text) && read(&text, root).name.is_none());
if !virtual_root {
return Vec::new();
}
metadata(root)
.map(|json| documentable_members(&json))
.unwrap_or_default()
}
pub fn library_packages(root: &Path) -> Vec<String> {
metadata(root)
.map(|json| libraries(&json))
.unwrap_or_default()
}
fn metadata(root: &Path) -> Option<String> {
std::process::Command::new("cargo")
.args(["metadata", "--no-deps", "--format-version", "1"])
.current_dir(root)
.output()
.ok()
.filter(|output| output.status.success())
.map(|output| String::from_utf8_lossy(&output.stdout).into_owned())
}
fn packages_with(json: &str, kinds: &[&str]) -> Vec<String> {
let Ok(metadata) = serde_json::from_str::<serde_json::Value>(json) else {
return Vec::new();
};
let matches = |package: &serde_json::Value| {
package["targets"].as_array().is_some_and(|targets| {
targets.iter().any(|target| {
target["kind"].as_array().is_some_and(|found| {
found
.iter()
.any(|kind| kind.as_str().is_some_and(|kind| kinds.contains(&kind)))
})
})
})
};
metadata["packages"]
.as_array()
.into_iter()
.flatten()
.filter(|package| matches(package))
.filter_map(|package| package["name"].as_str().map(str::to_owned))
.collect()
}
const LIBRARY_KINDS: &[&str] = &["lib", "rlib", "dylib", "cdylib", "staticlib", "proc-macro"];
fn libraries(json: &str) -> Vec<String> {
packages_with(json, LIBRARY_KINDS)
}
fn documentable_members(json: &str) -> Vec<String> {
packages_with(json, &["lib", "rlib", "proc-macro", "bin"])
}
pub fn has_optional_features(root: &Path) -> bool {
std::fs::read_to_string(root.join("Cargo.toml"))
.map(|text| declares_optional_features(&text))
.unwrap_or(false)
}
fn declares_optional_features(text: &str) -> bool {
let mut in_features = false;
for line in text.lines() {
let line = line.trim();
if line.starts_with('[') {
in_features = line == "[features]";
continue;
}
if line.is_empty() || line.starts_with('#') {
continue;
}
if in_features {
let key = line.split('=').next().unwrap_or_default().trim();
if !key.is_empty() && key != "default" {
return true;
}
} else if line.replace(' ', "").contains("optional=true") {
return true;
}
}
false
}
pub const LOCKFILE_CHECK: &[&str] = &["metadata", "--locked", "--format-version", "1"];
pub struct Sibling {
pub name: String,
pub path: PathBuf,
}
pub fn siblings(root: &Path) -> Vec<Sibling> {
let Ok(text) = std::fs::read_to_string(root.join("Cargo.toml")) else {
return Vec::new();
};
if declares_workspace(&text) {
return Vec::new();
}
let mut found = Vec::new();
collect(root, &text, root, 1, &mut found);
found
}
const DEPTH: usize = 3;
fn collect(root: &Path, root_text: &str, dir: &Path, depth: usize, found: &mut Vec<Sibling>) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let path = entry.path();
if !path.is_dir() {
continue;
}
let Some(dir_name) = path.file_name().and_then(OsStr::to_str) else {
continue;
};
if dir_name.starts_with('.') || matches!(dir_name, "target" | "node_modules") {
continue;
}
match std::fs::read_to_string(path.join("Cargo.toml")) {
Ok(text) => {
if wired_to(root, root_text, &path, &text) {
let name = read(&text, &path)
.name
.unwrap_or_else(|| dir_name.to_owned());
found.push(Sibling { name, path });
}
}
Err(_) if depth < DEPTH => collect(root, root_text, &path, depth + 1, found),
Err(_) => {}
}
}
}
fn wired_to(root: &Path, root_text: &str, dir: &Path, text: &str) -> bool {
points_at(dir, text, root) || points_at(root, root_text, dir)
}
fn points_at(from: &Path, text: &str, target: &Path) -> bool {
let Ok(target) = target.canonicalize() else {
return false;
};
text.lines().filter_map(path_value).any(|value| {
from.join(value)
.canonicalize()
.is_ok_and(|path| path == target)
})
}
fn path_value(line: &str) -> Option<&str> {
let rest = line.split_once("path")?.1.trim_start();
let rest = rest.strip_prefix('=')?.trim_start();
let quote = rest.chars().next().filter(|c| *c == '"' || *c == '\'')?;
let rest = &rest[quote.len_utf8()..];
Some(&rest[..rest.find(quote)?])
}
pub const OPTIONAL_CHECKS: &[(&str, &str, &[&str])] = &[("docs.rs", "docs-rs", &["docs-rs"])];
#[cfg(test)]
mod tests {
use super::*;
fn project(toml: &str, root: &Path) -> Project {
read(toml, root)
}
#[test]
fn only_members_with_something_to_document_are_listed() {
let json = r#"{"packages":[
{"name":"core","targets":[{"kind":["lib"]}]},
{"name":"app","targets":[{"kind":["bin"]}]},
{"name":"macros","targets":[{"kind":["proc-macro"]}]},
{"name":"bench","targets":[{"kind":["bench"]}]}]}"#;
assert_eq!(documentable_members(json), ["core", "app", "macros"]);
assert!(documentable_members("not json").is_empty());
}
#[test]
fn packages_with_a_library_are_the_ones_rustdoc_needs_lib_for() {
let json = r#"{"packages":[
{"name":"both","targets":[{"kind":["lib"]},{"kind":["bin"]}]},
{"name":"app","targets":[{"kind":["bin"]}]},
{"name":"wasm","targets":[{"kind":["cdylib","rlib"]}]},
{"name":"macros","targets":[{"kind":["proc-macro"]}]}]}"#;
assert_eq!(libraries(json), ["both", "wasm", "macros"]);
}
#[test]
fn a_virtual_workspace_lists_its_members_and_a_package_does_not() {
let dir = tempfile::tempdir().expect("temp dir");
std::fs::create_dir_all(dir.path().join("crates/a/src")).expect("dirs");
std::fs::write(dir.path().join("crates/a/src/lib.rs"), "").expect("write");
std::fs::write(
dir.path().join("crates/a/Cargo.toml"),
"[package]\nname = \"a\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
)
.expect("write");
std::fs::write(
dir.path().join("Cargo.toml"),
"[workspace]\nmembers = [\"crates/a\"]\nresolver = \"2\"\n",
)
.expect("write");
assert_eq!(virtual_members(dir.path()), ["a"]);
std::fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"r\"\n\n[workspace]\n",
)
.expect("write");
assert!(virtual_members(dir.path()).is_empty());
}
#[test]
fn the_package_name_is_read() {
let dir = tempfile::tempdir().expect("temp dir");
let found = project(
"[package]\nname = \"opi\"\nversion = \"0.1.0\"\n",
dir.path(),
);
assert_eq!(found.name.as_deref(), Some("opi"));
}
#[test]
fn a_name_outside_the_package_section_is_not_the_package_name() {
let dir = tempfile::tempdir().expect("temp dir");
let toml = "[workspace]\nmembers = [\"a\"]\n\n[[bin]]\nname = \"other\"\n";
assert_eq!(project(toml, dir.path()).name, None);
}
#[test]
fn a_workspace_with_a_package_still_has_its_name() {
let dir = tempfile::tempdir().expect("temp dir");
let toml = "[workspace]\nmembers = [\"a\"]\n\n[package]\nname = \"root\"\n";
assert_eq!(project(toml, dir.path()).name.as_deref(), Some("root"));
}
#[test]
fn projectwide_commands_carry_workspace_flags() {
let dir = tempfile::tempdir().expect("temp dir");
std::fs::create_dir(dir.path().join("src")).expect("mkdir");
std::fs::write(dir.path().join("src/main.rs"), "fn main() {}").expect("write");
let found = project("[package]\nname = \"app\"\n", dir.path());
for name in ["build", "check", "test", "clippy"] {
let command = commands(&found)
.into_iter()
.find(|command| command.name == name)
.unwrap_or_else(|| panic!("{name} is offered"));
assert!(
command.args.contains(&"--workspace"),
"{name} is missing --workspace: {:?}",
command.args
);
}
let fmt = commands(&found)
.into_iter()
.find(|command| command.name == "fmt")
.expect("fmt is offered");
assert!(fmt.args.contains(&"--all"), "fmt is missing --all");
for name in ["run", "doc"] {
let command = commands(&found)
.into_iter()
.find(|command| command.name == name)
.unwrap_or_else(|| panic!("{name} is offered"));
assert!(
!command.args.contains(&"--workspace"),
"{name} should not carry --workspace"
);
}
}
#[test]
fn health_checks_carry_workspace_flags() {
for (name, args) in CHECKS {
match *name {
"Format" => assert!(args.contains(&"--all"), "Format is missing --all"),
"Docs" => assert!(
!args.contains(&"--workspace"),
"Docs documents one target, not a workspace"
),
_ => assert!(
args.contains(&"--workspace"),
"{name} is missing --workspace: {args:?}"
),
}
}
}
#[test]
fn the_documentation_checks_cover_both_feature_sets() {
let docs = CHECKS
.iter()
.find(|(name, _)| *name == "Docs")
.expect("Docs is checked");
assert!(
!docs.1.contains(&"--all-features"),
"Docs should document the set `cargo doc` documents"
);
for (name, args) in FEATURE_CHECKS {
assert!(
args.contains(&"--all-features"),
"{name} is the other feature set: {args:?}"
);
assert!(
!args.contains(&"--workspace"),
"{name} documents one target, not a workspace"
);
}
}
fn manifest(root: &Path, relative: &str, toml: &str) {
let dir = root.join(relative);
std::fs::create_dir_all(&dir).expect("mkdir");
std::fs::write(dir.join("Cargo.toml"), toml).expect("write");
}
const PACKAGE: &str = "[package]\nname = \"root\"\nversion = \"0.1.0\"\n";
fn wired(name: &str, up: &str) -> String {
format!("[package]\nname = \"{name}\"\n\n[dependencies]\nroot = {{ path = \"{up}\" }}\n")
}
#[test]
fn a_package_outside_any_workspace_is_found_by_its_name() {
let dir = tempfile::tempdir().expect("temp dir");
std::fs::write(dir.path().join("Cargo.toml"), PACKAGE).expect("write");
manifest(
dir.path(),
"bindings/node",
&wired("runemark-node", "../.."),
);
let found = siblings(dir.path());
assert_eq!(found.len(), 1, "the binding is outside the root package");
assert_eq!(found[0].name, "runemark-node");
assert_eq!(found[0].path, dir.path().join("bindings/node"));
}
#[test]
fn a_crate_the_root_depends_on_by_path_counts_the_other_way_round() {
let dir = tempfile::tempdir().expect("temp dir");
std::fs::write(
dir.path().join("Cargo.toml"),
format!("{PACKAGE}\n[dependencies]\ninner = {{ path = \"crates/inner\" }}\n"),
)
.expect("write");
manifest(dir.path(), "crates/inner", "[package]\nname = \"inner\"\n");
let found = siblings(dir.path());
assert_eq!(found.len(), 1, "the root builds it, but never tests it");
assert_eq!(found[0].name, "inner");
}
#[test]
fn a_fixture_crate_wired_to_nothing_is_not_the_project() {
let dir = tempfile::tempdir().expect("temp dir");
std::fs::write(dir.path().join("Cargo.toml"), PACKAGE).expect("write");
manifest(
dir.path(),
"demo/fixtures/pulse",
"[package]\nname = \"pulse\"\nversion = \"0.4.2\"\n\n[dependencies]\n",
);
assert!(siblings(dir.path()).is_empty());
}
#[test]
fn a_workspace_root_answers_for_what_lies_below_it() {
let dir = tempfile::tempdir().expect("temp dir");
std::fs::write(
dir.path().join("Cargo.toml"),
"[workspace]\nmembers = [\"crates/*\"]\n",
)
.expect("write");
manifest(dir.path(), "crates/one", "[package]\nname = \"one\"\n");
assert!(
siblings(dir.path()).is_empty(),
"--workspace already reaches a member, and an excluded package is excluded on purpose"
);
}
#[test]
fn build_output_and_installed_dependencies_are_not_the_project() {
let dir = tempfile::tempdir().expect("temp dir");
std::fs::write(dir.path().join("Cargo.toml"), PACKAGE).expect("write");
manifest(
dir.path(),
"target/package/dep-1.0",
&wired("dep", "../../.."),
);
manifest(dir.path(), "node_modules/thing", &wired("thing", "../.."));
manifest(dir.path(), ".cargo/vendored", &wired("vendored", "../.."));
assert!(siblings(dir.path()).is_empty());
}
#[test]
fn a_manifest_ends_the_descent_in_its_branch() {
let dir = tempfile::tempdir().expect("temp dir");
std::fs::write(dir.path().join("Cargo.toml"), PACKAGE).expect("write");
manifest(dir.path(), "nested", &wired("nested", ".."));
manifest(dir.path(), "nested/inner", &wired("inner", "../.."));
let found = siblings(dir.path());
assert_eq!(found.len(), 1, "what lies under a package is the package's");
assert_eq!(found[0].name, "nested");
}
#[test]
fn a_package_buried_deeper_than_the_descent_is_left_alone() {
let dir = tempfile::tempdir().expect("temp dir");
std::fs::write(dir.path().join("Cargo.toml"), PACKAGE).expect("write");
manifest(dir.path(), "a/b/c/deep", &wired("deep", "../../../.."));
assert!(siblings(dir.path()).is_empty());
}
#[test]
fn a_path_key_is_read_only_as_an_assignment() {
assert_eq!(path_value("root = { path = \"../..\" }"), Some("../.."));
assert_eq!(path_value("path = '../x'"), Some("../x"));
assert_eq!(path_value("paths = [\"../..\"]"), None);
assert_eq!(path_value("version = \"1.0\""), None);
}
#[test]
fn a_second_feature_set_exists_only_where_a_feature_does() {
assert!(!declares_optional_features(
"[package]\nname = \"a\"\n\n[dependencies]\nserde = \"1\"\n"
));
assert!(!declares_optional_features(
"[package]\nname = \"a\"\n\n[features]\ndefault = []\n"
));
assert!(declares_optional_features(
"[package]\nname = \"a\"\n\n[features]\nselect = [\"dep:libc\"]\n"
));
assert!(declares_optional_features(
"[dependencies]\nindicatif = { version = \"0.18\", optional = true }\n"
));
assert!(!declares_optional_features(
"[features]\n# select = [\"dep:libc\"]\n"
));
}
#[test]
fn a_library_has_nothing_to_run() {
let dir = tempfile::tempdir().expect("temp dir");
std::fs::create_dir(dir.path().join("src")).expect("mkdir");
std::fs::write(dir.path().join("src/lib.rs"), "").expect("write");
let found = project("[package]\nname = \"lib\"\n", dir.path());
assert!(!found.runnable);
assert!(
!commands(&found).iter().any(|command| command.name == "run"),
"offering `cargo run` for a library would fail on use"
);
}
#[test]
fn a_binary_is_runnable() {
let dir = tempfile::tempdir().expect("temp dir");
std::fs::create_dir(dir.path().join("src")).expect("mkdir");
std::fs::write(dir.path().join("src/main.rs"), "fn main() {}").expect("write");
let found = project("[package]\nname = \"app\"\n", dir.path());
assert!(found.runnable);
assert!(commands(&found).iter().any(|command| command.name == "run"));
}
#[test]
fn a_bin_directory_counts_as_runnable() {
let dir = tempfile::tempdir().expect("temp dir");
std::fs::create_dir_all(dir.path().join("src/bin")).expect("mkdir");
assert!(project("[package]\nname = \"x\"\n", dir.path()).runnable);
}
#[test]
fn discovery_walks_up_like_cargo_does() {
let dir = tempfile::tempdir().expect("temp dir");
std::fs::write(dir.path().join("Cargo.toml"), "[package]\nname = \"x\"\n").expect("write");
let deep = dir.path().join("src").join("nested");
std::fs::create_dir_all(&deep).expect("mkdir");
let (found, root) = discover(&deep).expect("discover");
assert_eq!(found.name.as_deref(), Some("x"));
assert_eq!(
root.canonicalize().expect("canonicalize"),
dir.path().canonicalize().expect("canonicalize")
);
}
fn workspace(dir: &Path, root_toml: &str) -> PathBuf {
std::fs::write(dir.join("Cargo.toml"), root_toml).expect("write");
let member = dir.join("crates").join("app");
std::fs::create_dir_all(&member).expect("mkdir");
std::fs::write(member.join("Cargo.toml"), "[package]\nname = \"app\"\n").expect("write");
member
}
#[test]
fn a_crate_in_a_workspace_resolves_to_the_workspace_root() {
let dir = tempfile::tempdir().expect("temp dir");
let member = workspace(dir.path(), "[workspace]\nmembers = [\"crates/*\"]\n");
let (_, root) = discover(&member).expect("discover");
assert_eq!(
root.canonicalize().expect("canonicalize"),
dir.path().canonicalize().expect("canonicalize")
);
}
#[test]
fn a_parent_without_a_workspace_does_not_capture_the_crate() {
let dir = tempfile::tempdir().expect("temp dir");
let member = workspace(dir.path(), "[package]\nname = \"outer\"\n");
let (_, root) = discover(&member).expect("discover");
assert_eq!(
root.canonicalize().expect("canonicalize"),
member.canonicalize().expect("canonicalize")
);
}
#[test]
fn the_outermost_workspace_wins() {
let dir = tempfile::tempdir().expect("temp dir");
let member = workspace(dir.path(), "[workspace]\nmembers = [\"crates/*\"]\n");
std::fs::write(
member.join("Cargo.toml"),
"[workspace]\n\n[package]\nname = \"app\"\n",
)
.expect("write");
let (_, root) = discover(&member).expect("discover");
assert_eq!(
root.canonicalize().expect("canonicalize"),
dir.path().canonicalize().expect("canonicalize")
);
}
#[test]
fn the_workspace_root_names_the_project() {
let dir = tempfile::tempdir().expect("temp dir");
let member = workspace(
dir.path(),
"[workspace]\nmembers = [\"crates/*\"]\n\n[package]\nname = \"repo\"\n",
);
let (found, _) = discover(&member).expect("discover");
assert_eq!(
found.name.as_deref(),
Some("repo"),
"the crate stood in is not what the repository is called"
);
}
#[test]
fn a_virtual_workspace_root_has_no_name_to_give() {
let dir = tempfile::tempdir().expect("temp dir");
let member = workspace(dir.path(), "[workspace]\nmembers = [\"crates/*\"]\n");
let (found, _) = discover(&member).expect("discover");
assert_eq!(found.name, None, "falls back to the directory name instead");
}
#[test]
fn a_binary_crate_in_a_workspace_keeps_its_run_entry() {
let dir = tempfile::tempdir().expect("temp dir");
let member = workspace(dir.path(), "[workspace]\nmembers = [\"crates/*\"]\n");
std::fs::create_dir(member.join("src")).expect("mkdir");
std::fs::write(member.join("src/main.rs"), "fn main() {}").expect("write");
let (found, _) = discover(&member).expect("discover");
assert!(found.runnable);
assert!(commands(&found).iter().any(|command| command.name == "run"));
}
#[test]
fn a_workspace_sub_table_is_still_a_workspace() {
assert!(declares_workspace(
"[workspace.dependencies]\nserde = \"1\"\n"
));
assert!(declares_workspace(
"[package]\nname = \"x\"\n\n[workspace]\n"
));
assert!(!declares_workspace("[package]\nname = \"x\"\n"));
}
#[test]
fn a_cargo_subcommand_is_an_executable_named_for_it() {
let dir = tempfile::tempdir().expect("temp dir");
std::fs::write(dir.path().join("cargo-pretend"), "").expect("write");
let path = dir.path().as_os_str();
assert!(lists(path, "pretend"));
assert!(!lists(path, "audit"), "only what is actually there");
}
#[test]
fn an_empty_path_finds_nothing() {
assert!(!lists(OsStr::new(""), "audit"));
}
#[test]
fn an_optional_check_calls_the_subcommand_it_is_gated_on() {
for (name, subcommand, args) in OPTIONAL_CHECKS {
assert_eq!(
args.first(),
Some(subcommand),
"{name} is gated on cargo-{subcommand}"
);
}
}
#[test]
fn no_manifest_is_simply_absent() {
let dir = tempfile::tempdir().expect("temp dir");
assert!(discover(dir.path()).is_none_or(|(_, root)| root != dir.path()));
}
}