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