mod cmd;
mod report;
mod rpc;
mod util;
use std::cmp::Reverse;
use std::collections::HashMap;
use std::fs;
use std::path::Path;
use std::str::FromStr;
use std::{ffi::OsString, path::PathBuf};
use anyhow::{Context, Result};
use clap::{ArgMatches, CommandFactory, FromArgMatches};
use colored::Colorize;
use figment::Figment;
use figment::providers::{Data, Format as _, Json, Toml, Yaml};
use figment::value::Value;
use itertools::Itertools;
use postcard_schema::Schema;
use probe_rs::{Target, probe::list::Lister};
use report::Report;
use serde::{Deserialize, Serialize};
use time::{OffsetDateTime, UtcOffset};
use crate::rpc::client::RpcClient;
use crate::rpc::functions::RpcApp;
use crate::util::logging::setup_logging;
use crate::util::parse_u32;
use crate::util::parse_u64;
const MAX_LOG_FILES: usize = 20;
type ConfigPreset = HashMap<String, Value>;
#[derive(Debug, Default, Clone, Serialize, Deserialize)]
pub(crate) struct Config {
#[cfg(feature = "remote")]
pub server: cmd::serve::ServerConfig,
pub presets: HashMap<String, ConfigPreset>,
}
#[derive(clap::Parser)]
#[clap(
name = "probe-rs",
about = "The probe-rs CLI",
version = env!("PROBE_RS_VERSION"),
long_version = env!("PROBE_RS_LONG_VERSION")
)]
struct Cli {
#[clap(long, global = true, help_heading = "DEBUG LOG CONFIGURATION")]
log_file: Option<PathBuf>,
#[clap(long, global = true, help_heading = "DEBUG LOG CONFIGURATION")]
log_to_folder: bool,
#[clap(
long,
short,
global = true,
help_heading = "LOG CONFIGURATION",
value_name = "PATH",
require_equals = true,
num_args = 0..=1,
default_missing_value = "./report.zip"
)]
report: Option<PathBuf>,
#[cfg(feature = "remote")]
#[arg(
long,
global = true,
env = "PROBE_RS_REMOTE_HOST",
help_heading = "REMOTE CONFIGURATION"
)]
host: Option<String>,
#[cfg(feature = "remote")]
#[arg(
long,
global = true,
env = "PROBE_RS_REMOTE_TOKEN",
help_heading = "REMOTE CONFIGURATION"
)]
token: Option<String>,
#[clap(subcommand)]
subcommand: Subcommand,
#[arg(long, global = true, env = "PROBE_RS_CONFIG_PRESET")]
preset: Option<String>,
}
impl Cli {
async fn run(self, client: RpcClient, _config: Config, utc_offset: UtcOffset) -> Result<()> {
let lister = Lister::new();
match self.subcommand {
Subcommand::DapServer(cmd) => {
let log_path = self.log_file.as_deref();
cmd::dap_server::run(cmd, &lister, utc_offset, log_path).await
}
#[cfg(feature = "remote")]
Subcommand::Serve(cmd) => cmd.run(_config.server).await,
Subcommand::List(cmd) => cmd.run(client).await,
Subcommand::Info(cmd) => cmd.run(client).await,
Subcommand::Gdb(cmd) => cmd.run(&mut *client.registry().await, &lister),
Subcommand::Reset(cmd) => cmd.run(client).await,
Subcommand::Debug(cmd) => {
cmd.run(&mut *client.registry().await, &lister, utc_offset)
.await
}
Subcommand::Download(cmd) => cmd.run(client).await,
Subcommand::Run(cmd) => cmd.run(client, utc_offset).await,
Subcommand::Attach(cmd) => cmd.run(client, utc_offset).await,
Subcommand::Verify(cmd) => cmd.run(client).await,
Subcommand::Erase(cmd) => cmd.run(client).await,
Subcommand::Trace(cmd) => cmd.run(&mut *client.registry().await, &lister),
Subcommand::Itm(cmd) => cmd.run(&mut *client.registry().await, &lister),
Subcommand::Chip(cmd) => cmd.run(client).await,
Subcommand::Benchmark(cmd) => cmd.run(&mut *client.registry().await, &lister),
Subcommand::Profile(cmd) => cmd.run(&mut *client.registry().await, &lister),
Subcommand::Read(cmd) => cmd.run(client).await,
Subcommand::Write(cmd) => cmd.run(client).await,
Subcommand::Complete(cmd) => cmd.run(&lister),
Subcommand::Mi(cmd) => cmd.run(),
}
}
fn elf(&self) -> Option<PathBuf> {
match self.subcommand {
Subcommand::Download(ref cmd) => Some(cmd.path.clone()),
Subcommand::Run(ref cmd) => Some(cmd.shared_options.path.clone()),
Subcommand::Attach(ref cmd) => Some(cmd.run.shared_options.path.clone()),
Subcommand::Verify(ref cmd) => Some(cmd.path.clone()),
_ => None,
}
}
}
#[derive(clap::Subcommand)]
enum Subcommand {
DapServer(cmd::dap_server::Cmd),
List(cmd::list::Cmd),
Info(cmd::info::Cmd),
Reset(cmd::reset::Cmd),
Gdb(cmd::gdb_server::Cmd),
Debug(cmd::debug::Cmd),
Download(cmd::download::Cmd),
Verify(cmd::verify::Cmd),
Erase(cmd::erase::Cmd),
#[clap(name = "run")]
Run(cmd::run::Cmd),
#[clap(name = "attach")]
Attach(cmd::attach::Cmd),
#[clap(name = "trace")]
Trace(cmd::trace::Cmd),
#[clap(name = "itm")]
Itm(cmd::itm::Cmd),
Chip(cmd::chip::Cmd),
Benchmark(cmd::benchmark::Cmd),
Profile(cmd::profile::ProfileCmd),
#[cfg(feature = "remote")]
Serve(cmd::serve::Cmd),
Read(cmd::read::Cmd),
Write(cmd::write::Cmd),
Complete(cmd::complete::Cmd),
Mi(cmd::mi::Cmd),
}
impl Subcommand {
#[cfg(feature = "remote")]
fn is_remote_cmd(&self) -> bool {
matches!(
self,
Self::List(_)
| Self::Read(_)
| Self::Write(_)
| Self::Reset(_)
| Self::Chip(_)
| Self::Info(_)
| Self::Download(_)
| Self::Attach(_)
| Self::Run(_)
| Self::Erase(_)
| Self::Verify(_)
)
}
}
#[derive(clap::Parser, Serialize, Deserialize)]
pub(crate) struct CoreOptions {
#[clap(long, default_value = "0")]
core: usize,
}
#[derive(clap::Parser, Clone, Serialize, Deserialize, Debug, Default, Schema)]
#[serde(default)]
pub struct BinaryCliOptions {
#[clap(long, value_parser = parse_u64, help_heading = "DOWNLOAD CONFIGURATION")]
base_address: Option<u64>,
#[clap(long, value_parser = parse_u32, default_value = "0", help_heading = "DOWNLOAD CONFIGURATION")]
skip: u32,
}
#[derive(clap::Parser, Clone, Serialize, Deserialize, Debug, Default, Schema)]
#[serde(default)]
pub struct IdfCliOptions {
#[clap(long, help_heading = "DOWNLOAD CONFIGURATION")]
idf_bootloader: Option<String>,
#[clap(long, help_heading = "DOWNLOAD CONFIGURATION")]
idf_partition_table: Option<String>,
#[clap(long, help_heading = "DOWNLOAD CONFIGURATION")]
idf_target_app_partition: Option<String>,
}
#[derive(clap::Parser, Clone, Serialize, Deserialize, Debug, Default, Schema)]
#[serde(default)]
pub struct ElfCliOptions {
#[clap(long, help_heading = "DOWNLOAD CONFIGURATION")]
skip_section: Vec<String>,
}
#[derive(clap::Parser, Clone, Serialize, Deserialize, Debug, Default, Schema)]
#[serde(default)]
pub struct FormatOptions {
#[clap(
value_enum,
ignore_case = true,
long,
help_heading = "DOWNLOAD CONFIGURATION"
)]
binary_format: Option<FormatKind>,
#[clap(flatten)]
bin_options: BinaryCliOptions,
#[clap(flatten)]
idf_options: IdfCliOptions,
#[clap(flatten)]
elf_options: ElfCliOptions,
}
#[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq, Clone, Copy, Schema)]
pub enum FormatKind {
Bin,
Hex,
#[default]
Elf,
Idf,
Uf2,
}
impl FormatKind {
pub fn from_optional(s: Option<&str>) -> Result<Self, String> {
match s {
Some(format) => Self::from_str(format),
None => Ok(Self::default()),
}
}
}
impl FromStr for FormatKind {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match &s.to_lowercase()[..] {
"bin" | "binary" => Ok(Self::Bin),
"hex" | "ihex" | "intelhex" => Ok(Self::Hex),
"elf" => Ok(Self::Elf),
"uf2" => Ok(Self::Uf2),
"idf" | "esp-idf" | "espidf" => Ok(Self::Idf),
_ => Err(format!("Format '{s}' is unknown.")),
}
}
}
impl From<FormatKind> for probe_rs::flashing::FormatKind {
fn from(kind: FormatKind) -> Self {
match kind {
FormatKind::Bin => probe_rs::flashing::FormatKind::Bin,
FormatKind::Hex => probe_rs::flashing::FormatKind::Hex,
FormatKind::Elf => probe_rs::flashing::FormatKind::Elf,
FormatKind::Uf2 => probe_rs::flashing::FormatKind::Uf2,
FormatKind::Idf => probe_rs::flashing::FormatKind::Idf,
}
}
}
impl FormatOptions {
pub fn to_format_kind(&self, target: &Target) -> FormatKind {
self.binary_format.unwrap_or_else(|| {
FormatKind::from_optional(target.default_format.as_deref())
.expect("Failed to parse a default binary format. This shouldn't happen.")
})
}
}
fn default_logfile_location() -> Result<PathBuf> {
let project_dirs = directories::ProjectDirs::from("rs", "probe-rs", "probe-rs")
.context("the application storage directory could not be determined")?;
let directory = project_dirs.data_dir();
let logname = sanitize_filename::sanitize_with_options(
format!(
"{}.log",
OffsetDateTime::now_local()?.unix_timestamp_nanos() / 1_000_000
),
sanitize_filename::Options {
replacement: "_",
..Default::default()
},
);
fs::create_dir_all(directory)
.with_context(|| format!("{} could not be created", directory.display()))?;
let log_path = directory.join(logname);
Ok(log_path)
}
fn prune_logs(directory: &Path) -> Result<(), anyhow::Error> {
let mut log_files = fs::read_dir(directory)?
.filter_map(|entry| {
let entry = entry.ok()?;
let path = entry.path();
if path.extension().is_some_and(|e| e == "log") {
let metadata = fs::metadata(&path).ok()?;
let last_modified = metadata.created().ok()?;
Some((path, last_modified))
} else {
None
}
})
.collect_vec();
log_files.sort_unstable_by_key(|(_, b)| Reverse(*b));
for (path, _) in log_files.iter().skip(MAX_LOG_FILES) {
fs::remove_file(path)?;
}
Ok(())
}
fn multicall_check<'list>(args: &'list [OsString], want: &str) -> Option<&'list [OsString]> {
let argv0 = Path::new(&args[0]);
if let Some(command) = argv0.file_stem().and_then(|f| f.to_str()) {
if command == want {
return Some(args);
}
}
if let Some(command) = args.get(1).and_then(|f| f.to_str()) {
if command == want {
return Some(&args[1..]);
}
}
None
}
#[tokio::main]
async fn main() -> Result<()> {
let utc_offset = UtcOffset::current_local_offset().unwrap_or(UtcOffset::UTC);
let mut args: Vec<_> = std::env::args_os().collect();
if let Some(args) = multicall_check(&args, "cargo-flash") {
cmd::cargo_flash::main(args);
return Ok(());
}
if let Some(args) = multicall_check(&args, "cargo-embed") {
cmd::cargo_embed::main(args, utc_offset).await;
return Ok(());
}
reject_format_arg(&args)?;
let config = load_config().context("Failed to load configuration.")?;
let mut matches = Cli::command().get_matches_from(&args);
if apply_config_preset(&config, &matches, &mut args)? {
matches = Cli::command().ignore_errors(true).get_matches_from(args);
}
let mut cli = match Cli::from_arg_matches(&matches) {
Ok(matches) => matches,
Err(err) => err.exit(),
};
if cli.log_file.is_none() && (cli.log_to_folder || cli.report.is_some()) {
let location =
default_logfile_location().context("Unable to determine default log file location.")?;
prune_logs(
location
.parent()
.expect("A file parent directory. Please report this as a bug."),
)?;
cli.log_file = Some(location);
};
let log_path = cli.log_file.clone();
let _logger_guard = if matches!(cli.subcommand, Subcommand::DapServer(_)) {
Ok(None)
} else {
setup_logging(log_path.as_deref(), None)
};
let elf = cli.elf();
let report_path = cli.report.clone();
#[cfg(feature = "remote")]
if let Some(host) = cli.host.as_deref() {
let client = rpc::client::connect(host, cli.token.clone()).await?;
anyhow::ensure!(
cli.subcommand.is_remote_cmd(),
"The subcommand is not supported in remote mode."
);
cli.run(client, config, utc_offset).await?;
return Ok(());
}
let (mut local_server, tx, rx) = RpcApp::create_server(16, rpc::functions::ProbeAccess::All);
let handle = tokio::spawn(async move { local_server.run().await });
let client = RpcClient::new_local_from_wire(tx, rx);
let result = cli.run(client, config, utc_offset).await;
_ = handle.await.unwrap();
compile_report(result, report_path, elf, log_path.as_deref())
}
fn apply_config_preset(
config: &Config,
matches: &ArgMatches,
args: &mut Vec<OsString>,
) -> anyhow::Result<bool> {
let Some(preset) = matches.get_one::<String>("preset") else {
return Ok(false);
};
let Some(preset) = config.presets.get(preset) else {
anyhow::bail!("Config preset '{preset}' not found.");
};
let mut args_modified = false;
for (arg, value) in preset {
let flag = format!("--{}", arg).into();
if args.contains(&flag) {
continue;
}
if let Value::Bool(_, false) = value {
continue;
}
args_modified = true;
args.push(flag);
match value {
Value::String(_, value) => args.push(value.into()),
Value::Num(_, num) => {
if let Some(uint) = num.to_u128() {
args.push(format!("{}", uint).into())
} else if let Some(int) = num.to_i128() {
args.push(format!("{}", int).into())
} else if let Some(float) = num.to_f64() {
args.push(format!("{}", float).into())
} else {
unreachable!()
}
}
Value::Bool(_, _) => {}
_ => anyhow::bail!("Unsupported value: {:?}", value),
}
}
Ok(args_modified)
}
fn reject_format_arg(args: &[OsString]) -> anyhow::Result<()> {
if let Some(format_arg_pos) = args.iter().position(|arg| arg == "--format") {
if let Some(format_arg) = args.get(format_arg_pos + 1) {
if let Some(format_arg) = format_arg.to_str() {
if FormatKind::from_str(format_arg).is_ok() {
anyhow::bail!(
"--format has been renamed to --binary-format. Please use --binary-format {0} instead of --format {0}",
format_arg
);
}
}
}
}
Ok(())
}
fn compile_report(
result: Result<()>,
path: Option<PathBuf>,
elf: Option<PathBuf>,
log_path: Option<&Path>,
) -> Result<()> {
let Err(error) = result else {
return Ok(());
};
let Some(path) = path else {
return Err(error);
};
let command = std::env::args_os();
let report = Report::new(command, error, elf.as_deref(), log_path)?;
report.zip(&path)?;
eprintln!(
"{}",
format!(
"The compiled report has been written to {}.",
path.display()
)
.blue()
);
eprintln!("{}", "Please upload it with your issue on Github.".blue());
eprintln!(
"{}",
"You can create an issue by following this URL:".blue()
);
let base = "https://github.com/probe-rs/probe-rs/issues/new";
let meta = format!("```json\n{}\n```", serde_json::to_string_pretty(&report)?);
let body = urlencoding::encode(&meta);
let error = format!("{:#}", report.error);
let title = urlencoding::encode(&error);
eprintln!("{base}?labels=bug&title={title}&body={body}");
Ok(())
}
fn load_config() -> anyhow::Result<Config> {
let mut paths = vec![PathBuf::from(".")];
if let Ok(exe) = std::env::current_exe() {
paths.push(exe.parent().unwrap().to_path_buf());
}
if let Some(home) = directories::UserDirs::new().map(|user| user.home_dir().to_path_buf()) {
paths.push(home);
}
let files = [".probe-rs"];
let default_config = serde_json::to_string_pretty(&Config::default()).unwrap();
let mut figment = Figment::from(Data::<Json>::string(&default_config));
for path in paths {
for file in files {
figment = figment
.merge(Toml::file(path.join(format!("{file}.toml"))))
.merge(Json::file(path.join(format!("{file}.json"))))
.merge(Yaml::file(path.join(format!("{file}.yaml"))))
.merge(Yaml::file(path.join(format!("{file}.yml"))));
}
}
let config = figment.extract::<Config>()?;
Ok(config)
}
#[cfg(test)]
mod test {
use crate::multicall_check;
#[test]
fn argument_preprocessing() {
fn os_strs(args: &[&str]) -> Vec<std::ffi::OsString> {
args.iter().map(|s| s.into()).collect()
}
assert_eq!(
multicall_check(&os_strs(&["probe-rs", "cargo-embed", "-h"]), "cargo-embed").unwrap(),
os_strs(&["cargo-embed", "-h"])
);
assert_eq!(
multicall_check(
&os_strs(&["probe-rs", "cargo-flash", "--chip", "esp32c2"]),
"cargo-flash"
)
.unwrap(),
os_strs(&["cargo-flash", "--chip", "esp32c2"])
);
}
}