cargo_e/e_target.rs
1// src/e_target.rs
2use anyhow::{Context, Result};
3use std::{
4 collections::HashMap,
5 ffi::OsString,
6 fs,
7 path::{Path, PathBuf},
8};
9
10#[derive(Debug, Clone)]
11pub enum TargetOrigin {
12 DefaultBinary(PathBuf),
13 SingleFile(PathBuf),
14 MultiFile(PathBuf),
15 SubProject(PathBuf),
16 Named(OsString),
17}
18
19#[derive(Debug, Clone, PartialEq, Hash, Eq, Copy)]
20pub enum TargetKind {
21 Unknown,
22 Example,
23 ExtendedExample,
24 Binary,
25 ExtendedBinary,
26 Bench,
27 Test,
28 Manifest, // For browsing the entire Cargo.toml or package-level targets.
29 ManifestTauri,
30 ManifestTauriExample,
31 ManifestDioxusExample,
32 ManifestDioxus,
33}
34
35#[derive(Debug, Clone)]
36pub struct CargoTarget {
37 pub name: String,
38 pub display_name: String,
39 pub manifest_path: PathBuf,
40 pub kind: TargetKind,
41 pub extended: bool,
42 pub origin: Option<TargetOrigin>,
43}
44
45impl CargoTarget {
46 /// Constructs a CargoTarget from a source file.
47 ///
48 /// Reads the file at `file_path` and determines the target kind based on:
49 /// - Tauri configuration (e.g. if the manifest's parent is "src-tauri" or a Tauri config exists),
50 /// - Dioxus markers in the file contents,
51 /// - And finally, if the file contains "fn main", using its parent directory (examples vs bin) to decide.
52 ///
53 /// If none of these conditions are met, returns None.
54 pub fn from_source_file(
55 stem: &std::ffi::OsStr,
56 file_path: &Path,
57 manifest_path: &Path,
58 example: bool,
59 extended: bool,
60 ) -> Option<Self> {
61 let file_path = fs::canonicalize(&file_path).unwrap_or(file_path.to_path_buf());
62 let file_contents = std::fs::read_to_string(&file_path).unwrap_or_default();
63 let (kind, new_manifest) = crate::e_discovery::determine_target_kind_and_manifest(
64 manifest_path,
65 &file_path,
66 &file_contents,
67 example,
68 extended,
69 None,
70 );
71 if kind == TargetKind::Unknown {
72 return None;
73 }
74 let name = stem.to_string_lossy().to_string();
75 Some(CargoTarget {
76 name: name.clone(),
77 display_name: name,
78 manifest_path: new_manifest.to_path_buf(),
79 kind,
80 extended,
81 origin: Some(TargetOrigin::SingleFile(file_path.to_path_buf())),
82 })
83 }
84
85 /// Constructs a CargoTarget from a folder by trying to locate a runnable source file.
86 ///
87 /// The function attempts the following candidate paths in order:
88 /// 1. A file named `<folder_name>.rs` in the folder.
89 /// 2. `src/main.rs` inside the folder.
90 /// 3. `main.rs` at the folder root.
91 /// 4. Otherwise, it scans the folder for any `.rs` file containing `"fn main"`.
92 ///
93 /// Once a candidate is found, it reads its contents and calls `determine_target_kind`
94 /// to refine the target kind based on Tauri or Dioxus markers. The `extended` flag
95 /// indicates whether the target should be marked as extended (for instance, if the folder
96 /// is a subdirectory of the primary "examples" or "bin" folder).
97 ///
98 /// Returns Some(CargoTarget) if a runnable file is found, or None otherwise.
99 pub fn from_folder(
100 folder: &Path,
101 manifest_path: &Path,
102 example: bool,
103 _extended: bool,
104 ) -> Option<Self> {
105 // If the folder contains its own Cargo.toml, treat it as a subproject.
106 let sub_manifest = folder.join("Cargo.toml");
107 if sub_manifest.exists() {
108 // Use the folder's name as the candidate target name.
109 let folder_name = folder.file_name()?.to_string_lossy().to_string();
110 // Determine the display name from the parent folder.
111 let display_name = if let Some(parent) = folder.parent() {
112 let parent_name = parent.file_name()?.to_string_lossy();
113 if parent_name == folder_name {
114 // If the parent's name equals the folder's name, try using the grandparent.
115 if let Some(grandparent) = parent.parent() {
116 grandparent.file_name()?.to_string_lossy().to_string()
117 } else {
118 folder_name.clone()
119 }
120 } else {
121 parent_name.to_string()
122 }
123 } else {
124 folder_name.clone()
125 };
126
127 let sub_manifest =
128 fs::canonicalize(&sub_manifest).unwrap_or(sub_manifest.to_path_buf());
129 println!("Subproject found: {}", sub_manifest.display());
130 println!("{}", &folder_name);
131 return Some(CargoTarget {
132 name: folder_name.clone(),
133 display_name,
134 manifest_path: sub_manifest.clone(),
135 // For a subproject, we initially mark it as Manifest;
136 // later refinement may resolve it further.
137 kind: TargetKind::Manifest,
138 extended: true,
139 origin: Some(TargetOrigin::SubProject(sub_manifest)),
140 });
141 }
142 // Extract the folder's name.
143 let folder_name = folder.file_name()?.to_str()?;
144
145 /// Returns Some(candidate) only if the file exists and its contents contain "fn main".
146 fn candidate_with_main(candidate: PathBuf) -> Option<PathBuf> {
147 if candidate.exists() {
148 let contents = fs::read_to_string(&candidate).unwrap_or_default();
149 if contents.contains("fn main") {
150 return Some(candidate);
151 }
152 }
153 None
154 }
155
156 // In your from_folder function, for example:
157 let candidate = if let Some(candidate) =
158 candidate_with_main(folder.join(format!("{}.rs", folder_name)))
159 {
160 candidate
161 } else if let Some(candidate) = candidate_with_main(folder.join("src/main.rs")) {
162 candidate
163 } else if let Some(candidate) = candidate_with_main(folder.join("main.rs")) {
164 candidate
165 } else {
166 // Otherwise, scan the folder for any .rs file containing "fn main"
167 let mut found = None;
168 if let Ok(entries) = fs::read_dir(folder) {
169 for entry in entries.flatten() {
170 let path = entry.path();
171 if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("rs") {
172 if let Some(candidate) = candidate_with_main(path) {
173 found = Some(candidate);
174 break;
175 }
176 }
177 }
178 }
179 found?
180 };
181
182 // // First candidate: folder/<folder_name>.rs
183 // let candidate = if folder.join(format!("{}.rs", folder_name)).exists() {
184 // folder.join(format!("{}.rs", folder_name))
185 // } else if folder.join("src/main.rs").exists() {
186 // folder.join("src/main.rs")
187 // } else if folder.join("main.rs").exists() {
188 // folder.join("main.rs")
189 // } else {
190 // // Otherwise, scan the folder for any .rs file containing "fn main"
191 // let mut found = None;
192 // if let Ok(entries) = fs::read_dir(folder) {
193 // for entry in entries.flatten() {
194 // let path = entry.path();
195 // if path.is_file() && path.extension().and_then(|s| s.to_str()) == Some("rs") {
196 // let contents = fs::read_to_string(&path).unwrap_or_default();
197 // if contents.contains("fn main") {
198 // found = Some(path);
199 // break;
200 // }
201 // }
202 // }
203 // }
204 // found?
205 // };
206
207 let candidate = fs::canonicalize(&candidate).unwrap_or(candidate.to_path_buf());
208 // Compute the extended flag based on the candidate file location.
209 let extended = crate::e_discovery::is_extended_target(manifest_path, &candidate);
210
211 // // Determine a fallback target kind based on the folder name.
212 // let fallback_kind = if folder_name.to_lowercase() == "examples" {
213 // if extended {
214 // TargetKind::ExtendedExample
215 // } else {
216 // TargetKind::Example
217 // }
218 // } else if folder_name.to_lowercase() == "bin" {
219 // if extended {
220 // TargetKind::ExtendedBinary
221 // } else {
222 // TargetKind::Binary
223 // }
224 // } else {
225 // TargetKind::Example
226 // };
227
228 // Read the candidate file's contents.
229 let file_contents = std::fs::read_to_string(&candidate).unwrap_or_default();
230
231 // Use our helper to determine if any special configuration applies.
232 let (kind, new_manifest) = crate::e_discovery::determine_target_kind_and_manifest(
233 manifest_path,
234 &candidate,
235 &file_contents,
236 example,
237 extended,
238 None,
239 );
240 if kind == TargetKind::Unknown {
241 return None;
242 }
243
244 // Determine the candidate file's stem in lowercase.
245 let candidate_stem = candidate.file_stem()?.to_str()?.to_lowercase();
246 let name = if candidate_stem == "main" {
247 candidate
248 .parent()
249 .and_then(|p| p.parent())
250 .and_then(|gp| {
251 gp.file_name().and_then(|s| s.to_str()).and_then(|s| {
252 if s.to_lowercase() == "examples" {
253 // Use candidate's parent folder's name.
254 candidate
255 .parent()
256 .and_then(|p| p.file_name())
257 .and_then(|s| s.to_str())
258 .map(|s| s.to_string())
259 } else {
260 None
261 }
262 })
263 })
264 .unwrap_or(candidate_stem)
265 } else {
266 candidate_stem
267 };
268 Some(CargoTarget {
269 name: name.clone(),
270 display_name: name,
271 manifest_path: new_manifest.to_path_buf(),
272 kind,
273 extended,
274 origin: Some(TargetOrigin::SingleFile(candidate)),
275 })
276 }
277 /// Returns a refined CargoTarget based on its file contents and location.
278 /// This function is pure; it takes an immutable CargoTarget and returns a new one.
279 /// If the target's origin is either SingleFile or DefaultBinary, it reads the file and uses
280 /// `determine_target_kind` to update the kind accordingly.
281 pub fn refined_target(target: &CargoTarget) -> CargoTarget {
282 let mut refined = target.clone();
283
284 // Operate only if the target has a file to inspect.
285 let file_path = match &refined.origin {
286 Some(TargetOrigin::SingleFile(path)) | Some(TargetOrigin::DefaultBinary(path)) => path,
287 _ => return refined,
288 };
289
290 let file_path = fs::canonicalize(&file_path).unwrap_or(file_path.to_path_buf());
291 let file_contents = std::fs::read_to_string(&file_path).unwrap_or_default();
292
293 let (new_kind, new_manifest) = crate::e_discovery::determine_target_kind_and_manifest(
294 &refined.manifest_path,
295 &file_path,
296 &file_contents,
297 refined.is_example(),
298 refined.extended,
299 Some(refined.kind),
300 );
301 refined.kind = new_kind;
302 refined.manifest_path = new_manifest;
303 refined
304 }
305
306 /// Expands a subproject CargoTarget into multiple runnable targets.
307 ///
308 /// If the given target's origin is a subproject (i.e. its Cargo.toml is in a subfolder),
309 /// this function loads that Cargo.toml and uses `get_runnable_targets` to discover its runnable targets.
310 /// It then flattens and returns them as a single `Vec<CargoTarget>`.
311 pub fn expand_subproject(target: &CargoTarget) -> Result<Vec<CargoTarget>> {
312 // Ensure the target is a subproject.
313 if let Some(TargetOrigin::SubProject(sub_manifest)) = &target.origin {
314 // Use get_runnable_targets to get targets defined in the subproject.
315 let (bins, examples, benches, tests) =
316 crate::e_manifest::get_runnable_targets(sub_manifest).with_context(|| {
317 format!(
318 "Failed to get runnable targets from {}",
319 sub_manifest.display()
320 )
321 })?;
322 let mut sub_targets = Vec::new();
323 sub_targets.extend(bins);
324 sub_targets.extend(examples);
325 sub_targets.extend(benches);
326 sub_targets.extend(tests);
327
328 // Optionally mark these targets as extended.
329 for t in &mut sub_targets {
330 t.extended = true;
331 match t.kind {
332 TargetKind::Example => t.kind = TargetKind::ExtendedExample,
333 TargetKind::Binary => t.kind = TargetKind::ExtendedBinary,
334 _ => {} // For other kinds, you may leave them unchanged.
335 }
336 }
337 Ok(sub_targets)
338 } else {
339 // If the target is not a subproject, return an empty vector.
340 Ok(vec![])
341 }
342 }
343
344 /// Expands subproject targets in the given map.
345 /// For every target with a SubProject origin, this function removes the original target,
346 /// expands it using `expand_subproject`, and then inserts the expanded targets.
347 /// The expanded targets have their display names modified to include the original folder name as a prefix.
348 /// This version replaces any existing target with the same key.
349 pub fn expand_subprojects_in_place(
350 targets_map: &mut HashMap<(String, String), CargoTarget>,
351 ) -> Result<()> {
352 // Collect keys for targets that are subprojects.
353 let sub_keys: Vec<(String, String)> = targets_map
354 .iter()
355 .filter_map(|(key, target)| {
356 if let Some(TargetOrigin::SubProject(_)) = target.origin {
357 Some(key.clone())
358 } else {
359 None
360 }
361 })
362 .collect();
363
364 for key in sub_keys {
365 if let Some(sub_target) = targets_map.remove(&key) {
366 // Expand the subproject target.
367 let expanded_targets = Self::expand_subproject(&sub_target)?;
368 for mut new_target in expanded_targets {
369 // Update the display name to include the subproject folder name.
370 // For example, if sub_target.display_name was "foo" and new_target.name is "bar",
371 // the new display name becomes "foo > bar".
372 new_target.display_name =
373 format!("{} > {}", sub_target.display_name, new_target.name);
374 // Create a key for the expanded target.
375 let new_key = Self::target_key(&new_target);
376 // Replace any existing target with the same key.
377 targets_map.insert(new_key, new_target);
378 }
379 }
380 }
381 Ok(())
382 }
383 // /// Expands subproject targets in `targets`. Any target whose origin is a SubProject
384 // /// is replaced by the targets returned by `expand_subproject`. If the expansion fails,
385 // /// you can choose to log the error and keep the original target, or remove it.
386 // pub fn expand_subprojects_in_place(
387 // targets_map: &mut HashMap<(String, String), CargoTarget>
388 // ) -> anyhow::Result<()> {
389 // // Collect keys for subproject targets.
390 // let sub_keys: Vec<(String, String)> = targets_map
391 // .iter()
392 // .filter_map(|(key, target)| {
393 // if let Some(crate::e_target::TargetOrigin::SubProject(_)) = target.origin {
394 // Some(key.clone())
395 // } else {
396 // None
397 // }
398 // })
399 // .collect();
400
401 // // For each subproject target, remove it from the map, expand it, and insert the new targets.
402 // for key in sub_keys {
403 // if let Some(sub_target) = targets_map.remove(&key) {
404 // let expanded = Self::expand_subproject(&sub_target)?;
405 // for new_target in expanded {
406 // let new_key = CargoTarget::target_key(&new_target);
407 // targets_map.entry(new_key).or_insert(new_target);
408 // }
409 // }
410 // }
411 // Ok(())
412 // }
413
414 /// Creates a unique key for a target based on its manifest path and name.
415 pub fn target_key(target: &CargoTarget) -> (String, String) {
416 let manifest = target
417 .manifest_path
418 .canonicalize()
419 .unwrap_or_else(|_| target.manifest_path.clone())
420 .to_string_lossy()
421 .into_owned();
422 let name = target.name.clone();
423 (manifest, name)
424 }
425
426 /// Expands a subproject target into multiple targets and inserts them into the provided HashMap,
427 /// using (manifest, name) as a key to avoid duplicates.
428 pub fn expand_subproject_into_map(
429 target: &CargoTarget,
430 map: &mut std::collections::HashMap<(String, String), CargoTarget>,
431 ) -> Result<(), Box<dyn std::error::Error>> {
432 // Only operate if the target is a subproject.
433 if let Some(crate::e_target::TargetOrigin::SubProject(sub_manifest)) = &target.origin {
434 // Discover targets in the subproject.
435 let (bins, examples, benches, tests) =
436 crate::e_manifest::get_runnable_targets(sub_manifest)?;
437 let mut new_targets = Vec::new();
438 new_targets.extend(bins);
439 new_targets.extend(examples);
440 new_targets.extend(benches);
441 new_targets.extend(tests);
442 // Mark these targets as extended.
443 for t in &mut new_targets {
444 t.extended = true;
445 }
446 // Insert each new target if not already present.
447 for new in new_targets {
448 let key = CargoTarget::target_key(&new);
449 map.entry(key).or_insert(new.clone());
450 println!("Inserted subproject target: {}", new.name);
451 }
452 }
453 Ok(())
454 }
455
456 /// Returns true if the target is an example.
457 pub fn is_example(&self) -> bool {
458 matches!(
459 self.kind,
460 TargetKind::Example
461 | TargetKind::ExtendedExample
462 | TargetKind::ManifestDioxusExample
463 | TargetKind::ManifestTauriExample
464 )
465 }
466}
467
468/// Returns the "depth" of a path, i.e. the number of components.
469pub fn path_depth(path: &Path) -> usize {
470 path.components().count()
471}
472
473/// Deduplicates targets that share the same (name, origin key). If duplicates are found,
474/// the target with the manifest path of greater depth is kept.
475pub fn dedup_targets(targets: Vec<CargoTarget>) -> Vec<CargoTarget> {
476 let mut grouped: HashMap<(String, Option<String>), CargoTarget> = HashMap::new();
477
478 for target in targets {
479 // We'll group targets by (target.name, origin_key)
480 // Create an origin key if available by canonicalizing the origin path.
481 let origin_key = target.origin.as_ref().and_then(|origin| match origin {
482 TargetOrigin::SingleFile(path)
483 | TargetOrigin::DefaultBinary(path)
484 | TargetOrigin::SubProject(path) => path
485 .canonicalize()
486 .ok()
487 .map(|p| p.to_string_lossy().into_owned()),
488 _ => None,
489 });
490 let key = (target.name.clone(), origin_key);
491
492 grouped
493 .entry(key)
494 .and_modify(|existing| {
495 let current_depth = path_depth(&target.manifest_path);
496 let existing_depth = path_depth(&existing.manifest_path);
497 // If the current target's manifest path is deeper, replace the existing target.
498 if current_depth > existing_depth {
499 *existing = target.clone();
500 }
501 })
502 .or_insert(target);
503 }
504
505 grouped.into_values().collect()
506}