cargo_e/
e_target.rs

1// src/e_target.rs
2use anyhow::{Context, Result};
3use log::debug;
4use std::{
5    collections::HashMap,
6    ffi::OsString,
7    fs,
8    path::{Path, PathBuf},
9};
10use toml::Value;
11
12#[derive(Debug, Clone)]
13pub enum TargetOrigin {
14    DefaultBinary(PathBuf),
15    SingleFile(PathBuf),
16    MultiFile(PathBuf),
17    SubProject(PathBuf),
18    Named(OsString),
19}
20
21#[derive(Debug, Clone, PartialEq, Hash, Eq, Copy)]
22pub enum TargetKind {
23    Unknown,
24    Example,
25    ExtendedExample,
26    Binary,
27    ExtendedBinary,
28    Bench,
29    Test,
30    Manifest, // For browsing the entire Cargo.toml or package-level targets.
31    ManifestTauri,
32    ManifestTauriExample,
33    ManifestDioxusExample,
34    ManifestDioxus,
35}
36
37#[derive(Debug, Clone)]
38pub struct CargoTarget {
39    pub name: String,
40    pub display_name: String,
41    pub manifest_path: PathBuf,
42    pub kind: TargetKind,
43    pub extended: bool,
44    pub origin: Option<TargetOrigin>,
45}
46
47impl CargoTarget {
48    /// Constructs a CargoTarget from a source file.
49    ///
50    /// Reads the file at `file_path` and determines the target kind based on:
51    /// - Tauri configuration (e.g. if the manifest's parent is "src-tauri" or a Tauri config exists),
52    /// - Dioxus markers in the file contents,
53    /// - And finally, if the file contains "fn main", using its parent directory (examples vs bin) to decide.
54    ///
55    /// If none of these conditions are met, returns None.
56    pub fn from_source_file(
57        stem: &std::ffi::OsStr,
58        file_path: &Path,
59        manifest_path: &Path,
60        example: bool,
61        extended: bool,
62    ) -> Option<Self> {
63        let file_path = fs::canonicalize(&file_path).unwrap_or(file_path.to_path_buf());
64        let file_contents = std::fs::read_to_string(&file_path).unwrap_or_default();
65        let (kind, new_manifest) = crate::e_discovery::determine_target_kind_and_manifest(
66            manifest_path,
67            &file_path,
68            &file_contents,
69            example,
70            extended,
71            None,
72        );
73        if kind == TargetKind::Unknown {
74            return None;
75        }
76        let name = stem.to_string_lossy().to_string();
77        Some(CargoTarget {
78            name: name.clone(),
79            display_name: name,
80            manifest_path: new_manifest.to_path_buf(),
81            kind,
82            extended,
83            origin: Some(TargetOrigin::SingleFile(file_path.to_path_buf())),
84        })
85    }
86
87    /// Updates the target's name and display_name by interrogating the candidate file and its manifest.
88    pub fn figure_main_name(&mut self) {
89        // Only operate if we have a candidate file path.
90        let candidate = match &self.origin {
91            Some(TargetOrigin::SingleFile(path)) | Some(TargetOrigin::DefaultBinary(path)) => path,
92            _ => {
93                debug!("No candidate file found in target.origin; skipping name determination");
94                return;
95            }
96        };
97
98        // Get the candidate file's stem in lowercase.
99        let candidate_stem = candidate
100            .file_stem()
101            .and_then(|s| s.to_str())
102            .map(|s| s.to_lowercase())
103            .unwrap_or_default();
104        debug!("Candidate stem: {}", candidate_stem);
105
106        // Start with folder-based logic.
107        let mut name = if candidate_stem == "main" {
108            if let Some(parent_dir) = candidate.parent() {
109                if let Some(parent_name) = parent_dir.file_name().and_then(|s| s.to_str()) {
110                    debug!("Candidate parent folder: {}", parent_name);
111                    if parent_name.eq_ignore_ascii_case("src") {
112                        // If candidate is src/main.rs, take the parent of "src".
113                        parent_dir
114                            .parent()
115                            .and_then(|proj_dir| proj_dir.file_name())
116                            .and_then(|s| s.to_str())
117                            .map(|s| s.to_string())
118                            .unwrap_or(candidate_stem.clone())
119                    } else if parent_name.eq_ignore_ascii_case("examples") {
120                        // If candidate is in an examples folder, use the candidate's parent folder's name.
121                        candidate
122                            .parent()
123                            .and_then(|p| p.file_name())
124                            .and_then(|s| s.to_str())
125                            .map(|s| s.to_string())
126                            .unwrap_or(candidate_stem.clone())
127                    } else {
128                        candidate_stem.clone()
129                    }
130                } else {
131                    candidate_stem.clone()
132                }
133            } else {
134                candidate_stem.clone()
135            }
136        } else {
137            candidate_stem.clone()
138        };
139        debug!("Name after folder-based logic: {}", name);
140
141        // If the candidate stem is "main", interrogate the manifest.
142        if candidate_stem == "main" {
143            let manifest_contents = fs::read_to_string(&self.manifest_path).unwrap_or_default();
144            if let Ok(manifest_toml) = manifest_contents.parse::<Value>() {
145                // Check for any [[bin]] entries.
146                if let Some(bins) = manifest_toml.get("bin").and_then(|v| v.as_array()) {
147                    debug!("Found {} [[bin]] entries", bins.len());
148                    if let Some(bin_name) = bins.iter().find_map(|bin| {
149                        if let Some(path_str) = bin.get("path").and_then(|p| p.as_str()) {
150                            debug!("Checking bin entry with path: {}", path_str);
151                            if path_str == "src/bin/main.rs" {
152                                let bn = bin
153                                    .get("name")
154                                    .and_then(|n| n.as_str())
155                                    .map(|s| s.to_string());
156                                debug!("Found matching bin with name: {:?}", bn);
157                                return bn;
158                            }
159                        }
160                        None
161                    }) {
162                        debug!("Using bin name from manifest: {}", bin_name);
163                        name = bin_name;
164                    } else if let Some(pkg) = manifest_toml.get("package") {
165                        debug!("No matching [[bin]] entry; checking [package] section");
166                        name = pkg
167                            .get("name")
168                            .and_then(|n| n.as_str())
169                            .unwrap_or(&name)
170                            .to_string();
171                        debug!("Using package name from manifest: {}", name);
172                    }
173                } else if let Some(pkg) = manifest_toml.get("package") {
174                    debug!("No [[bin]] section found; using [package] section");
175                    name = pkg
176                        .get("name")
177                        .and_then(|n| n.as_str())
178                        .unwrap_or(&name)
179                        .to_string();
180                    debug!("Using package name from manifest: {}", name);
181                } else {
182                    debug!(
183                        "Manifest does not contain [[bin]] or [package] sections; keeping name: {}",
184                        name
185                    );
186                }
187            } else {
188                debug!("Failed to parse manifest TOML; keeping name: {}", name);
189            }
190        }
191
192        debug!("Final determined name: {}", name);
193        if name.eq("main") {
194            panic!("Name is main");
195        }
196        self.name = name.clone();
197        self.display_name = name;
198    }
199
200    /// Constructs a CargoTarget from a folder by trying to locate a runnable source file.
201    ///
202    /// The function attempts the following candidate paths in order:
203    /// 1. A file named `<folder_name>.rs` in the folder.
204    /// 2. `src/main.rs` inside the folder.
205    /// 3. `main.rs` at the folder root.
206    /// 4. Otherwise, it scans the folder for any `.rs` file containing `"fn main"`.
207    ///
208    /// Once a candidate is found, it reads its contents and calls `determine_target_kind`
209    /// to refine the target kind based on Tauri or Dioxus markers. The `extended` flag
210    /// indicates whether the target should be marked as extended (for instance, if the folder
211    /// is a subdirectory of the primary "examples" or "bin" folder).
212    ///
213    /// Returns Some(CargoTarget) if a runnable file is found, or None otherwise.
214    pub fn from_folder(
215        folder: &Path,
216        manifest_path: &Path,
217        example: bool,
218        _extended: bool,
219    ) -> Option<Self> {
220        // If the folder contains its own Cargo.toml, treat it as a subproject.
221        let sub_manifest = folder.join("Cargo.toml");
222        if sub_manifest.exists() {
223            // Use the folder's name as the candidate target name.
224            let folder_name = folder.file_name()?.to_string_lossy().to_string();
225            // Determine the display name from the parent folder.
226            let display_name = if let Some(parent) = folder.parent() {
227                let parent_name = parent.file_name()?.to_string_lossy();
228                if parent_name == folder_name {
229                    // If the parent's name equals the folder's name, try using the grandparent.
230                    if let Some(grandparent) = parent.parent() {
231                        grandparent.file_name()?.to_string_lossy().to_string()
232                    } else {
233                        folder_name.clone()
234                    }
235                } else {
236                    parent_name.to_string()
237                }
238            } else {
239                folder_name.clone()
240            };
241
242            let sub_manifest =
243                fs::canonicalize(&sub_manifest).unwrap_or(sub_manifest.to_path_buf());
244            debug!("Subproject found: {}", sub_manifest.display());
245            debug!("{}", &folder_name);
246            return Some(CargoTarget {
247                name: folder_name.clone(),
248                display_name,
249                manifest_path: sub_manifest.clone(),
250                // For a subproject, we initially mark it as Manifest;
251                // later refinement may resolve it further.
252                kind: TargetKind::Manifest,
253                extended: true,
254                origin: Some(TargetOrigin::SubProject(sub_manifest)),
255            });
256        }
257        // Extract the folder's name.
258        let folder_name = folder.file_name()?.to_str()?;
259
260        /// Returns Some(candidate) only if the file exists and its contents contain "fn main".
261        fn candidate_with_main(candidate: PathBuf) -> Option<PathBuf> {
262            if candidate.exists() {
263                let contents = fs::read_to_string(&candidate).unwrap_or_default();
264                if contents.contains("fn main") {
265                    return Some(candidate);
266                }
267            }
268            None
269        }
270
271        // In your from_folder function, for example:
272        let candidate = if let Some(candidate) =
273            candidate_with_main(folder.join(format!("{}.rs", folder_name)))
274        {
275            candidate
276        } else if let Some(candidate) = candidate_with_main(folder.join("src/main.rs")) {
277            candidate
278        } else if let Some(candidate) = candidate_with_main(folder.join("main.rs")) {
279            candidate
280        } else {
281            // Otherwise, scan the folder for any .rs file containing "fn main"
282            let mut found = None;
283            if let Ok(entries) = fs::read_dir(folder) {
284                for entry in entries.flatten() {
285                    let path = entry.path();
286                    if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("rs") {
287                        if let Some(candidate) = candidate_with_main(path) {
288                            found = Some(candidate);
289                            break;
290                        }
291                    }
292                }
293            }
294            found?
295        };
296
297        let candidate = fs::canonicalize(&candidate).unwrap_or(candidate.to_path_buf());
298        // Compute the extended flag based on the candidate file location.
299        let extended = crate::e_discovery::is_extended_target(manifest_path, &candidate);
300
301        // Read the candidate file's contents.
302        let file_contents = std::fs::read_to_string(&candidate).unwrap_or_default();
303
304        // Use our helper to determine if any special configuration applies.
305        let (kind, new_manifest) = crate::e_discovery::determine_target_kind_and_manifest(
306            manifest_path,
307            &candidate,
308            &file_contents,
309            example,
310            extended,
311            None,
312        );
313        if kind == TargetKind::Unknown {
314            return None;
315        }
316
317        // Determine the candidate file's stem in lowercase.
318        let name = candidate.file_stem()?.to_str()?.to_lowercase();
319        //         let name = if candidate_stem == "main" {
320        //     if let Some(parent_dir) = candidate.parent() {
321        //         if let Some(parent_name) = parent_dir.file_name().and_then(|s| s.to_str()) {
322        //             if parent_name.eq_ignore_ascii_case("src") {
323        //                 // If candidate is src/main.rs, take the parent of "src".
324        //                 parent_dir.parent()
325        //                     .and_then(|proj_dir| proj_dir.file_name())
326        //                     .and_then(|s| s.to_str())
327        //                     .map(|s| s.to_string())
328        //                     .unwrap_or(candidate_stem.clone())
329        //             } else if parent_name.eq_ignore_ascii_case("examples") {
330        //                 // If candidate is in the examples folder (e.g. examples/main.rs),
331        //                 // use the candidate's parent folder's name.
332        //                 candidate.parent()
333        //                     .and_then(|p| p.file_name())
334        //                     .and_then(|s| s.to_str())
335        //                     .map(|s| s.to_string())
336        //                     .unwrap_or(candidate_stem.clone())
337        //             } else {
338        //                 // Fall back to the candidate_stem if no special case matches.
339        //                 candidate_stem.clone()
340        //             }
341        //         } else {
342        //             candidate_stem.clone()
343        //         }
344        //     } else {
345        //         candidate_stem.clone()
346        //     }
347        // } else {
348        //     candidate_stem.clone()
349        // };
350        // let name = if candidate_stem.clone() == "main" {
351        //     // Read the manifest contents.
352        //     let manifest_contents = fs::read_to_string(manifest_path).unwrap_or_default();
353        //     if let Ok(manifest_toml) = manifest_contents.parse::<toml::Value>() {
354        //         // Look for any [[bin]] entries.
355        //         if let Some(bins) = manifest_toml.get("bin").and_then(|v| v.as_array()) {
356        //             if let Some(bin_name) = bins.iter().find_map(|bin| {
357        //                 if let Some(path_str) = bin.get("path").and_then(|p| p.as_str()) {
358        //                     if path_str == "src/bin/main.rs" {
359        //                         return bin.get("name").and_then(|n| n.as_str()).map(|s| s.to_string());
360        //                     }
361        //                 }
362        //                 None
363        //             }) {
364        //                 // Found a bin with the matching path; use its name.
365        //                 bin_name
366        //             } else if let Some(pkg) = manifest_toml.get("package") {
367        //                 // No matching bin entry, so use the package name.
368        //                 pkg.get("name").and_then(|n| n.as_str()).unwrap_or(&candidate_stem).to_string()
369        //             } else {
370        //                 candidate_stem.to_string()
371        //             }
372        //         } else if let Some(pkg) = manifest_toml.get("package") {
373        //             // No [[bin]] section; use the package name.
374        //             pkg.get("name").and_then(|n| n.as_str()).unwrap_or(&candidate_stem).to_string()
375        //         } else {
376        //             candidate_stem.to_string()
377        //         }
378        //     } else {
379        //         candidate_stem.to_string()
380        //     }
381        // } else {
382        //     candidate_stem.to_string()
383        // };
384        let mut target = CargoTarget {
385            name: name.clone(),
386            display_name: name,
387            manifest_path: new_manifest.to_path_buf(),
388            kind,
389            extended,
390            origin: Some(TargetOrigin::SingleFile(candidate)),
391        };
392        // Call the method to update name based on the candidate and manifest.
393        target.figure_main_name();
394        Some(target)
395    }
396    /// Returns a refined CargoTarget based on its file contents and location.
397    /// This function is pure; it takes an immutable CargoTarget and returns a new one.
398    /// If the target's origin is either SingleFile or DefaultBinary, it reads the file and uses
399    /// `determine_target_kind` to update the kind accordingly.
400    pub fn refined_target(target: &CargoTarget) -> CargoTarget {
401        let mut refined = target.clone();
402
403        // Operate only if the target has a file to inspect.
404        let file_path = match &refined.origin {
405            Some(TargetOrigin::SingleFile(path)) | Some(TargetOrigin::DefaultBinary(path)) => path,
406            _ => return refined,
407        };
408
409        let file_path = fs::canonicalize(&file_path).unwrap_or(file_path.to_path_buf());
410        let file_contents = std::fs::read_to_string(&file_path).unwrap_or_default();
411
412        let (new_kind, new_manifest) = crate::e_discovery::determine_target_kind_and_manifest(
413            &refined.manifest_path,
414            &file_path,
415            &file_contents,
416            refined.is_example(),
417            refined.extended,
418            Some(refined.kind),
419        );
420        refined.kind = new_kind;
421        refined.manifest_path = new_manifest;
422        refined.figure_main_name();
423        refined
424    }
425
426    /// Expands a subproject CargoTarget into multiple runnable targets.
427    ///
428    /// If the given target's origin is a subproject (i.e. its Cargo.toml is in a subfolder),
429    /// this function loads that Cargo.toml and uses `get_runnable_targets` to discover its runnable targets.
430    /// It then flattens and returns them as a single `Vec<CargoTarget>`.
431    pub fn expand_subproject(target: &CargoTarget) -> Result<Vec<CargoTarget>> {
432        // Ensure the target is a subproject.
433        if let Some(TargetOrigin::SubProject(sub_manifest)) = &target.origin {
434            // Use get_runnable_targets to get targets defined in the subproject.
435            let (bins, examples, benches, tests) =
436                crate::e_manifest::get_runnable_targets(sub_manifest).with_context(|| {
437                    format!(
438                        "Failed to get runnable targets from {}",
439                        sub_manifest.display()
440                    )
441                })?;
442            let mut sub_targets = Vec::new();
443            sub_targets.extend(bins);
444            sub_targets.extend(examples);
445            sub_targets.extend(benches);
446            sub_targets.extend(tests);
447
448            // Optionally mark these targets as extended.
449            for t in &mut sub_targets {
450                t.extended = true;
451                match t.kind {
452                    TargetKind::Example => t.kind = TargetKind::ExtendedExample,
453                    TargetKind::Binary => t.kind = TargetKind::ExtendedBinary,
454                    _ => {} // For other kinds, you may leave them unchanged.
455                }
456            }
457            Ok(sub_targets)
458        } else {
459            // If the target is not a subproject, return an empty vector.
460            Ok(vec![])
461        }
462    }
463
464    /// Expands subproject targets in the given map.
465    /// For every target with a SubProject origin, this function removes the original target,
466    /// expands it using `expand_subproject`, and then inserts the expanded targets.
467    /// The expanded targets have their display names modified to include the original folder name as a prefix.
468    /// This version replaces any existing target with the same key.
469    pub fn expand_subprojects_in_place(
470        targets_map: &mut HashMap<(String, String), CargoTarget>,
471    ) -> Result<()> {
472        // Collect keys for targets that are subprojects.
473        let sub_keys: Vec<(String, String)> = targets_map
474            .iter()
475            .filter_map(|(key, target)| {
476                if let Some(TargetOrigin::SubProject(_)) = target.origin {
477                    Some(key.clone())
478                } else {
479                    None
480                }
481            })
482            .collect();
483
484        for key in sub_keys {
485            if let Some(sub_target) = targets_map.remove(&key) {
486                // Expand the subproject target.
487                let expanded_targets = Self::expand_subproject(&sub_target)?;
488                for mut new_target in expanded_targets {
489                    // Update the display name to include the subproject folder name.
490                    // For example, if sub_target.display_name was "foo" and new_target.name is "bar",
491                    // the new display name becomes "foo > bar".
492                    new_target.display_name =
493                        format!("{} > {}", sub_target.display_name, new_target.name);
494                    // Create a key for the expanded target.
495                    let new_key = Self::target_key(&new_target);
496                    // Replace any existing target with the same key.
497                    targets_map.insert(new_key, new_target);
498                }
499            }
500        }
501        Ok(())
502    }
503    // /// Expands subproject targets in `targets`. Any target whose origin is a SubProject
504    // /// is replaced by the targets returned by `expand_subproject`. If the expansion fails,
505    // /// you can choose to log the error and keep the original target, or remove it.
506    // pub fn expand_subprojects_in_place(
507    //     targets_map: &mut HashMap<(String, String), CargoTarget>
508    // ) -> anyhow::Result<()> {
509    //     // Collect keys for subproject targets.
510    //     let sub_keys: Vec<(String, String)> = targets_map
511    //         .iter()
512    //         .filter_map(|(key, target)| {
513    //             if let Some(crate::e_target::TargetOrigin::SubProject(_)) = target.origin {
514    //                 Some(key.clone())
515    //             } else {
516    //                 None
517    //             }
518    //         })
519    //         .collect();
520
521    //     // For each subproject target, remove it from the map, expand it, and insert the new targets.
522    //     for key in sub_keys {
523    //         if let Some(sub_target) = targets_map.remove(&key) {
524    //             let expanded = Self::expand_subproject(&sub_target)?;
525    //             for new_target in expanded {
526    //                 let new_key = CargoTarget::target_key(&new_target);
527    //                 targets_map.entry(new_key).or_insert(new_target);
528    //             }
529    //         }
530    //     }
531    //     Ok(())
532    // }
533
534    /// Creates a unique key for a target based on its manifest path and name.
535    pub fn target_key(target: &CargoTarget) -> (String, String) {
536        let manifest = target
537            .manifest_path
538            .canonicalize()
539            .unwrap_or_else(|_| target.manifest_path.clone())
540            .to_string_lossy()
541            .into_owned();
542        let name = target.name.clone();
543        (manifest, name)
544    }
545
546    /// Expands a subproject target into multiple targets and inserts them into the provided HashMap,
547    /// using (manifest, name) as a key to avoid duplicates.
548    pub fn expand_subproject_into_map(
549        target: &CargoTarget,
550        map: &mut std::collections::HashMap<(String, String), CargoTarget>,
551    ) -> Result<(), Box<dyn std::error::Error>> {
552        // Only operate if the target is a subproject.
553        if let Some(crate::e_target::TargetOrigin::SubProject(sub_manifest)) = &target.origin {
554            // Discover targets in the subproject.
555            let (bins, examples, benches, tests) =
556                crate::e_manifest::get_runnable_targets(sub_manifest)?;
557            let mut new_targets = Vec::new();
558            new_targets.extend(bins);
559            new_targets.extend(examples);
560            new_targets.extend(benches);
561            new_targets.extend(tests);
562            // Mark these targets as extended.
563            for t in &mut new_targets {
564                t.extended = true;
565            }
566            // Insert each new target if not already present.
567            for new in new_targets {
568                let key = CargoTarget::target_key(&new);
569                map.entry(key).or_insert(new.clone());
570            }
571        }
572        Ok(())
573    }
574
575    /// Returns true if the target is an example.
576    pub fn is_example(&self) -> bool {
577        matches!(
578            self.kind,
579            TargetKind::Example
580                | TargetKind::ExtendedExample
581                | TargetKind::ManifestDioxusExample
582                | TargetKind::ManifestTauriExample
583        )
584    }
585}
586
587/// Returns the "depth" of a path, i.e. the number of components.
588pub fn path_depth(path: &Path) -> usize {
589    path.components().count()
590}
591
592/// Deduplicates targets that share the same (name, origin key). If duplicates are found,
593/// the target with the manifest path of greater depth is kept.
594pub fn dedup_targets(targets: Vec<CargoTarget>) -> Vec<CargoTarget> {
595    let mut grouped: HashMap<(String, Option<String>), CargoTarget> = HashMap::new();
596
597    for target in targets {
598        // We'll group targets by (target.name, origin_key)
599        // Create an origin key if available by canonicalizing the origin path.
600        let origin_key = target.origin.as_ref().and_then(|origin| match origin {
601            TargetOrigin::SingleFile(path)
602            | TargetOrigin::DefaultBinary(path)
603            | TargetOrigin::SubProject(path) => path
604                .canonicalize()
605                .ok()
606                .map(|p| p.to_string_lossy().into_owned()),
607            _ => None,
608        });
609        let key = (target.name.clone(), origin_key);
610
611        grouped
612            .entry(key)
613            .and_modify(|existing| {
614                let current_depth = path_depth(&target.manifest_path);
615                let existing_depth = path_depth(&existing.manifest_path);
616                // If the current target's manifest path is deeper, replace the existing target.
617                if current_depth > existing_depth {
618                    *existing = target.clone();
619                }
620            })
621            .or_insert(target);
622    }
623
624    grouped.into_values().collect()
625}