use super::{BloatDir, EnforcePolicy, PackageManager, dir_size, enforce_two_tier};
use crate::declared::{Gap, RebuildCheck, on_path};
use anyhow::Result;
use std::fs;
use std::path::{Path, PathBuf};
pub struct Cargo;
fn workspace_lockfile(path: &Path) -> Option<PathBuf> {
let mut dir = path;
loop {
let candidate = dir.join("Cargo.lock");
if candidate.exists() {
return Some(candidate);
}
if dir.join(".git").exists() {
return None;
}
dir = dir.parent()?;
}
}
impl PackageManager for Cargo {
fn name(&self) -> &'static str {
"cargo"
}
fn detect(&self, path: &Path) -> bool {
path.join("Cargo.toml").exists()
}
fn bloat_dirs(&self, path: &Path) -> Vec<BloatDir> {
let mut dirs = Vec::new();
let target_path = path.join("target");
if target_path.exists() {
dirs.push(BloatDir {
name: "target".to_string(),
path: target_path.clone(),
size_bytes: dir_size(&target_path),
shared_bytes: 0,
});
}
dirs
}
fn enforce_lockfile(&self, path: &Path, policy: EnforcePolicy) -> Result<()> {
if path.join("target").join("criterion").is_dir() {
crate::output::print_warning(&format!(
"{}: `target/criterion` holds benchmark history that a rebuild does not \
bring back — copy it first if the baselines matter.",
crate::output::clean_path(path)
));
}
let lockfile = workspace_lockfile(path).unwrap_or_else(|| path.join("Cargo.lock"));
enforce_two_tier(
&lockfile,
"cargo",
&["metadata", "--locked", "--format-version", "1"],
&["generate-lockfile"],
path,
policy,
)
}
fn restore(&self, _path: &Path, _timeout: std::time::Duration) -> Result<()> {
println!("Rust target/ will regenerate on next cargo build");
Ok(())
}
fn lockfiles(&self) -> &'static [&'static str] {
&["Cargo.lock"]
}
fn opt_in(&self) -> bool {
true
}
}
pub(crate) struct CargoSubcommands;
impl RebuildCheck for CargoSubcommands {
fn tools(&self) -> &'static [&'static str] {
&["cargo"]
}
fn gap(&self, repo_path: &Path, _tool: &str, args: &[&str]) -> Option<Gap> {
cargo_subcommand_gap(repo_path, args)
}
}
const CARGO_BUILTINS: &[&str] = &[
"add",
"b",
"bench",
"build",
"c",
"check",
"clean",
"clippy",
"config",
"d",
"doc",
"fetch",
"fix",
"fmt",
"generate-lockfile",
"help",
"info",
"init",
"install",
"locate-project",
"login",
"logout",
"metadata",
"miri",
"new",
"owner",
"package",
"pkgid",
"publish",
"r",
"read-manifest",
"remove",
"report",
"run",
"rustc",
"rustdoc",
"search",
"t",
"test",
"tree",
"uninstall",
"unpublish",
"update",
"vendor",
"verify-project",
"version",
"yank",
];
fn cargo_subcommand_gap(repo_path: &Path, args: &[&str]) -> Option<Gap> {
let mut i = 0;
while args.get(i).is_some_and(|arg| arg.starts_with('+')) {
i += 1;
}
let sub = *args.get(i)?;
if sub.starts_with('-') || CARGO_BUILTINS.contains(&sub) {
return None;
}
if on_path(&format!("cargo-{sub}")) {
return None;
}
if cargo_aliases_exist(repo_path) {
return None;
}
Some(Gap {
what: format!("`{sub}` is not a cargo subcommand and no `cargo-{sub}` is on this machine"),
fix: format!("Install whatever provides `cargo {sub}`, or fix the command."),
})
}
fn cargo_aliases_exist(repo_path: &Path) -> bool {
let mut candidates = vec![
repo_path.join(".cargo").join("config.toml"),
repo_path.join(".cargo").join("config"),
];
let home = std::env::var_os("CARGO_HOME")
.map(PathBuf::from)
.or_else(|| dirs::home_dir().map(|dir| dir.join(".cargo")));
if let Some(home) = home {
candidates.push(home.join("config.toml"));
candidates.push(home.join("config"));
}
candidates.iter().any(|path| {
fs::read_to_string(path)
.map(|text| text.lines().any(|l| l.trim_start().starts_with("[alias")))
.unwrap_or(false)
})
}
#[cfg(test)]
mod tests {
use super::*;
use std::fs;
use std::fs::File;
use tempfile::tempdir;
#[test]
fn cargo_is_opt_in() {
assert!(Cargo.opt_in());
}
#[test]
fn test_name() {
let adapter = Cargo;
assert_eq!(adapter.name(), "cargo");
}
#[test]
fn a_default_pass_never_rewrites_a_stale_lockfile() {
if !super::super::binary_available("cargo") {
return;
}
let dir = tempdir().unwrap();
fs::write(
dir.path().join("Cargo.toml"),
"[package]\nname = \"stale\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n\
[dependencies]\nserde = \"1\"\n",
)
.unwrap();
fs::create_dir(dir.path().join("src")).unwrap();
fs::write(dir.path().join("src").join("lib.rs"), "").unwrap();
let stale = "version = 3\n\n[[package]]\nname = \"stale\"\nversion = \"0.1.0\"\n";
fs::write(dir.path().join("Cargo.lock"), stale).unwrap();
let result = Cargo.enforce_lockfile(dir.path(), EnforcePolicy::default());
assert!(
result.is_err(),
"a lockfile that cannot resolve the manifest must not pass verification"
);
assert_eq!(
fs::read_to_string(dir.path().join("Cargo.lock")).unwrap(),
stale,
"the read-only verification rewrote Cargo.lock"
);
}
#[test]
fn test_detect_positive() {
let dir = tempdir().unwrap();
File::create(dir.path().join("Cargo.toml")).unwrap();
let adapter = Cargo;
assert!(adapter.detect(dir.path()));
}
#[test]
fn test_detect_negative() {
let dir = tempdir().unwrap();
let adapter = Cargo;
assert!(!adapter.detect(dir.path()));
}
#[test]
fn test_bloat_dirs_present() {
let dir = tempdir().unwrap();
fs::create_dir(dir.path().join("target")).unwrap();
let adapter = Cargo;
let dirs = adapter.bloat_dirs(dir.path());
assert_eq!(dirs.len(), 1);
assert_eq!(dirs[0].name, "target");
}
#[test]
fn test_bloat_dirs_absent() {
let dir = tempdir().unwrap();
let adapter = Cargo;
let dirs = adapter.bloat_dirs(dir.path());
assert!(dirs.is_empty());
}
}