embedded_runner/
cfg.rs

1use std::path::{Path, PathBuf};
2
3use object::{Object, ObjectSymbol};
4use path_slash::{PathBufExt, PathExt};
5use tera::{Context, Tera};
6
7#[derive(Debug, Clone, clap::Parser)]
8pub struct CliConfig {
9    #[arg(long, short = 'v')]
10    pub verbose: bool,
11    #[command(subcommand)]
12    pub cmd: Cmd,
13}
14
15#[derive(Debug, Clone)]
16pub struct ResolvedConfig {
17    pub runner_cfg: RunnerConfig,
18    pub verbose: bool,
19    pub workspace_dir: PathBuf,
20    pub embedded_dir: PathBuf,
21}
22
23#[derive(Debug, thiserror::Error)]
24pub enum ConfigError {
25    #[error("{}", .0)]
26    Path(#[from] crate::path::PathError),
27    #[error("{}", .0)]
28    Fs(#[from] std::io::Error),
29    #[error("{}", .0)]
30    DeToml(#[from] toml::de::Error),
31}
32
33pub fn get_cfg(runner_cfg: &Option<PathBuf>, verbose: bool) -> Result<ResolvedConfig, ConfigError> {
34    let workspace_dir = crate::path::get_cargo_root()?;
35    let embedded_dir: PathBuf = workspace_dir.join(".embedded/");
36
37    if !embedded_dir.exists() {
38        std::fs::create_dir(embedded_dir.clone())?;
39    }
40
41    let runner_cfg = runner_cfg
42        .clone()
43        .unwrap_or(embedded_dir.join("runner.toml"));
44    let runner_cfg: RunnerConfig = match std::fs::read_to_string(&runner_cfg) {
45        Ok(runner_cfg) => toml::from_str(&runner_cfg)?,
46        Err(_) => {
47            log::warn!(
48                "No runner config found at '{}'. Using default config.",
49                runner_cfg.display()
50            );
51            RunnerConfig::default()
52        }
53    };
54
55    Ok(ResolvedConfig {
56        runner_cfg,
57        verbose,
58        workspace_dir,
59        embedded_dir,
60    })
61}
62
63#[derive(Debug, Clone, clap::Parser)]
64pub enum Cmd {
65    Run(RunCmdConfig),
66    Collect(CollectCmdConfig),
67}
68
69#[derive(Debug, Clone, clap::Parser)]
70pub struct RunCmdConfig {
71    /// Filepath to a TOML file that contains the runner configuration.
72    ///
73    /// Default: `.embedded/runner.toml`
74    #[arg(long)]
75    pub runner_cfg: Option<PathBuf>,
76    /// `true`: Uses RTT commands to communicate with SEGGER GDB instead of the `monitor rtt` commands from OpenOCD.
77    ///
78    /// This setting overwrites the one optionally set in the runner configuration.
79    #[arg(long)]
80    pub segger_gdb: Option<bool>,
81    #[arg(long)]
82    /// Optional name for the test run.
83    ///
84    /// Default: Absolut filepath of the executed binary.
85    pub run_name: Option<String>,
86    /// Optional path to a directory that is used to store test results and logs
87    ///
88    /// Default: `<binary filepath>_runner` (`<binary filepath>` gets substituted with the filepath set for the `binary` argument).
89    #[arg(long)]
90    pub output_dir: Option<PathBuf>,
91    /// Path to look for custom JSON data that is linked with the test run.
92    ///
93    /// Default: `.embedded/test_run_data.json`
94    #[arg(long)]
95    pub data_filepath: Option<PathBuf>,
96    /// Filepath to the binary that should be run on the embedded device.
97    pub binary: PathBuf,
98}
99
100#[derive(Debug, Clone, clap::Parser)]
101pub struct CollectCmdConfig {
102    pub output: Option<PathBuf>,
103}
104
105#[derive(Debug, Default, Clone, serde::Deserialize)]
106pub struct RunnerConfig {
107    pub load: Option<String>,
108    #[serde(alias = "pre-exit")]
109    pub pre_exit: Option<String>,
110    #[serde(alias = "openocd-cfg")]
111    pub openocd_cfg: Option<PathBuf>,
112    #[serde(alias = "gdb-connection")]
113    pub gdb_connection: Option<String>,
114    #[serde(alias = "gdb-logfile")]
115    pub gdb_logfile: Option<PathBuf>,
116    #[serde(alias = "pre-runner")]
117    pub pre_runner: Option<Command>,
118    #[serde(alias = "pre-runner-windows")]
119    pub pre_runner_windows: Option<Command>,
120    #[serde(alias = "post-runner")]
121    pub post_runner: Option<Command>,
122    #[serde(alias = "post-runner-windows")]
123    pub post_runner_windows: Option<Command>,
124    #[serde(alias = "rtt-port")]
125    pub rtt_port: Option<u16>,
126    #[serde(alias = "windows-sleep")]
127    pub windows_sleep: Option<bool>,
128    #[serde(alias = "extern-coverage")]
129    pub extern_coverage: Option<ExternCoverageConfig>,
130    /// `true`: Uses RTT commands to communicate with SEGGER GDB instead of the `monitor rtt` commands from OpenOCD.
131    ///
132    /// Default: `false`
133    #[serde(alias = "segger-gdb", default)]
134    pub segger_gdb: bool,
135    /// Path to look for custom JSON data that is linked with the test run.
136    ///
137    /// Default: `.embedded/test_run_data.json`
138    #[serde(alias = "data-filepath", alias = "test-run-data-filepath")]
139    pub data_filepath: Option<PathBuf>,
140}
141
142#[derive(Debug, Clone, serde::Deserialize)]
143pub struct ExternCoverageConfig {
144    /// Coverage format of the given file.
145    ///
146    /// Currently supported formats: CoberturaV4
147    pub format: covcon::format::CoverageFormat,
148    pub filepath: PathBuf,
149}
150
151#[derive(Debug, Clone, serde::Deserialize)]
152pub struct Command {
153    pub name: String,
154    pub args: Vec<String>,
155}
156
157#[derive(Debug, thiserror::Error)]
158pub enum CfgError {
159    #[error("Could not find rtt block in binary. Cause: {}", .0)]
160    FindingRttBlock(String),
161    #[error("Could not build the template context. Cause: {}", .0)]
162    BuildingTemplateContext(String),
163    #[error("Could not resolve the load section. Cause: {}", .0)]
164    ResolvingLoad(String),
165    #[error("Could not resolve the pre-exit section. Cause: {}", .0)]
166    ResolvingPreExit(String),
167}
168
169impl RunnerConfig {
170    pub fn gdb_script(
171        &self,
172        binary: &Path,
173        output_dir: &Path,
174        segger_gdb: bool,
175    ) -> Result<String, CfgError> {
176        let context = build_template_context(binary)?;
177        let resolved_load = if let Some(load) = &self.load {
178            Tera::one_off(load, &context, false)
179                .map_err(|err| CfgError::ResolvingLoad(err.to_string()))?
180        } else {
181            "load".to_string()
182        };
183        let (rtt_address, rtt_length) = find_rtt_block(binary)?;
184
185        #[cfg(target_os = "windows")]
186        let sleep_cmd = "timeout";
187        #[cfg(not(target_os = "windows"))]
188        let sleep_cmd = "sleep";
189
190        let sleep_cmd = if self.windows_sleep == Some(true) {
191            "sleep"
192        } else {
193            sleep_cmd
194        };
195
196        let gdb_logfile = self
197            .gdb_logfile
198            .clone()
199            .unwrap_or(output_dir.join("gdb.log"));
200        let gdb_logfile = gdb_logfile
201            .to_slash()
202            .expect("GDB logfile must be a valid filepath.");
203
204        let gdb_conn = if let Some(gdb_conn) = &self.gdb_connection {
205            format!("target extended-remote {gdb_conn}\nset logging file {gdb_logfile}")
206        } else {
207            let openocd_cfg = self
208                .openocd_cfg
209                .clone()
210                .unwrap_or(PathBuf::from(".embedded/openocd.cfg"));
211            let openocd_cfg = openocd_cfg
212                .to_slash()
213                .expect("OpenOCD configuration file must be a valid filepath.");
214            format!("target extended-remote | openocd -c \"gdb_port pipe; log_output {gdb_logfile}\" -f {openocd_cfg}")
215        };
216
217        let rtt_section = if segger_gdb {
218            format!(
219                "
220monitor exec SetRTTSearchRanges 0x{rtt_address:x} 0x{rtt_length:x}
221monitor exec SetRTTChannel 0
222            "
223            )
224        } else {
225            format!(
226                "
227monitor rtt setup 0x{:x} {} \"SEGGER RTT\"
228monitor rtt start
229monitor rtt server start {} 0
230            ",
231                rtt_address,
232                rtt_length,
233                self.rtt_port.unwrap_or(super::DEFAULT_RTT_PORT)
234            )
235        };
236
237        let pre_exit_section = if let Some(pre_exit_template) = &self.pre_exit {
238            Tera::one_off(pre_exit_template, &context, false)
239                .map_err(|err| CfgError::ResolvingPreExit(err.to_string()))?
240        } else {
241            String::new()
242        };
243
244        Ok(format!(
245            "
246set pagination off
247
248{gdb_conn}
249
250{resolved_load}
251
252b main
253continue
254
255{rtt_section}
256
257shell {sleep_cmd} 1
258
259continue
260
261shell {sleep_cmd} 1
262
263{pre_exit_section}
264
265quit        
266"
267        ))
268    }
269}
270
271fn find_rtt_block(binary: &Path) -> Result<(u64, u64), CfgError> {
272    let data = std::fs::read(binary).map_err(|err| {
273        CfgError::FindingRttBlock(format!("Could not read binary file. Cause: {err}"))
274    })?;
275    let file = object::File::parse(&*data).map_err(|err| {
276        CfgError::FindingRttBlock(format!("Could not parse binary file. Cause: {err}"))
277    })?;
278
279    for symbol in file.symbols() {
280        if symbol.name() == Ok("_SEGGER_RTT") {
281            return Ok((symbol.address(), symbol.size()));
282        }
283    }
284
285    Err(CfgError::FindingRttBlock(
286        "No _SEGGER_RTT symbol in binary!".to_string(),
287    ))
288}
289
290fn build_template_context(binary: &Path) -> Result<Context, CfgError> {
291    let mut context = Context::new();
292    let parent = binary.parent().map(|p| p.to_path_buf()).unwrap_or_default();
293    context.insert(
294        "binary_path",
295        &parent
296            .to_slash()
297            .expect("Binary path has only valid Unicode characters."),
298    );
299    context.insert(
300        "binary_filepath_noextension",
301        &Path::join(
302            &parent,
303            binary.file_stem().ok_or_else(|| {
304                CfgError::BuildingTemplateContext(format!(
305                    "Given binary '{}' has no valid filename.",
306                    binary.display()
307                ))
308            })?,
309        )
310        .to_slash()
311        .expect("Binary path has only valid Unicode characters."),
312    );
313    context.insert(
314        "binary_filepath",
315        &binary
316            .to_slash()
317            .expect("Binary path has only valid Unicode characters."),
318    );
319
320    Ok(context)
321}
322
323#[cfg(test)]
324mod test {
325    use std::path::PathBuf;
326
327    use crate::cfg::build_template_context;
328
329    use super::find_rtt_block;
330
331    #[test]
332    fn load_template() {
333        let load = "load \"{{ binary_path }}/debug_config.ihex\"
334load \"{{ binary_filepath_noextension }}.ihex\"
335file \"{{ binary_filepath }}\"";
336
337        let binary = PathBuf::from("./target/debug/hello.exe");
338
339        let context = build_template_context(&binary).unwrap();
340        let resolved = tera::Tera::one_off(load, &context, false).unwrap();
341
342        assert!(
343            resolved.contains("target/debug/debug_config.ihex"),
344            "Binary path not resolved."
345        );
346        assert!(
347            resolved.contains("target/debug/hello.ihex"),
348            "Binary file path without extension not resolved."
349        );
350        assert!(
351            resolved.contains("target/debug/hello.exe"),
352            "Binary file path with extension not resolved."
353        );
354    }
355
356    #[test]
357    fn rtt_block_in_binary() {
358        let binary = PathBuf::from("test_binaries/emb-runner-test");
359
360        let (address, size) = find_rtt_block(&binary).unwrap();
361        dbg!(address);
362        dbg!(size);
363    }
364}