Skip to main content

cargo_reclaim/
integration.rs

1use std::collections::HashSet;
2use std::path::{Path, PathBuf};
3use std::time::SystemTime;
4
5use crate::active_process::{ActiveObservationProvider, ActiveObservationScope};
6use crate::error::{ReclaimError, ReclaimResult};
7use crate::inventory::snapshot_path;
8use crate::inventory::{InventoryOptions, planner_candidates_from_target_root_with_context};
9use crate::inventory::{
10    append_fingerprint_group_candidates, append_stale_deps_candidates,
11    append_stale_incremental_candidates,
12};
13use crate::model::{ArtifactClass, Plan, PlanInput, PlanSkip, PlanSkipReason};
14use crate::planner::{
15    ActiveObservation, PlannerCandidate, PlannerOptions, TargetContext, WholeTargetMode,
16    build_plan_with_active_observation,
17};
18use crate::policy::PolicyKind;
19use crate::scanner::{
20    ScanItem, ScanSkip, ScanSkipReason, ScannerOptions, TargetCandidateKind, scan_roots,
21};
22
23pub struct BuildPlanFromScanItemsRequest<'a, I> {
24    pub input: PlanInput,
25    pub policy: PolicyKind,
26    pub items: I,
27    pub scanner_options: &'a ScannerOptions,
28    pub inventory_options: &'a InventoryOptions,
29    pub planner_options: &'a PlannerOptions,
30    pub active_observation: &'a ActiveObservation,
31    pub now: SystemTime,
32}
33
34pub struct BuildPlanFromScanItemsWithProviderRequest<'a, I, P> {
35    pub input: PlanInput,
36    pub policy: PolicyKind,
37    pub items: I,
38    pub scanner_options: &'a ScannerOptions,
39    pub inventory_options: &'a InventoryOptions,
40    pub planner_options: &'a PlannerOptions,
41    pub active_observation_provider: &'a P,
42    pub now: SystemTime,
43}
44
45pub fn build_plan_from_roots(
46    roots: impl IntoIterator<Item = impl Into<PathBuf>>,
47    policy: PolicyKind,
48    scanner_options: &ScannerOptions,
49    inventory_options: &InventoryOptions,
50) -> ReclaimResult<Plan> {
51    build_plan_from_roots_with_active_observation(
52        roots,
53        policy,
54        scanner_options,
55        inventory_options,
56        &PlannerOptions::default(),
57        &ActiveObservation::not_attempted(),
58        SystemTime::now(),
59    )
60}
61
62pub fn build_plan_from_roots_with_active_observation(
63    roots: impl IntoIterator<Item = impl Into<PathBuf>>,
64    policy: PolicyKind,
65    scanner_options: &ScannerOptions,
66    inventory_options: &InventoryOptions,
67    planner_options: &PlannerOptions,
68    active_observation: &ActiveObservation,
69    now: SystemTime,
70) -> ReclaimResult<Plan> {
71    let roots = roots.into_iter().map(Into::into).collect::<Vec<_>>();
72    let input = PlanInput::new(roots.clone())?;
73    let items = scan_roots(roots, scanner_options)?;
74
75    build_plan_from_scan_items_with_active_observation(BuildPlanFromScanItemsRequest {
76        input,
77        policy,
78        items,
79        scanner_options,
80        inventory_options,
81        planner_options,
82        active_observation,
83        now,
84    })
85}
86
87pub fn build_plan_from_roots_with_options(
88    roots: impl IntoIterator<Item = impl Into<PathBuf>>,
89    policy: PolicyKind,
90    scanner_options: &ScannerOptions,
91    inventory_options: &InventoryOptions,
92    planner_options: &PlannerOptions,
93    now: SystemTime,
94) -> ReclaimResult<Plan> {
95    build_plan_from_roots_with_active_observation(
96        roots,
97        policy,
98        scanner_options,
99        inventory_options,
100        planner_options,
101        &ActiveObservation::not_attempted(),
102        now,
103    )
104}
105
106pub fn build_plan_from_roots_with_active_observation_provider(
107    roots: impl IntoIterator<Item = impl Into<PathBuf>>,
108    policy: PolicyKind,
109    scanner_options: &ScannerOptions,
110    inventory_options: &InventoryOptions,
111    planner_options: &PlannerOptions,
112    active_observation_provider: &impl ActiveObservationProvider,
113    now: SystemTime,
114) -> ReclaimResult<Plan> {
115    let roots = roots.into_iter().map(Into::into).collect::<Vec<_>>();
116    let input = PlanInput::new(roots.clone())?;
117    let items = scan_roots(roots, scanner_options)?;
118    let active_observation =
119        active_observation_provider.observe(&active_observation_scope_from_scan_items(&items));
120
121    build_plan_from_scan_items_with_active_observation_impl(BuildPlanFromScanItemsActiveRequest {
122        input,
123        policy,
124        items,
125        scanner_options,
126        inventory_options,
127        planner_options,
128        active_observation: &active_observation,
129        now,
130    })
131}
132
133pub fn build_plan_from_scan_items_with_active_observation_provider<I, P>(
134    request: BuildPlanFromScanItemsWithProviderRequest<'_, I, P>,
135) -> ReclaimResult<Plan>
136where
137    I: IntoIterator<Item = ScanItem>,
138    P: ActiveObservationProvider,
139{
140    let BuildPlanFromScanItemsWithProviderRequest {
141        input,
142        policy,
143        items,
144        scanner_options,
145        inventory_options,
146        planner_options,
147        active_observation_provider,
148        now,
149    } = request;
150    let items = items.into_iter().collect::<Vec<_>>();
151    let scope = active_observation_scope_from_scan_items(&items);
152    let active_observation = active_observation_provider.observe(&scope);
153
154    build_plan_from_scan_items_with_active_observation_impl(BuildPlanFromScanItemsActiveRequest {
155        input,
156        policy,
157        items,
158        scanner_options,
159        inventory_options,
160        planner_options,
161        active_observation: &active_observation,
162        now,
163    })
164}
165
166pub fn build_plan_from_scan_items(
167    input: PlanInput,
168    policy: PolicyKind,
169    items: impl IntoIterator<Item = ScanItem>,
170    scanner_options: &ScannerOptions,
171    inventory_options: &InventoryOptions,
172) -> ReclaimResult<Plan> {
173    let planner_options = PlannerOptions::default();
174    let active_observation = ActiveObservation::not_attempted();
175
176    build_plan_from_scan_items_with_active_observation(BuildPlanFromScanItemsRequest {
177        input,
178        policy,
179        items,
180        scanner_options,
181        inventory_options,
182        planner_options: &planner_options,
183        active_observation: &active_observation,
184        now: SystemTime::now(),
185    })
186}
187
188pub fn build_plan_from_scan_items_with_options(
189    input: PlanInput,
190    policy: PolicyKind,
191    items: impl IntoIterator<Item = ScanItem>,
192    scanner_options: &ScannerOptions,
193    inventory_options: &InventoryOptions,
194    planner_options: &PlannerOptions,
195    now: SystemTime,
196) -> ReclaimResult<Plan> {
197    let active_observation = ActiveObservation::not_attempted();
198
199    build_plan_from_scan_items_with_active_observation(BuildPlanFromScanItemsRequest {
200        input,
201        policy,
202        items,
203        scanner_options,
204        inventory_options,
205        planner_options,
206        active_observation: &active_observation,
207        now,
208    })
209}
210
211pub fn active_observation_scope_from_scan_items(items: &[ScanItem]) -> ActiveObservationScope {
212    ActiveObservationScope::from_target_contexts(items.iter().filter_map(|item| {
213        let ScanItem::TargetCandidate(target_candidate) = item else {
214            return None;
215        };
216        target_candidate.target_context.clone()
217    }))
218}
219
220pub fn build_plan_from_scan_items_with_active_observation<I>(
221    request: BuildPlanFromScanItemsRequest<'_, I>,
222) -> ReclaimResult<Plan>
223where
224    I: IntoIterator<Item = ScanItem>,
225{
226    let BuildPlanFromScanItemsRequest {
227        input,
228        policy,
229        items,
230        scanner_options,
231        inventory_options,
232        planner_options,
233        active_observation,
234        now,
235    } = request;
236    build_plan_from_scan_items_with_active_observation_impl(BuildPlanFromScanItemsActiveRequest {
237        input,
238        policy,
239        items: items.into_iter().collect::<Vec<_>>(),
240        scanner_options,
241        inventory_options,
242        planner_options,
243        active_observation,
244        now,
245    })
246}
247
248struct BuildPlanFromScanItemsActiveRequest<'a> {
249    input: PlanInput,
250    policy: PolicyKind,
251    items: Vec<ScanItem>,
252    scanner_options: &'a ScannerOptions,
253    inventory_options: &'a InventoryOptions,
254    planner_options: &'a PlannerOptions,
255    active_observation: &'a ActiveObservation,
256    now: SystemTime,
257}
258
259fn build_plan_from_scan_items_with_active_observation_impl(
260    request: BuildPlanFromScanItemsActiveRequest<'_>,
261) -> ReclaimResult<Plan> {
262    let BuildPlanFromScanItemsActiveRequest {
263        input,
264        policy,
265        items,
266        scanner_options,
267        inventory_options,
268        planner_options,
269        active_observation,
270        now,
271    } = request;
272    let mut seen_target_roots = HashSet::new();
273    let mut candidates = Vec::new();
274    let mut skipped_paths = Vec::new();
275    let scanner_skipped_paths = &scanner_options.skipped_paths;
276
277    for item in items {
278        let target_candidate = match item {
279            ScanItem::TargetCandidate(target_candidate) => target_candidate,
280            ScanItem::Skipped(skip) => {
281                skipped_paths.push(plan_skip_from_scan_skip(skip)?);
282                continue;
283            }
284            ScanItem::CargoProject(_) => continue,
285        };
286
287        if target_candidate.kind != TargetCandidateKind::CargoTargetDir {
288            continue;
289        }
290
291        let Some(evidence) = target_candidate.evidence else {
292            continue;
293        };
294        if evidence.is_weak_name_only() && !scanner_options.allow_name_only_targets {
295            continue;
296        }
297
298        if !seen_target_roots.insert(target_candidate.path.clone()) {
299            continue;
300        }
301
302        let target_context = target_candidate
303            .target_context
304            .unwrap_or_else(|| TargetContext::new(&target_candidate.path));
305        let mut inventory_options = inventory_options.clone();
306        inventory_options
307            .skipped_paths
308            .extend(scanner_skipped_paths.iter().cloned());
309
310        if planner_options.whole_target_mode == WholeTargetMode::Off
311            || has_skipped_descendant(&target_candidate.path, &inventory_options)
312        {
313            let mut target_candidates = match planner_candidates_from_target_root_with_context(
314                &target_candidate.path,
315                evidence.clone(),
316                target_context.clone(),
317                &inventory_options,
318            ) {
319                Ok(candidates) => candidates,
320                Err(error) => {
321                    if push_vanished_inventory_skip(&error, &mut skipped_paths)? {
322                        continue;
323                    }
324                    return Err(error);
325                }
326            };
327            match append_fingerprint_group_candidates(
328                &target_candidate.path,
329                &evidence,
330                &target_context,
331                &inventory_options,
332                &planner_options.keep_rustc_hashes,
333                &mut target_candidates,
334            ) {
335                Ok(()) => {}
336                Err(error) => {
337                    if push_vanished_inventory_skip(&error, &mut skipped_paths)? {
338                        continue;
339                    }
340                    return Err(error);
341                }
342            }
343            match append_stale_deps_candidates(
344                &target_candidate.path,
345                &evidence,
346                &target_context,
347                &inventory_options,
348                &planner_options.keep_rustc_hashes,
349                &mut target_candidates,
350            ) {
351                Ok(()) => {}
352                Err(error) => {
353                    if push_vanished_inventory_skip(&error, &mut skipped_paths)? {
354                        continue;
355                    }
356                    return Err(error);
357                }
358            }
359            match append_stale_incremental_candidates(
360                &target_candidate.path,
361                &evidence,
362                &target_context,
363                &inventory_options,
364                &mut target_candidates,
365            ) {
366                Ok(()) => {}
367                Err(error) => {
368                    if push_vanished_inventory_skip(&error, &mut skipped_paths)? {
369                        continue;
370                    }
371                    return Err(error);
372                }
373            }
374            candidates.extend(target_candidates);
375        } else {
376            let snapshot = match snapshot_path(&target_candidate.path, &inventory_options) {
377                Ok(snapshot) => snapshot,
378                Err(error) => {
379                    if push_vanished_inventory_skip(&error, &mut skipped_paths)? {
380                        continue;
381                    }
382                    return Err(error);
383                }
384            };
385            candidates.push(
386                PlannerCandidate::new(snapshot, ArtifactClass::WholeTarget, evidence)
387                    .with_target_context(target_context),
388            );
389        }
390    }
391
392    let plan = build_plan_with_active_observation(
393        input,
394        policy,
395        candidates,
396        planner_options,
397        active_observation,
398        now,
399    )?;
400    Ok(Plan::with_skipped_paths(
401        plan.input,
402        plan.entries,
403        skipped_paths,
404    ))
405}
406
407fn push_vanished_inventory_skip(
408    error: &ReclaimError,
409    skipped_paths: &mut Vec<PlanSkip>,
410) -> ReclaimResult<bool> {
411    let ReclaimError::MissingInventoryPath { path } = error else {
412        return Ok(false);
413    };
414
415    skipped_paths.push(PlanSkip::new(
416        path.clone(),
417        PlanSkipReason::VanishedDuringInventory,
418        Some("path vanished while building the cleanup plan; this usually means an active build changed target contents".to_string()),
419    )?);
420    Ok(true)
421}
422
423fn plan_skip_from_scan_skip(skip: ScanSkip) -> ReclaimResult<PlanSkip> {
424    let (reason, message) = match skip.reason {
425        ScanSkipReason::DefaultIgnoredDir => (PlanSkipReason::DefaultIgnoredDir, None),
426        ScanSkipReason::ConfiguredIgnoredPath => (PlanSkipReason::ConfiguredIgnoredPath, None),
427        ScanSkipReason::SymlinkNotFollowed => (PlanSkipReason::SymlinkNotFollowed, None),
428        ScanSkipReason::CrossFilesystem => (PlanSkipReason::CrossFilesystem, None),
429        ScanSkipReason::WeakNameOnlySuppressed => (PlanSkipReason::WeakNameOnlySuppressed, None),
430        ScanSkipReason::AlreadyVisited => (PlanSkipReason::AlreadyVisited, None),
431        ScanSkipReason::CargoConfigUnsupported { message } => {
432            (PlanSkipReason::CargoConfigUnsupported, Some(message))
433        }
434        ScanSkipReason::CargoConfigProblem { message } => {
435            (PlanSkipReason::CargoConfigProblem, Some(message))
436        }
437        ScanSkipReason::ReadError { message } => (PlanSkipReason::ReadError, Some(message)),
438    };
439
440    PlanSkip::new(skip.path, reason, message)
441}
442
443fn has_skipped_descendant(target_root: &Path, inventory_options: &InventoryOptions) -> bool {
444    let target_root = lexically_normalize(target_root);
445    inventory_options.skipped_paths.iter().any(|skipped| {
446        let skipped = lexically_normalize(skipped);
447        skipped != target_root && skipped.starts_with(&target_root)
448    })
449}
450
451fn lexically_normalize(path: &Path) -> PathBuf {
452    let mut normalized = PathBuf::new();
453
454    for component in path.components() {
455        match component {
456            std::path::Component::CurDir => {}
457            std::path::Component::ParentDir => {
458                normalized.pop();
459            }
460            other => normalized.push(other.as_os_str()),
461        }
462    }
463
464    normalized
465}