cargo_e/
e_discovery.rs

1// src/e_discovery.rs
2use std::{
3    fs,
4    fs::File,
5    io::{self, BufRead, BufReader},
6    path::{Path, PathBuf},
7};
8
9use crate::e_target::{CargoTarget, TargetKind};
10use anyhow::{anyhow, Context, Result};
11
12pub fn scan_tests_directory(manifest_path: &Path) -> Result<Vec<String>> {
13    // Determine the project root from the manifest's parent directory.
14    let project_root = manifest_path
15        .parent()
16        .ok_or_else(|| anyhow!("Unable to determine project root from manifest"))?;
17
18    // Construct the path to the tests directory.
19    let tests_dir = project_root.join("tests");
20    let mut tests = Vec::new();
21
22    // Only scan if the tests directory exists and is a directory.
23    if tests_dir.exists() && tests_dir.is_dir() {
24        for entry in fs::read_dir(tests_dir)? {
25            let entry = entry?;
26            let path = entry.path();
27            // Only consider files with a `.rs` extension.
28            if path.is_file() {
29                if let Some(ext) = path.extension() {
30                    if ext == "rs" {
31                        if let Some(stem) = path.file_stem() {
32                            tests.push(stem.to_string_lossy().to_string());
33                        }
34                    }
35                }
36            }
37        }
38    }
39
40    Ok(tests)
41}
42
43pub fn scan_examples_directory(
44    manifest_path: &Path,
45    examples_folder: &str,
46) -> Result<Vec<CargoTarget>> {
47    // Determine the project root from the manifest's parent directory.
48    let project_root = manifest_path
49        .parent()
50        .ok_or_else(|| anyhow::anyhow!("Unable to determine project root"))?;
51    let examples_dir = project_root.join(examples_folder);
52    let mut targets = Vec::new();
53
54    if examples_dir.exists() && examples_dir.is_dir() {
55        for entry in fs::read_dir(&examples_dir)
56            .with_context(|| format!("Reading directory {:?}", examples_dir))?
57        {
58            let entry = entry?;
59            let path = entry.path();
60            if path.is_file() {
61                // Assume that any .rs file in examples/ is an example.
62                if let Some(ext) = path.extension() {
63                    if ext == "rs" {
64                        if let Some(stem) = path.file_stem() {
65                            if let Some(target) = CargoTarget::from_source_file(
66                                stem,
67                                &path,
68                                manifest_path,
69                                true,
70                                false,
71                            ) {
72                                targets.push(target);
73                            }
74                        }
75                    }
76                }
77            } else if path.is_dir() {
78                if let Some(target) = CargoTarget::from_folder(&path, &manifest_path, true, true) {
79                    if target.kind == TargetKind::Unknown {
80                        continue;
81                    }
82                    targets.push(target);
83                }
84            }
85        }
86    }
87
88    Ok(targets)
89}
90
91/// Try to detect a “script” kind by reading *one* first line.
92/// Returns Ok(Some(...)) if it matches either marker, Ok(None) otherwise.
93/// Any I/O error is propagated.
94fn detect_script_kind(path: &Path) -> io::Result<Option<TargetKind>> {
95    let file = File::open(path)?;
96    let mut reader = BufReader::new(file);
97    let mut first_line = String::new();
98    reader.read_line(&mut first_line)?;
99
100    // must start with `#`
101    if !first_line.starts_with('#') {
102        return Ok(None);
103    }
104    // now check your two markers
105    if first_line.contains("scriptisto") {
106        return Ok(Some(TargetKind::ScriptScriptisto));
107    }
108    if first_line.contains("rust-script") {
109        return Ok(Some(TargetKind::ScriptRustScript));
110    }
111    Ok(None)
112}
113
114/// Determines the target kind and (optionally) an updated manifest path based on:
115/// - Tauri configuration: If the parent directory of the original manifest contains a
116///   "tauri.conf.json", and also a Cargo.toml exists in that same directory, then update the manifest path
117///   and return ManifestTauri.
118/// - Dioxus markers: If the file contents contain any Dioxus markers, return either ManifestDioxusExample
119///   (if `example` is true) or ManifestDioxus.
120/// - Otherwise, if the file contains "fn main", decide based on the candidate's parent folder name.
121///   If the parent is "examples" (or "bin"), return the corresponding Example/Binary (or extended variant).
122/// - If none of these conditions match, return Example as a fallback.
123///
124/// Returns a tuple of (TargetKind, updated_manifest_path).
125pub fn determine_target_kind_and_manifest(
126    manifest_path: &Path,
127    candidate: &Path,
128    file_contents: &str,
129    example: bool,
130    extended: bool,
131    _toml_specified: bool,
132    incoming_kind: Option<TargetKind>,
133) -> (TargetKind, PathBuf) {
134    // Start with the original manifest path.
135    let mut new_manifest = manifest_path.to_path_buf();
136
137    if let Ok(Some(script_kind)) = detect_script_kind(candidate) {
138        return (script_kind, new_manifest);
139    }
140    // If the incoming kind is already known (Test or Bench), return it.
141    if let Some(kind) = incoming_kind {
142        if kind == TargetKind::Test || kind == TargetKind::Bench {
143            return (kind, new_manifest);
144        }
145    }
146    // Tauri detection: check if the manifest's parent or candidate's parent contains tauri config.
147    let tauri_detected = manifest_path
148        .parent()
149        .and_then(|p| p.file_name())
150        .map(|s| s.to_string_lossy().eq_ignore_ascii_case("src-tauri"))
151        .unwrap_or(false)
152        || manifest_path
153            .parent()
154            .map(|p| p.join("tauri.conf.json"))
155            .map_or(false, |p| p.exists())
156        || manifest_path
157            .parent()
158            .map(|p| p.join("src-tauri"))
159            .map_or(false, |p| p.exists())
160        || candidate
161            .parent()
162            .map(|p| p.join("tauri.conf.json"))
163            .map_or(false, |p| p.exists());
164
165    // println!(
166    //     "{} {} {} {}",
167    //     manifest_path.display(),
168    //     candidate.display(),
169    //     tauri_detected,
170    //     toml_specified
171    // );
172    if tauri_detected {
173        if example {
174            return (TargetKind::ManifestTauriExample, new_manifest);
175        }
176        // If the candidate's parent contains tauri.conf.json, update the manifest path if there's a Cargo.toml there.
177        if let Some(candidate_parent) = candidate.parent() {
178            let candidate_manifest = candidate_parent.join("Cargo.toml");
179            if candidate_manifest.exists() {
180                new_manifest = candidate_manifest;
181            }
182        }
183        return (TargetKind::ManifestTauri, new_manifest);
184    }
185
186    // Dioxus detection
187    if file_contents.contains("dioxus::") {
188        let kind = if example {
189            TargetKind::ManifestDioxusExample
190        } else {
191            TargetKind::ManifestDioxus
192        };
193        return (kind, new_manifest);
194    }
195
196    // leptos detection
197    if file_contents.contains("leptos::") {
198        return (TargetKind::ManifestLeptos, new_manifest);
199    }
200
201    // Check if the file contains "fn main"
202    if file_contents.contains("fn main") {
203        let kind = if example {
204            if extended {
205                TargetKind::ExtendedExample
206            } else {
207                TargetKind::Example
208            }
209        } else if extended {
210            TargetKind::ExtendedBinary
211        } else {
212            TargetKind::Binary
213        };
214        return (kind, new_manifest);
215    }
216    // Check if the file contains a #[test] attribute; if so, mark it as a test.
217    if file_contents.contains("#[test]") {
218        return (TargetKind::Test, new_manifest);
219    }
220
221    let kind = if example {
222        if extended {
223            TargetKind::UnknownExtendedExample
224        } else {
225            TargetKind::UnknownExample
226        }
227    } else if extended {
228        TargetKind::UnknownExtendedBinary
229    } else {
230        TargetKind::UnknownBinary
231    };
232    (kind, new_manifest)
233    // Default fallback.
234    // (TargetKind::Unknown, "errorNOfnMAIN".into())
235}
236
237/// Returns true if the candidate file is not located directly in the project root.
238pub fn is_extended_target(manifest_path: &Path, candidate: &Path) -> bool {
239    if let Some(project_root) = manifest_path.parent() {
240        // If the candidate's parent is not the project root, it's nested (i.e. extended).
241        candidate
242            .parent()
243            .map(|p| p != project_root)
244            .unwrap_or(false)
245    } else {
246        false
247    }
248}
249
250// #[cfg(test)]
251// mod tests {
252//     use super::*;
253//     use std::fs;
254//     use tempfile::tempdir;
255
256//     #[test]
257//     fn test_discover_targets_no_manifest() {
258//         let temp = tempdir().unwrap();
259//         // With no Cargo.toml, we expect an empty list.
260//         let targets = discover_targets(temp.path()).unwrap();
261//         assert!(targets.is_empty());
262//     }
263
264//     #[test]
265//     fn test_discover_targets_with_manifest_and_example() {
266//         let temp = tempdir().unwrap();
267//         // Create a dummy Cargo.toml.
268//         let manifest_path = temp.path().join("Cargo.toml");
269//         fs::write(&manifest_path, "[package]\nname = \"dummy\"\n").unwrap();
270
271//         // Create an examples directory with a dummy example file.
272//         let examples_dir = temp.path().join("examples");
273//         fs::create_dir(&examples_dir).unwrap();
274//         let example_file = examples_dir.join("example1.rs");
275//         fs::write(&example_file, "fn main() {}").unwrap();
276
277//         let targets = discover_targets(temp.path()).unwrap();
278//         // Expect at least two targets: one for the manifest and one for the example.
279//         assert!(targets.len() >= 2);
280
281//         let example_target = targets
282//             .iter()
283//             .find(|t| t.kind == TargetKind::Example && t.name == "example1");
284//         assert!(example_target.is_some());
285//     }
286// }
287
288pub fn scan_directory_for_targets(scan_dir: &Path, be_silent: bool) -> Vec<CargoTarget> {
289    let mut targets = Vec::new();
290    let mut dirs_to_visit = vec![scan_dir.to_path_buf()]; // Use a stack for iterative traversal
291
292    // Collect all manifest paths found in the directory tree
293    let mut manifest_paths = Vec::new();
294
295    while let Some(current_dir) = dirs_to_visit.pop() {
296        if let Ok(entries) = fs::read_dir(&current_dir) {
297            for entry in entries.flatten() {
298                let path = entry.path();
299                if path.is_dir() {
300                    // // Skip directories that contain ".." or the system separator in their path
301                    // if path.to_string_lossy().contains(&format!("..{}", std::path::MAIN_SEPARATOR)) {
302                    //     if let Ok(current_dir) = std::env::current_dir() {
303                    //         // Avoid infinite recursion if the scan_dir is the current working directory
304                    //         println!(
305                    //             "DEBUG: scan_dir = {}, current_dir = {}, path = {}",
306                    //             scan_dir.display(),
307                    //             current_dir.display(),
308                    //             path.display()
309                    //         );
310                    //         // and we're traversing into it again (e.g., via symlink or path confusion)
311                    //         if path == current_dir {
312                    //             continue;
313                    //         }
314                    //     }
315                    // }
316                    // Skip irrelevant directories
317                    if path.file_name().map_or(false, |name| {
318                        name == "node_modules" || name == "target" || name == "build"
319                    }) {
320                        continue;
321                    }
322                    dirs_to_visit.push(path); // Add subdirectory to stack
323                } else if path.file_name().map_or(false, |name| name == "Cargo.toml") {
324                    manifest_paths.push(Some(path));
325                }
326            }
327        } else if !be_silent {
328            eprintln!("Failed to read directory: {}", current_dir.display());
329        }
330    }
331    // Print the manifest paths and wait for 5 seconds
332    if !be_silent {
333        for manifest in &manifest_paths {
334            if let Some(path) = manifest {
335                println!("Found Cargo.toml at: {}", path.display());
336            }
337        }
338    }
339    // Now call the parallel collector if any manifests were found
340    if !manifest_paths.is_empty() {
341        #[cfg(feature = "concurrent")]
342        {
343            let file_targets = crate::e_collect::collect_all_targets_parallel(
344                manifest_paths,
345                false, // workspace
346                std::thread::available_parallelism()
347                    .map(|n| n.get())
348                    .unwrap_or(4),
349                be_silent,
350            )
351            .unwrap_or_default();
352
353            if !be_silent {
354                println!(
355                    "Found {} targets in scanned directories",
356                    file_targets.len()
357                );
358            }
359            targets.extend(file_targets);
360        }
361        #[cfg(not(feature = "concurrent"))]
362        {
363            for manifest in manifest_paths {
364                if let Some(path) = manifest {
365                    match crate::e_collect::collect_all_targets(
366                        Some(path.clone()),
367                        false, // workspace
368                        std::thread::available_parallelism()
369                            .map(|n| n.get())
370                            .unwrap_or(4),
371                        false, // be_silent
372                        false, // print_parent
373                    ) {
374                        Ok(file_targets) => {
375                            if !be_silent {
376                                println!(
377                                    "Found {} targets in {}",
378                                    file_targets.len(),
379                                    path.display()
380                                );
381                            }
382                            targets.extend(file_targets);
383                        }
384                        Err(e) => {
385                            eprintln!("Error processing {}: {}", path.display(), e);
386                        }
387                    }
388                }
389            }
390        }
391    }
392
393    targets
394}
395//                     dirs_to_visit.push(path); // Add subdirectory to stack
396//                 } else if path.file_name().map_or(false, |name| name == "Cargo.toml") {
397//                     if !be_silent {
398//                       println!("Found Cargo.toml at: {}", path.display());
399//                     }
400//                     match crate::e_collect::collect_all_targets(
401//                         Some(path.clone()),
402//                         false,
403//                         std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4),
404//                         false,
405//                     ) {
406//                         Ok(file_targets) => {
407//                             if !be_silent {
408//                             println!(
409//                                 "Found {} targets in {}",
410//                                 file_targets.len(),
411//                                 path.display()
412//                             );
413//                             }
414//                             targets.extend(file_targets);
415//                         }
416//                         Err(e) => {
417//                             eprintln!("Error processing {}: {}", path.display(), e);
418//                         }
419//                     }
420//                 }
421//             }
422//         } else {
423//             eprintln!("Failed to read directory: {}", current_dir.display());
424//         }
425//     }
426
427//     targets
428
429// }