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