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 #[arg(long)]
75 pub runner_cfg: Option<PathBuf>,
76 #[arg(long)]
80 pub segger_gdb: Option<bool>,
81 #[arg(long)]
82 pub run_name: Option<String>,
86 #[arg(long)]
90 pub output_dir: Option<PathBuf>,
91 #[arg(long)]
95 pub data_filepath: Option<PathBuf>,
96 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 #[serde(alias = "segger-gdb", default)]
134 pub segger_gdb: bool,
135 #[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 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}