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