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"],
description: "Build in release mode",
group: "Build",
},
Command {
name: "check",
args: &["check", "--all-targets"],
description: "Type-check without building",
group: "Build",
},
Command {
name: "test",
args: &["test", "--all-features"],
description: "Run the test suite",
group: "Quality",
},
Command {
name: "clippy",
args: &[
"clippy",
"--all-targets",
"--all-features",
"--",
"-D",
"warnings",
],
description: "Lint with warnings denied",
group: "Quality",
},
Command {
name: "fmt",
args: &["fmt"],
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"]),
(
"Lint",
&[
"clippy",
"--all-targets",
"--all-features",
"--",
"-D",
"warnings",
],
),
(
"Docs",
&["rustdoc", "--all-features", "--", "-D", "warnings"],
),
("Tests", &["test", "--all-features"]),
];
#[cfg(test)]
mod tests {
use super::*;
fn project(toml: &str, root: &Path) -> Project {
read(toml, root)
}
#[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 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 no_manifest_is_simply_absent() {
let dir = tempfile::tempdir().expect("temp dir");
assert!(discover(dir.path()).is_none_or(|(_, root)| root != dir.path()));
}
}