cargo_e/
e_target.rs

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