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