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