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