cargo_e/e_collect.rs
1use crate::e_target::{CargoTarget, TargetKind, TargetOrigin};
2use crate::{e_workspace, prelude::*};
3use std::collections::HashMap;
4use std::process::Output;
5/// Helper function that runs a Cargo command with a given manifest path.
6/// If it detects the workspace error, it temporarily patches the manifest (by
7/// appending an empty `[workspace]` table), re-runs the command, and then restores
8/// the original file.
9fn run_cargo_with_opt_out(args: &[&str], manifest_path: &Path) -> Result<Output, Box<dyn Error>> {
10 // Run the initial command.
11 let output = Command::new("cargo")
12 .args(args)
13 .arg("--manifest-path")
14 .arg(manifest_path)
15 .output()?;
16
17 let stderr_str = String::from_utf8_lossy(&output.stderr);
18 let workspace_error_marker = "current package believes it's in a workspace when it's not:";
19
20 // If we detect the workspace error, patch the manifest.
21 if stderr_str.contains(workspace_error_marker) {
22 // Backup the original manifest.
23 let original = fs::read_to_string(manifest_path)?;
24
25 // Only patch if the manifest doesn't already opt out.
26 if !original.contains("[workspace]") {
27 // Append an empty [workspace] table.
28 let patched = format!("{}\n[workspace]\n", original);
29 fs::write(manifest_path, &patched)?;
30
31 // Re-run the command with the patched manifest.
32 let patched_output = Command::new("cargo")
33 .args(args)
34 .arg("--manifest-path")
35 .arg(manifest_path)
36 .output()?;
37
38 // Restore the original manifest.
39 fs::write(manifest_path, original)?;
40
41 return Ok(patched_output);
42 }
43 }
44
45 Ok(output)
46}
47
48/// Runs `cargo run --bin` (without specifying a binary name) so that Cargo prints an error with
49/// a list of available binary targets. Then parses that list to return a vector of Example instances,
50/// using the provided prefix.
51pub fn collect_binaries(
52 prefix: &str,
53 manifest_path: &Path,
54 extended: bool,
55) -> Result<Vec<CargoTarget>, Box<dyn Error>> {
56 // Run the Cargo command using our helper.
57 let output = run_cargo_with_opt_out(&["run", "--bin"], manifest_path)?;
58 let stderr_str = String::from_utf8_lossy(&output.stderr);
59 debug!("DEBUG {} {} ", prefix, manifest_path.display());
60 debug!("DEBUG: stderr (binaries) = {:?}", stderr_str);
61
62 let bin_names = crate::parse_available(&stderr_str, "binaries");
63
64 let binaries = bin_names
65 .into_iter()
66 .map(|name| {
67 let target_kind = if let Some(parent) = manifest_path.parent() {
68 if parent.file_name().and_then(|s| s.to_str()) == Some("src-tauri") {
69 TargetKind::ManifestTauri
70 } else if extended {
71 TargetKind::ExtendedBinary
72 } else {
73 TargetKind::Binary
74 }
75 } else if extended {
76 TargetKind::ExtendedBinary
77 } else {
78 TargetKind::Binary
79 };
80 // let target_kind = TargetKind::Binary;
81
82 // let display_name = name.clone();
83 // format!("builtin binary: {}", name);
84 let display_name = if prefix.starts_with('$') {
85 format!("{} > binary > {}", prefix, name)
86 } else if extended {
87 format!("{} {}", prefix, name)
88 } else if prefix.starts_with("builtin") {
89 format!("builtin binary: {}", name)
90 } else {
91 format!("{} {}", prefix, name)
92 // name.clone()
93 };
94 CargoTarget {
95 name: name.clone(),
96 display_name,
97 manifest_path: manifest_path.into(),
98 kind: target_kind,
99 extended,
100 toml_specified: true,
101 origin: Some(TargetOrigin::TomlSpecified(manifest_path.to_path_buf())),
102 }
103 })
104 .collect();
105
106 Ok(binaries)
107}
108
109/// Runs `cargo run --example` so that Cargo lists available examples,
110/// then parses the stderr output to return a vector of Example instances.
111pub fn collect_examples(
112 prefix: &str,
113 manifest_path: &Path,
114 extended: bool,
115) -> Result<Vec<CargoTarget>, Box<dyn Error>> {
116 // Run the Cargo command using our helper.
117 let output = run_cargo_with_opt_out(&["run", "--example"], manifest_path)?;
118 debug!("DEBUG {} {} ", prefix, manifest_path.display());
119 let stderr_str = String::from_utf8_lossy(&output.stderr);
120 debug!("DEBUG: stderr (examples) = {:?}", stderr_str);
121
122 let names = crate::parse_available(&stderr_str, "examples");
123 if names.len() > 0 {
124 debug!("DEBUG: example names = {:?}", names);
125 }
126
127 let examples = names
128 .into_iter()
129 .map(|name| {
130 // let target_kind = if extended {
131 // TargetKind::ExtendedExample
132 // } else {
133 // TargetKind::Example
134 // };
135 // let target_kind = TargetKind::Example;
136 let target_kind = if let Some(parent) = manifest_path.parent() {
137 if parent.file_name().and_then(|s| s.to_str()) == Some("src-tauri") {
138 TargetKind::ManifestTauri
139 } else if extended {
140 TargetKind::ExtendedExample
141 } else {
142 TargetKind::Example
143 }
144 } else if extended {
145 TargetKind::ExtendedExample
146 } else {
147 TargetKind::Example
148 };
149
150 let display_name = name.clone();
151 // format!("builtin example: {}", name);
152 // if prefix.starts_with('$') {
153 // format!("{} > example > {}", prefix, name)
154 // } else if extended {
155 // format!("{} {}", prefix, name)
156 // } else if prefix.starts_with("builtin") {
157 // format!("builtin example: {}", name)
158 // } else {
159 // format!("{} {}", prefix, name)
160 // };
161 let target = CargoTarget {
162 name: name.clone(),
163 display_name,
164 manifest_path: manifest_path.into(),
165 kind: target_kind,
166 extended,
167 toml_specified: true,
168 origin: Some(TargetOrigin::TomlSpecified(manifest_path.to_path_buf())),
169 };
170 target
171 })
172 .collect();
173
174 Ok(examples)
175}
176
177// --- Concurrent or sequential collection ---
178pub fn collect_samples(
179 _workspace_mode: bool,
180 manifest_infos: Vec<(String, PathBuf, bool)>,
181 __max_concurrency: usize,
182) -> Result<Vec<CargoTarget>, Box<dyn Error>> {
183 let mut all_samples = Vec::new();
184
185 #[cfg(feature = "concurrent")]
186 {
187 use threadpool::ThreadPool;
188 let pool = ThreadPool::new(__max_concurrency);
189 let (tx, rx) = mpsc::channel();
190
191 let start_concurrent = Instant::now();
192 for (_prefix, manifest_path, _extended) in manifest_infos {
193 let tx = tx.clone();
194 let manifest_clone = manifest_path.clone();
195 pool.execute(move || {
196 let prefix_clone = _prefix.clone(); // Define prefix_clone here
197 // 1. Collect the builtin stuff
198 let mut builtin_examples = Vec::new();
199 if let Ok(mut ex) =
200 collect_examples(&prefix_clone, &manifest_clone, _workspace_mode)
201 {
202 builtin_examples.append(&mut ex);
203 }
204 let mut builtin_bins = Vec::new();
205 if let Ok(mut bins) =
206 collect_binaries(&prefix_clone, &manifest_clone, _workspace_mode)
207 {
208 builtin_bins.append(&mut bins);
209 }
210 debug!(
211 "DEBUG: {} builtin examples = {:?}",
212 &manifest_clone.display(),
213 builtin_examples
214 );
215 debug!(
216 "DEBUG: {} builtin binaries = {:?}",
217 &manifest_clone.display(),
218 builtin_bins
219 );
220 // 2. Get the “official” runnable ones
221 let (runnable_bins, runnable_examples, benches, tests) =
222 crate::e_manifest::get_runnable_targets(&manifest_clone).unwrap_or_default();
223
224 // if nothing at all, skip
225 if runnable_bins.is_empty()
226 && runnable_examples.is_empty()
227 && builtin_bins.is_empty()
228 && builtin_examples.is_empty()
229 {
230 return;
231 }
232
233 // 1) Start with all the builtins in order
234 let mut bins = builtin_bins;
235
236 // 2) For each runnable:
237 // – if it matches a builtin by name, overwrite that slot
238 // – otherwise push to the end
239 for runnable in runnable_bins {
240 if let Some(idx) = bins.iter().position(|b| b.name == runnable.name) {
241 bins[idx] = runnable;
242 } else {
243 bins.push(runnable);
244 }
245 }
246
247 let mut examples = builtin_examples;
248 for runnable in runnable_examples {
249 if let Some(idx) = examples.iter().position(|e| e.name == runnable.name) {
250 examples[idx] = runnable;
251 } else {
252 examples.push(runnable);
253 }
254 }
255
256 // // 3. Merge bins: start with runnable, then override/insert builtin by name
257 // let mut bins_map: HashMap<_, _> = runnable_bins
258 // .into_iter()
259 // .map(|bin| (bin.name.clone(), bin))
260 // .collect();
261 // // for bin in builtin_bins {
262 // // bins_map.insert(bin.name.clone(), bin);
263 // // }
264 // let bins: Vec<_> = bins_map.into_values().collect();
265
266 // // 4. Same for examples
267 // let mut ex_map: HashMap<_, _> = runnable_examples
268 // .into_iter()
269 // .map(|ex| (ex.name.clone(), ex))
270 // .collect();
271 // // for ex in builtin_examples {
272 // // ex_map.insert(ex.name.clone(), ex);
273 // // }
274 // let examples: Vec<_> = ex_map.into_values().collect();
275
276 debug!("DEBUG: merged examples = {:#?}", examples);
277 // 5. Now combine everything
278 let all_targets = bins
279 .into_iter()
280 .chain(examples)
281 .chain(benches)
282 .chain(tests)
283 .collect::<Vec<_>>();
284 // let mut results = Vec::new();
285 // let prefix_clone = _prefix.clone(); // Define prefix_clone here
286 // if let Ok(mut ex) = collect_examples(&prefix_clone, &manifest_clone, _workspace_mode) {
287 // results.append(&mut ex);
288 // }
289 // if let Ok(mut bins) = collect_binaries(&prefix_clone, &manifest_clone, _workspace_mode) {
290 // results.append(&mut bins);
291 // }
292 // // Retrieve the runnable targets from the manifest.
293 // let (bins, examples, benches, tests) =
294 // crate::e_manifest::get_runnable_targets(&manifest_clone).unwrap_or_default();
295
296 // // If there are no examples or binaries, return early.
297 // if bins.is_empty() && examples.is_empty() {
298 // return;
299 // }
300
301 // // Combine all targets.
302 // let all_targets = bins
303 // .into_iter()
304 // .chain(results)
305 // .chain(examples)
306 // .chain(benches)
307 // .chain(tests)
308 // .collect::<Vec<_>>();
309 // log::debug!("DEBUG: all_targets = {:?}", all_targets);
310 // Now refine each target using the new pure method.
311 // If you implemented it as an associated method `refined()`:
312
313 let refined_targets: Vec<_> = all_targets
314 .into_iter()
315 .map(|t| CargoTarget::refined_target(&t))
316 .collect();
317 tx.send(refined_targets).expect("Failed to send results");
318 // tx.send(all_targets).expect("Failed to send results");
319 // let mut results = Vec::new();
320 // results.extend(bins);
321 // results.extend(examples);
322 // results.extend(benches);
323 // results.extend(tests);
324 // tx.send(results).expect("Failed to send results");
325
326 // let etargets =
327 // crate::e_discovery::discover_targets(manifest_clone.parent().unwrap()).unwrap();
328 // for target in etargets {
329 // let m = target.manifest_path.clone();
330 // // let manifest_path = Path::new(&m);
331 // // if target.name.contains("html") {
332 // // std::process::exit(0);
333 // // }
334 // if let Some(existing) = results.iter_mut().find(|r| r.name == target.name) {
335 // debug!("REPLACING {}",target.name);
336 // *existing = target;
337 // } else {
338 // debug!("ADDING {}",target.name);
339 // results.push(target);
340 // }
341
342 // // Check if the target is extended and that its name is not already present in results.
343 // // if target.extended {
344 // // let new_prefix = format!("examples/{}", &prefix_clone);
345 // // // Collect extended examples.
346 // // if let Ok(ex_ext) = collect_examples(&new_prefix, manifest_path, true) {
347 // // for ex in ex_ext {
348 // // if !results.iter().any(|r| r.name == ex.name) {
349 // // results.push(ex);
350 // // }
351 // // }
352 // // }
353
354 // // // Collect extended binaries.
355 // // if let Ok(bins_ext) = collect_binaries(&new_prefix, manifest_path, true) {
356 // // for bin in bins_ext {
357 // // if !results.iter().any(|r| r.name == bin.name) {
358 // // results.push(bin);
359 // // }
360 // // }
361 // // }
362 // // }
363 // }
364 // tx.send(results).expect("Failed to send results");
365 });
366 }
367 drop(tx);
368 pool.join(); // Wait for all tasks to finish.
369 let duration_concurrent = start_concurrent.elapsed();
370 debug!(
371 "timing: {} threads took {:?}",
372 __max_concurrency, duration_concurrent
373 );
374
375 for samples in rx {
376 debug!("DEBUG: samples = {:#?}", samples);
377 all_samples.extend(samples);
378 }
379 }
380
381 // Sequential fallback: process one manifest at a time.
382 #[cfg(not(feature = "concurrent"))]
383 {
384 let start_seq = Instant::now();
385 for (_prefix, manifest_path, _extended) in manifest_infos {
386 let (bins, examples, benches, tests) =
387 crate::e_manifest::get_runnable_targets(&manifest_path).unwrap_or_default();
388
389 // Merge all targets into one collection.
390 all_samples.extend(bins);
391 all_samples.extend(examples);
392 all_samples.extend(benches);
393 all_samples.extend(tests);
394
395 // if let Ok(mut ex) = collect_examples(&prefix, &manifest_path, extended) {
396 // all_samples.append(&mut ex);
397 // }
398 // if let Ok(mut bins) = collect_binaries(&prefix, &manifest_path, extended) {
399 // all_samples.append(&mut bins);
400 // }
401 // let manifest_path = Path::new(&manifest_path);
402 // let new_prefix = format!("examples/{}", &prefix);
403 // // Collect extended examples.
404 // if let Ok(ex_ext) = collect_examples(&new_prefix, manifest_path, true) {
405 // for ex in ex_ext {
406 // if !all_samples.iter().any(|r| r.name == ex.name) {
407 // all_samples.push(ex);
408 // }
409 // }
410 // }
411
412 // Collect extended binaries.
413 // if let Ok(bins_ext) = collect_binaries(&new_prefix, manifest_path, true) {
414 // for bin in bins_ext {
415 // if !all_samples.iter().any(|r| r.name == bin.name) {
416 // all_samples.push(bin);
417 // }
418 // }
419 // }
420 }
421 let duration_seq = start_seq.elapsed();
422 debug!("timing: Sequential processing took {:?}", duration_seq);
423 }
424 // First, refine all collected targets.
425 let initial_targets: Vec<_> = all_samples
426 .into_iter()
427 .map(|t| CargoTarget::refined_target(&t))
428 .collect();
429
430 // Build a HashMap keyed by (manifest, name) to deduplicate targets.
431 let mut targets_map: HashMap<(String, String), CargoTarget> = HashMap::new();
432 // for target in initial_targets.into_iter() {
433 // let key = CargoTarget::target_key(&target);
434 // targets_map.entry(key).or_insert(target);
435 // }
436 for target in initial_targets {
437 let key = CargoTarget::target_key(&target);
438 targets_map
439 .entry(key)
440 .and_modify(|existing| {
441 // inline match‐upgrading, wrapped in dbg! to print old, new and result
442 existing.kind = match (&existing.kind, &target.kind) {
443 (TargetKind::Example, TargetKind::ExtendedExample) => {
444 // you had Example, new is ExtendedExample → upgrade
445 target.kind.clone()
446 }
447 (TargetKind::Binary, TargetKind::ExtendedBinary) => target.kind.clone(),
448 (_, TargetKind::ManifestTauriExample) | (_, TargetKind::ManifestTauri) => {
449 // println!("DEBUG: Tauri {}", target.name);
450 target.kind.clone()
451 }
452 // your custom case: anything → Dioxius
453 (_, TargetKind::ManifestDioxus) => {
454 // println!("DEBUG: Dioxus {}", target.name);
455 target.kind.clone()
456 }
457 (_, TargetKind::ManifestDioxusExample) => {
458 // println!("DEBUG: DioxusExample {}", target.name);
459 target.kind.clone()
460 }
461 // else keep old
462 (old_kind, _) => old_kind.clone(),
463 };
464 })
465 .or_insert(target);
466 }
467
468 // Expand subprojects in place.
469 CargoTarget::expand_subprojects_in_place(&mut targets_map)?;
470
471 // Finally, collect all unique targets.
472 let refined_targets: Vec<CargoTarget> = targets_map.into_values().collect();
473 // Now do an additional deduplication pass based on origin and name.
474 let deduped_targets = crate::e_target::dedup_targets(refined_targets);
475 Ok(deduped_targets)
476 // return Ok(refined_targets);
477
478 // let mut target_map: std::collections::HashMap<String, CargoTarget> =
479 // std::collections::HashMap::new();
480
481 // // Group targets by name and choose one based on workspace_mode:
482 // for target in all_samples {
483 // target_map
484 // .entry(target.name.clone())
485 // .and_modify(|existing| {
486 // if workspace_mode {
487 // // In workspace mode, extended targets override builtins.
488 // if target.extended && !existing.extended {
489 // *existing = target.clone();
490 // }
491 // } else {
492 // // In normal mode, builtin targets (non-extended) override extended.
493 // if !target.extended && existing.extended {
494 // *existing = target.clone();
495 // }
496 // }
497 // })
498 // .or_insert(target.clone());
499 // }
500
501 // let mut combined = Vec::new();
502 // for target in target_map.into_values() {
503 // combined.push(target);
504 // }
505
506 // Ok(combined)
507 // Ok(all_samples)
508}
509
510use std::fs;
511use std::path::Path;
512
513/// Returns the "depth" of a path (number of components)
514pub fn path_depth(path: &Path) -> usize {
515 path.components().count()
516}
517
518/// Deduplicates targets by their canonicalized origin, gives priority to `toml_specified`,
519/// and ensures single-file targets override default binaries when appropriate.
520pub fn dedup_single_file_over_default_binary(targets: Vec<CargoTarget>) -> Vec<CargoTarget> {
521 let mut map: HashMap<Option<String>, CargoTarget> = HashMap::new();
522
523 for target in targets {
524 // Compute canonical origin key
525 let origin_key = target.origin.as_ref().and_then(|origin| match origin {
526 TargetOrigin::SingleFile(path)
527 | TargetOrigin::DefaultBinary(path)
528 | TargetOrigin::SubProject(path) => fs::canonicalize(path)
529 .ok()
530 .map(|p| p.to_string_lossy().into_owned()),
531 _ => None,
532 });
533
534 // Use Some(key) or None as map key
535 let entry_key = origin_key.clone();
536
537 if let Some(existing) = map.get(&entry_key) {
538 // 1) Prioritize toml_specified
539 if target.toml_specified && !existing.toml_specified {
540 map.insert(entry_key.clone(), target);
541 continue;
542 }
543 if !target.toml_specified && existing.toml_specified {
544 // keep existing
545 continue;
546 }
547
548 // 2) If one is SingleFile and other DefaultBinary, keep SingleFile
549 match (&target.origin, &existing.origin) {
550 (Some(TargetOrigin::SingleFile(_)), Some(TargetOrigin::DefaultBinary(_))) => {
551 map.insert(entry_key.clone(), target);
552 continue;
553 }
554 (Some(TargetOrigin::DefaultBinary(_)), Some(TargetOrigin::SingleFile(_))) => {
555 continue;
556 }
557 _ => {}
558 }
559
560 // 3) Otherwise, choose deeper manifest path
561 let current_depth = path_depth(&target.manifest_path);
562 let existing_depth = path_depth(&existing.manifest_path);
563 if current_depth > existing_depth {
564 map.insert(entry_key.clone(), target);
565 }
566 } else {
567 // No collision yet, insert
568 map.insert(entry_key, target);
569 }
570 }
571
572 map.into_values().collect()
573}
574
575pub fn collect_all_targets(
576 use_workspace: bool,
577 max_concurrency: usize,
578) -> Result<Vec<CargoTarget>, Box<dyn std::error::Error>> {
579 use std::path::PathBuf;
580 let mut manifest_infos: Vec<(String, PathBuf, bool)> = Vec::new();
581
582 // Locate the package manifest in the current directory.
583 let bi = PathBuf::from(crate::locate_manifest(false)?);
584 // We're in workspace mode if the flag is set or if the current Cargo.toml is a workspace manifest.
585 let in_workspace = use_workspace || e_workspace::is_workspace_manifest(bi.as_path());
586
587 if in_workspace {
588 // Use an explicit workspace manifest if requested; otherwise, assume the current Cargo.toml is the workspace manifest.
589 let ws = if use_workspace {
590 PathBuf::from(crate::locate_manifest(true)?)
591 } else {
592 bi.clone()
593 };
594 // Get workspace members (each member's Cargo.toml) using your helper.
595 let ws_members =
596 e_workspace::get_workspace_member_manifest_paths(ws.as_path()).unwrap_or_default();
597 // Build a numbered list of member names (using just the member directory name).
598 let member_displays: Vec<String> = ws_members
599 .iter()
600 .enumerate()
601 .map(|(i, (member, _))| format!("{}. {}", i + 1, member))
602 .collect();
603 // Print the workspace line: "<workspace_root>/<package>/Cargo.toml [1. member, 2. member, ...]"
604 println!(
605 "workspace: {} [{}]",
606 format_workspace(&ws, &bi),
607 member_displays.join(", ")
608 );
609 // Always print the package line.
610 println!("package: {}", format_package(&bi));
611 manifest_infos.push(("-".to_string(), bi.clone(), false));
612 for (member, member_manifest) in ws_members {
613 debug!(" member: {}", format_package(&member_manifest));
614 manifest_infos.push((format!("${}", member), member_manifest, true));
615 }
616 } else {
617 // Not in workspace mode: simply print the package manifest.
618 println!("package: {}", format_package(&bi));
619 manifest_infos.push(("-".to_string(), bi.clone(), false));
620 }
621
622 let samples = collect_samples(use_workspace, manifest_infos, max_concurrency)?;
623 // Deduplicate targets: if a SingleFile and DefaultBinary share the same origin, keep only the SingleFile.
624 // let deduped_samples = dedup_single_file_over_default_binary(samples);
625 // Ok(deduped_samples)
626 Ok(samples)
627}
628
629/// Same as `collect_all_targets` but does not print workspace/package debug info.
630pub fn collect_all_targets_silent(
631 use_workspace: bool,
632 max_concurrency: usize,
633) -> Result<Vec<CargoTarget>, Box<dyn std::error::Error>> {
634 use std::path::PathBuf;
635 let mut manifest_infos: Vec<(String, PathBuf, bool)> = Vec::new();
636
637 // Locate the package manifest
638 let bi = PathBuf::from(crate::locate_manifest(false)?);
639 let in_workspace = use_workspace || e_workspace::is_workspace_manifest(bi.as_path());
640
641 if in_workspace {
642 let ws = if use_workspace {
643 PathBuf::from(crate::locate_manifest(true)?)
644 } else {
645 bi.clone()
646 };
647 let ws_members =
648 e_workspace::get_workspace_member_manifest_paths(ws.as_path()).unwrap_or_default();
649 manifest_infos.push(("-".to_string(), bi.clone(), false));
650 for (member, member_manifest) in ws_members {
651 manifest_infos.push((format!("${}", member), member_manifest, true));
652 }
653 } else {
654 manifest_infos.push(("-".to_string(), bi.clone(), false));
655 }
656
657 let samples = collect_samples(use_workspace, manifest_infos, max_concurrency)?;
658 let deduped_samples = dedup_single_file_over_default_binary(samples);
659 Ok(deduped_samples)
660}
661
662// Formats the package manifest as "<package>/Cargo.toml"
663fn format_package(manifest: &Path) -> String {
664 let pkg = manifest
665 .parent()
666 .and_then(|p| p.file_name())
667 .map(|s| s.to_string_lossy())
668 .unwrap_or_default();
669 let file = manifest.file_name().unwrap().to_string_lossy();
670 format!("{}/{}", pkg, file)
671}
672
673// Formats the workspace manifest as "<workspace_root>/<package>/Cargo.toml"
674fn format_workspace(ws_manifest: &Path, bi: &Path) -> String {
675 let ws_root = ws_manifest.parent().expect("No workspace root");
676 let ws_name = ws_root
677 .file_name()
678 .map(|s| s.to_string_lossy())
679 .unwrap_or_default();
680 let bi_pkg = bi
681 .parent()
682 .and_then(|p| p.file_name())
683 .map(|s| s.to_string_lossy())
684 .unwrap_or_default();
685 format!(
686 "{}/{}/{}",
687 ws_name,
688 bi_pkg,
689 ws_manifest.file_name().unwrap().to_string_lossy()
690 )
691}