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