cargo_e/
e_collect.rs

1use crate::e_target::{CargoTarget, TargetKind, TargetOrigin};
2use crate::{e_workspace, prelude::*};
3use std::collections::HashMap;
4use std::process::Output;
5/// Helper function that runs a Cargo command with a given manifest path.
6/// If it detects the workspace error, it temporarily patches the manifest (by
7/// appending an empty `[workspace]` table), re-runs the command, and then restores
8/// the original file.
9fn run_cargo_with_opt_out(args: &[&str], manifest_path: &Path) -> Result<Output, Box<dyn Error>> {
10    // Run the initial command.
11    let output = Command::new("cargo")
12        .args(args)
13        .arg("--manifest-path")
14        .arg(manifest_path)
15        .output()?;
16
17    let stderr_str = String::from_utf8_lossy(&output.stderr);
18    let workspace_error_marker = "current package believes it's in a workspace when it's not:";
19
20    // If we detect the workspace error, patch the manifest.
21    if stderr_str.contains(workspace_error_marker) {
22        // Backup the original manifest.
23        let original = fs::read_to_string(manifest_path)?;
24
25        // Only patch if the manifest doesn't already opt out.
26        if !original.contains("[workspace]") {
27            // Append an empty [workspace] table.
28            let patched = format!("{}\n[workspace]\n", original);
29            fs::write(manifest_path, &patched)?;
30
31            // Re-run the command with the patched manifest.
32            let patched_output = Command::new("cargo")
33                .args(args)
34                .arg("--manifest-path")
35                .arg(manifest_path)
36                .output()?;
37
38            // Restore the original manifest.
39            fs::write(manifest_path, original)?;
40
41            return Ok(patched_output);
42        }
43    }
44
45    Ok(output)
46}
47
48/// Runs `cargo run --bin` (without specifying a binary name) so that Cargo prints an error with
49/// a list of available binary targets. Then parses that list to return a vector of Example instances,
50/// using the provided prefix.
51pub fn collect_binaries(
52    prefix: &str,
53    manifest_path: &Path,
54    extended: bool,
55) -> Result<Vec<CargoTarget>, Box<dyn Error>> {
56    // Run the Cargo command using our helper.
57    let output = run_cargo_with_opt_out(&["run", "--bin"], manifest_path)?;
58    let stderr_str = String::from_utf8_lossy(&output.stderr);
59    debug!("DEBUG {} {} ", prefix, manifest_path.display());
60    debug!("DEBUG: stderr (binaries) = {:?}", stderr_str);
61
62    let bin_names = crate::parse_available(&stderr_str, "binaries");
63
64    let binaries = bin_names
65        .into_iter()
66        .map(|name| {
67            let target_kind = if let Some(parent) = manifest_path.parent() {
68                if parent.file_name().and_then(|s| s.to_str()) == Some("src-tauri") {
69                    TargetKind::ManifestTauri
70                } else if extended {
71                    TargetKind::ExtendedBinary
72                } else {
73                    TargetKind::Binary
74                }
75            } else if extended {
76                TargetKind::ExtendedBinary
77            } else {
78                TargetKind::Binary
79            };
80
81            let display_name = if prefix.starts_with('$') {
82                format!("{} > binary > {}", prefix, name)
83            } else if extended {
84                format!("{} {}", prefix, name)
85            } else if prefix.starts_with("builtin") {
86                format!("builtin binary: {}", name)
87            } else {
88                format!("{} {}", prefix, name)
89                // name.clone()
90            };
91            CargoTarget {
92                name: name.clone(),
93                display_name,
94                manifest_path: manifest_path.into(),
95                kind: target_kind,
96                extended,
97                toml_specified: false,
98                origin: Some(TargetOrigin::SubProject(manifest_path.to_path_buf())),
99            }
100        })
101        .collect();
102
103    Ok(binaries)
104}
105
106/// Runs `cargo run --example` so that Cargo lists available examples,
107/// then parses the stderr output to return a vector of Example instances.
108pub fn collect_examples(
109    prefix: &str,
110    manifest_path: &Path,
111    extended: bool,
112) -> Result<Vec<CargoTarget>, Box<dyn Error>> {
113    // Run the Cargo command using our helper.
114    let output = run_cargo_with_opt_out(&["run", "--example"], manifest_path)?;
115    debug!("DEBUG {} {} ", prefix, manifest_path.display());
116    let stderr_str = String::from_utf8_lossy(&output.stderr);
117    debug!("DEBUG: stderr (examples) = {:?}", stderr_str);
118
119    let names = crate::parse_available(&stderr_str, "examples");
120    if names.len() > 0 {
121        debug!("DEBUG: example names = {:?}", names);
122    }
123
124    let examples = names
125        .into_iter()
126        .map(|name| {
127            let target_kind = if extended {
128                TargetKind::ExtendedExample
129            } else {
130                TargetKind::Example
131            };
132
133            let display_name = if prefix.starts_with('$') {
134                format!("{} > example > {}", prefix, name)
135            } else if extended {
136                format!("{} {}", prefix, name)
137            } else if prefix.starts_with("builtin") {
138                format!("builtin example: {}", name)
139            } else {
140                format!("{} {}", prefix, name)
141            };
142            let target = CargoTarget {
143                name: name.clone(),
144                display_name,
145                manifest_path: manifest_path.into(),
146                kind: target_kind,
147                extended,
148                toml_specified: true,
149                origin: Some(TargetOrigin::SubProject(manifest_path.to_path_buf())),
150            };
151            target
152        })
153        .collect();
154
155    Ok(examples)
156}
157
158// --- Concurrent or sequential collection ---
159pub fn collect_samples(
160    _workspace_mode: bool,
161    manifest_infos: Vec<(String, PathBuf, bool)>,
162    __max_concurrency: usize,
163) -> Result<Vec<CargoTarget>, Box<dyn Error>> {
164    let mut all_samples = Vec::new();
165
166    #[cfg(feature = "concurrent")]
167    {
168        use threadpool::ThreadPool;
169        let pool = ThreadPool::new(__max_concurrency);
170        let (tx, rx) = mpsc::channel();
171
172        let start_concurrent = Instant::now();
173        for (_prefix, manifest_path, _extended) in manifest_infos {
174            let tx = tx.clone();
175            let manifest_clone = manifest_path.clone();
176            pool.execute(move || {
177                // Retrieve the runnable targets from the manifest.
178                let (bins, examples, benches, tests) =
179                    crate::e_manifest::get_runnable_targets(&manifest_clone).unwrap_or_default();
180
181                crate::e_manifest::get_runnable_targets(&manifest_clone).unwrap_or_default();
182                // If there are no examples or binaries, return early.
183                if bins.is_empty() && examples.is_empty() {
184                    return;
185                }
186
187                // Combine all targets.
188                let all_targets = bins
189                    .into_iter()
190                    .chain(examples)
191                    .chain(benches)
192                    .chain(tests)
193                    .collect::<Vec<_>>();
194
195                // Now refine each target using the new pure method.
196                // If you implemented it as an associated method `refined()`:
197
198                let refined_targets: Vec<_> = all_targets
199                    .into_iter()
200                    .map(|t| CargoTarget::refined_target(&t))
201                    .collect();
202                tx.send(refined_targets).expect("Failed to send results");
203                //  let mut results = Vec::new();
204                //  results.extend(bins);
205                //  results.extend(examples);
206                //  results.extend(benches);
207                //  results.extend(tests);
208                //  tx.send(results).expect("Failed to send results");
209
210                // let mut results = Vec::new();
211                // if let Ok(mut ex) = collect_examples(&prefix_clone, &manifest_clone, extended) {
212                //     results.append(&mut ex);
213                // }
214                // if let Ok(mut bins) = collect_binaries(&prefix_clone, &manifest_clone, extended) {
215                //     results.append(&mut bins);
216                // }
217                // let etargets =
218                // crate::e_discovery::discover_targets(manifest_clone.parent().unwrap()).unwrap();
219                // for target in etargets {
220                //     let m = target.manifest_path.clone();
221                //     // let manifest_path = Path::new(&m);
222                //     // if target.name.contains("html") {
223                //     //     std::process::exit(0);
224                //     // }
225                //     if let Some(existing) = results.iter_mut().find(|r| r.name == target.name) {
226                //        debug!("REPLACING {}",target.name);
227                //         *existing = target;
228                //     } else {
229                //         debug!("ADDING {}",target.name);
230                //         results.push(target);
231                //     }
232
233                //     // Check if the target is extended and that its name is not already present in results.
234                //     // if target.extended {
235                //     //     let new_prefix = format!("examples/{}", &prefix_clone);
236                //     //     // Collect extended examples.
237                //     //     if let Ok(ex_ext) = collect_examples(&new_prefix, manifest_path, true) {
238                //     //         for ex in ex_ext {
239                //     //             if !results.iter().any(|r| r.name == ex.name) {
240                //     //                 results.push(ex);
241                //     //             }
242                //     //         }
243                //     //     }
244
245                //     //     // Collect extended binaries.
246                //     //     if let Ok(bins_ext) = collect_binaries(&new_prefix, manifest_path, true) {
247                //     //         for bin in bins_ext {
248                //     //             if !results.iter().any(|r| r.name == bin.name) {
249                //     //                 results.push(bin);
250                //     //             }
251                //     //         }
252                //     //     }
253                //     // }
254                // }
255                // tx.send(results).expect("Failed to send results");
256            });
257        }
258        drop(tx);
259        pool.join(); // Wait for all tasks to finish.
260        let duration_concurrent = start_concurrent.elapsed();
261        debug!(
262            "timing: {} threads took {:?}",
263            __max_concurrency, duration_concurrent
264        );
265
266        for samples in rx {
267            all_samples.extend(samples);
268        }
269    }
270
271    // Sequential fallback: process one manifest at a time.
272    #[cfg(not(feature = "concurrent"))]
273    {
274        let start_seq = Instant::now();
275        for (_prefix, manifest_path, _extended) in manifest_infos {
276            let (bins, examples, benches, tests) =
277                crate::e_manifest::get_runnable_targets(&manifest_path).unwrap_or_default();
278
279            // Merge all targets into one collection.
280            all_samples.extend(bins);
281            all_samples.extend(examples);
282            all_samples.extend(benches);
283            all_samples.extend(tests);
284
285            // if let Ok(mut ex) = collect_examples(&prefix, &manifest_path, extended) {
286            //     all_samples.append(&mut ex);
287            // }
288            // if let Ok(mut bins) = collect_binaries(&prefix, &manifest_path, extended) {
289            //     all_samples.append(&mut bins);
290            // }
291            // let manifest_path = Path::new(&manifest_path);
292            // let new_prefix = format!("examples/{}", &prefix);
293            // // Collect extended examples.
294            // if let Ok(ex_ext) = collect_examples(&new_prefix, manifest_path, true) {
295            //     for ex in ex_ext {
296            //         if !all_samples.iter().any(|r| r.name == ex.name) {
297            //             all_samples.push(ex);
298            //         }
299            //     }
300            // }
301
302            // Collect extended binaries.
303            // if let Ok(bins_ext) = collect_binaries(&new_prefix, manifest_path, true) {
304            //     for bin in bins_ext {
305            //         if !all_samples.iter().any(|r| r.name == bin.name) {
306            //             all_samples.push(bin);
307            //         }
308            //     }
309            // }
310        }
311        let duration_seq = start_seq.elapsed();
312        debug!("timing: Sequential processing took {:?}", duration_seq);
313    }
314    // First, refine all collected targets.
315    let initial_targets: Vec<_> = all_samples
316        .into_iter()
317        .map(|t| CargoTarget::refined_target(&t))
318        .collect();
319
320    // Build a HashMap keyed by (manifest, name) to deduplicate targets.
321    let mut targets_map: HashMap<(String, String), CargoTarget> = HashMap::new();
322    for target in initial_targets.into_iter() {
323        let key = CargoTarget::target_key(&target);
324        targets_map.entry(key).or_insert(target);
325    }
326
327    // Expand subprojects in place.
328    CargoTarget::expand_subprojects_in_place(&mut targets_map)?;
329
330    // Finally, collect all unique targets.
331    let refined_targets: Vec<CargoTarget> = targets_map.into_values().collect();
332    // Now do an additional deduplication pass based on origin and name.
333    let deduped_targets = crate::e_target::dedup_targets(refined_targets);
334    return Ok(deduped_targets);
335    //    return Ok(refined_targets);
336
337    // let mut target_map: std::collections::HashMap<String, CargoTarget> =
338    //     std::collections::HashMap::new();
339
340    // // Group targets by name and choose one based on workspace_mode:
341    // for target in all_samples {
342    //     target_map
343    //         .entry(target.name.clone())
344    //         .and_modify(|existing| {
345    //             if workspace_mode {
346    //                 // In workspace mode, extended targets override builtins.
347    //                 if target.extended && !existing.extended {
348    //                     *existing = target.clone();
349    //                 }
350    //             } else {
351    //                 // In normal mode, builtin targets (non-extended) override extended.
352    //                 if !target.extended && existing.extended {
353    //                     *existing = target.clone();
354    //                 }
355    //             }
356    //         })
357    //         .or_insert(target.clone());
358    // }
359
360    // let mut combined = Vec::new();
361    // for target in target_map.into_values() {
362    //     combined.push(target);
363    // }
364
365    // Ok(combined)
366    // Ok(all_samples)
367}
368
369use log::warn;
370use std::fs;
371use std::path::Path;
372
373/// Returns the "depth" of a path (number of components)
374pub fn path_depth(path: &Path) -> usize {
375    path.components().count()
376}
377
378/// Deduplicates targets by their canonicalized origin.
379/// In particular, if two targets share the same origin key and one is a SingleFile
380/// and the other is a DefaultBinary, only the SingleFile target is kept.
381pub fn dedup_single_file_over_default_binary(targets: Vec<CargoTarget>) -> Vec<CargoTarget> {
382    let mut map: HashMap<Option<String>, CargoTarget> = HashMap::new();
383
384    for target in targets {
385        // Try to get a canonicalized string for the target's origin.
386        let origin_key = target.origin.as_ref().and_then(|origin| match origin {
387            TargetOrigin::SingleFile(path)
388            | TargetOrigin::DefaultBinary(path)
389            | TargetOrigin::SubProject(path) => fs::canonicalize(path)
390                .ok()
391                .map(|p| p.to_string_lossy().into_owned()),
392            _ => None,
393        });
394
395        if let Some(key) = origin_key.clone() {
396            if let Some(existing) = map.get(&Some(key.clone())) {
397                // If one target is SingleFile and the other is DefaultBinary, keep the SingleFile.
398                match (&target.origin, &existing.origin) {
399                    (Some(TargetOrigin::SingleFile(_)), Some(TargetOrigin::DefaultBinary(_))) => {
400                        map.insert(Some(key), target);
401                    }
402                    (Some(TargetOrigin::DefaultBinary(_)), Some(TargetOrigin::SingleFile(_))) => {
403                        // Do nothing: keep the existing SingleFile.
404                    }
405                    _ => {
406                        // Otherwise, choose the target with a deeper manifest path.
407                        let current_depth = path_depth(&target.manifest_path);
408                        let existing_depth = path_depth(&existing.manifest_path);
409                        if current_depth > existing_depth {
410                            map.insert(Some(key), target);
411                        }
412                    }
413                }
414            } else {
415                map.insert(Some(key), target);
416            }
417        } else {
418            // For targets with no origin key, use None.
419            if let Some(existing) = map.get(&None) {
420                // Optionally, compare further; here we simply warn.
421                warn!(
422                    "Duplicate target with no origin: Existing: {:?} vs New: {:?}",
423                    existing, target
424                );
425            } else {
426                map.insert(None, target);
427            }
428        }
429    }
430
431    map.into_values().collect()
432}
433
434pub fn collect_all_targets(
435    use_workspace: bool,
436    max_concurrency: usize,
437) -> Result<Vec<CargoTarget>, Box<dyn std::error::Error>> {
438    use std::path::PathBuf;
439    let mut manifest_infos: Vec<(String, PathBuf, bool)> = Vec::new();
440
441    // Locate the package manifest in the current directory.
442    let bi = PathBuf::from(crate::locate_manifest(false)?);
443    // We're in workspace mode if the flag is set or if the current Cargo.toml is a workspace manifest.
444    let in_workspace = use_workspace || e_workspace::is_workspace_manifest(bi.as_path());
445
446    if in_workspace {
447        // Use an explicit workspace manifest if requested; otherwise, assume the current Cargo.toml is the workspace manifest.
448        let ws = if use_workspace {
449            PathBuf::from(crate::locate_manifest(true)?)
450        } else {
451            bi.clone()
452        };
453        // Get workspace members (each member's Cargo.toml) using your helper.
454        let ws_members =
455            e_workspace::get_workspace_member_manifest_paths(ws.as_path()).unwrap_or_default();
456        // Build a numbered list of member names (using just the member directory name).
457        let member_displays: Vec<String> = ws_members
458            .iter()
459            .enumerate()
460            .map(|(i, (member, _))| format!("{}. {}", i + 1, member))
461            .collect();
462        // Print the workspace line: "<workspace_root>/<package>/Cargo.toml [1. member, 2. member, ...]"
463        println!(
464            "workspace: {} [{}]",
465            format_workspace(&ws, &bi),
466            member_displays.join(", ")
467        );
468        // Always print the package line.
469        println!("package: {}", format_package(&bi));
470        manifest_infos.push(("-".to_string(), bi.clone(), false));
471        for (member, member_manifest) in ws_members {
472            manifest_infos.push((format!("${}", member), member_manifest, true));
473        }
474    } else {
475        // Not in workspace mode: simply print the package manifest.
476        println!("package: {}", format_package(&bi));
477        manifest_infos.push(("-".to_string(), bi.clone(), false));
478    }
479
480    let samples = collect_samples(use_workspace, manifest_infos, max_concurrency)?;
481    // Deduplicate targets: if a SingleFile and DefaultBinary share the same origin, keep only the SingleFile.
482    let deduped_samples = dedup_single_file_over_default_binary(samples);
483    Ok(deduped_samples)
484}
485
486// Formats the package manifest as "<package>/Cargo.toml"
487fn format_package(manifest: &Path) -> String {
488    let pkg = manifest
489        .parent()
490        .and_then(|p| p.file_name())
491        .map(|s| s.to_string_lossy())
492        .unwrap_or_default();
493    let file = manifest.file_name().unwrap().to_string_lossy();
494    format!("{}/{}", pkg, file)
495}
496
497// Formats the workspace manifest as "<workspace_root>/<package>/Cargo.toml"
498fn format_workspace(ws_manifest: &Path, bi: &Path) -> String {
499    let ws_root = ws_manifest.parent().expect("No workspace root");
500    let ws_name = ws_root
501        .file_name()
502        .map(|s| s.to_string_lossy())
503        .unwrap_or_default();
504    let bi_pkg = bi
505        .parent()
506        .and_then(|p| p.file_name())
507        .map(|s| s.to_string_lossy())
508        .unwrap_or_default();
509    format!(
510        "{}/{}/{}",
511        ws_name,
512        bi_pkg,
513        ws_manifest.file_name().unwrap().to_string_lossy()
514    )
515}