cargo-reclaim 0.2.1

Safe Cargo cleanup for target directories, stale artifacts, and Cargo home caches
Documentation
use std::time::SystemTime;

use crate::ReclaimResult;
use crate::model::{Plan, PlanAction, PlanEntry, PlanInput};
use crate::policy::PolicyKind;

use super::foundation::plan_candidate_for_policy;
use super::{ActiveObservation, PlannerCandidate, PlannerOptions};

pub fn plan_candidate(candidate: PlannerCandidate, policy: PolicyKind) -> ReclaimResult<PlanEntry> {
    plan_candidate_with_options(
        candidate,
        policy,
        &PlannerOptions::default(),
        SystemTime::now(),
    )
}

pub fn plan_candidate_with_options(
    candidate: PlannerCandidate,
    policy: PolicyKind,
    options: &PlannerOptions,
    now: SystemTime,
) -> ReclaimResult<PlanEntry> {
    plan_candidate_with_active_observation(
        candidate,
        policy,
        options,
        &ActiveObservation::not_attempted(),
        now,
    )
}

pub fn plan_candidate_with_active_observation(
    candidate: PlannerCandidate,
    policy: PolicyKind,
    options: &PlannerOptions,
    active_observation: &ActiveObservation,
    now: SystemTime,
) -> ReclaimResult<PlanEntry> {
    plan_candidate_for_policy(policy, candidate, options, active_observation, now)
}

pub fn build_plan(
    input: PlanInput,
    policy: PolicyKind,
    candidates: impl IntoIterator<Item = PlannerCandidate>,
) -> ReclaimResult<Plan> {
    build_plan_with_options(
        input,
        policy,
        candidates,
        &PlannerOptions::default(),
        SystemTime::now(),
    )
}

pub fn build_plan_with_options(
    input: PlanInput,
    policy: PolicyKind,
    candidates: impl IntoIterator<Item = PlannerCandidate>,
    options: &PlannerOptions,
    now: SystemTime,
) -> ReclaimResult<Plan> {
    build_plan_with_active_observation(
        input,
        policy,
        candidates,
        options,
        &ActiveObservation::not_attempted(),
        now,
    )
}

pub fn build_plan_with_active_observation(
    input: PlanInput,
    policy: PolicyKind,
    candidates: impl IntoIterator<Item = PlannerCandidate>,
    options: &PlannerOptions,
    active_observation: &ActiveObservation,
    now: SystemTime,
) -> ReclaimResult<Plan> {
    let entries = candidates
        .into_iter()
        .map(|candidate| {
            plan_candidate_with_active_observation(
                candidate,
                policy,
                options,
                active_observation,
                now,
            )
        })
        .collect::<ReclaimResult<Vec<_>>>()?;
    let entries = apply_minimum_reclaim_budget(entries, options)?;

    Ok(Plan::new(input, entries))
}

fn apply_minimum_reclaim_budget(
    mut entries: Vec<PlanEntry>,
    options: &PlannerOptions,
) -> ReclaimResult<Vec<PlanEntry>> {
    let Some(minimum_reclaim_bytes) = options.minimum_reclaim_bytes else {
        return Ok(entries);
    };
    if minimum_reclaim_bytes == 0 {
        return Ok(entries);
    }

    let mut selected_delete_indices = entries
        .iter()
        .enumerate()
        .filter(|(_, entry)| entry.action == PlanAction::Delete)
        .map(|(index, entry)| {
            (
                index,
                budget_priority(entry),
                std::cmp::Reverse(entry.snapshot.size_bytes),
                entry.snapshot.path.clone(),
            )
        })
        .collect::<Vec<_>>();
    selected_delete_indices.sort_by(|left, right| {
        left.1
            .cmp(&right.1)
            .then_with(|| left.2.cmp(&right.2))
            .then_with(|| left.3.cmp(&right.3))
    });

    let mut selected_bytes = 0u64;
    let mut keep_deleting = vec![false; entries.len()];
    for (index, _, _, _) in selected_delete_indices {
        if selected_bytes >= minimum_reclaim_bytes {
            break;
        }
        keep_deleting[index] = true;
        selected_bytes = selected_bytes.saturating_add(entries[index].snapshot.size_bytes);
    }

    for (index, entry) in entries.iter_mut().enumerate() {
        if entry.action == PlanAction::Delete && !keep_deleting[index] {
            *entry = PlanEntry::preserved(
                entry.snapshot.clone(),
                entry.artifact_class,
                entry.evidence.clone(),
                "budgeted cleanup goal is already satisfied by higher-priority delete candidates",
            )?;
        }
    }

    Ok(entries)
}

fn budget_priority(entry: &PlanEntry) -> u8 {
    match entry.artifact_class {
        crate::model::ArtifactClass::Tmp => 0,
        crate::model::ArtifactClass::Incremental => 1,
        crate::model::ArtifactClass::FingerprintGroupIntermediate => 2,
        crate::model::ArtifactClass::DepInfo | crate::model::ArtifactClass::ObjectMetadata => 3,
        crate::model::ArtifactClass::WholeTarget => 4,
        _ => 5,
    }
}