use std::ffi::OsStr;
use std::io::stdout;
use std::str::FromStr;
use bmputil::bmp::{BmpDevice, BmpMatcher, FirmwareType};
use bmputil::metadata::download_metadata;
#[cfg(windows)]
use bmputil::windows;
use bmputil::{AllowDangerous, BmpParams, FlashParams};
use clap::builder::TypedValueParser;
use clap::builder::styling::Styles;
use clap::{Arg, ArgAction, Args, Command, CommandFactory, Parser, Subcommand};
use clap_complete::{Shell, generate};
use color_eyre::eyre::{Context, OptionExt, Result};
use directories::ProjectDirs;
use log::{debug, error, info, warn};
#[derive(Parser)]
#[command(
version,
about,
styles(style()),
disable_colored_help(false),
arg_required_else_help(true)
)]
struct CliArguments
{
#[arg(global = true, short = 's', long = "serial", alias = "serial-number")]
serial_number: Option<String>,
#[arg(global = true, long = "index", value_parser = usize::from_str)]
index: Option<usize>,
#[arg(global = true, short = 'p', long = "port")]
port: Option<String>,
#[cfg(windows)]
#[arg(global = true, long = "windows-wdi-install-mode", value_parser = u32::from_str, hide = true)]
windows_wdi_install_mode: Option<u32>,
#[command(subcommand)]
pub subcommand: ToplevelCommmands,
}
#[derive(Subcommand)]
enum ToplevelCommmands
{
Probe(ProbeArguments),
Target,
Server,
Debug,
Complete(CompletionArguments),
}
#[derive(Args)]
struct ProbeArguments
{
#[arg(global = true, long = "allow-dangerous-options", hide = true, default_value_t = AllowDangerous::Never)]
#[arg(value_enum)]
allow_dangerous_options: AllowDangerous,
#[command(subcommand)]
subcommand: ProbeCommmands,
}
#[derive(Subcommand)]
#[command(arg_required_else_help(true))]
enum ProbeCommmands
{
Info(InfoArguments),
Update(UpdateArguments),
Switch(SwitchArguments),
Reboot(RebootArguments),
#[cfg(windows)]
InstallDrivers(DriversArguments),
}
#[derive(Args)]
struct InfoArguments
{
#[arg(long = "list-targets", default_value_t = false)]
list_targets: bool,
}
#[derive(Args)]
struct UpdateArguments
{
firmware_binary: Option<String>,
#[arg(long = "override-firmware-type", hide_short_help = true, value_enum)]
override_firmware_type: Option<FirmwareType>,
#[arg(long = "force-override-flash", hide = true, default_value_t = false, value_parser = ConfirmedBoolParser {})]
#[arg(action = ArgAction::Set)]
force_override_flash: bool,
#[arg(short = 'f', long = "force", default_value_t = false)]
force: bool,
#[arg(long = "use-rc", default_value_t = false)]
use_rc: bool,
#[command(subcommand)]
subcommand: Option<UpdateCommands>,
}
#[derive(Subcommand)]
enum UpdateCommands
{
List,
}
#[derive(Args)]
struct SwitchArguments
{
#[arg(long = "override-firmware-type", hide_short_help = true, value_enum)]
override_firmware_type: Option<FirmwareType>,
#[arg(long = "force-override-flash", hide = true, default_value_t = false, value_parser = ConfirmedBoolParser {})]
#[arg(action = ArgAction::Set)]
force_override_flash: bool,
}
#[derive(Args)]
#[group(multiple = false)]
struct RebootArguments
{
#[arg(long = "dfu", default_value_t = false)]
dfu: bool,
#[arg(long = "repeat", default_value_t = false)]
repeat: bool,
}
#[cfg(windows)]
#[derive(Args)]
struct DriversArguments
{
#[arg(long = "force", default_value_t = false)]
force: bool,
}
#[derive(Args)]
struct CompletionArguments
{
shell: Shell,
}
impl BmpParams for CliArguments
{
fn index(&self) -> Option<usize>
{
self.index
}
fn serial_number(&self) -> Option<&str>
{
self.serial_number.as_deref()
}
}
impl FlashParams for CliArguments
{
fn allow_dangerous_options(&self) -> AllowDangerous
{
match &self.subcommand {
ToplevelCommmands::Probe(probe_args) => probe_args.allow_dangerous_options,
_ => AllowDangerous::Never,
}
}
fn override_firmware_type(&self) -> Option<FirmwareType>
{
match &self.subcommand {
ToplevelCommmands::Probe(probe_args) => match &probe_args.subcommand {
ProbeCommmands::Update(flash_args) => flash_args.override_firmware_type,
ProbeCommmands::Switch(switch_args) => switch_args.override_firmware_type,
_ => None,
},
_ => None,
}
}
}
#[derive(Clone)]
struct ConfirmedBoolParser {}
impl TypedValueParser for ConfirmedBoolParser
{
type Value = bool;
fn parse_ref(&self, cmd: &Command, _arg: Option<&Arg>, value: &OsStr) -> Result<Self::Value, clap::Error>
{
let value = value
.to_str()
.ok_or_else(|| clap::Error::new(clap::error::ErrorKind::InvalidUtf8).with_cmd(cmd))?;
Ok(value == "really")
}
}
fn reboot_command(cli_args: &CliArguments, reboot_args: &RebootArguments) -> Result<()>
{
let matcher = BmpMatcher::from_params(cli_args);
let mut results = matcher.find_matching_probes();
let mut dev = results.pop_single("detach").map_err(|kind| kind.error())?;
use bmputil::usb::DfuOperatingMode::*;
if reboot_args.dfu {
return match dev.operating_mode() {
Runtime => {
println!("Rebooting probe into bootloader...");
dev.detach_and_destroy().wrap_err("detaching device")
},
FirmwareUpgrade => {
println!("Probe already in bootloader, nothing to do.");
Ok(())
},
};
}
if reboot_args.repeat {
println!("Switching probe between bootloader and firmware...");
return dev.detach_and_destroy().wrap_err("detaching device");
}
match dev.operating_mode() {
Runtime => {
println!("Rebooting probe...");
dev.detach_and_enumerate().wrap_err("detaching device")?;
},
FirmwareUpgrade => println!("Rebooting probe into firmware..."),
}
dev.detach_and_destroy().wrap_err("detaching device")
}
fn update_probe(cli_args: &CliArguments, flash_args: &UpdateArguments, paths: &ProjectDirs) -> Result<()>
{
use bmputil::switcher::{download_firmware, pick_firmware};
let matcher = BmpMatcher::from_params(cli_args);
let mut results = matcher.find_matching_probes();
let probe = results.pop_single("flash").map_err(|kind| kind.error())?;
let file_name = match &flash_args.firmware_binary {
Some(file_path) => file_path.into(),
None => {
let identity = &probe.firmware_identity()?;
let cache = paths.cache_dir();
let metadata = download_metadata(cache)?;
let (latest_version, latest_release) = metadata
.latest(flash_args.use_rc)
.ok_or_eyre("Could not determine the latest release of the firmware")?;
let latest_firmware = latest_release.firmware.get(
&identity
.variant()
.ok_or_eyre("Device appears to be in bootloader, so cannot determine probe type")?,
);
let latest_firmware = if let Some(firmware) = latest_firmware {
firmware
} else {
error!("Cannot find suitable firmware for your probe from the pre-built releases");
return Ok(());
};
if identity.version >= latest_version && !flash_args.force {
info!(
"Latest release {} is not newer than firmware version {}, not updating",
latest_version, identity.version
);
return Ok(());
}
let latest_version_str = latest_version.to_string();
if identity.version < latest_version {
info!("Upgrading probe firmware from {} to {}", identity.version, latest_version_str);
} else if flash_args.force {
warn!(
"Forcibly downgrading firmware from {} to {}",
identity.version, latest_version_str
);
}
let firmware_variant = match latest_firmware.variants.len() {
1 => latest_firmware.variants.values().next().unwrap(),
_ => match pick_firmware(latest_version_str.as_str(), latest_firmware)? {
Some(variant) => variant,
None => {
println!("firmware variant selection cancelled, stopping operation");
return Ok(());
},
},
};
download_firmware(firmware_variant, paths.cache_dir())?
},
};
bmputil::flasher::flash_probe(cli_args, probe, file_name)
}
fn display_releases(paths: &ProjectDirs) -> Result<()>
{
let cache = paths.cache_dir();
let metadata = download_metadata(cache)?;
for (version, release) in metadata.releases {
info!("Details of release {version}:");
info!("-> Release includes BMDA builds? {}", release.includes_bmda);
info!(
"-> Release done for probes: {}",
release
.firmware
.keys()
.map(|p| p.to_string())
.collect::<Vec<_>>()
.join(", ")
);
for (probe, firmware) in release.firmware {
info!(
"-> probe {} has {} firmware variants",
probe.to_string(),
firmware.variants.len()
);
for (variant, download) in firmware.variants {
info!(" -> Firmware variant {}", variant);
info!(
" -> {} will be downloaded as {}",
download.friendly_name,
download.file_name.display()
);
info!(" -> Variant will be downloaded from {}", download.uri);
}
}
if let Some(bmda) = release.bmda {
info!("-> Release contains BMDA for {} OSes", bmda.len());
for (os, bmda_arch) in bmda {
info!(
" -> {} release is for {} architectures",
os.to_string(),
bmda_arch.binaries.len()
);
for (arch, binary) in bmda_arch.binaries {
info!(" -> BMDA binary for {}", arch.to_string());
info!(" -> Name of executable in archive: {}", binary.file_name.display());
info!(" -> Archive will be downloaded from {}", binary.uri);
}
}
}
}
Ok(())
}
fn list_targets(probe: BmpDevice) -> Result<()>
{
let remote = probe.bmd_serial_interface()?.remote()?;
let archs = remote.supported_architectures()?;
if let Some(archs) = archs {
info!("Probe supports the following target architectures: {archs}");
} else {
info!("Could not determine what target architectures your probe supports - please upgrade your firmware.");
}
let families = remote.supported_families()?;
if let Some(families) = families {
info!("Probe supports the following target families: {families}");
} else {
info!("Could not determine what target families your probe supports - please upgrade your firmware.");
}
Ok(())
}
fn info_command(cli_args: &CliArguments, info_args: &InfoArguments) -> Result<()>
{
let matcher = BmpMatcher::from_params(cli_args);
let mut results = matcher.find_matching_probes();
if info_args.list_targets {
return list_targets(results.pop_single("list targets").map_err(|kind| kind.error())?);
}
let devices = results.pop_all()?;
let multiple = devices.len() > 1;
for (index, dev) in devices.iter().enumerate() {
debug!("Probe identity: {}", dev.firmware_identity()?);
println!("Found: {dev}");
if multiple {
println!(" Index: {index}\n");
}
}
Ok(())
}
fn style() -> clap::builder::Styles
{
Styles::styled()
.usage(
anstyle::Style::new()
.fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Yellow)))
.bold(),
)
.header(
anstyle::Style::new()
.bold()
.fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Yellow))),
)
.literal(anstyle::Style::new().fg_color(Some(anstyle::Color::Ansi(anstyle::AnsiColor::Green))))
}
fn main() -> Result<()>
{
color_eyre::install()?;
env_logger::Builder::new()
.filter_level(log::LevelFilter::Info)
.parse_default_env()
.init();
let cli_args = CliArguments::parse();
#[cfg(windows)]
match cli_args.subcommand {
ToplevelCommmands::Probe(ProbeArguments {
subcommand: ProbeCommmands::InstallDrivers(_),
..
}) => (),
_ => {
windows::ensure_access(
cli_args.windows_wdi_install_mode,
false, false, );
},
}
let paths = match ProjectDirs::from("org", "black-magic", "bmputil") {
Some(paths) => paths,
None => {
error!("Failed to get program working paths");
std::process::exit(2);
},
};
match &cli_args.subcommand {
ToplevelCommmands::Probe(probe_args) => match &probe_args.subcommand {
ProbeCommmands::Info(info_args) => info_command(&cli_args, info_args),
ProbeCommmands::Update(update_args) => {
if let Some(subcommand) = &update_args.subcommand {
match subcommand {
UpdateCommands::List => display_releases(&paths),
}
} else {
update_probe(&cli_args, update_args, &paths)
}
},
ProbeCommmands::Switch(_) => bmputil::switcher::switch_firmware(&cli_args, &paths),
ProbeCommmands::Reboot(reboot_args) => reboot_command(&cli_args, reboot_args),
#[cfg(windows)]
ProbeCommmands::InstallDrivers(driver_args) => {
windows::ensure_access(
cli_args.windows_wdi_install_mode,
true, driver_args.force,
);
Ok(())
},
},
ToplevelCommmands::Target => {
warn!("Command space reserved for future tool version");
Ok(())
},
ToplevelCommmands::Server => {
warn!("Command space reserved for future tool version");
Ok(())
},
ToplevelCommmands::Debug => {
warn!("Command space reserved for future tool version");
Ok(())
},
ToplevelCommmands::Complete(comp_args) => {
let mut cmd = CliArguments::command();
generate(comp_args.shell, &mut cmd, "bmputil-cli", &mut stdout());
Ok(())
},
}
}