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(¤t_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// }