use crate::add_timing::{begin_work, end_work};
use crate::diagnostic_log::debug;
use crate::package_repair_request::RepairRequest;
use crate::package_repair_cargo_concurrent::{ConcurrentWork, ItemLabel, map_concurrent};
use crate::package_repair_cargo_lock::{read_lock, restore_lock};
use crate::package_repair_cargo_workspaces::{MANIFEST, Workspace, distinct_workspaces, manifest_directories, read_workspace};
use crate::package_repair_command::{ToolCommand, ToolEnd, ToolLimits, ToolRun, find_program, run_tool};
use crate::package_repair_report::{Finding, Outcome, Tool, missing_tool};
use crate::package_repair_tree_clone::clone_tree;
use crate::shell_quote::shell_quote;
use std::ffi::OsString;
use std::path::{Path, PathBuf};
use std::time::Instant;
const TAG: &str = "packageRepairCargo";
const PROGRAM: &str = "cargo";
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum BuildStatePlan {
Nothing(String),
CleanOnly,
CloneThenClean {
from: PathBuf,
to: PathBuf,
},
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct StateFacts {
pub source_build: Option<PathBuf>,
pub source_build_exists: bool,
pub destination_build_exists: bool,
pub destination_target_exists: bool,
}
pub fn plan_build_state(destination: &Workspace, destination_root: &Path, facts: &StateFacts) -> BuildStatePlan {
let Some(source_build) = facts.source_build.as_ref() else {
return BuildStatePlan::Nothing(String::from("the source has no such workspace, so no state was carried"));
};
if *source_build == destination.build_directory {
return BuildStatePlan::Nothing(String::from("the source and the worktree share one build directory, which a clean would damage"));
}
if destination.build_directory.starts_with(destination_root) {
return if facts.destination_build_exists || facts.destination_target_exists {
BuildStatePlan::CleanOnly
} else {
BuildStatePlan::Nothing(String::from("the build directory is inside the worktree and was not carried"))
};
}
if facts.destination_build_exists {
return BuildStatePlan::Nothing(String::from("the worktree's build directory already holds state of its own"));
}
if facts.source_build_exists {
return BuildStatePlan::CloneThenClean { from: source_build.clone(), to: destination.build_directory.clone() };
}
return if facts.destination_target_exists { BuildStatePlan::CleanOnly } else { BuildStatePlan::Nothing(String::from("neither side holds build state")) };
}
pub fn clean_arguments(members: &[String]) -> Vec<OsString> {
let mut arguments: Vec<OsString> = ["clean", "--offline"].iter().map(OsString::from).collect();
for member in members {
arguments.push(OsString::from("--package"));
arguments.push(OsString::from(member));
}
return arguments;
}
fn manual_command(root: &Path, members: &[String]) -> String {
let mut words: Vec<String> = Vec::new();
for word in clean_arguments(members) {
words.push(shell_quote(word.as_encoded_bytes()));
}
return format!("cd {} && {PROGRAM} {}", shell_quote(root.as_os_str().as_encoded_bytes()), words.join(" "));
}
enum Done {
Skipped,
Cleaned {
cloned: bool,
left_out: usize,
},
Failed(Finding),
}
fn repair_workspace(cargo: &Path, workspace: &Workspace, destination_root: &Path, request: &RepairRequest<'_>, clone_workers: usize) -> Done {
let manual: String = manual_command(workspace.root.as_path(), workspace.members.as_slice());
let Ok(relative) = workspace.root.strip_prefix(destination_root) else {
debug(TAG, format!("{} lies outside {} and is left alone", workspace.root.display(), destination_root.display()).as_str());
return Done::Skipped;
};
let source_workspace: PathBuf = request.source.join(relative);
let source_build: Option<PathBuf> = if source_workspace.join(MANIFEST).is_file() {
match timed_read_workspace("cargoMetadataSource", cargo, source_workspace.as_path(), request.environment.limits()) {
Ok(found) => Some(found.build_directory),
Err(reason) => {
debug(TAG, format!("the source workspace {} cannot be read: {reason}", source_workspace.display()).as_str());
None
}
}
} else {
None
};
let facts: StateFacts = StateFacts {
source_build_exists: match source_build.as_ref() {
Some(path) => path.exists(),
None => false,
},
source_build,
destination_build_exists: workspace.build_directory.exists(),
destination_target_exists: workspace.target_directory.exists(),
};
let plan: BuildStatePlan = plan_build_state(workspace, destination_root, &facts);
debug(TAG, format!("{}: {plan:?}", workspace.root.display()).as_str());
let (cloned, left_out): (bool, usize) = match &plan {
BuildStatePlan::Nothing(_) => return Done::Skipped,
BuildStatePlan::CleanOnly => (false, 0),
BuildStatePlan::CloneThenClean { from, to } => {
match timed_clone_tree(from.as_path(), to.as_path(), clone_workers) {
Ok(made) => {
debug(TAG, format!("cloned {} entries from {} to {}", made.made, from.display(), to.display()).as_str());
for path in &made.left_out {
debug(TAG, format!("{} could not be read and is not in the clone", from.join(path).display()).as_str());
}
(true, made.left_out.len())
}
Err(reason) => return workspace_failed(manual.as_str(), format!("the build state could not be cloned: {reason}")),
}
}
};
let command: ToolCommand = ToolCommand { arguments: clean_arguments(workspace.members.as_slice()), directory: workspace.root.clone(), set: Vec::new() };
let lock_before: Option<Vec<u8>> = read_lock(workspace.root.as_path());
let run: ToolRun = timed_clean(cargo, &command, request.environment.limits());
restore_lock(workspace.root.as_path(), lock_before.as_deref());
return match run.end {
ToolEnd::Succeeded => Done::Cleaned { cloned, left_out },
ToolEnd::Failed(reason) => workspace_failed(manual.as_str(), reason),
ToolEnd::TimedOut => workspace_failed(manual.as_str(), String::from("it showed no sign of work for the quiet window and was stopped")),
ToolEnd::CeilingReached(ceiling) => workspace_failed(manual.as_str(), crate::package_repair_report::ceiling_reason(crate::package_repair_report::Tool::Cargo, ceiling)),
};
}
fn left_out_words(left_out: usize) -> String {
return match left_out {
0 => String::new(),
1 => String::from("; 1 unreadable entry was left out"),
many => format!("; {many} unreadable entries were left out"),
};
}
fn workspace_failed(manual: &str, reason: String) -> Done {
return Done::Failed(Finding { tool: Tool::Cargo, outcome: Outcome::Failed { reason, run_instead: manual.to_owned() } });
}
fn timed_read_workspace(name: &str, cargo: &Path, directory: &Path, limits: ToolLimits) -> Result<Workspace, String> {
let began: Instant = begin_work();
let read: Result<Workspace, String> = read_workspace(cargo, directory, limits);
end_work(name, began);
return read;
}
fn timed_clone_tree(from: &Path, to: &Path, clone_workers: usize) -> Result<crate::package_repair_tree_clone::Cloned, String> {
let began: Instant = begin_work();
let made: Result<crate::package_repair_tree_clone::Cloned, String> = clone_tree(from, to, clone_workers);
end_work("cargoCloneTree", began);
return made;
}
fn timed_clean(cargo: &Path, command: &ToolCommand, limits: ToolLimits) -> ToolRun {
let began: Instant = begin_work();
let run: ToolRun = run_tool(cargo, command, limits);
end_work("cargoClean", began);
return run;
}
struct ReadingLabel;
impl ItemLabel<PathBuf> for ReadingLabel {
fn label(&self, directory: &PathBuf) -> String {
return format!("reading the Cargo workspace at {}", directory.display());
}
}
struct RepairLabel;
impl ItemLabel<Workspace> for RepairLabel {
fn label(&self, workspace: &Workspace) -> String {
return format!("the repair of the Cargo workspace at {}", workspace.root.display());
}
}
struct ReadWorkspaceJob<'a> {
cargo: &'a Path,
request: &'a RepairRequest<'a>,
}
impl ConcurrentWork<PathBuf, Result<Workspace, String>> for ReadWorkspaceJob<'_> {
fn run(&self, directory: &PathBuf) -> Result<Workspace, String> {
return timed_read_workspace("cargoMetadataWorktree", self.cargo, directory.as_path(), self.request.environment.limits());
}
}
struct RepairWorkspaceJob<'a> {
cargo: &'a Path,
destination_root: &'a Path,
request: &'a RepairRequest<'a>,
clone_workers: usize,
}
impl ConcurrentWork<Workspace, Done> for RepairWorkspaceJob<'_> {
fn run(&self, workspace: &Workspace) -> Done {
return timed_repair_workspace(self, workspace);
}
}
fn timed_repair_workspace(job: &RepairWorkspaceJob<'_>, workspace: &Workspace) -> Done {
let began: Instant = begin_work();
let done: Done = repair_workspace(job.cargo, workspace, job.destination_root, job.request, job.clone_workers);
end_work("cargoRepairWorkspace", began);
return done;
}
pub fn repair_cargo(request: &RepairRequest<'_>) -> Vec<Finding> {
let started: Instant = Instant::now();
let directories: Vec<PathBuf> = match manifest_directories(request) {
Ok(found) => found,
Err(reason) => {
debug(TAG, format!("{}: {reason}", request.destination.display()).as_str());
return Vec::new();
}
};
if directories.is_empty() {
return Vec::new();
}
let Some(cargo) = find_program(PROGRAM, request.environment.path()) else {
return vec![missing_tool(Tool::Cargo, PROGRAM, "cargo clean --offline --package <member> in each Cargo workspace of the worktree")];
};
let workers: usize = request.environment.workers;
let reader: ReadWorkspaceJob<'_> = ReadWorkspaceJob { cargo: cargo.as_path(), request };
let mut read: Vec<Result<Workspace, String>> = Vec::new();
for outcome in map_concurrent(directories.as_slice(), workers, &ReadingLabel, &reader) {
read.push(match outcome {
Ok(inner) => inner,
Err(reason) => Err(reason),
});
}
let mut findings: Vec<Finding> = Vec::new();
let mut found: Vec<Workspace> = Vec::new();
for (directory, outcome) in directories.iter().zip(read) {
match outcome {
Ok(workspace) => found.push(workspace),
Err(reason) => findings.push(Finding { tool: Tool::Cargo, outcome: Outcome::Failed { reason, run_instead: manual_command(directory.as_path(), &[String::from("<member>")]) } }),
}
}
let workspaces: Vec<Workspace> = distinct_workspaces(found);
let destination_root: PathBuf = match std::fs::canonicalize(request.destination) {
Ok(real) => real,
Err(_error) => request.destination.to_path_buf(),
};
let clone_workers: usize = if workspaces.len() > 1 { 1 } else { workers };
let repairer: RepairWorkspaceJob<'_> = RepairWorkspaceJob { cargo: cargo.as_path(), destination_root: destination_root.as_path(), request, clone_workers };
let mut done: Vec<Done> = Vec::new();
for (outcome, workspace) in map_concurrent(workspaces.as_slice(), workers, &RepairLabel, &repairer).into_iter().zip(workspaces.iter()) {
done.push(match outcome {
Ok(finished) => finished,
Err(reason) => Done::Failed(Finding { tool: Tool::Cargo, outcome: Outcome::Failed { reason, run_instead: manual_command(workspace.root.as_path(), &[String::from("<member>")]) } }),
});
}
let mut cleaned: usize = 0;
let mut cloned: usize = 0;
let mut left_out: usize = 0;
for outcome in done {
match outcome {
Done::Skipped => {}
Done::Cleaned { cloned: was_cloned, left_out: unreadable } => {
cleaned += 1;
cloned += usize::from(was_cloned);
left_out += unreadable;
}
Done::Failed(finding) => findings.push(finding),
}
}
if cleaned > 0 {
let workspace_noun: &str = if cleaned == 1 { "workspace" } else { "workspaces" };
let directory_noun: &str = if cloned == 1 { "build directory" } else { "build directories" };
findings.push(Finding { tool: Tool::Cargo, outcome: Outcome::Repaired { detail: format!("Cargo kept dependency builds and cleaned the worktree's own crates in {cleaned} {workspace_noun} ({cloned} {directory_noun} cloned{})", left_out_words(left_out)), millis: started.elapsed().as_millis() } });
}
return findings;
}
#[cfg(test)]
#[path = "package_repair_cargo_tests.rs"]
mod tests;