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}