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