Skip to main content

cargo_attach/
lib.rs

1use std::{convert::Infallible, os::unix::process::CommandExt, path::PathBuf, process::Command};
2
3use cargo_metadata::{MetadataCommand, camino::Utf8Path};
4use walkdir::WalkDir;
5
6use crate::args::Args;
7
8mod args;
9mod conf;
10
11type Result<T> = std::result::Result<T, Box<dyn std::error::Error>>;
12
13pub fn attach(args: Args) -> Result<Infallible> {
14    if args.release && args.debug {
15        return Err("the release and debug flags may not be used together".into());
16    }
17
18    if args.bin.is_some() && args.example.is_some() {
19        return Err("the bin and example options may not be used together".into());
20    }
21
22    let metadata = (MetadataCommand::new().no_deps().exec())
23        // Trim trailing newlines from Cargo's errors.
24        .map_err(|e| e.to_string().trim().to_owned())?;
25
26    let Some(package) = metadata.root_package() else {
27        return Err("could not determine which package to use binaries from".into());
28    };
29
30    let config = if args.probe_args.is_empty() || args.target.is_none() {
31        conf::load_config(package)?
32    } else {
33        None
34    };
35
36    let binary_names = if let Some(binary) = args.bin.or(args.example) {
37        vec![binary]
38    } else {
39        let targets = package.targets.iter();
40        let binaries = targets.filter(|t| t.is_bin() || t.is_example());
41
42        binaries.map(|t| t.name.to_owned()).collect()
43    };
44
45    let build_mode = if args.release {
46        Some("release".to_owned())
47    } else if args.debug {
48        Some("debug".to_owned())
49    } else {
50        None
51    };
52
53    let build_target = if args.target.is_none() {
54        if let Some(config) = &config {
55            conf::find_build_target(config)
56        } else {
57            None
58        }
59    } else {
60        args.target
61    };
62
63    let probe_args = if args.probe_args.is_empty() {
64        if let Some(config) = &config {
65            conf::find_probe_args(config, build_target.as_ref())?
66        } else {
67            vec![]
68        }
69    } else {
70        args.probe_args
71    };
72
73    let Some(executable) = find_executable(
74        &metadata.target_directory,
75        binary_names,
76        build_mode,
77        build_target,
78    ) else {
79        return Err(format!("no matching executable found for package {}", package.name).into());
80    };
81
82    let error = Command::new("probe-rs")
83        .arg("attach")
84        .args(probe_args)
85        .arg(executable)
86        .exec();
87
88    Err(error.into())
89}
90
91fn find_executable(
92    base: &Utf8Path,
93    binary_names: Vec<String>,
94    build_mode: Option<String>,
95    build_target: Option<String>,
96) -> Option<PathBuf> {
97    let target_files = WalkDir::new(base)
98        .max_depth(4)
99        .into_iter()
100        .filter_map(|e| e.ok())
101        .filter(|e| e.file_type().is_file());
102
103    let executables = target_files
104        .filter(|e| binary_names.iter().any(|x| x.as_str() == e.file_name()))
105        .filter(|e| {
106            let path = e.path().strip_prefix(base).unwrap().parent().unwrap();
107
108            let matches_build_mode = build_mode
109                .as_deref()
110                .is_none_or(|m| path.iter().any(|c| c == m));
111
112            let matches_target_triple = build_target
113                .as_deref()
114                .is_none_or(|t| path.iter().any(|c| c == t));
115
116            matches_build_mode && matches_target_triple
117        });
118
119    executables
120        .max_by_key(|e| e.metadata().unwrap().modified().unwrap())
121        .map(|e| e.into_path())
122}