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}