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                        // If candidate is in an examples folder, use the candidate's parent folder's name.
541                        candidate
542                            .parent()
543                            .and_then(|p| p.file_name())
544                            .and_then(|s| s.to_str())
545                            .map(|s| s.to_string())
546                            .unwrap_or(candidate_stem.clone())
547                    } else {
548                        parent_name.into()
549                    }
550                } else {
551                    candidate_stem.clone()
552                }
553            } else {
554                candidate_stem.clone()
555            };
556            trace!("Folder-based name: {}-{}", candidate.display(), folder_name);
557            // Only override if the folder-based name is different from "main".
558            if folder_name != "main" {
559                name = folder_name;
560            }
561        }
562
563        trace!("Final determined name: {}", name);
564        if name.eq("main") {
565            panic!("Name is main");
566        }
567        if is_toml_specified {
568            self.toml_specified = true;
569        }
570        self.name = name.clone();
571        self.display_name = name;
572        Ok(())
573    }
574    /// Constructs a CargoTarget from a folder by trying to locate a runnable source file.
575    ///
576    /// The function attempts the following candidate paths in order:
577    /// 1. A file named `<folder_name>.rs` in the folder.
578    /// 2. `src/main.rs` inside the folder.
579    /// 3. `main.rs` at the folder root.
580    /// 4. Otherwise, it scans the folder for any `.rs` file containing `"fn main"`.
581    ///
582    /// Once a candidate is found, it reads its contents and calls `determine_target_kind`
583    /// to refine the target kind based on Tauri or Dioxus markers. The `extended` flag
584    /// indicates whether the target should be marked as extended (for instance, if the folder
585    /// is a subdirectory of the primary "examples" or "bin" folder).
586    ///
587    /// Returns Some(CargoTarget) if a runnable file is found, or None otherwise.
588    pub fn from_folder(
589        folder: &Path,
590        manifest_path: &Path,
591        example: bool,
592        _extended: bool,
593    ) -> Option<Self> {
594        // If the folder contains its own Cargo.toml, treat it as a subproject.
595        let sub_manifest = folder.join("Cargo.toml");
596        if sub_manifest.exists() {
597            // Use the folder's name as the candidate target name.
598            let folder_name = folder.file_name()?.to_string_lossy().to_string();
599            // Determine the display name from the parent folder.
600            let display_name = if let Some(parent) = folder.parent() {
601                let parent_name = parent.file_name()?.to_string_lossy();
602                if parent_name == folder_name {
603                    // If the parent's name equals the folder's name, try using the grandparent.
604                    if let Some(grandparent) = parent.parent() {
605                        grandparent.file_name()?.to_string_lossy().to_string()
606                    } else {
607                        folder_name.clone()
608                    }
609                } else {
610                    parent_name.to_string()
611                }
612            } else {
613                folder_name.clone()
614            };
615
616            let sub_manifest =
617                fs::canonicalize(&sub_manifest).unwrap_or(sub_manifest.to_path_buf());
618            trace!("Subproject found: {}", sub_manifest.display());
619            trace!("{}", &folder_name);
620            return Some(CargoTarget {
621                name: folder_name.clone(),
622                display_name,
623                manifest_path: sub_manifest.clone(),
624                // For a subproject, we initially mark it as Manifest;
625                // later refinement may resolve it further.
626                kind: TargetKind::Manifest,
627                toml_specified: false,
628                extended: true,
629                origin: Some(TargetOrigin::SubProject(sub_manifest)),
630            });
631        }
632        // Extract the folder's name.
633        let folder_name = folder.file_name()?.to_str()?;
634
635        /// Returns Some(candidate) only if the file exists and its contents contain "fn main".
636        fn candidate_with_main(candidate: PathBuf) -> Option<PathBuf> {
637            if candidate.exists() {
638                let contents = fs::read_to_string(&candidate).unwrap_or_default();
639                if contents.contains("fn main") {
640                    return Some(candidate);
641                }
642            }
643            None
644        }
645
646        // In your from_folder function, for example:
647        let candidate = if let Some(candidate) =
648            candidate_with_main(folder.join(format!("{}.rs", folder_name)))
649        {
650            candidate
651        } else if let Some(candidate) = candidate_with_main(folder.join("src/main.rs")) {
652            candidate
653        } else if let Some(candidate) = candidate_with_main(folder.join("main.rs")) {
654            candidate
655        } else {
656            // Otherwise, scan the folder for any .rs file containing "fn main"
657            let mut found = None;
658            if let Ok(entries) = fs::read_dir(folder) {
659                for entry in entries.flatten() {
660                    let path = entry.path();
661                    if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("rs") {
662                        if let Some(candidate) = candidate_with_main(path) {
663                            found = Some(candidate);
664                            break;
665                        }
666                    }
667                }
668            }
669            found?
670        };
671
672        let candidate = fs::canonicalize(&candidate).unwrap_or(candidate.to_path_buf());
673        // Compute the extended flag based on the candidate file location.
674        let extended = crate::e_discovery::is_extended_target(manifest_path, &candidate);
675
676        // Read the candidate file's contents.
677        let file_contents = std::fs::read_to_string(&candidate).unwrap_or_default();
678
679        // Use our helper to determine if any special configuration applies.
680        let (kind, new_manifest) = crate::e_discovery::determine_target_kind_and_manifest(
681            manifest_path,
682            &candidate,
683            &file_contents,
684            example,
685            extended,
686            false,
687            None,
688        );
689        if kind == TargetKind::Unknown {
690            return None;
691        }
692
693        // Determine the candidate file's stem in lowercase.
694        let name = candidate.file_stem()?.to_str()?.to_lowercase();
695        //         let name = if candidate_stem == "main" {
696        //     if let Some(parent_dir) = candidate.parent() {
697        //         if let Some(parent_name) = parent_dir.file_name().and_then(|s| s.to_str()) {
698        //             if parent_name.eq_ignore_ascii_case("src") {
699        //                 // If candidate is src/main.rs, take the parent of "src".
700        //                 parent_dir.parent()
701        //                     .and_then(|proj_dir| proj_dir.file_name())
702        //                     .and_then(|s| s.to_str())
703        //                     .map(|s| s.to_string())
704        //                     .unwrap_or(candidate_stem.clone())
705        //             } else if parent_name.eq_ignore_ascii_case("examples") {
706        //                 // If candidate is in the examples folder (e.g. examples/main.rs),
707        //                 // use the candidate's parent folder's name.
708        //                 candidate.parent()
709        //                     .and_then(|p| p.file_name())
710        //                     .and_then(|s| s.to_str())
711        //                     .map(|s| s.to_string())
712        //                     .unwrap_or(candidate_stem.clone())
713        //             } else {
714        //                 // Fall back to the candidate_stem if no special case matches.
715        //                 candidate_stem.clone()
716        //             }
717        //         } else {
718        //             candidate_stem.clone()
719        //         }
720        //     } else {
721        //         candidate_stem.clone()
722        //     }
723        // } else {
724        //     candidate_stem.clone()
725        // };
726        // let name = if candidate_stem.clone() == "main" {
727        //     // Read the manifest contents.
728        //     let manifest_contents = fs::read_to_string(manifest_path).unwrap_or_default();
729        //     if let Ok(manifest_toml) = manifest_contents.parse::<toml::Value>() {
730        //         // Look for any [[bin]] entries.
731        //         if let Some(bins) = manifest_toml.get("bin").and_then(|v| v.as_array()) {
732        //             if let Some(bin_name) = bins.iter().find_map(|bin| {
733        //                 if let Some(path_str) = bin.get("path").and_then(|p| p.as_str()) {
734        //                     if path_str == "src/bin/main.rs" {
735        //                         return bin.get("name").and_then(|n| n.as_str()).map(|s| s.to_string());
736        //                     }
737        //                 }
738        //                 None
739        //             }) {
740        //                 // Found a bin with the matching path; use its name.
741        //                 bin_name
742        //             } else if let Some(pkg) = manifest_toml.get("package") {
743        //                 // No matching bin entry, so use the package name.
744        //                 pkg.get("name").and_then(|n| n.as_str()).unwrap_or(&candidate_stem).to_string()
745        //             } else {
746        //                 candidate_stem.to_string()
747        //             }
748        //         } else if let Some(pkg) = manifest_toml.get("package") {
749        //             // No [[bin]] section; use the package name.
750        //             pkg.get("name").and_then(|n| n.as_str()).unwrap_or(&candidate_stem).to_string()
751        //         } else {
752        //             candidate_stem.to_string()
753        //         }
754        //     } else {
755        //         candidate_stem.to_string()
756        //     }
757        // } else {
758        //     candidate_stem.to_string()
759        // };
760        let mut target = CargoTarget {
761            name: name.clone(),
762            display_name: name,
763            manifest_path: new_manifest.to_path_buf(),
764            kind,
765            extended,
766            toml_specified: false,
767            origin: Some(TargetOrigin::SingleFile(candidate)),
768        };
769        // Call the method to update name based on the candidate and manifest.
770        target.figure_main_name().ok();
771        Some(target)
772    }
773    /// Returns a refined CargoTarget based on its file contents and location.
774    /// This function is pure; it takes an immutable CargoTarget and returns a new one.
775    /// If the target's origin is either SingleFile or DefaultBinary, it reads the file and uses
776    /// `determine_target_kind` to update the kind accordingly.
777    pub fn refined_target(target: &CargoTarget) -> CargoTarget {
778        let mut refined = target.clone();
779
780        // if target.toml_specified {
781        //     // If the target is already specified in the manifest, return it as is.
782        //     return refined;
783        // }
784        // Operate only if the target has a file to inspect.
785        let file_path = match &refined.origin {
786            Some(TargetOrigin::SingleFile(path)) | Some(TargetOrigin::DefaultBinary(path)) => path,
787            _ => return refined,
788        };
789
790        let file_path = fs::canonicalize(&file_path).unwrap_or(file_path.to_path_buf());
791        let file_contents = std::fs::read_to_string(&file_path).unwrap_or_default();
792
793        let (new_kind, new_manifest) = crate::e_discovery::determine_target_kind_and_manifest(
794            &refined.manifest_path,
795            &file_path,
796            &file_contents,
797            refined.is_example(),
798            refined.extended,
799            refined.toml_specified,
800            Some(refined.kind),
801        );
802        if new_kind == TargetKind::ManifestDioxus {}
803        refined.kind = new_kind;
804        refined.manifest_path = new_manifest;
805        refined.figure_main_name().ok();
806        refined
807    }
808
809    /// Expands a subproject CargoTarget into multiple runnable targets.
810    ///
811    /// If the given target's origin is a subproject (i.e. its Cargo.toml is in a subfolder),
812    /// this function loads that Cargo.toml and uses `get_runnable_targets` to discover its runnable targets.
813    /// It then flattens and returns them as a single `Vec<CargoTarget>`.
814    pub fn expand_subproject(target: &CargoTarget) -> Result<Vec<CargoTarget>> {
815        // Ensure the target is a subproject.
816        if let Some(TargetOrigin::SubProject(sub_manifest)) = &target.origin {
817            // Use get_runnable_targets to get targets defined in the subproject.
818            let (bins, examples, benches, tests) =
819                crate::e_manifest::get_runnable_targets(sub_manifest).with_context(|| {
820                    format!(
821                        "Failed to get runnable targets from {}",
822                        sub_manifest.display()
823                    )
824                })?;
825            let mut sub_targets = Vec::new();
826            sub_targets.extend(bins);
827            sub_targets.extend(examples);
828            sub_targets.extend(benches);
829            sub_targets.extend(tests);
830
831            // // Optionally mark these targets as extended.
832            // for t in &mut sub_targets {
833            //     if !t.toml_specified {
834
835            //     t.extended = true;
836            //     match t.kind {
837            //         TargetKind::Example => t.kind = TargetKind::ExtendedExample,
838            //         TargetKind::Binary => t.kind = TargetKind::ExtendedBinary,
839            //         _ => {} // For other kinds, you may leave them unchanged.
840            //     }
841            //     }
842            // }
843            Ok(sub_targets)
844        } else {
845            // If the target is not a subproject, return an empty vector.
846            Ok(vec![])
847        }
848    }
849
850    pub fn expand_subprojects_in_place(
851        targets_map: &mut HashMap<(String, String), CargoTarget>,
852    ) -> Result<()> {
853        // collect subproject keys…
854        let sub_keys: Vec<_> = targets_map
855            .iter()
856            .filter_map(|(key, t)| {
857                matches!(t.origin, Some(TargetOrigin::SubProject(_))).then(|| key.clone())
858            })
859            .collect();
860        log::trace!("Subproject keys: {:?}", sub_keys);
861        for key in sub_keys {
862            if let Some(sub_target) = targets_map.remove(&key) {
863                let expanded = Self::expand_subproject(&sub_target)?;
864                for mut new_target in expanded {
865                    log::trace!(
866                        "Expanding subproject target: {} -> {}",
867                        sub_target.display_name,
868                        new_target.display_name
869                    );
870                    // carry forward the toml_specified flag from the original
871                    //    new_target.toml_specified |= sub_target.toml_specified;
872
873                    let new_key = Self::target_key(&new_target);
874
875                    match targets_map.entry(new_key) {
876                        Entry::Vacant(e) => {
877                            new_target.display_name =
878                                format!("{} > {}", sub_target.display_name, new_target.name);
879                            e.insert(new_target);
880                        }
881                        Entry::Occupied(mut e) => {
882                            // if the existing one is toml-specified, keep it
883                            if e.get().toml_specified {
884                                new_target.toml_specified = true;
885                            }
886                            e.insert(new_target);
887                        }
888                    }
889                }
890            }
891        }
892
893        Ok(())
894    }
895
896    // /// Expands subproject targets in `targets`. Any target whose origin is a SubProject
897    // /// is replaced by the targets returned by `expand_subproject`. If the expansion fails,
898    // /// you can choose to log the error and keep the original target, or remove it.
899    // pub fn expand_subprojects_in_place(
900    //     targets_map: &mut HashMap<(String, String), CargoTarget>
901    // ) -> anyhow::Result<()> {
902    //     // Collect keys for subproject targets.
903    //     let sub_keys: Vec<(String, String)> = targets_map
904    //         .iter()
905    //         .filter_map(|(key, target)| {
906    //             if let Some(crate::e_target::TargetOrigin::SubProject(_)) = target.origin {
907    //                 Some(key.clone())
908    //             } else {
909    //                 None
910    //             }
911    //         })
912    //         .collect();
913
914    //     // For each subproject target, remove it from the map, expand it, and insert the new targets.
915    //     for key in sub_keys {
916    //         if let Some(sub_target) = targets_map.remove(&key) {
917    //             let expanded = Self::expand_subproject(&sub_target)?;
918    //             for new_target in expanded {
919    //                 let new_key = CargoTarget::target_key(&new_target);
920    //                 targets_map.entry(new_key).or_insert(new_target);
921    //             }
922    //         }
923    //     }
924    //     Ok(())
925    // }
926
927    /// Creates a unique key for a target based on its manifest path and name.
928    pub fn target_key(target: &CargoTarget) -> (String, String) {
929        let manifest = target
930            .manifest_path
931            .canonicalize()
932            .unwrap_or_else(|_| target.manifest_path.clone())
933            .to_string_lossy()
934            .into_owned();
935        let name = target.name.clone();
936        (manifest, name)
937    }
938
939    /// Expands a subproject target into multiple targets and inserts them into the provided HashMap,
940    /// using (manifest, name) as a key to avoid duplicates.
941    pub fn expand_subproject_into_map(
942        target: &CargoTarget,
943        map: &mut std::collections::HashMap<(String, String), CargoTarget>,
944    ) -> Result<(), Box<dyn std::error::Error>> {
945        // Only operate if the target is a subproject.
946        if let Some(crate::e_target::TargetOrigin::SubProject(sub_manifest)) = &target.origin {
947            // Discover targets in the subproject.
948            let (bins, examples, benches, tests) =
949                crate::e_manifest::get_runnable_targets(sub_manifest)?;
950            let mut new_targets = Vec::new();
951            new_targets.extend(bins);
952            new_targets.extend(examples);
953            new_targets.extend(benches);
954            new_targets.extend(tests);
955
956            // Mark these targets as extended.
957            // for t in &mut new_targets {
958            //     t.extended = true;
959            // }
960            // Insert each new target if not already present.
961            for new in new_targets {
962                let key = CargoTarget::target_key(&new);
963                if let Some(existing) = map.get_mut(&key) {
964                    // If they already specified this with --manifest-path, leave it untouched:
965                    if existing.toml_specified {
966                        continue;
967                    }
968                } else {
969                    map.insert(key, new);
970                }
971                // let key = CargoTarget::target_key(&new);
972                // map.entry(key).or_insert(new.clone());
973            }
974        }
975        Ok(())
976    }
977
978    /// Returns true if the target is an example.
979    pub fn is_example(&self) -> bool {
980        matches!(
981            self.kind,
982            TargetKind::Example
983                | TargetKind::UnknownExample
984                | TargetKind::UnknownExtendedExample
985                | TargetKind::ExtendedExample
986                | TargetKind::ManifestDioxusExample
987                | TargetKind::ManifestTauriExample
988        )
989    }
990}
991
992/// Deduplicates `CargoTarget` entries by `name`, applying strict priority rules.
993///
994/// Priority Rules:
995/// 1. If the incoming target's `TargetKind` is **greater than `Manifest`**, it overrides any existing lower-priority target,
996///    regardless of `TargetOrigin` (including `TomlSpecified`).
997/// 2. If both the existing and incoming targets have `TargetKind > Manifest`, prefer the one with the higher `TargetKind`.
998/// 3. If neither target is high-priority (`<= Manifest`), compare `(TargetOrigin, TargetKind)` using natural enum ordering.
999/// 4. If origin and kind are equal, prefer the target with the deeper `manifest_path`.
1000/// 5. If any target in the group has `toml_specified = true`, ensure the final target reflects this.
1001///
1002/// This guarantees deterministic, priority-driven deduplication while respecting special framework targets.
1003pub fn dedup_targets(targets: Vec<CargoTarget>) -> Vec<CargoTarget> {
1004    let mut grouped: HashMap<String, CargoTarget> = HashMap::new();
1005
1006    for target in &targets {
1007        let key = target.name.clone();
1008
1009        grouped
1010            .entry(key)
1011            .and_modify(|existing| {
1012                let target_high = target.kind > TargetKind::Manifest;
1013                let existing_high = existing.kind > TargetKind::Manifest;
1014
1015                // Rule 1: If target is high-priority (> Manifest)
1016                if target_high {
1017                    if !existing_high || target.kind > existing.kind {
1018                        let was_toml_specified = existing.toml_specified;
1019                        *existing = target.clone();
1020                        existing.toml_specified |= was_toml_specified | target.toml_specified;
1021                    }
1022                    return; // High-priority kinds dominate
1023                }
1024
1025                // Rule 2: Both kinds are normal (<= Manifest)
1026                if target.kind > existing.kind {
1027                    let was_toml_specified = existing.toml_specified;
1028                    *existing = target.clone();
1029                    existing.toml_specified |= was_toml_specified | target.toml_specified;
1030                    return;
1031                }
1032
1033                // Rule 3: If kinds are equal, compare origin
1034                if target.kind == existing.kind {
1035                    if target.origin.clone() > existing.origin.clone() {
1036                        let was_toml_specified = existing.toml_specified;
1037                        *existing = target.clone();
1038                        existing.toml_specified |= was_toml_specified | target.toml_specified;
1039                        return;
1040                    }
1041
1042                    // Rule 4: If origin is also equal, compare path depth
1043                    if target.origin == existing.origin {
1044                        if path_depth(&target.manifest_path) > path_depth(&existing.manifest_path) {
1045                            let was_toml_specified = existing.toml_specified;
1046                            *existing = target.clone();
1047                            existing.toml_specified |= was_toml_specified | target.toml_specified;
1048                        }
1049                    }
1050                }
1051                // No replacement needed if none of the conditions matched
1052            })
1053            .or_insert(target.clone());
1054    }
1055
1056    let toml_specified_names: HashSet<String> = targets
1057        .iter()
1058        .filter(|t| matches!(t.origin, Some(TargetOrigin::TomlSpecified(_))))
1059        .map(|t| t.name.clone())
1060        .collect();
1061
1062    // Update toml_specified flag based on origin analysis
1063    for target in grouped.values_mut() {
1064        if toml_specified_names.contains(&target.name) {
1065            target.toml_specified = true;
1066        }
1067    }
1068
1069    // Collect, then sort by (kind, name)
1070    let mut sorted_targets: Vec<_> = grouped.into_values().collect();
1071
1072    sorted_targets.sort_by_key(|t| (t.kind.clone(), t.name.clone()));
1073
1074    sorted_targets
1075}
1076
1077/// Calculates the depth of a path (number of components).
1078fn path_depth(path: &Path) -> usize {
1079    path.components().count()
1080}
1081
1082// /// Returns the "depth" of a path, i.e. the number of components.
1083// pub fn path_depth(path: &Path) -> usize {
1084//     path.components().count()
1085// }
1086
1087// /// Deduplicates targets that share the same (name, origin key). If duplicates are found,
1088// /// the target with the manifest path of greater depth is kept.
1089// pub fn dedup_targets(targets: Vec<CargoTarget>) -> Vec<CargoTarget> {
1090//     let mut grouped: HashMap<(String, Option<String>), CargoTarget> = HashMap::new();
1091
1092//     for target in targets {
1093//         // We'll group targets by (target.name, origin_key)
1094//         // Create an origin key if available by canonicalizing the origin path.
1095//         let origin_key = target.origin.as_ref().and_then(|origin| match origin {
1096//             TargetOrigin::SingleFile(path)
1097//             | TargetOrigin::DefaultBinary(path)
1098//             | TargetOrigin::TomlSpecified(path)
1099//             | TargetOrigin::SubProject(path) => path
1100//                 .canonicalize()
1101//                 .ok()
1102//                 .map(|p| p.to_string_lossy().into_owned()),
1103//             _ => None,
1104//         });
1105//         let key = (target.name.clone(), origin_key);
1106
1107//         grouped
1108//             .entry(key)
1109//             .and_modify(|existing| {
1110//                 let current_depth = path_depth(&target.manifest_path);
1111//                 let existing_depth = path_depth(&existing.manifest_path);
1112//                 // If the current target's manifest path is deeper, replace the existing target.
1113//                 if current_depth > existing_depth {
1114//                     println!(
1115//                         "{} {} Replacing {:?} {:?} with {:?} {:?} manifest path: {} -> {}",
1116//                         target.name,
1117//                         existing.name,
1118//                         target.kind,
1119//                         existing.kind,
1120//                         target.origin,
1121//                         existing.origin,
1122//                         existing.manifest_path.display(),
1123//                         target.manifest_path.display()
1124//                     );
1125//                     *existing = target.clone();
1126//                 }
1127//             })
1128//             .or_insert(target);
1129//     }
1130
1131//     grouped.into_values().collect()
1132// }