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            } else {
378                match &self.origin {
379                    Some(TargetOrigin::DefaultBinary(_path)) => {
380                        // Check for any [package] section.
381                        if let Some(pkg) = manifest_toml.get("package") {
382                            trace!("Found [package] section in manifest");
383                            if let Some(name_value) = pkg.get("name").and_then(|v| v.as_str()) {
384                                trace!("Using package name from manifest: {}", name_value);
385                                name = name_value.to_string();
386                                candidate_stem = name.clone();
387                            } else {
388                                trace!("No package name found in manifest; keeping name: {}", name);
389                            }
390                        }
391                    }
392                    _ => {}
393                };
394            }
395
396            // if let Some(bins) = manifest_toml.get("bin").and_then(|v| v.as_array()) {
397            //     trace!("Found {} [[bin]] entries", bins.len());
398            //     // Iterate over the bin entries and use absolute paths for comparison.
399            //     if let Some(bin_name) = bins.iter().find_map(|bin| {
400            //         if let (Some(rel_path_str), Some(bn)) = (
401            //             bin.get("path").and_then(|p| p.as_str()),
402            //             bin.get("name").and_then(|n| n.as_str()),
403            //         ) {
404            //             // Construct the expected absolute path for the candidate file.
405            //             let manifest_parent = self.manifest_path.parent().unwrap_or_else(|| Path::new(""));
406            //             let expected_path = fs::canonicalize(manifest_parent.join(rel_path_str)).ok()?;
407            //             let candidate_abs = fs::canonicalize(candidate).ok()?;
408            //             trace!(
409            //                 "{} Expected candidate absolute path: {:?}, actual candidate absolute path: {:?}",
410            //                 candidate.display(),
411            //                 expected_path,
412            //                 candidate_abs
413            //             );
414            //             if expected_path == candidate_abs {
415            //                 trace!(
416            //                     "{} Found matching bin with name: {}",
417            //                     candidate.display(),
418            //                     bn
419            //                 );
420            //                 return Some(bn.to_string());
421            //             }
422            //         }
423            //         None
424            //     }) {
425            //         trace!("{} Using bin name from manifest: {}", candidate.display(), bin_name);
426            //         name = bin_name;
427            //     }
428            //}
429        } else {
430            trace!("Failed to open manifest {:?}", &self.manifest_path);
431            trace!("Failed to parse manifest TOML; keeping name: {}", name);
432        }
433
434        // Only if the candidate stem is "main", apply folder-based logic after manifest processing.
435        if candidate_stem == "main" {
436            let folder_name = if let Some(parent_dir) = candidate.parent() {
437                if let Some(parent_name) = parent_dir.file_name().and_then(|s| s.to_str()) {
438                    trace!("Candidate parent folder: {}", parent_name);
439                    if parent_name.eq_ignore_ascii_case("src")
440                        || parent_name.eq_ignore_ascii_case("src-tauri")
441                    {
442                        // If candidate is src/main.rs, take the parent of "src".
443                        let p = parent_dir
444                            .parent()
445                            .and_then(|proj_dir| proj_dir.file_name())
446                            .and_then(|s| s.to_str())
447                            .map(|s| s.to_string())
448                            .unwrap_or(candidate_stem.clone());
449                        if p.eq("src-tauri") {
450                            let maybe_name = parent_dir
451                                .parent()
452                                .and_then(|proj_dir| proj_dir.parent())
453                                .and_then(|proj_dir| proj_dir.file_name())
454                                .and_then(|s| s.to_str())
455                                .map(String::from);
456                            match maybe_name {
457                                Some(name) => name,
458                                None => candidate_stem.clone(),
459                            }
460                        } else {
461                            p
462                        }
463                    } else if parent_name.eq_ignore_ascii_case("examples") {
464                        // If candidate is in an examples folder, use the candidate's parent folder's name.
465                        candidate
466                            .parent()
467                            .and_then(|p| p.file_name())
468                            .and_then(|s| s.to_str())
469                            .map(|s| s.to_string())
470                            .unwrap_or(candidate_stem.clone())
471                    } else {
472                        parent_name.into()
473                    }
474                } else {
475                    candidate_stem.clone()
476                }
477            } else {
478                candidate_stem.clone()
479            };
480            trace!("Folder-based name: {}-{}", candidate.display(), folder_name);
481            // Only override if the folder-based name is different from "main".
482            if folder_name != "main" {
483                name = folder_name;
484            }
485        }
486
487        trace!("Final determined name: {}", name);
488        if name.eq("main") {
489            panic!("Name is main");
490        }
491        if is_toml_specified {
492            self.toml_specified = true;
493        }
494        self.name = name.clone();
495        self.display_name = name;
496    }
497    /// Constructs a CargoTarget from a folder by trying to locate a runnable source file.
498    ///
499    /// The function attempts the following candidate paths in order:
500    /// 1. A file named `<folder_name>.rs` in the folder.
501    /// 2. `src/main.rs` inside the folder.
502    /// 3. `main.rs` at the folder root.
503    /// 4. Otherwise, it scans the folder for any `.rs` file containing `"fn main"`.
504    ///
505    /// Once a candidate is found, it reads its contents and calls `determine_target_kind`
506    /// to refine the target kind based on Tauri or Dioxus markers. The `extended` flag
507    /// indicates whether the target should be marked as extended (for instance, if the folder
508    /// is a subdirectory of the primary "examples" or "bin" folder).
509    ///
510    /// Returns Some(CargoTarget) if a runnable file is found, or None otherwise.
511    pub fn from_folder(
512        folder: &Path,
513        manifest_path: &Path,
514        example: bool,
515        _extended: bool,
516    ) -> Option<Self> {
517        // If the folder contains its own Cargo.toml, treat it as a subproject.
518        let sub_manifest = folder.join("Cargo.toml");
519        if sub_manifest.exists() {
520            // Use the folder's name as the candidate target name.
521            let folder_name = folder.file_name()?.to_string_lossy().to_string();
522            // Determine the display name from the parent folder.
523            let display_name = if let Some(parent) = folder.parent() {
524                let parent_name = parent.file_name()?.to_string_lossy();
525                if parent_name == folder_name {
526                    // If the parent's name equals the folder's name, try using the grandparent.
527                    if let Some(grandparent) = parent.parent() {
528                        grandparent.file_name()?.to_string_lossy().to_string()
529                    } else {
530                        folder_name.clone()
531                    }
532                } else {
533                    parent_name.to_string()
534                }
535            } else {
536                folder_name.clone()
537            };
538
539            let sub_manifest =
540                fs::canonicalize(&sub_manifest).unwrap_or(sub_manifest.to_path_buf());
541            trace!("Subproject found: {}", sub_manifest.display());
542            trace!("{}", &folder_name);
543            return Some(CargoTarget {
544                name: folder_name.clone(),
545                display_name,
546                manifest_path: sub_manifest.clone(),
547                // For a subproject, we initially mark it as Manifest;
548                // later refinement may resolve it further.
549                kind: TargetKind::Manifest,
550                toml_specified: true,
551                extended: true,
552                origin: Some(TargetOrigin::SubProject(sub_manifest)),
553            });
554        }
555        // Extract the folder's name.
556        let folder_name = folder.file_name()?.to_str()?;
557
558        /// Returns Some(candidate) only if the file exists and its contents contain "fn main".
559        fn candidate_with_main(candidate: PathBuf) -> Option<PathBuf> {
560            if candidate.exists() {
561                let contents = fs::read_to_string(&candidate).unwrap_or_default();
562                if contents.contains("fn main") {
563                    return Some(candidate);
564                }
565            }
566            None
567        }
568
569        // In your from_folder function, for example:
570        let candidate = if let Some(candidate) =
571            candidate_with_main(folder.join(format!("{}.rs", folder_name)))
572        {
573            candidate
574        } else if let Some(candidate) = candidate_with_main(folder.join("src/main.rs")) {
575            candidate
576        } else if let Some(candidate) = candidate_with_main(folder.join("main.rs")) {
577            candidate
578        } else {
579            // Otherwise, scan the folder for any .rs file containing "fn main"
580            let mut found = None;
581            if let Ok(entries) = fs::read_dir(folder) {
582                for entry in entries.flatten() {
583                    let path = entry.path();
584                    if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("rs") {
585                        if let Some(candidate) = candidate_with_main(path) {
586                            found = Some(candidate);
587                            break;
588                        }
589                    }
590                }
591            }
592            found?
593        };
594
595        let candidate = fs::canonicalize(&candidate).unwrap_or(candidate.to_path_buf());
596        // Compute the extended flag based on the candidate file location.
597        let extended = crate::e_discovery::is_extended_target(manifest_path, &candidate);
598
599        // Read the candidate file's contents.
600        let file_contents = std::fs::read_to_string(&candidate).unwrap_or_default();
601
602        // Use our helper to determine if any special configuration applies.
603        let (kind, new_manifest) = crate::e_discovery::determine_target_kind_and_manifest(
604            manifest_path,
605            &candidate,
606            &file_contents,
607            example,
608            extended,
609            None,
610        );
611        if kind == TargetKind::Unknown {
612            return None;
613        }
614
615        // Determine the candidate file's stem in lowercase.
616        let name = candidate.file_stem()?.to_str()?.to_lowercase();
617        //         let name = if candidate_stem == "main" {
618        //     if let Some(parent_dir) = candidate.parent() {
619        //         if let Some(parent_name) = parent_dir.file_name().and_then(|s| s.to_str()) {
620        //             if parent_name.eq_ignore_ascii_case("src") {
621        //                 // If candidate is src/main.rs, take the parent of "src".
622        //                 parent_dir.parent()
623        //                     .and_then(|proj_dir| proj_dir.file_name())
624        //                     .and_then(|s| s.to_str())
625        //                     .map(|s| s.to_string())
626        //                     .unwrap_or(candidate_stem.clone())
627        //             } else if parent_name.eq_ignore_ascii_case("examples") {
628        //                 // If candidate is in the examples folder (e.g. examples/main.rs),
629        //                 // use the candidate's parent folder's name.
630        //                 candidate.parent()
631        //                     .and_then(|p| p.file_name())
632        //                     .and_then(|s| s.to_str())
633        //                     .map(|s| s.to_string())
634        //                     .unwrap_or(candidate_stem.clone())
635        //             } else {
636        //                 // Fall back to the candidate_stem if no special case matches.
637        //                 candidate_stem.clone()
638        //             }
639        //         } else {
640        //             candidate_stem.clone()
641        //         }
642        //     } else {
643        //         candidate_stem.clone()
644        //     }
645        // } else {
646        //     candidate_stem.clone()
647        // };
648        // let name = if candidate_stem.clone() == "main" {
649        //     // Read the manifest contents.
650        //     let manifest_contents = fs::read_to_string(manifest_path).unwrap_or_default();
651        //     if let Ok(manifest_toml) = manifest_contents.parse::<toml::Value>() {
652        //         // Look for any [[bin]] entries.
653        //         if let Some(bins) = manifest_toml.get("bin").and_then(|v| v.as_array()) {
654        //             if let Some(bin_name) = bins.iter().find_map(|bin| {
655        //                 if let Some(path_str) = bin.get("path").and_then(|p| p.as_str()) {
656        //                     if path_str == "src/bin/main.rs" {
657        //                         return bin.get("name").and_then(|n| n.as_str()).map(|s| s.to_string());
658        //                     }
659        //                 }
660        //                 None
661        //             }) {
662        //                 // Found a bin with the matching path; use its name.
663        //                 bin_name
664        //             } else if let Some(pkg) = manifest_toml.get("package") {
665        //                 // No matching bin entry, so use the package name.
666        //                 pkg.get("name").and_then(|n| n.as_str()).unwrap_or(&candidate_stem).to_string()
667        //             } else {
668        //                 candidate_stem.to_string()
669        //             }
670        //         } else if let Some(pkg) = manifest_toml.get("package") {
671        //             // No [[bin]] section; use the package name.
672        //             pkg.get("name").and_then(|n| n.as_str()).unwrap_or(&candidate_stem).to_string()
673        //         } else {
674        //             candidate_stem.to_string()
675        //         }
676        //     } else {
677        //         candidate_stem.to_string()
678        //     }
679        // } else {
680        //     candidate_stem.to_string()
681        // };
682        let mut target = CargoTarget {
683            name: name.clone(),
684            display_name: name,
685            manifest_path: new_manifest.to_path_buf(),
686            kind,
687            extended,
688            toml_specified: false,
689            origin: Some(TargetOrigin::SingleFile(candidate)),
690        };
691        // Call the method to update name based on the candidate and manifest.
692        target.figure_main_name();
693        Some(target)
694    }
695    /// Returns a refined CargoTarget based on its file contents and location.
696    /// This function is pure; it takes an immutable CargoTarget and returns a new one.
697    /// If the target's origin is either SingleFile or DefaultBinary, it reads the file and uses
698    /// `determine_target_kind` to update the kind accordingly.
699    pub fn refined_target(target: &CargoTarget) -> CargoTarget {
700        let mut refined = target.clone();
701
702        // Operate only if the target has a file to inspect.
703        let file_path = match &refined.origin {
704            Some(TargetOrigin::SingleFile(path)) | Some(TargetOrigin::DefaultBinary(path)) => path,
705            _ => return refined,
706        };
707
708        let file_path = fs::canonicalize(&file_path).unwrap_or(file_path.to_path_buf());
709        let file_contents = std::fs::read_to_string(&file_path).unwrap_or_default();
710
711        let (new_kind, new_manifest) = crate::e_discovery::determine_target_kind_and_manifest(
712            &refined.manifest_path,
713            &file_path,
714            &file_contents,
715            refined.is_example(),
716            refined.extended,
717            Some(refined.kind),
718        );
719        refined.kind = new_kind;
720        refined.manifest_path = new_manifest;
721        refined.figure_main_name();
722        refined
723    }
724
725    /// Expands a subproject CargoTarget into multiple runnable targets.
726    ///
727    /// If the given target's origin is a subproject (i.e. its Cargo.toml is in a subfolder),
728    /// this function loads that Cargo.toml and uses `get_runnable_targets` to discover its runnable targets.
729    /// It then flattens and returns them as a single `Vec<CargoTarget>`.
730    pub fn expand_subproject(target: &CargoTarget) -> Result<Vec<CargoTarget>> {
731        // Ensure the target is a subproject.
732        if let Some(TargetOrigin::SubProject(sub_manifest)) = &target.origin {
733            // Use get_runnable_targets to get targets defined in the subproject.
734            let (bins, examples, benches, tests) =
735                crate::e_manifest::get_runnable_targets(sub_manifest).with_context(|| {
736                    format!(
737                        "Failed to get runnable targets from {}",
738                        sub_manifest.display()
739                    )
740                })?;
741            let mut sub_targets = Vec::new();
742            sub_targets.extend(bins);
743            sub_targets.extend(examples);
744            sub_targets.extend(benches);
745            sub_targets.extend(tests);
746
747            // Optionally mark these targets as extended.
748            for t in &mut sub_targets {
749                t.extended = true;
750                match t.kind {
751                    TargetKind::Example => t.kind = TargetKind::ExtendedExample,
752                    TargetKind::Binary => t.kind = TargetKind::ExtendedBinary,
753                    _ => {} // For other kinds, you may leave them unchanged.
754                }
755            }
756            Ok(sub_targets)
757        } else {
758            // If the target is not a subproject, return an empty vector.
759            Ok(vec![])
760        }
761    }
762
763    /// Expands subproject targets in the given map.
764    /// For every target with a SubProject origin, this function removes the original target,
765    /// expands it using `expand_subproject`, and then inserts the expanded targets.
766    /// The expanded targets have their display names modified to include the original folder name as a prefix.
767    /// This version replaces any existing target with the same key.
768    pub fn expand_subprojects_in_place(
769        targets_map: &mut HashMap<(String, String), CargoTarget>,
770    ) -> Result<()> {
771        // Collect keys for targets that are subprojects.
772        let sub_keys: Vec<(String, String)> = targets_map
773            .iter()
774            .filter_map(|(key, target)| {
775                if let Some(TargetOrigin::SubProject(_)) = target.origin {
776                    Some(key.clone())
777                } else {
778                    None
779                }
780            })
781            .collect();
782
783        for key in sub_keys {
784            if let Some(sub_target) = targets_map.remove(&key) {
785                // Expand the subproject target.
786                let expanded_targets = Self::expand_subproject(&sub_target)?;
787                for mut new_target in expanded_targets {
788                    // Update the display name to include the subproject folder name.
789                    // For example, if sub_target.display_name was "foo" and new_target.name is "bar",
790                    // the new display name becomes "foo > bar".
791                    new_target.display_name =
792                        format!("{} > {}", sub_target.display_name, new_target.name);
793                    // Create a key for the expanded target.
794                    let new_key = Self::target_key(&new_target);
795                    // Replace any existing target with the same key.
796                    targets_map.insert(new_key, new_target);
797                }
798            }
799        }
800        Ok(())
801    }
802    // /// Expands subproject targets in `targets`. Any target whose origin is a SubProject
803    // /// is replaced by the targets returned by `expand_subproject`. If the expansion fails,
804    // /// you can choose to log the error and keep the original target, or remove it.
805    // pub fn expand_subprojects_in_place(
806    //     targets_map: &mut HashMap<(String, String), CargoTarget>
807    // ) -> anyhow::Result<()> {
808    //     // Collect keys for subproject targets.
809    //     let sub_keys: Vec<(String, String)> = targets_map
810    //         .iter()
811    //         .filter_map(|(key, target)| {
812    //             if let Some(crate::e_target::TargetOrigin::SubProject(_)) = target.origin {
813    //                 Some(key.clone())
814    //             } else {
815    //                 None
816    //             }
817    //         })
818    //         .collect();
819
820    //     // For each subproject target, remove it from the map, expand it, and insert the new targets.
821    //     for key in sub_keys {
822    //         if let Some(sub_target) = targets_map.remove(&key) {
823    //             let expanded = Self::expand_subproject(&sub_target)?;
824    //             for new_target in expanded {
825    //                 let new_key = CargoTarget::target_key(&new_target);
826    //                 targets_map.entry(new_key).or_insert(new_target);
827    //             }
828    //         }
829    //     }
830    //     Ok(())
831    // }
832
833    /// Creates a unique key for a target based on its manifest path and name.
834    pub fn target_key(target: &CargoTarget) -> (String, String) {
835        let manifest = target
836            .manifest_path
837            .canonicalize()
838            .unwrap_or_else(|_| target.manifest_path.clone())
839            .to_string_lossy()
840            .into_owned();
841        let name = target.name.clone();
842        (manifest, name)
843    }
844
845    /// Expands a subproject target into multiple targets and inserts them into the provided HashMap,
846    /// using (manifest, name) as a key to avoid duplicates.
847    pub fn expand_subproject_into_map(
848        target: &CargoTarget,
849        map: &mut std::collections::HashMap<(String, String), CargoTarget>,
850    ) -> Result<(), Box<dyn std::error::Error>> {
851        // Only operate if the target is a subproject.
852        if let Some(crate::e_target::TargetOrigin::SubProject(sub_manifest)) = &target.origin {
853            // Discover targets in the subproject.
854            let (bins, examples, benches, tests) =
855                crate::e_manifest::get_runnable_targets(sub_manifest)?;
856            let mut new_targets = Vec::new();
857            new_targets.extend(bins);
858            new_targets.extend(examples);
859            new_targets.extend(benches);
860            new_targets.extend(tests);
861            // Mark these targets as extended.
862            for t in &mut new_targets {
863                t.extended = true;
864            }
865            // Insert each new target if not already present.
866            for new in new_targets {
867                let key = CargoTarget::target_key(&new);
868                map.entry(key).or_insert(new.clone());
869            }
870        }
871        Ok(())
872    }
873
874    /// Returns true if the target is an example.
875    pub fn is_example(&self) -> bool {
876        matches!(
877            self.kind,
878            TargetKind::Example
879                | TargetKind::ExtendedExample
880                | TargetKind::ManifestDioxusExample
881                | TargetKind::ManifestTauriExample
882        )
883    }
884}
885
886/// Returns the "depth" of a path, i.e. the number of components.
887pub fn path_depth(path: &Path) -> usize {
888    path.components().count()
889}
890
891/// Deduplicates targets that share the same (name, origin key). If duplicates are found,
892/// the target with the manifest path of greater depth is kept.
893pub fn dedup_targets(targets: Vec<CargoTarget>) -> Vec<CargoTarget> {
894    let mut grouped: HashMap<(String, Option<String>), CargoTarget> = HashMap::new();
895
896    for target in targets {
897        // We'll group targets by (target.name, origin_key)
898        // Create an origin key if available by canonicalizing the origin path.
899        let origin_key = target.origin.as_ref().and_then(|origin| match origin {
900            TargetOrigin::SingleFile(path)
901            | TargetOrigin::DefaultBinary(path)
902            | TargetOrigin::SubProject(path) => path
903                .canonicalize()
904                .ok()
905                .map(|p| p.to_string_lossy().into_owned()),
906            _ => None,
907        });
908        let key = (target.name.clone(), origin_key);
909
910        grouped
911            .entry(key)
912            .and_modify(|existing| {
913                let current_depth = path_depth(&target.manifest_path);
914                let existing_depth = path_depth(&existing.manifest_path);
915                // If the current target's manifest path is deeper, replace the existing target.
916                if current_depth > existing_depth {
917                    *existing = target.clone();
918                }
919            })
920            .or_insert(target);
921    }
922
923    grouped.into_values().collect()
924}