probe_rs_cli_util/
common_options.rs

1#![allow(clippy::needless_doctest_main)]
2//! Collection of `#[derive(StructOpt)] struct`s common to programs that
3//! extend then functionality of cargo-flash.
4//!
5//! Example usage:
6//! ```no_run
7//! use clap::Parser;
8//! use probe_rs_cli_util::common_options::FlashOptions;
9//!
10//! #[derive(clap::Parser)]
11//! struct Opts {
12//!     #[clap(long = "some-opt")]
13//!     opt: String,
14//!
15//!     #[clap(flatten)]
16//!     flash_options: FlashOptions,
17//! }
18//!
19//! fn main() {
20//!     let opts = Opts::parse();
21//!
22//!     opts.flash_options.probe_options.maybe_load_chip_desc().unwrap();
23//!
24//!     // handle --list-{chips,probes}
25//!     if opts.flash_options.early_exit(std::io::stdout()).unwrap() {
26//!         return;
27//!     }
28//!
29//!     let target_session = opts.flash_options.probe_options.simple_attach().unwrap();
30//!
31//!     // ...
32//! }
33//! ```
34use crate::ArtifactError;
35
36use std::{fs::File, io::Write, path::Path, path::PathBuf};
37
38use byte_unit::Byte;
39use clap;
40use probe_rs::{
41    config::{RegistryError, TargetSelector},
42    flashing::{FileDownloadError, FlashError, FlashLoader},
43    DebugProbeError, DebugProbeSelector, FakeProbe, Permissions, Probe, Session, Target,
44    WireProtocol,
45};
46
47/// Common options when flashing a target device.
48#[derive(Debug, clap::Parser)]
49pub struct FlashOptions {
50    #[clap(name = "list-chips", long = "list-chips")]
51    pub list_chips: bool,
52    #[clap(
53        name = "list-probes",
54        long = "list-probes",
55        help = "Lists all the connected probes that can be seen.\n\
56        If udev rules or permissions are wrong, some probes might not be listed."
57    )]
58    pub list_probes: bool,
59    #[clap(name = "disable-progressbars", long = "disable-progressbars")]
60    pub disable_progressbars: bool,
61    #[clap(
62        long = "disable-double-buffering",
63        help = "Use this flag to disable double-buffering when downloading flash data.  If download fails during\
64        programming with timeout errors, try this option."
65    )]
66    pub disable_double_buffering: bool,
67    #[clap(
68        name = "reset-halt",
69        long = "reset-halt",
70        help = "Use this flag to reset and halt (instead of just a reset) the attached core after flashing the target."
71    )]
72    pub reset_halt: bool,
73    #[clap(
74        name = "level",
75        long = "log",
76        help = "Use this flag to set the log level.\n\
77        Default is `warning`. Possible choices are [error, warning, info, debug, trace]."
78    )]
79    pub log: Option<log::Level>,
80    #[clap(
81        name = "restore-unwritten",
82        long = "restore-unwritten",
83        help = "Enable this flag to restore all bytes erased in the sector erase but not overwritten by any page."
84    )]
85    pub restore_unwritten: bool,
86    #[clap(
87        name = "filename",
88        long = "flash-layout",
89        help = "Requests the flash builder to output the layout into the given file in SVG format."
90    )]
91    pub flash_layout_output_path: Option<String>,
92    #[clap(
93        name = "elf file",
94        long = "elf",
95        help = "The path to the ELF file to be flashed."
96    )]
97    pub elf: Option<PathBuf>,
98    #[clap(
99        name = "directory",
100        long = "work-dir",
101        help = "The work directory from which cargo-flash should operate from."
102    )]
103    pub work_dir: Option<PathBuf>,
104    #[clap(flatten)]
105    /// Arguments which are forwarded to 'cargo build'.
106    pub cargo_options: CargoOptions,
107    #[clap(flatten)]
108    /// Argument relating to probe/chip selection/configuration.
109    pub probe_options: ProbeOptions,
110}
111
112impl FlashOptions {
113    /// Whether calling program should exit prematurely. Effectively
114    /// handles --list-{probes,chips}.
115    ///
116    /// If `Ok(false)` is returned, calling program should continue executing.
117    ///
118    /// Note: [ProbeOptions::maybe_load_chip_desc] should be called before this function.
119    pub fn early_exit(&self, f: impl Write) -> Result<bool, OperationError> {
120        if self.list_probes {
121            list_connected_probes(f)?;
122            return Ok(true);
123        }
124
125        if self.list_chips {
126            print_families(f)?;
127            return Ok(true);
128        }
129
130        Ok(false)
131    }
132}
133
134/// Common options and logic when interfacing with a [Probe].
135#[derive(clap::Parser, Debug)]
136pub struct ProbeOptions {
137    #[structopt(long)]
138    pub chip: Option<String>,
139    #[structopt(name = "chip description file path", long = "chip-description-path")]
140    pub chip_description_path: Option<PathBuf>,
141
142    /// Protocol used to connect to chip. Possible options: [swd, jtag]
143    #[structopt(long, help_heading = "PROBE CONFIGURATION")]
144    pub protocol: Option<WireProtocol>,
145
146    /// Use this flag to select a specific probe in the list.
147    ///
148    /// Use '--probe VID:PID' or '--probe VID:PID:Serial' if you have more than one probe with the same VID:PID.",
149    #[structopt(long = "probe", help_heading = "PROBE CONFIGURATION")]
150    pub probe_selector: Option<DebugProbeSelector>,
151    #[clap(
152        long,
153        help = "The protocol speed in kHz.",
154        help_heading = "PROBE CONFIGURATION"
155    )]
156    pub speed: Option<u32>,
157    #[structopt(
158        long = "connect-under-reset",
159        help = "Use this flag to assert the nreset & ntrst pins during attaching the probe to the chip."
160    )]
161    pub connect_under_reset: bool,
162    #[structopt(long = "dry-run")]
163    pub dry_run: bool,
164    #[structopt(
165        long = "allow-erase-all",
166        help = "Use this flag to allow all memory, including security keys and 3rd party firmware, to be erased \
167        even when it has read-only protection."
168    )]
169    pub allow_erase_all: bool,
170}
171
172impl ProbeOptions {
173    /// Add targets contained in file given by --chip-description-path
174    /// to probe-rs registery.
175    ///
176    /// Note: should be called before [FlashOptions::early_exit] and any other functions in [ProbeOptions].
177    pub fn maybe_load_chip_desc(&self) -> Result<(), OperationError> {
178        if let Some(ref cdp) = self.chip_description_path {
179            let file = File::open(Path::new(cdp))?;
180            probe_rs::config::add_target_from_yaml(file).map_err(|error| {
181                OperationError::FailedChipDescriptionParsing {
182                    source: error,
183                    path: cdp.clone(),
184                }
185            })
186        } else {
187            Ok(())
188        }
189    }
190
191    /// Resolves a resultant target selector from passed [ProbeOptions].
192    pub fn get_target_selector(&self) -> Result<TargetSelector, OperationError> {
193        let target = if let Some(chip_name) = &self.chip {
194            let target = probe_rs::config::get_target_by_name(chip_name).map_err(|error| {
195                OperationError::ChipNotFound {
196                    source: error,
197                    name: chip_name.clone(),
198                }
199            })?;
200
201            TargetSelector::Specified(target)
202        } else {
203            TargetSelector::Auto
204        };
205
206        Ok(target)
207    }
208
209    /// Attaches to specified probe and configures it.
210    pub fn attach_probe(&self) -> Result<Probe, OperationError> {
211        let mut probe = {
212            if self.dry_run {
213                Probe::from_specific_probe(Box::new(FakeProbe::new()));
214            }
215
216            // If we got a probe selector as an argument, open the probe
217            // matching the selector if possible.
218            match &self.probe_selector {
219                Some(selector) => {
220                    Probe::open(selector.clone()).map_err(OperationError::FailedToOpenProbe)
221                }
222                None => {
223                    // Only automatically select a probe if there is
224                    // only a single probe detected.
225                    let list = Probe::list_all();
226                    if list.len() > 1 {
227                        return Err(OperationError::MultipleProbesFound { number: list.len() });
228                    }
229
230                    if let Some(info) = list.first() {
231                        Probe::open(info).map_err(OperationError::FailedToOpenProbe)
232                    } else {
233                        Err(OperationError::NoProbesFound)
234                    }
235                }
236            }
237        }?;
238
239        if let Some(protocol) = self.protocol {
240            // Select protocol and speed
241            probe.select_protocol(protocol).map_err(|error| {
242                OperationError::FailedToSelectProtocol {
243                    source: error,
244                    protocol,
245                }
246            })?;
247        }
248
249        if let Some(speed) = self.speed {
250            let _actual_speed = probe.set_speed(speed).map_err(|error| {
251                OperationError::FailedToSelectProtocolSpeed {
252                    source: error,
253                    speed,
254                }
255            })?;
256        }
257
258        Ok(probe)
259    }
260
261    /// Attaches to target device session. Attaches under reset if
262    /// specified by [ProbeOptions::connect_under_reset].
263    pub fn attach_session(
264        &self,
265        probe: Probe,
266        target: TargetSelector,
267    ) -> Result<Session, OperationError> {
268        let mut permissions = Permissions::new();
269        if self.allow_erase_all {
270            permissions = permissions.allow_erase_all();
271        }
272
273        let session = if self.connect_under_reset {
274            probe.attach_under_reset(target, permissions)
275        } else {
276            probe.attach(target, permissions)
277        }
278        .map_err(|error| OperationError::AttachingFailed {
279            source: error,
280            connect_under_reset: self.connect_under_reset,
281        })?;
282
283        Ok(session)
284    }
285
286    /// Convenience method that attaches to the specified probe, target,
287    /// and target session.
288    pub fn simple_attach(&self) -> Result<Session, OperationError> {
289        let target = self.get_target_selector()?;
290        let probe = self.attach_probe()?;
291        let session = self.attach_session(probe, target)?;
292
293        Ok(session)
294    }
295
296    /// Builds a new flash loader for the given target and ELF. This
297    /// will check the ELF for validity and check what pages have to be
298    /// flashed etc.
299    pub fn build_flashloader(
300        &self,
301        session: &mut Session,
302        elf_path: &Path,
303    ) -> Result<FlashLoader, OperationError> {
304        let target = session.target();
305
306        // Create the flash loader
307        let mut loader = FlashLoader::new(target.memory_map.to_vec(), target.source().clone());
308
309        // Add data from the ELF.
310        let mut file = File::open(elf_path).map_err(|error| OperationError::FailedToOpenElf {
311            source: error,
312            path: elf_path.to_path_buf(),
313        })?;
314
315        // Try and load the ELF data.
316        loader
317            .load_elf_data(&mut file)
318            .map_err(OperationError::FailedToLoadElfData)?;
319
320        Ok(loader)
321    }
322}
323
324/// Common options used when building artifacts with cargo.
325#[derive(clap::Parser, Debug, Default)]
326pub struct CargoOptions {
327    #[clap(name = "binary", long = "bin", hide = true)]
328    pub bin: Option<String>,
329    #[clap(name = "example", long = "example", hide = true)]
330    pub example: Option<String>,
331    #[clap(name = "package", short = 'p', long = "package", hide = true)]
332    pub package: Option<String>,
333    #[clap(name = "release", long = "release", hide = true)]
334    pub release: bool,
335    #[clap(name = "target", long = "target", hide = true)]
336    pub target: Option<String>,
337    #[clap(name = "PATH", long = "manifest-path", hide = true)]
338    pub manifest_path: Option<PathBuf>,
339    #[clap(long, hide = true)]
340    pub no_default_features: bool,
341    #[clap(long, hide = true)]
342    pub all_features: bool,
343    #[clap(long, hide = true)]
344    pub features: Vec<String>,
345    #[clap(hide = true)]
346    /// Escape hatch: all args passed after a sentinel `--` end up here,
347    /// unprocessed. Used to pass arguments to cargo not declared in
348    /// [CargoOptions].
349    pub trailing_opts: Vec<String>,
350}
351
352impl CargoOptions {
353    /// Generates a suitable help string to append to your program's
354    /// --help. Example usage:
355    /// ```no_run
356    /// use probe_rs_cli_util::common_options::{FlashOptions, CargoOptions};
357    /// use probe_rs_cli_util::clap::{Parser, CommandFactory, FromArgMatches};
358    ///
359    /// let help_message = CargoOptions::help_message("cargo flash");
360    ///
361    /// let matches = FlashOptions::command()
362    ///     .bin_name("cargo flash")
363    ///     .after_help(&help_message)
364    ///     .get_matches_from(std::env::args());
365    /// let opts = FlashOptions::from_arg_matches(&matches);
366    /// ```
367    pub fn help_message(bin: &str) -> String {
368        format!(
369            r#"
370CARGO BUILD OPTIONS:
371
372    The following options are forwarded to 'cargo build':
373
374        --bin
375        --example
376    -p, --package
377        --release
378        --target
379        --manifest-path
380        --no-default-features
381        --all-features
382        --features
383
384    Additionally, all options passed after a sentinel '--'
385    are also forwarded.
386
387    For example, if you run the command '{bin} --release -- \
388    --some-cargo-flag', this means that 'cargo build \
389    --release --some-cargo-flag' will be called.
390"#
391        )
392    }
393
394    /// Generates list of arguments to cargo from a `CargoOptions`. For
395    /// example, if [CargoOptions::release] is set, resultant list will
396    /// contain a `"--release"`.
397    pub fn to_cargo_options(&self) -> Vec<String> {
398        // Handle known options
399        let mut args: Vec<String> = vec![];
400        macro_rules! maybe_push_str_opt {
401            ($field:expr, $name:expr) => {{
402                if let Some(value) = $field {
403                    args.push(format!("--{}", stringify!($name)));
404                    args.push(value.clone());
405                }
406            }};
407        }
408
409        maybe_push_str_opt!(&self.bin, bin);
410        maybe_push_str_opt!(&self.example, example);
411        maybe_push_str_opt!(&self.package, package);
412        if self.release {
413            args.push("--release".to_string());
414        }
415        maybe_push_str_opt!(&self.target, target);
416        if let Some(path) = &self.manifest_path {
417            args.push("--manifest-path".to_string());
418            args.push(path.display().to_string());
419        }
420        if self.no_default_features {
421            args.push("--no-default-features".to_string());
422        }
423        if self.all_features {
424            args.push("--all-features".to_string());
425        }
426        if !self.features.is_empty() {
427            args.push("--features".to_string());
428            args.push(self.features.join(","));
429        }
430
431        // handle unknown options (passed after sentinel '--')
432        args.append(&mut self.trailing_opts.clone());
433
434        args
435    }
436}
437
438#[derive(Debug, thiserror::Error)]
439pub enum OperationError {
440    #[error("No connected probes were found.")]
441    NoProbesFound,
442    #[error("Failed to list the target descriptions.")]
443    FailedToReadFamilies(#[source] RegistryError),
444    #[error("Failed to open the ELF file '{path}' for flashing.")]
445    FailedToOpenElf {
446        #[source]
447        source: std::io::Error,
448        path: PathBuf,
449    },
450    #[error("Failed to load the ELF data.")]
451    FailedToLoadElfData(#[source] FileDownloadError),
452    #[error("Failed to open the debug probe.")]
453    FailedToOpenProbe(#[source] DebugProbeError),
454    #[error("{number} probes were found.")]
455    MultipleProbesFound { number: usize },
456    #[error("The flashing procedure failed for '{path}'.")]
457    FlashingFailed {
458        #[source]
459        source: FlashError,
460        target: Box<Target>, // Box to reduce enum size
461        target_spec: Option<String>,
462        path: PathBuf,
463    },
464    #[error("Failed to parse the chip description '{path}'.")]
465    FailedChipDescriptionParsing {
466        #[source]
467        source: RegistryError,
468        path: PathBuf,
469    },
470    #[error("Failed to change the working directory to '{path}'.")]
471    FailedToChangeWorkingDirectory {
472        #[source]
473        source: std::io::Error,
474        path: PathBuf,
475    },
476    #[error("Failed to build the cargo project at '{path}'.")]
477    FailedToBuildExternalCargoProject {
478        #[source]
479        source: ArtifactError,
480        path: PathBuf,
481    },
482    #[error("Failed to build the cargo project.")]
483    FailedToBuildCargoProject(#[source] ArtifactError),
484    #[error("The chip '{name}' was not found in the database.")]
485    ChipNotFound {
486        #[source]
487        source: RegistryError,
488        name: String,
489    },
490    #[error("The protocol '{protocol}' could not be selected.")]
491    FailedToSelectProtocol {
492        #[source]
493        source: DebugProbeError,
494        protocol: WireProtocol,
495    },
496    #[error("The protocol speed could not be set to '{speed}' kHz.")]
497    FailedToSelectProtocolSpeed {
498        #[source]
499        source: DebugProbeError,
500        speed: u32,
501    },
502    #[error("Connecting to the chip was unsuccessful.")]
503    AttachingFailed {
504        #[source]
505        source: probe_rs::Error,
506        connect_under_reset: bool,
507    },
508    #[error("Failed to get a handle to the first core.")]
509    AttachingToCoreFailed(#[source] probe_rs::Error),
510    #[error("The reset of the target failed.")]
511    TargetResetFailed(#[source] probe_rs::Error),
512    #[error("The target could not be reset and halted.")]
513    TargetResetHaltFailed(#[source] probe_rs::Error),
514    #[error("Failed to write to file")]
515    IOError(#[source] std::io::Error),
516    #[error("probe-rs API was called in the wrong order.")]
517    InvalidAPIOrder,
518    #[error("Failed to parse CLI arguments.")]
519    CliArgument(#[from] clap::Error),
520}
521
522impl From<std::io::Error> for OperationError {
523    fn from(e: std::io::Error) -> Self {
524        OperationError::IOError(e)
525    }
526}
527
528/// Lists all connected debug probes.
529pub fn list_connected_probes(mut f: impl Write) -> Result<(), std::io::Error> {
530    let probes = Probe::list_all();
531
532    if !probes.is_empty() {
533        writeln!(f, "The following debug probes were found:")?;
534        for (num, link) in probes.iter().enumerate() {
535            writeln!(f, "[{num}]: {link:?}")?;
536        }
537    } else {
538        writeln!(f, "No debug probes were found.")?;
539    }
540
541    Ok(())
542}
543
544/// Print all the available families and their contained chips to the
545/// commandline.
546pub fn print_families(mut f: impl Write) -> Result<(), OperationError> {
547    writeln!(f, "Available chips:")?;
548    for family in probe_rs::config::families().map_err(OperationError::FailedToReadFamilies)? {
549        writeln!(f, "{}", &family.name)?;
550        writeln!(f, "    Variants:")?;
551        for variant in family.variants() {
552            writeln!(f, "        {}", variant.name)?;
553        }
554    }
555    Ok(())
556}
557
558fn get_range_len(range: &std::ops::Range<u64>) -> u64 {
559    range.end - range.start
560}
561
562/// Print all the available families and their contained chips to the
563/// commandline.
564pub fn print_chip_info(name: impl AsRef<str>, mut f: impl Write) -> anyhow::Result<()> {
565    writeln!(f, "{}", name.as_ref())?;
566    let target = probe_rs::config::get_target_by_name(name)?;
567    writeln!(f, "Cores ({}):", target.cores.len())?;
568    for core in target.cores {
569        writeln!(
570            f,
571            "    - {} ({:?})",
572            core.name.to_ascii_lowercase(),
573            core.core_type
574        )?;
575    }
576    for memory in target.memory_map {
577        match memory {
578            probe_rs::config::MemoryRegion::Ram(region) => writeln!(
579                f,
580                "RAM: {:#010x?} ({})",
581                &region.range,
582                Byte::from_bytes(get_range_len(&region.range) as u128).get_appropriate_unit(true)
583            )?,
584            probe_rs::config::MemoryRegion::Generic(region) => writeln!(
585                f,
586                "Generic: {:#010x?} ({})",
587                &region.range,
588                Byte::from_bytes(get_range_len(&region.range) as u128).get_appropriate_unit(true)
589            )?,
590            probe_rs::config::MemoryRegion::Nvm(region) => writeln!(
591                f,
592                "NVM: {:#010x?} ({})",
593                &region.range,
594                Byte::from_bytes(get_range_len(&region.range) as u128).get_appropriate_unit(true)
595            )?,
596        };
597    }
598    Ok(())
599}
600
601#[cfg(test)]
602mod tests {
603    use super::*;
604
605    #[test]
606    fn to_cargo_options() {
607        assert_eq!(
608            CargoOptions {
609                bin: Some("foobar".into()),
610                example: Some("foobar".into()),
611                package: Some("foobar".into()),
612                release: true,
613                target: Some("foobar".into()),
614                manifest_path: Some("/tmp/Cargo.toml".into()),
615                no_default_features: true,
616                all_features: true,
617                features: vec!["feat1".into(), "feat2".into()],
618                trailing_opts: vec!["--some-cargo-option".into()],
619            }
620            .to_cargo_options(),
621            [
622                "--bin",
623                "foobar",
624                "--example",
625                "foobar",
626                "--package",
627                "foobar",
628                "--release",
629                "--target",
630                "foobar",
631                "--manifest-path",
632                "/tmp/Cargo.toml",
633                "--no-default-features",
634                "--all-features",
635                "--features",
636                "feat1,feat2",
637                "--some-cargo-option",
638            ]
639        );
640    }
641}