cargo_e/
e_target.rs

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