cargo_e/e_target.rs
1// src/e_target.rs
2use anyhow::{Context, Result};
3use log::{debug, trace};
4use std::{
5 collections::HashMap,
6 ffi::OsString,
7 fs,
8 path::{Path, PathBuf},
9};
10use toml::Value;
11
12#[derive(Debug, Clone)]
13pub enum TargetOrigin {
14 DefaultBinary(PathBuf),
15 SingleFile(PathBuf),
16 MultiFile(PathBuf),
17 SubProject(PathBuf),
18 Named(OsString),
19 /// A target provided by a plugin, storing plugin file and reported source path
20 Plugin {
21 plugin_path: PathBuf,
22 reported: PathBuf,
23 },
24}
25
26#[derive(Debug, Clone, PartialEq, Hash, Eq, Copy)]
27pub enum TargetKind {
28 Unknown,
29 UnknownExample,
30 UnknownExtendedExample,
31 UnknownBinary,
32 UnknownExtendedBinary,
33 Example,
34 ExtendedExample,
35 Binary,
36 ExtendedBinary,
37 Bench,
38 Test,
39 Manifest, // For browsing the entire Cargo.toml or package-level targets.
40 ManifestTauri,
41 ManifestTauriExample,
42 ManifestDioxusExample,
43 ManifestDioxus,
44 ManifestLeptos,
45 ScriptRustScript,
46 ScriptScriptisto,
47 /// A target provided by an external plugin (script, WASM, etc.)
48 Plugin,
49}
50
51#[derive(Debug, Clone)]
52pub struct CargoTarget {
53 pub name: String,
54 pub display_name: String,
55 pub manifest_path: PathBuf,
56 pub kind: TargetKind,
57 pub extended: bool,
58 pub toml_specified: bool,
59 pub origin: Option<TargetOrigin>,
60}
61
62impl CargoTarget {
63 /// Constructs a CargoTarget from a source file.
64 ///
65 /// Reads the file at `file_path` and determines the target kind based on:
66 /// - Tauri configuration (e.g. if the manifest's parent is "src-tauri" or a Tauri config exists),
67 /// - Dioxus markers in the file contents,
68 /// - And finally, if the file contains "fn main", using its parent directory (examples vs bin) to decide.
69 ///
70 /// If none of these conditions are met, returns None.
71 pub fn from_source_file(
72 stem: &std::ffi::OsStr,
73 file_path: &Path,
74 manifest_path: &Path,
75 example: bool,
76 extended: bool,
77 ) -> Option<Self> {
78 let file_path = fs::canonicalize(&file_path).unwrap_or(file_path.to_path_buf());
79 let file_contents = std::fs::read_to_string(&file_path).unwrap_or_default();
80 let (kind, new_manifest) = crate::e_discovery::determine_target_kind_and_manifest(
81 manifest_path,
82 &file_path,
83 &file_contents,
84 example,
85 extended,
86 None,
87 );
88 if kind == TargetKind::Unknown {
89 return None;
90 }
91 let name = stem.to_string_lossy().to_string();
92 Some(CargoTarget {
93 name: name.clone(),
94 display_name: name,
95 manifest_path: new_manifest.to_path_buf(),
96 kind,
97 extended,
98 toml_specified: false,
99 origin: Some(TargetOrigin::SingleFile(file_path.to_path_buf())),
100 })
101 }
102
103 // /// Updates the target's name and display_name by interrogating the candidate file and its manifest.
104 // pub fn figure_main_name(&mut self) {
105 // // Only operate if we have a candidate file path.
106 // let candidate = match &self.origin {
107 // Some(TargetOrigin::SingleFile(path)) | Some(TargetOrigin::DefaultBinary(path)) => path,
108 // _ => {
109 // debug!("No candidate file found in target.origin; skipping name determination");
110 // return;
111 // }
112 // };
113 // println!("figure_main: {}", &candidate.display());
114 // // Get the candidate file's stem in lowercase.
115 // let candidate_stem = candidate
116 // .file_stem()
117 // .and_then(|s| s.to_str())
118 // .map(|s| s.to_lowercase())
119 // .unwrap_or_default();
120 // debug!("Candidate stem: {}", candidate_stem);
121
122 // // Start with folder-based logic.
123 // let mut name = if candidate_stem == "main" {
124 // if let Some(parent_dir) = candidate.parent() {
125 // if let Some(parent_name) = parent_dir.file_name().and_then(|s| s.to_str()) {
126 // debug!("Candidate parent folder: {}", parent_name);
127 // if parent_name.eq_ignore_ascii_case("src") {
128 // // If candidate is src/main.rs, take the parent of "src".
129 // parent_dir
130 // .parent()
131 // .and_then(|proj_dir| proj_dir.file_name())
132 // .and_then(|s| s.to_str())
133 // .map(|s| s.to_string())
134 // .unwrap_or(candidate_stem.clone())
135 // } else if parent_name.eq_ignore_ascii_case("examples") {
136 // // If candidate is in an examples folder, use the candidate's parent folder's name.
137 // candidate
138 // .parent()
139 // .and_then(|p| p.file_name())
140 // .and_then(|s| s.to_str())
141 // .map(|s| s.to_string())
142 // .unwrap_or(candidate_stem.clone())
143 // } else {
144 // candidate_stem.clone()
145 // }
146 // } else {
147 // candidate_stem.clone()
148 // }
149 // } else {
150 // candidate_stem.clone()
151 // }
152 // } else {
153 // candidate_stem.clone()
154 // };
155
156 // let mut package_manifest_name = String::new();
157 // // If the candidate stem is "main", interrogate the manifest.
158 // let manifest_contents = fs::read_to_string(&self.manifest_path).unwrap_or_default();
159 // if let Ok(manifest_toml) = manifest_contents.parse::<Value>() {
160 // if let Ok(manifest_toml) = manifest_contents.parse::<toml::Value>() {
161 // // Then try to retrieve the bin section.
162 // if let Some(bins) = manifest_toml.get("bin").and_then(|v| v.as_array()) {
163 // debug!("Found {} [[bin]] entries {:?}", bins.len(), bins);
164 // } else {
165 // debug!("No [[bin]] array found in manifest");
166 // }
167 // } else {
168 // debug!("Failed to parse manifest TOML");
169 // }
170 // debug!("Opened manifest {:?}",&self.manifest_path);
171 // // Check for any [[bin]] entries.
172 // if let Some(bins) = manifest_toml.get("bin").and_then(|v| v.as_array()) {
173 // debug!("Found {} [[bin]] entries", bins.len());
174 // if let Some(bin_name) = bins.iter().find_map(|bin| {
175 // if let Some(path_str) = bin.get("path").and_then(|p| p.as_str()) {
176 // let bp = bin
177 // .get("path")
178 // .and_then(|n| n.as_str())
179 // .map(|s| s.to_string());
180 // let bn = bin
181 // .get("name")
182 // .and_then(|n| n.as_str())
183 // .map(|s| s.to_string());
184 // debug!("Checking bin entry with path: {} {:?}", path_str, bp);
185 // if bp.as_deref().unwrap_or("") == path_str
186 // // && bn.as_deref().unwrap_or("") == candidate_stem
187 // {
188 // debug!("Found matching bin with name: {:?} {:?}=={:?}", bn,bp.as_deref().unwrap_or(""), path_str);
189 // name = bn.clone().unwrap_or_default();
190 // return bn.clone();
191 // }
192 // }
193 // None
194 // }) {
195 // //debug!("Using bin name from manifest: {} as {} ", name, bin_name);
196 // //name = bin_name;
197 // } else if let Some(pkg) = manifest_toml.get("package") {
198 // debug!("No matching [[bin]] entry; checking [package] section");
199 // name = pkg
200 // .get("name")
201 // .and_then(|n| n.as_str())
202 // .unwrap_or(&name)
203 // .to_string();
204 // debug!("Using package name from manifest: {}", name);
205 // }
206 // } else if let Some(pkg) = manifest_toml.get("package") {
207 // debug!("No [[bin]] section found; using [package] section");
208 // package_manifest_name = pkg
209 // .get("name")
210 // .and_then(|n| n.as_str())
211 // .unwrap_or(&name)
212 // .to_string();
213 // debug!("Using package name from manifest: {}", name);
214 // } else {
215 // debug!(
216 // "Manifest does not contain [[bin]] or [package] sections; keeping name: {}",
217 // name
218 // );
219 // }
220 // } else {
221 // debug!("Failed to open manifest {:?}",&self.manifest_path);
222 // debug!("Failed to parse manifest TOML; keeping name: {}", name);
223 // }
224
225 // debug!("Name after folder-based logic: {}", name);
226
227 // debug!("Final determined name: {}", name);
228 // if name.eq("main") {
229 // panic!("Name is main");
230 // }
231 // self.name = name.clone();
232 // self.display_name = name;
233 // }
234
235 pub fn figure_main_name(&mut self) {
236 let mut is_toml_specified = false;
237 // Only operate if we have a candidate file path.
238 let candidate = match &self.origin {
239 Some(TargetOrigin::SingleFile(path)) | Some(TargetOrigin::DefaultBinary(path)) => path,
240 _ => {
241 debug!("No candidate file found in target.origin; skipping name determination");
242 return;
243 }
244 };
245
246 trace!("figure_main: {:?}", &self.origin);
247
248 // Get the candidate file's stem in lowercase.
249 let mut candidate_stem = candidate
250 .file_stem()
251 .and_then(|s| s.to_str())
252 .map(|s| s.to_lowercase())
253 .unwrap_or_default();
254 trace!("Candidate stem: {}", candidate_stem);
255
256 // First, check if the manifest path from self matches what we find upward.
257 let candidate_dir = candidate.parent().unwrap_or(candidate);
258 let found_manifest_dir = crate::e_manifest::find_manifest_dir_from(candidate_dir);
259 if let Ok(found_dir) = found_manifest_dir {
260 let found_manifest = found_dir.join("Cargo.toml");
261 if found_manifest == self.manifest_path {
262 trace!(
263 "{} Manifest path matches candidate's upward search result: {:?}",
264 candidate.display(),
265 found_manifest
266 );
267 } else {
268 trace!(
269 "{} Manifest path mismatch. Found upward: {:?} but target.manifest_path is: {:?}"
270 , candidate.display(), found_manifest, self.manifest_path
271 );
272 // Compare depths.
273 let found_depth = found_manifest.components().count();
274 let target_depth = self.manifest_path.components().count();
275 if found_depth > target_depth {
276 // Before switching, compare the candidate's relative paths.
277 let orig_parent = self.manifest_path.parent().unwrap_or_else(|| Path::new(""));
278 let found_parent = found_manifest.parent().unwrap_or_else(|| Path::new(""));
279 let orig_rel = candidate.strip_prefix(orig_parent).ok();
280 let found_rel = candidate.strip_prefix(found_parent).ok();
281 if orig_rel == found_rel {
282 trace!(
283 "{} Relative path matches: {:?}",
284 candidate.display(),
285 orig_rel
286 );
287 self.manifest_path = found_manifest;
288 } else {
289 trace!(
290 "{} Relative path mismatch: original: {:?}, found: {:?}",
291 candidate.display(),
292 orig_rel,
293 found_rel
294 );
295 }
296 } else {
297 trace!(
298 "{} Keeping target manifest path (deeper or equal): {:?}",
299 candidate.display(),
300 self.manifest_path
301 );
302 }
303 }
304 } else {
305 trace!(
306 "Could not locate Cargo.toml upward from candidate: {:?}",
307 candidate
308 );
309 }
310
311 // Determine name via manifest processing.
312 let mut name = candidate_stem.clone();
313 let manifest_contents = fs::read_to_string(&self.manifest_path).unwrap_or_default();
314 if let Ok(manifest_toml) = manifest_contents.parse::<Value>() {
315 trace!(
316 "{} Opened manifest {:?}",
317 candidate.display(),
318 &self.manifest_path
319 );
320
321 // // First, check for any [[bin]] entries.
322 // if let Some(bins) = manifest_toml.get("bin").and_then(|v| v.as_array()) {
323 // trace!("Found {} [[bin]] entries", bins.len());
324 // // Iterate over the bin entries and use absolute paths for comparison.
325 // if let Some(bin_name) = bins.iter().find_map(|bin| {
326 // if let (Some(rel_path_str), Some(bn)) = (
327 // bin.get("path").and_then(|p| p.as_str()),
328 // bin.get("name").and_then(|n| n.as_str()),
329 // ) {
330 // // Construct the expected absolute path for the candidate file.
331 // let manifest_parent =
332 // self.manifest_path.parent().unwrap_or_else(|| Path::new(""));
333 // let expected_path =
334 // fs::canonicalize(manifest_parent.join(rel_path_str)).ok()?;
335 // let candidate_abs = fs::canonicalize(candidate).ok()?;
336 // trace!(
337 // "\n{}\n{:?}\nactual candidate absolute path:\n{:?}",
338 // candidate.display(),
339 // expected_path,
340 // candidate_abs
341 // );
342 // if expected_path == candidate_abs {
343 // trace!(
344 // "{} Found matching bin with name: {}",
345 // candidate.display(),
346 // bn
347 // );
348 // return Some(bn.to_string());
349 // }
350 // }
351 // None
352 // }) {
353 // trace!(
354 // "{} Using bin name from manifest: {}",
355 // candidate.display(),
356 // bin_name
357 // );
358 // name = bin_name.clone();
359 // candidate_stem = bin_name.into();
360 // }
361 // }
362 if let Some(bin_name) = crate::e_manifest::find_candidate_name(
363 &manifest_toml,
364 "bin",
365 candidate,
366 &self.manifest_path,
367 ) {
368 trace!(
369 "{} Using bin name from manifest: {}",
370 candidate.display(),
371 bin_name
372 );
373 is_toml_specified = true;
374 name = bin_name.clone();
375 candidate_stem = bin_name.into();
376 } else if let Some(example_name) = crate::e_manifest::find_candidate_name(
377 &manifest_toml,
378 "example",
379 candidate,
380 &self.manifest_path,
381 ) {
382 is_toml_specified = true;
383 trace!(
384 "{} Using example name from manifest: {}",
385 candidate.display(),
386 example_name
387 );
388 name = example_name.clone();
389 candidate_stem = example_name.into();
390 } else {
391 match &self.origin {
392 Some(TargetOrigin::DefaultBinary(_path)) => {
393 // Check for any [package] section.
394 if let Some(pkg) = manifest_toml.get("package") {
395 trace!("Found [package] section in manifest");
396 if let Some(name_value) = pkg.get("name").and_then(|v| v.as_str()) {
397 trace!("Using package name from manifest: {}", name_value);
398 name = name_value.to_string();
399 candidate_stem = name.clone();
400 } else {
401 trace!("No package name found in manifest; keeping name: {}", name);
402 }
403 }
404 }
405 _ => {}
406 };
407 }
408
409 // if let Some(bins) = manifest_toml.get("bin").and_then(|v| v.as_array()) {
410 // trace!("Found {} [[bin]] entries", bins.len());
411 // // Iterate over the bin entries and use absolute paths for comparison.
412 // if let Some(bin_name) = bins.iter().find_map(|bin| {
413 // if let (Some(rel_path_str), Some(bn)) = (
414 // bin.get("path").and_then(|p| p.as_str()),
415 // bin.get("name").and_then(|n| n.as_str()),
416 // ) {
417 // // Construct the expected absolute path for the candidate file.
418 // let manifest_parent = self.manifest_path.parent().unwrap_or_else(|| Path::new(""));
419 // let expected_path = fs::canonicalize(manifest_parent.join(rel_path_str)).ok()?;
420 // let candidate_abs = fs::canonicalize(candidate).ok()?;
421 // trace!(
422 // "{} Expected candidate absolute path: {:?}, actual candidate absolute path: {:?}",
423 // candidate.display(),
424 // expected_path,
425 // candidate_abs
426 // );
427 // if expected_path == candidate_abs {
428 // trace!(
429 // "{} Found matching bin with name: {}",
430 // candidate.display(),
431 // bn
432 // );
433 // return Some(bn.to_string());
434 // }
435 // }
436 // None
437 // }) {
438 // trace!("{} Using bin name from manifest: {}", candidate.display(), bin_name);
439 // name = bin_name;
440 // }
441 //}
442 } else {
443 trace!("Failed to open manifest {:?}", &self.manifest_path);
444 trace!("Failed to parse manifest TOML; keeping name: {}", name);
445 }
446
447 // Only if the candidate stem is "main", apply folder-based logic after manifest processing.
448 if candidate_stem == "main" {
449 let folder_name = if let Some(parent_dir) = candidate.parent() {
450 if let Some(parent_name) = parent_dir.file_name().and_then(|s| s.to_str()) {
451 trace!("Candidate parent folder: {}", parent_name);
452 if parent_name.eq_ignore_ascii_case("src")
453 || parent_name.eq_ignore_ascii_case("src-tauri")
454 {
455 // If candidate is src/main.rs, take the parent of "src".
456 let p = parent_dir
457 .parent()
458 .and_then(|proj_dir| proj_dir.file_name())
459 .and_then(|s| s.to_str())
460 .map(|s| s.to_string())
461 .unwrap_or(candidate_stem.clone());
462 if p.eq("src-tauri") {
463 let maybe_name = parent_dir
464 .parent()
465 .and_then(|proj_dir| proj_dir.parent())
466 .and_then(|proj_dir| proj_dir.file_name())
467 .and_then(|s| s.to_str())
468 .map(String::from);
469 match maybe_name {
470 Some(name) => name,
471 None => candidate_stem.clone(),
472 }
473 } else {
474 p
475 }
476 } else if parent_name.eq_ignore_ascii_case("examples") {
477 // If candidate is in an examples folder, use the candidate's parent folder's name.
478 candidate
479 .parent()
480 .and_then(|p| p.file_name())
481 .and_then(|s| s.to_str())
482 .map(|s| s.to_string())
483 .unwrap_or(candidate_stem.clone())
484 } else {
485 parent_name.into()
486 }
487 } else {
488 candidate_stem.clone()
489 }
490 } else {
491 candidate_stem.clone()
492 };
493 trace!("Folder-based name: {}-{}", candidate.display(), folder_name);
494 // Only override if the folder-based name is different from "main".
495 if folder_name != "main" {
496 name = folder_name;
497 }
498 }
499
500 trace!("Final determined name: {}", name);
501 if name.eq("main") {
502 panic!("Name is main");
503 }
504 if is_toml_specified {
505 self.toml_specified = true;
506 }
507 self.name = name.clone();
508 self.display_name = name;
509 }
510 /// Constructs a CargoTarget from a folder by trying to locate a runnable source file.
511 ///
512 /// The function attempts the following candidate paths in order:
513 /// 1. A file named `<folder_name>.rs` in the folder.
514 /// 2. `src/main.rs` inside the folder.
515 /// 3. `main.rs` at the folder root.
516 /// 4. Otherwise, it scans the folder for any `.rs` file containing `"fn main"`.
517 ///
518 /// Once a candidate is found, it reads its contents and calls `determine_target_kind`
519 /// to refine the target kind based on Tauri or Dioxus markers. The `extended` flag
520 /// indicates whether the target should be marked as extended (for instance, if the folder
521 /// is a subdirectory of the primary "examples" or "bin" folder).
522 ///
523 /// Returns Some(CargoTarget) if a runnable file is found, or None otherwise.
524 pub fn from_folder(
525 folder: &Path,
526 manifest_path: &Path,
527 example: bool,
528 _extended: bool,
529 ) -> Option<Self> {
530 // If the folder contains its own Cargo.toml, treat it as a subproject.
531 let sub_manifest = folder.join("Cargo.toml");
532 if sub_manifest.exists() {
533 // Use the folder's name as the candidate target name.
534 let folder_name = folder.file_name()?.to_string_lossy().to_string();
535 // Determine the display name from the parent folder.
536 let display_name = if let Some(parent) = folder.parent() {
537 let parent_name = parent.file_name()?.to_string_lossy();
538 if parent_name == folder_name {
539 // If the parent's name equals the folder's name, try using the grandparent.
540 if let Some(grandparent) = parent.parent() {
541 grandparent.file_name()?.to_string_lossy().to_string()
542 } else {
543 folder_name.clone()
544 }
545 } else {
546 parent_name.to_string()
547 }
548 } else {
549 folder_name.clone()
550 };
551
552 let sub_manifest =
553 fs::canonicalize(&sub_manifest).unwrap_or(sub_manifest.to_path_buf());
554 trace!("Subproject found: {}", sub_manifest.display());
555 trace!("{}", &folder_name);
556 return Some(CargoTarget {
557 name: folder_name.clone(),
558 display_name,
559 manifest_path: sub_manifest.clone(),
560 // For a subproject, we initially mark it as Manifest;
561 // later refinement may resolve it further.
562 kind: TargetKind::Manifest,
563 toml_specified: true,
564 extended: true,
565 origin: Some(TargetOrigin::SubProject(sub_manifest)),
566 });
567 }
568 // Extract the folder's name.
569 let folder_name = folder.file_name()?.to_str()?;
570
571 /// Returns Some(candidate) only if the file exists and its contents contain "fn main".
572 fn candidate_with_main(candidate: PathBuf) -> Option<PathBuf> {
573 if candidate.exists() {
574 let contents = fs::read_to_string(&candidate).unwrap_or_default();
575 if contents.contains("fn main") {
576 return Some(candidate);
577 }
578 }
579 None
580 }
581
582 // In your from_folder function, for example:
583 let candidate = if let Some(candidate) =
584 candidate_with_main(folder.join(format!("{}.rs", folder_name)))
585 {
586 candidate
587 } else if let Some(candidate) = candidate_with_main(folder.join("src/main.rs")) {
588 candidate
589 } else if let Some(candidate) = candidate_with_main(folder.join("main.rs")) {
590 candidate
591 } else {
592 // Otherwise, scan the folder for any .rs file containing "fn main"
593 let mut found = None;
594 if let Ok(entries) = fs::read_dir(folder) {
595 for entry in entries.flatten() {
596 let path = entry.path();
597 if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("rs") {
598 if let Some(candidate) = candidate_with_main(path) {
599 found = Some(candidate);
600 break;
601 }
602 }
603 }
604 }
605 found?
606 };
607
608 let candidate = fs::canonicalize(&candidate).unwrap_or(candidate.to_path_buf());
609 // Compute the extended flag based on the candidate file location.
610 let extended = crate::e_discovery::is_extended_target(manifest_path, &candidate);
611
612 // Read the candidate file's contents.
613 let file_contents = std::fs::read_to_string(&candidate).unwrap_or_default();
614
615 // Use our helper to determine if any special configuration applies.
616 let (kind, new_manifest) = crate::e_discovery::determine_target_kind_and_manifest(
617 manifest_path,
618 &candidate,
619 &file_contents,
620 example,
621 extended,
622 None,
623 );
624 if kind == TargetKind::Unknown {
625 return None;
626 }
627
628 // Determine the candidate file's stem in lowercase.
629 let name = candidate.file_stem()?.to_str()?.to_lowercase();
630 // let name = if candidate_stem == "main" {
631 // if let Some(parent_dir) = candidate.parent() {
632 // if let Some(parent_name) = parent_dir.file_name().and_then(|s| s.to_str()) {
633 // if parent_name.eq_ignore_ascii_case("src") {
634 // // If candidate is src/main.rs, take the parent of "src".
635 // parent_dir.parent()
636 // .and_then(|proj_dir| proj_dir.file_name())
637 // .and_then(|s| s.to_str())
638 // .map(|s| s.to_string())
639 // .unwrap_or(candidate_stem.clone())
640 // } else if parent_name.eq_ignore_ascii_case("examples") {
641 // // If candidate is in the examples folder (e.g. examples/main.rs),
642 // // use the candidate's parent folder's name.
643 // candidate.parent()
644 // .and_then(|p| p.file_name())
645 // .and_then(|s| s.to_str())
646 // .map(|s| s.to_string())
647 // .unwrap_or(candidate_stem.clone())
648 // } else {
649 // // Fall back to the candidate_stem if no special case matches.
650 // candidate_stem.clone()
651 // }
652 // } else {
653 // candidate_stem.clone()
654 // }
655 // } else {
656 // candidate_stem.clone()
657 // }
658 // } else {
659 // candidate_stem.clone()
660 // };
661 // let name = if candidate_stem.clone() == "main" {
662 // // Read the manifest contents.
663 // let manifest_contents = fs::read_to_string(manifest_path).unwrap_or_default();
664 // if let Ok(manifest_toml) = manifest_contents.parse::<toml::Value>() {
665 // // Look for any [[bin]] entries.
666 // if let Some(bins) = manifest_toml.get("bin").and_then(|v| v.as_array()) {
667 // if let Some(bin_name) = bins.iter().find_map(|bin| {
668 // if let Some(path_str) = bin.get("path").and_then(|p| p.as_str()) {
669 // if path_str == "src/bin/main.rs" {
670 // return bin.get("name").and_then(|n| n.as_str()).map(|s| s.to_string());
671 // }
672 // }
673 // None
674 // }) {
675 // // Found a bin with the matching path; use its name.
676 // bin_name
677 // } else if let Some(pkg) = manifest_toml.get("package") {
678 // // No matching bin entry, so use the package name.
679 // pkg.get("name").and_then(|n| n.as_str()).unwrap_or(&candidate_stem).to_string()
680 // } else {
681 // candidate_stem.to_string()
682 // }
683 // } else if let Some(pkg) = manifest_toml.get("package") {
684 // // No [[bin]] section; use the package name.
685 // pkg.get("name").and_then(|n| n.as_str()).unwrap_or(&candidate_stem).to_string()
686 // } else {
687 // candidate_stem.to_string()
688 // }
689 // } else {
690 // candidate_stem.to_string()
691 // }
692 // } else {
693 // candidate_stem.to_string()
694 // };
695 let mut target = CargoTarget {
696 name: name.clone(),
697 display_name: name,
698 manifest_path: new_manifest.to_path_buf(),
699 kind,
700 extended,
701 toml_specified: false,
702 origin: Some(TargetOrigin::SingleFile(candidate)),
703 };
704 // Call the method to update name based on the candidate and manifest.
705 target.figure_main_name();
706 Some(target)
707 }
708 /// Returns a refined CargoTarget based on its file contents and location.
709 /// This function is pure; it takes an immutable CargoTarget and returns a new one.
710 /// If the target's origin is either SingleFile or DefaultBinary, it reads the file and uses
711 /// `determine_target_kind` to update the kind accordingly.
712 pub fn refined_target(target: &CargoTarget) -> CargoTarget {
713 let mut refined = target.clone();
714
715 // Operate only if the target has a file to inspect.
716 let file_path = match &refined.origin {
717 Some(TargetOrigin::SingleFile(path)) | Some(TargetOrigin::DefaultBinary(path)) => path,
718 _ => return refined,
719 };
720
721 let file_path = fs::canonicalize(&file_path).unwrap_or(file_path.to_path_buf());
722 let file_contents = std::fs::read_to_string(&file_path).unwrap_or_default();
723
724 let (new_kind, new_manifest) = crate::e_discovery::determine_target_kind_and_manifest(
725 &refined.manifest_path,
726 &file_path,
727 &file_contents,
728 refined.is_example(),
729 refined.extended,
730 Some(refined.kind),
731 );
732 refined.kind = new_kind;
733 refined.manifest_path = new_manifest;
734 refined.figure_main_name();
735 refined
736 }
737
738 /// Expands a subproject CargoTarget into multiple runnable targets.
739 ///
740 /// If the given target's origin is a subproject (i.e. its Cargo.toml is in a subfolder),
741 /// this function loads that Cargo.toml and uses `get_runnable_targets` to discover its runnable targets.
742 /// It then flattens and returns them as a single `Vec<CargoTarget>`.
743 pub fn expand_subproject(target: &CargoTarget) -> Result<Vec<CargoTarget>> {
744 // Ensure the target is a subproject.
745 if let Some(TargetOrigin::SubProject(sub_manifest)) = &target.origin {
746 // Use get_runnable_targets to get targets defined in the subproject.
747 let (bins, examples, benches, tests) =
748 crate::e_manifest::get_runnable_targets(sub_manifest).with_context(|| {
749 format!(
750 "Failed to get runnable targets from {}",
751 sub_manifest.display()
752 )
753 })?;
754 let mut sub_targets = Vec::new();
755 sub_targets.extend(bins);
756 sub_targets.extend(examples);
757 sub_targets.extend(benches);
758 sub_targets.extend(tests);
759
760 // Optionally mark these targets as extended.
761 for t in &mut sub_targets {
762 t.extended = true;
763 match t.kind {
764 TargetKind::Example => t.kind = TargetKind::ExtendedExample,
765 TargetKind::Binary => t.kind = TargetKind::ExtendedBinary,
766 _ => {} // For other kinds, you may leave them unchanged.
767 }
768 }
769 Ok(sub_targets)
770 } else {
771 // If the target is not a subproject, return an empty vector.
772 Ok(vec![])
773 }
774 }
775
776 /// Expands subproject targets in the given map.
777 /// For every target with a SubProject origin, this function removes the original target,
778 /// expands it using `expand_subproject`, and then inserts the expanded targets.
779 /// The expanded targets have their display names modified to include the original folder name as a prefix.
780 /// This version replaces any existing target with the same key.
781 pub fn expand_subprojects_in_place(
782 targets_map: &mut HashMap<(String, String), CargoTarget>,
783 ) -> Result<()> {
784 // Collect keys for targets that are subprojects.
785 let sub_keys: Vec<(String, String)> = targets_map
786 .iter()
787 .filter_map(|(key, target)| {
788 if let Some(TargetOrigin::SubProject(_)) = target.origin {
789 Some(key.clone())
790 } else {
791 None
792 }
793 })
794 .collect();
795
796 for key in sub_keys {
797 if let Some(sub_target) = targets_map.remove(&key) {
798 // Expand the subproject target.
799 let expanded_targets = Self::expand_subproject(&sub_target)?;
800 for mut new_target in expanded_targets {
801 // Update the display name to include the subproject folder name.
802 // For example, if sub_target.display_name was "foo" and new_target.name is "bar",
803 // the new display name becomes "foo > bar".
804 new_target.display_name =
805 format!("{} > {}", sub_target.display_name, new_target.name);
806 // Create a key for the expanded target.
807 let new_key = Self::target_key(&new_target);
808 // Replace any existing target with the same key.
809 targets_map.insert(new_key, new_target);
810 }
811 }
812 }
813 Ok(())
814 }
815 // /// Expands subproject targets in `targets`. Any target whose origin is a SubProject
816 // /// is replaced by the targets returned by `expand_subproject`. If the expansion fails,
817 // /// you can choose to log the error and keep the original target, or remove it.
818 // pub fn expand_subprojects_in_place(
819 // targets_map: &mut HashMap<(String, String), CargoTarget>
820 // ) -> anyhow::Result<()> {
821 // // Collect keys for subproject targets.
822 // let sub_keys: Vec<(String, String)> = targets_map
823 // .iter()
824 // .filter_map(|(key, target)| {
825 // if let Some(crate::e_target::TargetOrigin::SubProject(_)) = target.origin {
826 // Some(key.clone())
827 // } else {
828 // None
829 // }
830 // })
831 // .collect();
832
833 // // For each subproject target, remove it from the map, expand it, and insert the new targets.
834 // for key in sub_keys {
835 // if let Some(sub_target) = targets_map.remove(&key) {
836 // let expanded = Self::expand_subproject(&sub_target)?;
837 // for new_target in expanded {
838 // let new_key = CargoTarget::target_key(&new_target);
839 // targets_map.entry(new_key).or_insert(new_target);
840 // }
841 // }
842 // }
843 // Ok(())
844 // }
845
846 /// Creates a unique key for a target based on its manifest path and name.
847 pub fn target_key(target: &CargoTarget) -> (String, String) {
848 let manifest = target
849 .manifest_path
850 .canonicalize()
851 .unwrap_or_else(|_| target.manifest_path.clone())
852 .to_string_lossy()
853 .into_owned();
854 let name = target.name.clone();
855 (manifest, name)
856 }
857
858 /// Expands a subproject target into multiple targets and inserts them into the provided HashMap,
859 /// using (manifest, name) as a key to avoid duplicates.
860 pub fn expand_subproject_into_map(
861 target: &CargoTarget,
862 map: &mut std::collections::HashMap<(String, String), CargoTarget>,
863 ) -> Result<(), Box<dyn std::error::Error>> {
864 // Only operate if the target is a subproject.
865 if let Some(crate::e_target::TargetOrigin::SubProject(sub_manifest)) = &target.origin {
866 // Discover targets in the subproject.
867 let (bins, examples, benches, tests) =
868 crate::e_manifest::get_runnable_targets(sub_manifest)?;
869 let mut new_targets = Vec::new();
870 new_targets.extend(bins);
871 new_targets.extend(examples);
872 new_targets.extend(benches);
873 new_targets.extend(tests);
874 // Mark these targets as extended.
875 for t in &mut new_targets {
876 t.extended = true;
877 }
878 // Insert each new target if not already present.
879 for new in new_targets {
880 let key = CargoTarget::target_key(&new);
881 map.entry(key).or_insert(new.clone());
882 }
883 }
884 Ok(())
885 }
886
887 /// Returns true if the target is an example.
888 pub fn is_example(&self) -> bool {
889 matches!(
890 self.kind,
891 TargetKind::Example
892 | TargetKind::UnknownExample
893 | TargetKind::UnknownExtendedExample
894 | TargetKind::ExtendedExample
895 | TargetKind::ManifestDioxusExample
896 | TargetKind::ManifestTauriExample
897 )
898 }
899}
900
901/// Returns the "depth" of a path, i.e. the number of components.
902pub fn path_depth(path: &Path) -> usize {
903 path.components().count()
904}
905
906/// Deduplicates targets that share the same (name, origin key). If duplicates are found,
907/// the target with the manifest path of greater depth is kept.
908pub fn dedup_targets(targets: Vec<CargoTarget>) -> Vec<CargoTarget> {
909 let mut grouped: HashMap<(String, Option<String>), CargoTarget> = HashMap::new();
910
911 for target in targets {
912 // We'll group targets by (target.name, origin_key)
913 // Create an origin key if available by canonicalizing the origin path.
914 let origin_key = target.origin.as_ref().and_then(|origin| match origin {
915 TargetOrigin::SingleFile(path)
916 | TargetOrigin::DefaultBinary(path)
917 | TargetOrigin::SubProject(path) => path
918 .canonicalize()
919 .ok()
920 .map(|p| p.to_string_lossy().into_owned()),
921 _ => None,
922 });
923 let key = (target.name.clone(), origin_key);
924
925 grouped
926 .entry(key)
927 .and_modify(|existing| {
928 let current_depth = path_depth(&target.manifest_path);
929 let existing_depth = path_depth(&existing.manifest_path);
930 // If the current target's manifest path is deeper, replace the existing target.
931 if current_depth > existing_depth {
932 *existing = target.clone();
933 }
934 })
935 .or_insert(target);
936 }
937
938 grouped.into_values().collect()
939}