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
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 debug!("Name after folder-based logic: {}", name);
140
141 // If the candidate stem is "main", interrogate the manifest.
142 if candidate_stem == "main" {
143 let manifest_contents = fs::read_to_string(&self.manifest_path).unwrap_or_default();
144 if let Ok(manifest_toml) = manifest_contents.parse::<Value>() {
145 // Check for any [[bin]] entries.
146 if let Some(bins) = manifest_toml.get("bin").and_then(|v| v.as_array()) {
147 debug!("Found {} [[bin]] entries", bins.len());
148 if let Some(bin_name) = bins.iter().find_map(|bin| {
149 if let Some(path_str) = bin.get("path").and_then(|p| p.as_str()) {
150 debug!("Checking bin entry with path: {}", path_str);
151 if path_str == "src/bin/main.rs" {
152 let bn = bin
153 .get("name")
154 .and_then(|n| n.as_str())
155 .map(|s| s.to_string());
156 debug!("Found matching bin with name: {:?}", bn);
157 return bn;
158 }
159 }
160 None
161 }) {
162 debug!("Using bin name from manifest: {}", bin_name);
163 name = bin_name;
164 } else if let Some(pkg) = manifest_toml.get("package") {
165 debug!("No matching [[bin]] entry; checking [package] section");
166 name = pkg
167 .get("name")
168 .and_then(|n| n.as_str())
169 .unwrap_or(&name)
170 .to_string();
171 debug!("Using package name from manifest: {}", name);
172 }
173 } else if let Some(pkg) = manifest_toml.get("package") {
174 debug!("No [[bin]] section found; using [package] section");
175 name = pkg
176 .get("name")
177 .and_then(|n| n.as_str())
178 .unwrap_or(&name)
179 .to_string();
180 debug!("Using package name from manifest: {}", name);
181 } else {
182 debug!(
183 "Manifest does not contain [[bin]] or [package] sections; keeping name: {}",
184 name
185 );
186 }
187 } else {
188 debug!("Failed to parse manifest TOML; keeping name: {}", name);
189 }
190 }
191
192 debug!("Final determined name: {}", name);
193 if name.eq("main") {
194 panic!("Name is main");
195 }
196 self.name = name.clone();
197 self.display_name = name;
198 }
199
200 /// Constructs a CargoTarget from a folder by trying to locate a runnable source file.
201 ///
202 /// The function attempts the following candidate paths in order:
203 /// 1. A file named `<folder_name>.rs` in the folder.
204 /// 2. `src/main.rs` inside the folder.
205 /// 3. `main.rs` at the folder root.
206 /// 4. Otherwise, it scans the folder for any `.rs` file containing `"fn main"`.
207 ///
208 /// Once a candidate is found, it reads its contents and calls `determine_target_kind`
209 /// to refine the target kind based on Tauri or Dioxus markers. The `extended` flag
210 /// indicates whether the target should be marked as extended (for instance, if the folder
211 /// is a subdirectory of the primary "examples" or "bin" folder).
212 ///
213 /// Returns Some(CargoTarget) if a runnable file is found, or None otherwise.
214 pub fn from_folder(
215 folder: &Path,
216 manifest_path: &Path,
217 example: bool,
218 _extended: bool,
219 ) -> Option<Self> {
220 // If the folder contains its own Cargo.toml, treat it as a subproject.
221 let sub_manifest = folder.join("Cargo.toml");
222 if sub_manifest.exists() {
223 // Use the folder's name as the candidate target name.
224 let folder_name = folder.file_name()?.to_string_lossy().to_string();
225 // Determine the display name from the parent folder.
226 let display_name = if let Some(parent) = folder.parent() {
227 let parent_name = parent.file_name()?.to_string_lossy();
228 if parent_name == folder_name {
229 // If the parent's name equals the folder's name, try using the grandparent.
230 if let Some(grandparent) = parent.parent() {
231 grandparent.file_name()?.to_string_lossy().to_string()
232 } else {
233 folder_name.clone()
234 }
235 } else {
236 parent_name.to_string()
237 }
238 } else {
239 folder_name.clone()
240 };
241
242 let sub_manifest =
243 fs::canonicalize(&sub_manifest).unwrap_or(sub_manifest.to_path_buf());
244 debug!("Subproject found: {}", sub_manifest.display());
245 debug!("{}", &folder_name);
246 return Some(CargoTarget {
247 name: folder_name.clone(),
248 display_name,
249 manifest_path: sub_manifest.clone(),
250 // For a subproject, we initially mark it as Manifest;
251 // later refinement may resolve it further.
252 kind: TargetKind::Manifest,
253 extended: true,
254 origin: Some(TargetOrigin::SubProject(sub_manifest)),
255 });
256 }
257 // Extract the folder's name.
258 let folder_name = folder.file_name()?.to_str()?;
259
260 /// Returns Some(candidate) only if the file exists and its contents contain "fn main".
261 fn candidate_with_main(candidate: PathBuf) -> Option<PathBuf> {
262 if candidate.exists() {
263 let contents = fs::read_to_string(&candidate).unwrap_or_default();
264 if contents.contains("fn main") {
265 return Some(candidate);
266 }
267 }
268 None
269 }
270
271 // In your from_folder function, for example:
272 let candidate = if let Some(candidate) =
273 candidate_with_main(folder.join(format!("{}.rs", folder_name)))
274 {
275 candidate
276 } else if let Some(candidate) = candidate_with_main(folder.join("src/main.rs")) {
277 candidate
278 } else if let Some(candidate) = candidate_with_main(folder.join("main.rs")) {
279 candidate
280 } else {
281 // Otherwise, scan the folder for any .rs file containing "fn main"
282 let mut found = None;
283 if let Ok(entries) = fs::read_dir(folder) {
284 for entry in entries.flatten() {
285 let path = entry.path();
286 if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("rs") {
287 if let Some(candidate) = candidate_with_main(path) {
288 found = Some(candidate);
289 break;
290 }
291 }
292 }
293 }
294 found?
295 };
296
297 let candidate = fs::canonicalize(&candidate).unwrap_or(candidate.to_path_buf());
298 // Compute the extended flag based on the candidate file location.
299 let extended = crate::e_discovery::is_extended_target(manifest_path, &candidate);
300
301 // Read the candidate file's contents.
302 let file_contents = std::fs::read_to_string(&candidate).unwrap_or_default();
303
304 // Use our helper to determine if any special configuration applies.
305 let (kind, new_manifest) = crate::e_discovery::determine_target_kind_and_manifest(
306 manifest_path,
307 &candidate,
308 &file_contents,
309 example,
310 extended,
311 None,
312 );
313 if kind == TargetKind::Unknown {
314 return None;
315 }
316
317 // Determine the candidate file's stem in lowercase.
318 let name = candidate.file_stem()?.to_str()?.to_lowercase();
319 // let name = if candidate_stem == "main" {
320 // if let Some(parent_dir) = candidate.parent() {
321 // if let Some(parent_name) = parent_dir.file_name().and_then(|s| s.to_str()) {
322 // if parent_name.eq_ignore_ascii_case("src") {
323 // // If candidate is src/main.rs, take the parent of "src".
324 // parent_dir.parent()
325 // .and_then(|proj_dir| proj_dir.file_name())
326 // .and_then(|s| s.to_str())
327 // .map(|s| s.to_string())
328 // .unwrap_or(candidate_stem.clone())
329 // } else if parent_name.eq_ignore_ascii_case("examples") {
330 // // If candidate is in the examples folder (e.g. examples/main.rs),
331 // // use the candidate's parent folder's name.
332 // candidate.parent()
333 // .and_then(|p| p.file_name())
334 // .and_then(|s| s.to_str())
335 // .map(|s| s.to_string())
336 // .unwrap_or(candidate_stem.clone())
337 // } else {
338 // // Fall back to the candidate_stem if no special case matches.
339 // candidate_stem.clone()
340 // }
341 // } else {
342 // candidate_stem.clone()
343 // }
344 // } else {
345 // candidate_stem.clone()
346 // }
347 // } else {
348 // candidate_stem.clone()
349 // };
350 // let name = if candidate_stem.clone() == "main" {
351 // // Read the manifest contents.
352 // let manifest_contents = fs::read_to_string(manifest_path).unwrap_or_default();
353 // if let Ok(manifest_toml) = manifest_contents.parse::<toml::Value>() {
354 // // Look for any [[bin]] entries.
355 // if let Some(bins) = manifest_toml.get("bin").and_then(|v| v.as_array()) {
356 // if let Some(bin_name) = bins.iter().find_map(|bin| {
357 // if let Some(path_str) = bin.get("path").and_then(|p| p.as_str()) {
358 // if path_str == "src/bin/main.rs" {
359 // return bin.get("name").and_then(|n| n.as_str()).map(|s| s.to_string());
360 // }
361 // }
362 // None
363 // }) {
364 // // Found a bin with the matching path; use its name.
365 // bin_name
366 // } else if let Some(pkg) = manifest_toml.get("package") {
367 // // No matching bin entry, so use the package name.
368 // pkg.get("name").and_then(|n| n.as_str()).unwrap_or(&candidate_stem).to_string()
369 // } else {
370 // candidate_stem.to_string()
371 // }
372 // } else if let Some(pkg) = manifest_toml.get("package") {
373 // // No [[bin]] section; use the package name.
374 // pkg.get("name").and_then(|n| n.as_str()).unwrap_or(&candidate_stem).to_string()
375 // } else {
376 // candidate_stem.to_string()
377 // }
378 // } else {
379 // candidate_stem.to_string()
380 // }
381 // } else {
382 // candidate_stem.to_string()
383 // };
384 let mut target = CargoTarget {
385 name: name.clone(),
386 display_name: name,
387 manifest_path: new_manifest.to_path_buf(),
388 kind,
389 extended,
390 origin: Some(TargetOrigin::SingleFile(candidate)),
391 };
392 // Call the method to update name based on the candidate and manifest.
393 target.figure_main_name();
394 Some(target)
395 }
396 /// Returns a refined CargoTarget based on its file contents and location.
397 /// This function is pure; it takes an immutable CargoTarget and returns a new one.
398 /// If the target's origin is either SingleFile or DefaultBinary, it reads the file and uses
399 /// `determine_target_kind` to update the kind accordingly.
400 pub fn refined_target(target: &CargoTarget) -> CargoTarget {
401 let mut refined = target.clone();
402
403 // Operate only if the target has a file to inspect.
404 let file_path = match &refined.origin {
405 Some(TargetOrigin::SingleFile(path)) | Some(TargetOrigin::DefaultBinary(path)) => path,
406 _ => return refined,
407 };
408
409 let file_path = fs::canonicalize(&file_path).unwrap_or(file_path.to_path_buf());
410 let file_contents = std::fs::read_to_string(&file_path).unwrap_or_default();
411
412 let (new_kind, new_manifest) = crate::e_discovery::determine_target_kind_and_manifest(
413 &refined.manifest_path,
414 &file_path,
415 &file_contents,
416 refined.is_example(),
417 refined.extended,
418 Some(refined.kind),
419 );
420 refined.kind = new_kind;
421 refined.manifest_path = new_manifest;
422 refined.figure_main_name();
423 refined
424 }
425
426 /// Expands a subproject CargoTarget into multiple runnable targets.
427 ///
428 /// If the given target's origin is a subproject (i.e. its Cargo.toml is in a subfolder),
429 /// this function loads that Cargo.toml and uses `get_runnable_targets` to discover its runnable targets.
430 /// It then flattens and returns them as a single `Vec<CargoTarget>`.
431 pub fn expand_subproject(target: &CargoTarget) -> Result<Vec<CargoTarget>> {
432 // Ensure the target is a subproject.
433 if let Some(TargetOrigin::SubProject(sub_manifest)) = &target.origin {
434 // Use get_runnable_targets to get targets defined in the subproject.
435 let (bins, examples, benches, tests) =
436 crate::e_manifest::get_runnable_targets(sub_manifest).with_context(|| {
437 format!(
438 "Failed to get runnable targets from {}",
439 sub_manifest.display()
440 )
441 })?;
442 let mut sub_targets = Vec::new();
443 sub_targets.extend(bins);
444 sub_targets.extend(examples);
445 sub_targets.extend(benches);
446 sub_targets.extend(tests);
447
448 // Optionally mark these targets as extended.
449 for t in &mut sub_targets {
450 t.extended = true;
451 match t.kind {
452 TargetKind::Example => t.kind = TargetKind::ExtendedExample,
453 TargetKind::Binary => t.kind = TargetKind::ExtendedBinary,
454 _ => {} // For other kinds, you may leave them unchanged.
455 }
456 }
457 Ok(sub_targets)
458 } else {
459 // If the target is not a subproject, return an empty vector.
460 Ok(vec![])
461 }
462 }
463
464 /// Expands subproject targets in the given map.
465 /// For every target with a SubProject origin, this function removes the original target,
466 /// expands it using `expand_subproject`, and then inserts the expanded targets.
467 /// The expanded targets have their display names modified to include the original folder name as a prefix.
468 /// This version replaces any existing target with the same key.
469 pub fn expand_subprojects_in_place(
470 targets_map: &mut HashMap<(String, String), CargoTarget>,
471 ) -> Result<()> {
472 // Collect keys for targets that are subprojects.
473 let sub_keys: Vec<(String, String)> = targets_map
474 .iter()
475 .filter_map(|(key, target)| {
476 if let Some(TargetOrigin::SubProject(_)) = target.origin {
477 Some(key.clone())
478 } else {
479 None
480 }
481 })
482 .collect();
483
484 for key in sub_keys {
485 if let Some(sub_target) = targets_map.remove(&key) {
486 // Expand the subproject target.
487 let expanded_targets = Self::expand_subproject(&sub_target)?;
488 for mut new_target in expanded_targets {
489 // Update the display name to include the subproject folder name.
490 // For example, if sub_target.display_name was "foo" and new_target.name is "bar",
491 // the new display name becomes "foo > bar".
492 new_target.display_name =
493 format!("{} > {}", sub_target.display_name, new_target.name);
494 // Create a key for the expanded target.
495 let new_key = Self::target_key(&new_target);
496 // Replace any existing target with the same key.
497 targets_map.insert(new_key, new_target);
498 }
499 }
500 }
501 Ok(())
502 }
503 // /// Expands subproject targets in `targets`. Any target whose origin is a SubProject
504 // /// is replaced by the targets returned by `expand_subproject`. If the expansion fails,
505 // /// you can choose to log the error and keep the original target, or remove it.
506 // pub fn expand_subprojects_in_place(
507 // targets_map: &mut HashMap<(String, String), CargoTarget>
508 // ) -> anyhow::Result<()> {
509 // // Collect keys for subproject targets.
510 // let sub_keys: Vec<(String, String)> = targets_map
511 // .iter()
512 // .filter_map(|(key, target)| {
513 // if let Some(crate::e_target::TargetOrigin::SubProject(_)) = target.origin {
514 // Some(key.clone())
515 // } else {
516 // None
517 // }
518 // })
519 // .collect();
520
521 // // For each subproject target, remove it from the map, expand it, and insert the new targets.
522 // for key in sub_keys {
523 // if let Some(sub_target) = targets_map.remove(&key) {
524 // let expanded = Self::expand_subproject(&sub_target)?;
525 // for new_target in expanded {
526 // let new_key = CargoTarget::target_key(&new_target);
527 // targets_map.entry(new_key).or_insert(new_target);
528 // }
529 // }
530 // }
531 // Ok(())
532 // }
533
534 /// Creates a unique key for a target based on its manifest path and name.
535 pub fn target_key(target: &CargoTarget) -> (String, String) {
536 let manifest = target
537 .manifest_path
538 .canonicalize()
539 .unwrap_or_else(|_| target.manifest_path.clone())
540 .to_string_lossy()
541 .into_owned();
542 let name = target.name.clone();
543 (manifest, name)
544 }
545
546 /// Expands a subproject target into multiple targets and inserts them into the provided HashMap,
547 /// using (manifest, name) as a key to avoid duplicates.
548 pub fn expand_subproject_into_map(
549 target: &CargoTarget,
550 map: &mut std::collections::HashMap<(String, String), CargoTarget>,
551 ) -> Result<(), Box<dyn std::error::Error>> {
552 // Only operate if the target is a subproject.
553 if let Some(crate::e_target::TargetOrigin::SubProject(sub_manifest)) = &target.origin {
554 // Discover targets in the subproject.
555 let (bins, examples, benches, tests) =
556 crate::e_manifest::get_runnable_targets(sub_manifest)?;
557 let mut new_targets = Vec::new();
558 new_targets.extend(bins);
559 new_targets.extend(examples);
560 new_targets.extend(benches);
561 new_targets.extend(tests);
562 // Mark these targets as extended.
563 for t in &mut new_targets {
564 t.extended = true;
565 }
566 // Insert each new target if not already present.
567 for new in new_targets {
568 let key = CargoTarget::target_key(&new);
569 map.entry(key).or_insert(new.clone());
570 }
571 }
572 Ok(())
573 }
574
575 /// Returns true if the target is an example.
576 pub fn is_example(&self) -> bool {
577 matches!(
578 self.kind,
579 TargetKind::Example
580 | TargetKind::ExtendedExample
581 | TargetKind::ManifestDioxusExample
582 | TargetKind::ManifestTauriExample
583 )
584 }
585}
586
587/// Returns the "depth" of a path, i.e. the number of components.
588pub fn path_depth(path: &Path) -> usize {
589 path.components().count()
590}
591
592/// Deduplicates targets that share the same (name, origin key). If duplicates are found,
593/// the target with the manifest path of greater depth is kept.
594pub fn dedup_targets(targets: Vec<CargoTarget>) -> Vec<CargoTarget> {
595 let mut grouped: HashMap<(String, Option<String>), CargoTarget> = HashMap::new();
596
597 for target in targets {
598 // We'll group targets by (target.name, origin_key)
599 // Create an origin key if available by canonicalizing the origin path.
600 let origin_key = target.origin.as_ref().and_then(|origin| match origin {
601 TargetOrigin::SingleFile(path)
602 | TargetOrigin::DefaultBinary(path)
603 | TargetOrigin::SubProject(path) => path
604 .canonicalize()
605 .ok()
606 .map(|p| p.to_string_lossy().into_owned()),
607 _ => None,
608 });
609 let key = (target.name.clone(), origin_key);
610
611 grouped
612 .entry(key)
613 .and_modify(|existing| {
614 let current_depth = path_depth(&target.manifest_path);
615 let existing_depth = path_depth(&existing.manifest_path);
616 // If the current target's manifest path is deeper, replace the existing target.
617 if current_depth > existing_depth {
618 *existing = target.clone();
619 }
620 })
621 .or_insert(target);
622 }
623
624 grouped.into_values().collect()
625}