cargo_e/e_target.rs
1// src/e_target.rs
2use anyhow::{Context, Result};
3use log::{debug, trace};
4use std::{
5 collections::HashMap,
6 ffi::OsString,
7 fs,
8 path::{Path, PathBuf},
9};
10use toml::Value;
11
12#[derive(Debug, Clone)]
13pub enum TargetOrigin {
14 DefaultBinary(PathBuf),
15 SingleFile(PathBuf),
16 MultiFile(PathBuf),
17 SubProject(PathBuf),
18 Named(OsString),
19}
20
21#[derive(Debug, Clone, PartialEq, Hash, Eq, Copy)]
22pub enum TargetKind {
23 Unknown,
24 UnknownExample,
25 UnknownExtendedExample,
26 UnknownBinary,
27 UnknownExtendedBinary,
28 Example,
29 ExtendedExample,
30 Binary,
31 ExtendedBinary,
32 Bench,
33 Test,
34 Manifest, // For browsing the entire Cargo.toml or package-level targets.
35 ManifestTauri,
36 ManifestTauriExample,
37 ManifestDioxusExample,
38 ManifestDioxus,
39 ManifestLeptos,
40 ScriptRustScript,
41 ScriptScriptisto,
42}
43
44#[derive(Debug, Clone)]
45pub struct CargoTarget {
46 pub name: String,
47 pub display_name: String,
48 pub manifest_path: PathBuf,
49 pub kind: TargetKind,
50 pub extended: bool,
51 pub toml_specified: bool,
52 pub origin: Option<TargetOrigin>,
53}
54
55impl CargoTarget {
56 /// Constructs a CargoTarget from a source file.
57 ///
58 /// Reads the file at `file_path` and determines the target kind based on:
59 /// - Tauri configuration (e.g. if the manifest's parent is "src-tauri" or a Tauri config exists),
60 /// - Dioxus markers in the file contents,
61 /// - And finally, if the file contains "fn main", using its parent directory (examples vs bin) to decide.
62 ///
63 /// If none of these conditions are met, returns None.
64 pub fn from_source_file(
65 stem: &std::ffi::OsStr,
66 file_path: &Path,
67 manifest_path: &Path,
68 example: bool,
69 extended: bool,
70 ) -> Option<Self> {
71 let file_path = fs::canonicalize(&file_path).unwrap_or(file_path.to_path_buf());
72 let file_contents = std::fs::read_to_string(&file_path).unwrap_or_default();
73 let (kind, new_manifest) = crate::e_discovery::determine_target_kind_and_manifest(
74 manifest_path,
75 &file_path,
76 &file_contents,
77 example,
78 extended,
79 None,
80 );
81 if kind == TargetKind::Unknown {
82 return None;
83 }
84 let name = stem.to_string_lossy().to_string();
85 Some(CargoTarget {
86 name: name.clone(),
87 display_name: name,
88 manifest_path: new_manifest.to_path_buf(),
89 kind,
90 extended,
91 toml_specified: false,
92 origin: Some(TargetOrigin::SingleFile(file_path.to_path_buf())),
93 })
94 }
95
96 // /// Updates the target's name and display_name by interrogating the candidate file and its manifest.
97 // pub fn figure_main_name(&mut self) {
98 // // Only operate if we have a candidate file path.
99 // let candidate = match &self.origin {
100 // Some(TargetOrigin::SingleFile(path)) | Some(TargetOrigin::DefaultBinary(path)) => path,
101 // _ => {
102 // debug!("No candidate file found in target.origin; skipping name determination");
103 // return;
104 // }
105 // };
106 // println!("figure_main: {}", &candidate.display());
107 // // Get the candidate file's stem in lowercase.
108 // let candidate_stem = candidate
109 // .file_stem()
110 // .and_then(|s| s.to_str())
111 // .map(|s| s.to_lowercase())
112 // .unwrap_or_default();
113 // debug!("Candidate stem: {}", candidate_stem);
114
115 // // Start with folder-based logic.
116 // let mut name = if candidate_stem == "main" {
117 // if let Some(parent_dir) = candidate.parent() {
118 // if let Some(parent_name) = parent_dir.file_name().and_then(|s| s.to_str()) {
119 // debug!("Candidate parent folder: {}", parent_name);
120 // if parent_name.eq_ignore_ascii_case("src") {
121 // // If candidate is src/main.rs, take the parent of "src".
122 // parent_dir
123 // .parent()
124 // .and_then(|proj_dir| proj_dir.file_name())
125 // .and_then(|s| s.to_str())
126 // .map(|s| s.to_string())
127 // .unwrap_or(candidate_stem.clone())
128 // } else if parent_name.eq_ignore_ascii_case("examples") {
129 // // If candidate is in an examples folder, use the candidate's parent folder's name.
130 // candidate
131 // .parent()
132 // .and_then(|p| p.file_name())
133 // .and_then(|s| s.to_str())
134 // .map(|s| s.to_string())
135 // .unwrap_or(candidate_stem.clone())
136 // } else {
137 // candidate_stem.clone()
138 // }
139 // } else {
140 // candidate_stem.clone()
141 // }
142 // } else {
143 // candidate_stem.clone()
144 // }
145 // } else {
146 // candidate_stem.clone()
147 // };
148
149 // let mut package_manifest_name = String::new();
150 // // If the candidate stem is "main", interrogate the manifest.
151 // let manifest_contents = fs::read_to_string(&self.manifest_path).unwrap_or_default();
152 // if let Ok(manifest_toml) = manifest_contents.parse::<Value>() {
153 // if let Ok(manifest_toml) = manifest_contents.parse::<toml::Value>() {
154 // // Then try to retrieve the bin section.
155 // if let Some(bins) = manifest_toml.get("bin").and_then(|v| v.as_array()) {
156 // debug!("Found {} [[bin]] entries {:?}", bins.len(), bins);
157 // } else {
158 // debug!("No [[bin]] array found in manifest");
159 // }
160 // } else {
161 // debug!("Failed to parse manifest TOML");
162 // }
163 // debug!("Opened manifest {:?}",&self.manifest_path);
164 // // Check for any [[bin]] entries.
165 // if let Some(bins) = manifest_toml.get("bin").and_then(|v| v.as_array()) {
166 // debug!("Found {} [[bin]] entries", bins.len());
167 // if let Some(bin_name) = bins.iter().find_map(|bin| {
168 // if let Some(path_str) = bin.get("path").and_then(|p| p.as_str()) {
169 // let bp = bin
170 // .get("path")
171 // .and_then(|n| n.as_str())
172 // .map(|s| s.to_string());
173 // let bn = bin
174 // .get("name")
175 // .and_then(|n| n.as_str())
176 // .map(|s| s.to_string());
177 // debug!("Checking bin entry with path: {} {:?}", path_str, bp);
178 // if bp.as_deref().unwrap_or("") == path_str
179 // // && bn.as_deref().unwrap_or("") == candidate_stem
180 // {
181 // debug!("Found matching bin with name: {:?} {:?}=={:?}", bn,bp.as_deref().unwrap_or(""), path_str);
182 // name = bn.clone().unwrap_or_default();
183 // return bn.clone();
184 // }
185 // }
186 // None
187 // }) {
188 // //debug!("Using bin name from manifest: {} as {} ", name, bin_name);
189 // //name = bin_name;
190 // } else if let Some(pkg) = manifest_toml.get("package") {
191 // debug!("No matching [[bin]] entry; checking [package] section");
192 // name = pkg
193 // .get("name")
194 // .and_then(|n| n.as_str())
195 // .unwrap_or(&name)
196 // .to_string();
197 // debug!("Using package name from manifest: {}", name);
198 // }
199 // } else if let Some(pkg) = manifest_toml.get("package") {
200 // debug!("No [[bin]] section found; using [package] section");
201 // package_manifest_name = pkg
202 // .get("name")
203 // .and_then(|n| n.as_str())
204 // .unwrap_or(&name)
205 // .to_string();
206 // debug!("Using package name from manifest: {}", name);
207 // } else {
208 // debug!(
209 // "Manifest does not contain [[bin]] or [package] sections; keeping name: {}",
210 // name
211 // );
212 // }
213 // } else {
214 // debug!("Failed to open manifest {:?}",&self.manifest_path);
215 // debug!("Failed to parse manifest TOML; keeping name: {}", name);
216 // }
217
218 // debug!("Name after folder-based logic: {}", name);
219
220 // debug!("Final determined name: {}", name);
221 // if name.eq("main") {
222 // panic!("Name is main");
223 // }
224 // self.name = name.clone();
225 // self.display_name = name;
226 // }
227
228 pub fn figure_main_name(&mut self) {
229 let mut is_toml_specified = false;
230 // Only operate if we have a candidate file path.
231 let candidate = match &self.origin {
232 Some(TargetOrigin::SingleFile(path)) | Some(TargetOrigin::DefaultBinary(path)) => path,
233 _ => {
234 debug!("No candidate file found in target.origin; skipping name determination");
235 return;
236 }
237 };
238
239 trace!("figure_main: {:?}", &self.origin);
240
241 // Get the candidate file's stem in lowercase.
242 let mut candidate_stem = candidate
243 .file_stem()
244 .and_then(|s| s.to_str())
245 .map(|s| s.to_lowercase())
246 .unwrap_or_default();
247 trace!("Candidate stem: {}", candidate_stem);
248
249 // First, check if the manifest path from self matches what we find upward.
250 let candidate_dir = candidate.parent().unwrap_or(candidate);
251 let found_manifest_dir = crate::e_manifest::find_manifest_dir_from(candidate_dir);
252 if let Ok(found_dir) = found_manifest_dir {
253 let found_manifest = found_dir.join("Cargo.toml");
254 if found_manifest == self.manifest_path {
255 trace!(
256 "{} Manifest path matches candidate's upward search result: {:?}",
257 candidate.display(),
258 found_manifest
259 );
260 } else {
261 trace!(
262 "{} Manifest path mismatch. Found upward: {:?} but target.manifest_path is: {:?}"
263 , candidate.display(), found_manifest, self.manifest_path
264 );
265 // Compare depths.
266 let found_depth = found_manifest.components().count();
267 let target_depth = self.manifest_path.components().count();
268 if found_depth > target_depth {
269 // Before switching, compare the candidate's relative paths.
270 let orig_parent = self.manifest_path.parent().unwrap_or_else(|| Path::new(""));
271 let found_parent = found_manifest.parent().unwrap_or_else(|| Path::new(""));
272 let orig_rel = candidate.strip_prefix(orig_parent).ok();
273 let found_rel = candidate.strip_prefix(found_parent).ok();
274 if orig_rel == found_rel {
275 trace!(
276 "{} Relative path matches: {:?}",
277 candidate.display(),
278 orig_rel
279 );
280 self.manifest_path = found_manifest;
281 } else {
282 trace!(
283 "{} Relative path mismatch: original: {:?}, found: {:?}",
284 candidate.display(),
285 orig_rel,
286 found_rel
287 );
288 }
289 } else {
290 trace!(
291 "{} Keeping target manifest path (deeper or equal): {:?}",
292 candidate.display(),
293 self.manifest_path
294 );
295 }
296 }
297 } else {
298 trace!(
299 "Could not locate Cargo.toml upward from candidate: {:?}",
300 candidate
301 );
302 }
303
304 // Determine name via manifest processing.
305 let mut name = candidate_stem.clone();
306 let manifest_contents = fs::read_to_string(&self.manifest_path).unwrap_or_default();
307 if let Ok(manifest_toml) = manifest_contents.parse::<Value>() {
308 trace!(
309 "{} Opened manifest {:?}",
310 candidate.display(),
311 &self.manifest_path
312 );
313
314 // // First, check for any [[bin]] entries.
315 // if let Some(bins) = manifest_toml.get("bin").and_then(|v| v.as_array()) {
316 // trace!("Found {} [[bin]] entries", bins.len());
317 // // Iterate over the bin entries and use absolute paths for comparison.
318 // if let Some(bin_name) = bins.iter().find_map(|bin| {
319 // if let (Some(rel_path_str), Some(bn)) = (
320 // bin.get("path").and_then(|p| p.as_str()),
321 // bin.get("name").and_then(|n| n.as_str()),
322 // ) {
323 // // Construct the expected absolute path for the candidate file.
324 // let manifest_parent =
325 // self.manifest_path.parent().unwrap_or_else(|| Path::new(""));
326 // let expected_path =
327 // fs::canonicalize(manifest_parent.join(rel_path_str)).ok()?;
328 // let candidate_abs = fs::canonicalize(candidate).ok()?;
329 // trace!(
330 // "\n{}\n{:?}\nactual candidate absolute path:\n{:?}",
331 // candidate.display(),
332 // expected_path,
333 // candidate_abs
334 // );
335 // if expected_path == candidate_abs {
336 // trace!(
337 // "{} Found matching bin with name: {}",
338 // candidate.display(),
339 // bn
340 // );
341 // return Some(bn.to_string());
342 // }
343 // }
344 // None
345 // }) {
346 // trace!(
347 // "{} Using bin name from manifest: {}",
348 // candidate.display(),
349 // bin_name
350 // );
351 // name = bin_name.clone();
352 // candidate_stem = bin_name.into();
353 // }
354 // }
355 if let Some(bin_name) = crate::e_manifest::find_candidate_name(
356 &manifest_toml,
357 "bin",
358 candidate,
359 &self.manifest_path,
360 ) {
361 trace!(
362 "{} Using bin name from manifest: {}",
363 candidate.display(),
364 bin_name
365 );
366 is_toml_specified = true;
367 name = bin_name.clone();
368 candidate_stem = bin_name.into();
369 } else if let Some(example_name) = crate::e_manifest::find_candidate_name(
370 &manifest_toml,
371 "example",
372 candidate,
373 &self.manifest_path,
374 ) {
375 is_toml_specified = true;
376 trace!(
377 "{} Using example name from manifest: {}",
378 candidate.display(),
379 example_name
380 );
381 name = example_name.clone();
382 candidate_stem = example_name.into();
383 } else {
384 match &self.origin {
385 Some(TargetOrigin::DefaultBinary(_path)) => {
386 // Check for any [package] section.
387 if let Some(pkg) = manifest_toml.get("package") {
388 trace!("Found [package] section in manifest");
389 if let Some(name_value) = pkg.get("name").and_then(|v| v.as_str()) {
390 trace!("Using package name from manifest: {}", name_value);
391 name = name_value.to_string();
392 candidate_stem = name.clone();
393 } else {
394 trace!("No package name found in manifest; keeping name: {}", name);
395 }
396 }
397 }
398 _ => {}
399 };
400 }
401
402 // if let Some(bins) = manifest_toml.get("bin").and_then(|v| v.as_array()) {
403 // trace!("Found {} [[bin]] entries", bins.len());
404 // // Iterate over the bin entries and use absolute paths for comparison.
405 // if let Some(bin_name) = bins.iter().find_map(|bin| {
406 // if let (Some(rel_path_str), Some(bn)) = (
407 // bin.get("path").and_then(|p| p.as_str()),
408 // bin.get("name").and_then(|n| n.as_str()),
409 // ) {
410 // // Construct the expected absolute path for the candidate file.
411 // let manifest_parent = self.manifest_path.parent().unwrap_or_else(|| Path::new(""));
412 // let expected_path = fs::canonicalize(manifest_parent.join(rel_path_str)).ok()?;
413 // let candidate_abs = fs::canonicalize(candidate).ok()?;
414 // trace!(
415 // "{} Expected candidate absolute path: {:?}, actual candidate absolute path: {:?}",
416 // candidate.display(),
417 // expected_path,
418 // candidate_abs
419 // );
420 // if expected_path == candidate_abs {
421 // trace!(
422 // "{} Found matching bin with name: {}",
423 // candidate.display(),
424 // bn
425 // );
426 // return Some(bn.to_string());
427 // }
428 // }
429 // None
430 // }) {
431 // trace!("{} Using bin name from manifest: {}", candidate.display(), bin_name);
432 // name = bin_name;
433 // }
434 //}
435 } else {
436 trace!("Failed to open manifest {:?}", &self.manifest_path);
437 trace!("Failed to parse manifest TOML; keeping name: {}", name);
438 }
439
440 // Only if the candidate stem is "main", apply folder-based logic after manifest processing.
441 if candidate_stem == "main" {
442 let folder_name = if let Some(parent_dir) = candidate.parent() {
443 if let Some(parent_name) = parent_dir.file_name().and_then(|s| s.to_str()) {
444 trace!("Candidate parent folder: {}", parent_name);
445 if parent_name.eq_ignore_ascii_case("src")
446 || parent_name.eq_ignore_ascii_case("src-tauri")
447 {
448 // If candidate is src/main.rs, take the parent of "src".
449 let p = parent_dir
450 .parent()
451 .and_then(|proj_dir| proj_dir.file_name())
452 .and_then(|s| s.to_str())
453 .map(|s| s.to_string())
454 .unwrap_or(candidate_stem.clone());
455 if p.eq("src-tauri") {
456 let maybe_name = parent_dir
457 .parent()
458 .and_then(|proj_dir| proj_dir.parent())
459 .and_then(|proj_dir| proj_dir.file_name())
460 .and_then(|s| s.to_str())
461 .map(String::from);
462 match maybe_name {
463 Some(name) => name,
464 None => candidate_stem.clone(),
465 }
466 } else {
467 p
468 }
469 } else if parent_name.eq_ignore_ascii_case("examples") {
470 // If candidate is in an examples folder, use the candidate's parent folder's name.
471 candidate
472 .parent()
473 .and_then(|p| p.file_name())
474 .and_then(|s| s.to_str())
475 .map(|s| s.to_string())
476 .unwrap_or(candidate_stem.clone())
477 } else {
478 parent_name.into()
479 }
480 } else {
481 candidate_stem.clone()
482 }
483 } else {
484 candidate_stem.clone()
485 };
486 trace!("Folder-based name: {}-{}", candidate.display(), folder_name);
487 // Only override if the folder-based name is different from "main".
488 if folder_name != "main" {
489 name = folder_name;
490 }
491 }
492
493 trace!("Final determined name: {}", name);
494 if name.eq("main") {
495 panic!("Name is main");
496 }
497 if is_toml_specified {
498 self.toml_specified = true;
499 }
500 self.name = name.clone();
501 self.display_name = name;
502 }
503 /// Constructs a CargoTarget from a folder by trying to locate a runnable source file.
504 ///
505 /// The function attempts the following candidate paths in order:
506 /// 1. A file named `<folder_name>.rs` in the folder.
507 /// 2. `src/main.rs` inside the folder.
508 /// 3. `main.rs` at the folder root.
509 /// 4. Otherwise, it scans the folder for any `.rs` file containing `"fn main"`.
510 ///
511 /// Once a candidate is found, it reads its contents and calls `determine_target_kind`
512 /// to refine the target kind based on Tauri or Dioxus markers. The `extended` flag
513 /// indicates whether the target should be marked as extended (for instance, if the folder
514 /// is a subdirectory of the primary "examples" or "bin" folder).
515 ///
516 /// Returns Some(CargoTarget) if a runnable file is found, or None otherwise.
517 pub fn from_folder(
518 folder: &Path,
519 manifest_path: &Path,
520 example: bool,
521 _extended: bool,
522 ) -> Option<Self> {
523 // If the folder contains its own Cargo.toml, treat it as a subproject.
524 let sub_manifest = folder.join("Cargo.toml");
525 if sub_manifest.exists() {
526 // Use the folder's name as the candidate target name.
527 let folder_name = folder.file_name()?.to_string_lossy().to_string();
528 // Determine the display name from the parent folder.
529 let display_name = if let Some(parent) = folder.parent() {
530 let parent_name = parent.file_name()?.to_string_lossy();
531 if parent_name == folder_name {
532 // If the parent's name equals the folder's name, try using the grandparent.
533 if let Some(grandparent) = parent.parent() {
534 grandparent.file_name()?.to_string_lossy().to_string()
535 } else {
536 folder_name.clone()
537 }
538 } else {
539 parent_name.to_string()
540 }
541 } else {
542 folder_name.clone()
543 };
544
545 let sub_manifest =
546 fs::canonicalize(&sub_manifest).unwrap_or(sub_manifest.to_path_buf());
547 trace!("Subproject found: {}", sub_manifest.display());
548 trace!("{}", &folder_name);
549 return Some(CargoTarget {
550 name: folder_name.clone(),
551 display_name,
552 manifest_path: sub_manifest.clone(),
553 // For a subproject, we initially mark it as Manifest;
554 // later refinement may resolve it further.
555 kind: TargetKind::Manifest,
556 toml_specified: true,
557 extended: true,
558 origin: Some(TargetOrigin::SubProject(sub_manifest)),
559 });
560 }
561 // Extract the folder's name.
562 let folder_name = folder.file_name()?.to_str()?;
563
564 /// Returns Some(candidate) only if the file exists and its contents contain "fn main".
565 fn candidate_with_main(candidate: PathBuf) -> Option<PathBuf> {
566 if candidate.exists() {
567 let contents = fs::read_to_string(&candidate).unwrap_or_default();
568 if contents.contains("fn main") {
569 return Some(candidate);
570 }
571 }
572 None
573 }
574
575 // In your from_folder function, for example:
576 let candidate = if let Some(candidate) =
577 candidate_with_main(folder.join(format!("{}.rs", folder_name)))
578 {
579 candidate
580 } else if let Some(candidate) = candidate_with_main(folder.join("src/main.rs")) {
581 candidate
582 } else if let Some(candidate) = candidate_with_main(folder.join("main.rs")) {
583 candidate
584 } else {
585 // Otherwise, scan the folder for any .rs file containing "fn main"
586 let mut found = None;
587 if let Ok(entries) = fs::read_dir(folder) {
588 for entry in entries.flatten() {
589 let path = entry.path();
590 if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("rs") {
591 if let Some(candidate) = candidate_with_main(path) {
592 found = Some(candidate);
593 break;
594 }
595 }
596 }
597 }
598 found?
599 };
600
601 let candidate = fs::canonicalize(&candidate).unwrap_or(candidate.to_path_buf());
602 // Compute the extended flag based on the candidate file location.
603 let extended = crate::e_discovery::is_extended_target(manifest_path, &candidate);
604
605 // Read the candidate file's contents.
606 let file_contents = std::fs::read_to_string(&candidate).unwrap_or_default();
607
608 // Use our helper to determine if any special configuration applies.
609 let (kind, new_manifest) = crate::e_discovery::determine_target_kind_and_manifest(
610 manifest_path,
611 &candidate,
612 &file_contents,
613 example,
614 extended,
615 None,
616 );
617 if kind == TargetKind::Unknown {
618 return None;
619 }
620
621 // Determine the candidate file's stem in lowercase.
622 let name = candidate.file_stem()?.to_str()?.to_lowercase();
623 // let name = if candidate_stem == "main" {
624 // if let Some(parent_dir) = candidate.parent() {
625 // if let Some(parent_name) = parent_dir.file_name().and_then(|s| s.to_str()) {
626 // if parent_name.eq_ignore_ascii_case("src") {
627 // // If candidate is src/main.rs, take the parent of "src".
628 // parent_dir.parent()
629 // .and_then(|proj_dir| proj_dir.file_name())
630 // .and_then(|s| s.to_str())
631 // .map(|s| s.to_string())
632 // .unwrap_or(candidate_stem.clone())
633 // } else if parent_name.eq_ignore_ascii_case("examples") {
634 // // If candidate is in the examples folder (e.g. examples/main.rs),
635 // // use the candidate's parent folder's name.
636 // candidate.parent()
637 // .and_then(|p| p.file_name())
638 // .and_then(|s| s.to_str())
639 // .map(|s| s.to_string())
640 // .unwrap_or(candidate_stem.clone())
641 // } else {
642 // // Fall back to the candidate_stem if no special case matches.
643 // candidate_stem.clone()
644 // }
645 // } else {
646 // candidate_stem.clone()
647 // }
648 // } else {
649 // candidate_stem.clone()
650 // }
651 // } else {
652 // candidate_stem.clone()
653 // };
654 // let name = if candidate_stem.clone() == "main" {
655 // // Read the manifest contents.
656 // let manifest_contents = fs::read_to_string(manifest_path).unwrap_or_default();
657 // if let Ok(manifest_toml) = manifest_contents.parse::<toml::Value>() {
658 // // Look for any [[bin]] entries.
659 // if let Some(bins) = manifest_toml.get("bin").and_then(|v| v.as_array()) {
660 // if let Some(bin_name) = bins.iter().find_map(|bin| {
661 // if let Some(path_str) = bin.get("path").and_then(|p| p.as_str()) {
662 // if path_str == "src/bin/main.rs" {
663 // return bin.get("name").and_then(|n| n.as_str()).map(|s| s.to_string());
664 // }
665 // }
666 // None
667 // }) {
668 // // Found a bin with the matching path; use its name.
669 // bin_name
670 // } else if let Some(pkg) = manifest_toml.get("package") {
671 // // No matching bin entry, so use the package name.
672 // pkg.get("name").and_then(|n| n.as_str()).unwrap_or(&candidate_stem).to_string()
673 // } else {
674 // candidate_stem.to_string()
675 // }
676 // } else if let Some(pkg) = manifest_toml.get("package") {
677 // // No [[bin]] section; use the package name.
678 // pkg.get("name").and_then(|n| n.as_str()).unwrap_or(&candidate_stem).to_string()
679 // } else {
680 // candidate_stem.to_string()
681 // }
682 // } else {
683 // candidate_stem.to_string()
684 // }
685 // } else {
686 // candidate_stem.to_string()
687 // };
688 let mut target = CargoTarget {
689 name: name.clone(),
690 display_name: name,
691 manifest_path: new_manifest.to_path_buf(),
692 kind,
693 extended,
694 toml_specified: false,
695 origin: Some(TargetOrigin::SingleFile(candidate)),
696 };
697 // Call the method to update name based on the candidate and manifest.
698 target.figure_main_name();
699 Some(target)
700 }
701 /// Returns a refined CargoTarget based on its file contents and location.
702 /// This function is pure; it takes an immutable CargoTarget and returns a new one.
703 /// If the target's origin is either SingleFile or DefaultBinary, it reads the file and uses
704 /// `determine_target_kind` to update the kind accordingly.
705 pub fn refined_target(target: &CargoTarget) -> CargoTarget {
706 let mut refined = target.clone();
707
708 // Operate only if the target has a file to inspect.
709 let file_path = match &refined.origin {
710 Some(TargetOrigin::SingleFile(path)) | Some(TargetOrigin::DefaultBinary(path)) => path,
711 _ => return refined,
712 };
713
714 let file_path = fs::canonicalize(&file_path).unwrap_or(file_path.to_path_buf());
715 let file_contents = std::fs::read_to_string(&file_path).unwrap_or_default();
716
717 let (new_kind, new_manifest) = crate::e_discovery::determine_target_kind_and_manifest(
718 &refined.manifest_path,
719 &file_path,
720 &file_contents,
721 refined.is_example(),
722 refined.extended,
723 Some(refined.kind),
724 );
725 refined.kind = new_kind;
726 refined.manifest_path = new_manifest;
727 refined.figure_main_name();
728 refined
729 }
730
731 /// Expands a subproject CargoTarget into multiple runnable targets.
732 ///
733 /// If the given target's origin is a subproject (i.e. its Cargo.toml is in a subfolder),
734 /// this function loads that Cargo.toml and uses `get_runnable_targets` to discover its runnable targets.
735 /// It then flattens and returns them as a single `Vec<CargoTarget>`.
736 pub fn expand_subproject(target: &CargoTarget) -> Result<Vec<CargoTarget>> {
737 // Ensure the target is a subproject.
738 if let Some(TargetOrigin::SubProject(sub_manifest)) = &target.origin {
739 // Use get_runnable_targets to get targets defined in the subproject.
740 let (bins, examples, benches, tests) =
741 crate::e_manifest::get_runnable_targets(sub_manifest).with_context(|| {
742 format!(
743 "Failed to get runnable targets from {}",
744 sub_manifest.display()
745 )
746 })?;
747 let mut sub_targets = Vec::new();
748 sub_targets.extend(bins);
749 sub_targets.extend(examples);
750 sub_targets.extend(benches);
751 sub_targets.extend(tests);
752
753 // Optionally mark these targets as extended.
754 for t in &mut sub_targets {
755 t.extended = true;
756 match t.kind {
757 TargetKind::Example => t.kind = TargetKind::ExtendedExample,
758 TargetKind::Binary => t.kind = TargetKind::ExtendedBinary,
759 _ => {} // For other kinds, you may leave them unchanged.
760 }
761 }
762 Ok(sub_targets)
763 } else {
764 // If the target is not a subproject, return an empty vector.
765 Ok(vec![])
766 }
767 }
768
769 /// Expands subproject targets in the given map.
770 /// For every target with a SubProject origin, this function removes the original target,
771 /// expands it using `expand_subproject`, and then inserts the expanded targets.
772 /// The expanded targets have their display names modified to include the original folder name as a prefix.
773 /// This version replaces any existing target with the same key.
774 pub fn expand_subprojects_in_place(
775 targets_map: &mut HashMap<(String, String), CargoTarget>,
776 ) -> Result<()> {
777 // Collect keys for targets that are subprojects.
778 let sub_keys: Vec<(String, String)> = targets_map
779 .iter()
780 .filter_map(|(key, target)| {
781 if let Some(TargetOrigin::SubProject(_)) = target.origin {
782 Some(key.clone())
783 } else {
784 None
785 }
786 })
787 .collect();
788
789 for key in sub_keys {
790 if let Some(sub_target) = targets_map.remove(&key) {
791 // Expand the subproject target.
792 let expanded_targets = Self::expand_subproject(&sub_target)?;
793 for mut new_target in expanded_targets {
794 // Update the display name to include the subproject folder name.
795 // For example, if sub_target.display_name was "foo" and new_target.name is "bar",
796 // the new display name becomes "foo > bar".
797 new_target.display_name =
798 format!("{} > {}", sub_target.display_name, new_target.name);
799 // Create a key for the expanded target.
800 let new_key = Self::target_key(&new_target);
801 // Replace any existing target with the same key.
802 targets_map.insert(new_key, new_target);
803 }
804 }
805 }
806 Ok(())
807 }
808 // /// Expands subproject targets in `targets`. Any target whose origin is a SubProject
809 // /// is replaced by the targets returned by `expand_subproject`. If the expansion fails,
810 // /// you can choose to log the error and keep the original target, or remove it.
811 // pub fn expand_subprojects_in_place(
812 // targets_map: &mut HashMap<(String, String), CargoTarget>
813 // ) -> anyhow::Result<()> {
814 // // Collect keys for subproject targets.
815 // let sub_keys: Vec<(String, String)> = targets_map
816 // .iter()
817 // .filter_map(|(key, target)| {
818 // if let Some(crate::e_target::TargetOrigin::SubProject(_)) = target.origin {
819 // Some(key.clone())
820 // } else {
821 // None
822 // }
823 // })
824 // .collect();
825
826 // // For each subproject target, remove it from the map, expand it, and insert the new targets.
827 // for key in sub_keys {
828 // if let Some(sub_target) = targets_map.remove(&key) {
829 // let expanded = Self::expand_subproject(&sub_target)?;
830 // for new_target in expanded {
831 // let new_key = CargoTarget::target_key(&new_target);
832 // targets_map.entry(new_key).or_insert(new_target);
833 // }
834 // }
835 // }
836 // Ok(())
837 // }
838
839 /// Creates a unique key for a target based on its manifest path and name.
840 pub fn target_key(target: &CargoTarget) -> (String, String) {
841 let manifest = target
842 .manifest_path
843 .canonicalize()
844 .unwrap_or_else(|_| target.manifest_path.clone())
845 .to_string_lossy()
846 .into_owned();
847 let name = target.name.clone();
848 (manifest, name)
849 }
850
851 /// Expands a subproject target into multiple targets and inserts them into the provided HashMap,
852 /// using (manifest, name) as a key to avoid duplicates.
853 pub fn expand_subproject_into_map(
854 target: &CargoTarget,
855 map: &mut std::collections::HashMap<(String, String), CargoTarget>,
856 ) -> Result<(), Box<dyn std::error::Error>> {
857 // Only operate if the target is a subproject.
858 if let Some(crate::e_target::TargetOrigin::SubProject(sub_manifest)) = &target.origin {
859 // Discover targets in the subproject.
860 let (bins, examples, benches, tests) =
861 crate::e_manifest::get_runnable_targets(sub_manifest)?;
862 let mut new_targets = Vec::new();
863 new_targets.extend(bins);
864 new_targets.extend(examples);
865 new_targets.extend(benches);
866 new_targets.extend(tests);
867 // Mark these targets as extended.
868 for t in &mut new_targets {
869 t.extended = true;
870 }
871 // Insert each new target if not already present.
872 for new in new_targets {
873 let key = CargoTarget::target_key(&new);
874 map.entry(key).or_insert(new.clone());
875 }
876 }
877 Ok(())
878 }
879
880 /// Returns true if the target is an example.
881 pub fn is_example(&self) -> bool {
882 matches!(
883 self.kind,
884 TargetKind::Example
885 | TargetKind::UnknownExample
886 | TargetKind::UnknownExtendedExample
887 | TargetKind::ExtendedExample
888 | TargetKind::ManifestDioxusExample
889 | TargetKind::ManifestTauriExample
890 )
891 }
892}
893
894/// Returns the "depth" of a path, i.e. the number of components.
895pub fn path_depth(path: &Path) -> usize {
896 path.components().count()
897}
898
899/// Deduplicates targets that share the same (name, origin key). If duplicates are found,
900/// the target with the manifest path of greater depth is kept.
901pub fn dedup_targets(targets: Vec<CargoTarget>) -> Vec<CargoTarget> {
902 let mut grouped: HashMap<(String, Option<String>), CargoTarget> = HashMap::new();
903
904 for target in targets {
905 // We'll group targets by (target.name, origin_key)
906 // Create an origin key if available by canonicalizing the origin path.
907 let origin_key = target.origin.as_ref().and_then(|origin| match origin {
908 TargetOrigin::SingleFile(path)
909 | TargetOrigin::DefaultBinary(path)
910 | TargetOrigin::SubProject(path) => path
911 .canonicalize()
912 .ok()
913 .map(|p| p.to_string_lossy().into_owned()),
914 _ => None,
915 });
916 let key = (target.name.clone(), origin_key);
917
918 grouped
919 .entry(key)
920 .and_modify(|existing| {
921 let current_depth = path_depth(&target.manifest_path);
922 let existing_depth = path_depth(&existing.manifest_path);
923 // If the current target's manifest path is deeper, replace the existing target.
924 if current_depth > existing_depth {
925 *existing = target.clone();
926 }
927 })
928 .or_insert(target);
929 }
930
931 grouped.into_values().collect()
932}