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