cargo_e/
e_collect.rs

1use crate::{e_workspace, prelude::*};
2use crate::{Example, TargetKind};
3use std::process::Output;
4/// Helper function that runs a Cargo command with a given manifest path.
5/// If it detects the workspace error, it temporarily patches the manifest (by
6/// appending an empty `[workspace]` table), re-runs the command, and then restores
7/// the original file.
8fn run_cargo_with_opt_out(args: &[&str], manifest_path: &Path) -> Result<Output, Box<dyn Error>> {
9    // Run the initial command.
10    let output = Command::new("cargo")
11        .args(args)
12        .arg("--manifest-path")
13        .arg(manifest_path)
14        .output()?;
15
16    let stderr_str = String::from_utf8_lossy(&output.stderr);
17    let workspace_error_marker = "current package believes it's in a workspace when it's not:";
18
19    // If we detect the workspace error, patch the manifest.
20    if stderr_str.contains(workspace_error_marker) {
21        // Backup the original manifest.
22        let original = fs::read_to_string(manifest_path)?;
23
24        // Only patch if the manifest doesn't already opt out.
25        if !original.contains("[workspace]") {
26            // Append an empty [workspace] table.
27            let patched = format!("{}\n[workspace]\n", original);
28            fs::write(manifest_path, &patched)?;
29
30            // Re-run the command with the patched manifest.
31            let patched_output = Command::new("cargo")
32                .args(args)
33                .arg("--manifest-path")
34                .arg(manifest_path)
35                .output()?;
36
37            // Restore the original manifest.
38            fs::write(manifest_path, original)?;
39
40            return Ok(patched_output);
41        }
42    }
43
44    Ok(output)
45}
46
47/// Runs `cargo run --bin` (without specifying a binary name) so that Cargo prints an error with
48/// a list of available binary targets. Then parses that list to return a vector of Example instances,
49/// using the provided prefix.
50pub fn collect_binaries(
51    prefix: &str,
52    manifest_path: &Path,
53    extended: bool,
54) -> Result<Vec<Example>, Box<dyn Error>> {
55    // Run the Cargo command using our helper.
56    let output = run_cargo_with_opt_out(&["run", "--bin"], manifest_path)?;
57    let stderr_str = String::from_utf8_lossy(&output.stderr);
58
59    let bin_names = crate::parse_available(&stderr_str, "binaries");
60
61    let binaries = bin_names
62        .into_iter()
63        .map(|name| {
64            let display_name = if prefix.starts_with('$') {
65                format!(
66                    "{} > binary > {} {}",
67                    prefix,
68                    name,
69                    manifest_path.to_string_lossy().into_owned()
70                )
71            } else if extended {
72                format!("{} {}", prefix, name)
73            } else if prefix.starts_with("builtin") {
74                format!("builtin binary: {}", name)
75            } else {
76                format!("{} {}", prefix, name)
77                // name.clone()
78            };
79
80            Example {
81                name: name.clone(),
82                display_name,
83                manifest_path: manifest_path.to_string_lossy().to_string(),
84                kind: if extended {
85                    TargetKind::ExtendedBinary
86                } else {
87                    TargetKind::Binary
88                },
89                extended,
90            }
91        })
92        .collect();
93
94    Ok(binaries)
95}
96
97/// Runs `cargo run --example` so that Cargo lists available examples,
98/// then parses the stderr output to return a vector of Example instances.
99pub fn collect_examples(
100    prefix: &str,
101    manifest_path: &Path,
102    extended: bool,
103) -> Result<Vec<Example>, Box<dyn Error>> {
104    // Run the Cargo command using our helper.
105    let output = run_cargo_with_opt_out(&["run", "--example"], manifest_path)?;
106    let stderr_str = String::from_utf8_lossy(&output.stderr);
107    debug!("DEBUG: stderr (examples) = {:?}", stderr_str);
108
109    let names = crate::parse_available(&stderr_str, "examples");
110    debug!("DEBUG: example names = {:?}", names);
111
112    let examples = names
113        .into_iter()
114        .map(|name| {
115            let display_name = if prefix.starts_with('$') {
116                format!("{} > example > {}", prefix, name)
117            } else if extended {
118                format!("{} {}", prefix, name)
119            } else if prefix.starts_with("builtin") {
120                format!("builtin example: {}", name)
121            } else {
122                format!("{} {}", prefix, name)
123                // name.clone()
124            };
125
126            Example {
127                name: name.clone(),
128                display_name,
129                manifest_path: manifest_path.to_string_lossy().to_string(),
130                kind: if extended {
131                    TargetKind::ExtendedExample
132                } else {
133                    TargetKind::Example
134                },
135                extended,
136            }
137        })
138        .collect();
139
140    Ok(examples)
141}
142
143// --- Concurrent or sequential collection ---
144pub fn collect_samples(
145    workspace_mode: bool,
146    manifest_infos: Vec<(String, PathBuf, bool)>,
147    __max_concurrency: usize,
148) -> Result<Vec<Example>, Box<dyn Error>> {
149    let mut all_samples = Vec::new();
150
151    #[cfg(feature = "concurrent")]
152    {
153        use threadpool::ThreadPool;
154        let pool = ThreadPool::new(__max_concurrency);
155        let (tx, rx) = mpsc::channel();
156
157        let start_concurrent = Instant::now();
158        for (prefix, manifest_path, extended) in manifest_infos {
159            let tx = tx.clone();
160            let prefix_clone = prefix.clone();
161            let manifest_clone = manifest_path.clone();
162            pool.execute(move || {
163                let mut results = Vec::new();
164                if let Ok(mut ex) = collect_examples(&prefix_clone, &manifest_clone, extended) {
165                    results.append(&mut ex);
166                }
167                if let Ok(mut bins) = collect_binaries(&prefix_clone, &manifest_clone, extended) {
168                    results.append(&mut bins);
169                }
170                let etargets =
171                    crate::e_discovery::discover_targets(manifest_clone.parent().unwrap()).unwrap();
172                for target in etargets {
173                    // Check if the target is extended and that its name is not already present in results.
174                    if target.extended && !results.iter().any(|r| r.name == target.name) {
175                        let manifest_path = Path::new(&target.manifest_path);
176                        let new_prefix = format!("examples/{}", &prefix_clone);
177                        // Collect extended examples.
178                        if let Ok(ex_ext) = collect_examples(&new_prefix, manifest_path, true) {
179                            for ex in ex_ext {
180                                if !results.iter().any(|r| r.name == ex.name) {
181                                    results.push(ex);
182                                }
183                            }
184                        }
185
186                        // Collect extended binaries.
187                        if let Ok(bins_ext) = collect_binaries(&new_prefix, manifest_path, true) {
188                            for bin in bins_ext {
189                                if !results.iter().any(|r| r.name == bin.name) {
190                                    results.push(bin);
191                                }
192                            }
193                        }
194                        // Derive the subproject directory from the extended target's manifest path.
195
196                        // // Convert CargoTarget to Example by mapping the fields appropriately.
197                        // let example = Example {
198                        //     name: target.name,
199                        //     display_name: target.display_name,
200                        //     manifest_path: target.manifest_path,
201                        //     kind: TargetKind::Extended, // Ensure this field is compatible with Example's type.
202                        //     extended: target.extended,
203                        // };
204                        // results.push(example);
205                    }
206                }
207                // for e in etargets {
208                //    println!("{} found", e.name);
209                // }
210                tx.send(results).expect("Failed to send results");
211            });
212        }
213        drop(tx);
214        pool.join(); // Wait for all tasks to finish.
215        let duration_concurrent = start_concurrent.elapsed();
216        debug!(
217            "timing: {} threads took {:?}",
218            __max_concurrency, duration_concurrent
219        );
220
221        for samples in rx {
222            all_samples.extend(samples);
223        }
224    }
225
226    // Sequential fallback: process one manifest at a time.
227    #[cfg(not(feature = "concurrent"))]
228    {
229        let start_seq = Instant::now();
230        for (prefix, manifest_path, extended) in manifest_infos {
231            if let Ok(mut ex) = collect_examples(&prefix, &manifest_path, extended) {
232                all_samples.append(&mut ex);
233            }
234            if let Ok(mut bins) = collect_binaries(&prefix, &manifest_path, extended) {
235                all_samples.append(&mut bins);
236            }
237            let manifest_path = Path::new(&manifest_path);
238            let new_prefix = format!("examples/{}", &prefix);
239            // Collect extended examples.
240            if let Ok(ex_ext) = collect_examples(&new_prefix, manifest_path, true) {
241                for ex in ex_ext {
242                    if !all_samples.iter().any(|r| r.name == ex.name) {
243                        all_samples.push(ex);
244                    }
245                }
246            }
247
248            // Collect extended binaries.
249            if let Ok(bins_ext) = collect_binaries(&new_prefix, manifest_path, true) {
250                for bin in bins_ext {
251                    if !all_samples.iter().any(|r| r.name == bin.name) {
252                        all_samples.push(bin);
253                    }
254                }
255            }
256        }
257        let duration_seq = start_seq.elapsed();
258        debug!("timing: Sequential processing took {:?}", duration_seq);
259    }
260    let mut target_map: std::collections::HashMap<String, crate::Example> =
261        std::collections::HashMap::new();
262
263    // Group targets by name and choose one based on workspace_mode:
264    for target in all_samples {
265        target_map
266            .entry(target.name.clone())
267            .and_modify(|existing| {
268                if workspace_mode {
269                    // In workspace mode, extended targets override builtins.
270                    if target.extended && !existing.extended {
271                        *existing = target.clone();
272                    }
273                } else {
274                    // In normal mode, builtin targets (non-extended) override extended.
275                    if !target.extended && existing.extended {
276                        *existing = target.clone();
277                    }
278                }
279            })
280            .or_insert(target.clone());
281    }
282
283    let mut combined = Vec::new();
284    for target in target_map.into_values() {
285        combined.push(target);
286    }
287
288    Ok(combined)
289    // Ok(all_samples)
290}
291
292pub fn collect_all_targets(
293    use_workspace: bool,
294    max_concurrency: usize,
295) -> Result<Vec<Example>, Box<dyn std::error::Error>> {
296    use std::path::PathBuf;
297    let mut manifest_infos: Vec<(String, PathBuf, bool)> = Vec::new();
298
299    // Locate the package manifest in the current directory.
300    let bi = PathBuf::from(crate::locate_manifest(false)?);
301    // We're in workspace mode if the flag is set or if the current Cargo.toml is a workspace manifest.
302    let in_workspace = use_workspace || e_workspace::is_workspace_manifest(bi.as_path());
303
304    if in_workspace {
305        // Use an explicit workspace manifest if requested; otherwise, assume the current Cargo.toml is the workspace manifest.
306        let ws = if use_workspace {
307            PathBuf::from(crate::locate_manifest(true)?)
308        } else {
309            bi.clone()
310        };
311        // Get workspace members (each member's Cargo.toml) using your helper.
312        let ws_members =
313            e_workspace::get_workspace_member_manifest_paths(ws.as_path()).unwrap_or_default();
314        // Build a numbered list of member names (using just the member directory name).
315        let member_displays: Vec<String> = ws_members
316            .iter()
317            .enumerate()
318            .map(|(i, (member, _))| format!("{}. {}", i + 1, member))
319            .collect();
320        // Print the workspace line: "<workspace_root>/<package>/Cargo.toml [1. member, 2. member, ...]"
321        println!(
322            "workspace: {} [{}]",
323            format_workspace(&ws, &bi),
324            member_displays.join(", ")
325        );
326        // Always print the package line.
327        println!("package: {}", format_package(&bi));
328        manifest_infos.push(("-".to_string(), bi.clone(), false));
329        for (member, member_manifest) in ws_members {
330            manifest_infos.push((format!("${}", member), member_manifest, true));
331        }
332    } else {
333        // Not in workspace mode: simply print the package manifest.
334        println!("package: {}", format_package(&bi));
335        manifest_infos.push(("-".to_string(), bi.clone(), false));
336    }
337
338    let samples = collect_samples(use_workspace, manifest_infos, max_concurrency)?;
339    Ok(samples)
340}
341
342// Formats the package manifest as "<package>/Cargo.toml"
343fn format_package(manifest: &Path) -> String {
344    let pkg = manifest
345        .parent()
346        .and_then(|p| p.file_name())
347        .map(|s| s.to_string_lossy())
348        .unwrap_or_default();
349    let file = manifest.file_name().unwrap().to_string_lossy();
350    format!("{}/{}", pkg, file)
351}
352
353// Formats the workspace manifest as "<workspace_root>/<package>/Cargo.toml"
354fn format_workspace(ws_manifest: &Path, bi: &Path) -> String {
355    let ws_root = ws_manifest.parent().expect("No workspace root");
356    let ws_name = ws_root
357        .file_name()
358        .map(|s| s.to_string_lossy())
359        .unwrap_or_default();
360    let bi_pkg = bi
361        .parent()
362        .and_then(|p| p.file_name())
363        .map(|s| s.to_string_lossy())
364        .unwrap_or_default();
365    format!(
366        "{}/{}/{}",
367        ws_name,
368        bi_pkg,
369        ws_manifest.file_name().unwrap().to_string_lossy()
370    )
371}