cargo_e/
e_target.rs

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