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