blob: 4515a838a6321a6455aad28e0cdca0197b4d14f1 [file] [log] [blame]
// Copyright lowRISC contributors.
// Licensed under the Apache License, Version 2.0, see LICENSE for details.
// SPDX-License-Identifier: Apache-2.0
#![feature(min_specialization)]
use anyhow::Result;
use atty::Stream;
use directories::ProjectDirs;
use log::LevelFilter;
use serde_annotate::Annotate;
use serde_annotate::ColorProfile;
use std::env::{args_os, ArgsOs};
use std::ffi::OsString;
use std::io::ErrorKind;
use std::iter::{IntoIterator, Iterator};
use std::path::PathBuf;
use structopt::clap::arg_enum;
use structopt::StructOpt;
mod command;
use opentitanlib::app::command::CommandDispatch;
use opentitanlib::app::TransportWrapper;
use opentitanlib::backend;
#[allow(clippy::large_enum_variant)]
#[derive(Debug, StructOpt, CommandDispatch)]
enum RootCommandHierarchy {
// Not flattened because `Bootstrap` is a leaf command.
Bootstrap(command::bootstrap::BootstrapCommand),
// Not flattened because `Console` is a leaf command.
Console(command::console::Console),
Gpio(command::gpio::GpioCommand),
Emulator(command::emulator::EmuCommand),
Fpga(command::fpga::FpgaCommand),
I2c(command::i2c::I2cCommand),
Image(command::image::Image),
NoOp(command::NoOp),
Otp(command::otp::Otp),
Rsa(command::rsa::Rsa),
Spi(command::spi::SpiCommand),
Transport(command::transport::TransportCommand),
Version(command::version::Version),
// Flattened because `Greetings` is a subcommand hierarchy.
#[cfg(feature = "demo_commands")]
#[structopt(flatten)]
Greetings(command::hello::Greetings),
}
arg_enum! {
#[derive(Clone, Copy, Debug)]
enum Format {
Json,
Json5,
HJson,
Yaml,
}
}
#[derive(Debug, StructOpt)]
#[structopt(
name = "opentitantool",
about = "A tool for interacting with OpenTitan chips."
)]
struct Opts {
#[structopt(
long,
default_value = "config",
help = "Filename of a default flagsfile. Relative to $XDG_CONFIG_HOME/opentitantool."
)]
rcfile: PathBuf,
#[structopt(long, default_value = "warn")]
logging: LevelFilter,
#[structopt(
short,
long,
possible_values = &Format::variants(),
case_insensitive = true,
default_value = "hjson",
help = "Preferred output format"
)]
format: Format,
#[structopt(short, long, parse(try_from_str), help = "Use color in the output")]
color: Option<bool>,
#[structopt(
long,
number_of_values(1),
help = "Parse and execute the argument as a command"
)]
exec: Vec<String>,
#[structopt(flatten)]
backend_opts: backend::BackendOpts,
#[structopt(subcommand)]
command: RootCommandHierarchy,
}
// Given some existing option configuration, maybe re-evaluate command
// line options by reading an `rcfile`.
fn parse_command_line(opts: Opts, mut args: ArgsOs) -> Result<Opts> {
// Initialize the logger if the user requested the non-defualt option.
let logging = opts.logging;
if logging != LevelFilter::Off {
env_logger::Builder::from_default_env()
.filter(None, opts.logging)
.init();
}
if opts.rcfile.as_os_str().is_empty() {
// No rcfile to parse.
return Ok(opts);
}
// Construct the rcfile path based on the user's config directory
// (ie: $HOME/.config/opentitantool/<filename>).
let rcfile = if let Some(base) = ProjectDirs::from("org", "opentitan", "opentitantool") {
base.config_dir().join(&opts.rcfile)
} else {
opts.rcfile
};
// argument[0] is the executable name.
let mut arguments = vec![args.next().unwrap()];
// Read in the rcfile and extend the argument list.
match std::fs::read_to_string(&rcfile) {
Ok(content) => {
for line in content.split('\n') {
// Strip basic comments as shellwords won't handle comments.
let (line, _) = line.split_once('#').unwrap_or((line, ""));
arguments.extend(shellwords::split(line)?.iter().map(OsString::from));
}
Ok(())
}
Err(e) if e.kind() == ErrorKind::NotFound => {
log::warn!("Could not read {:?}. Ignoring.", rcfile);
Ok(())
}
Err(e) => Err(anyhow::Error::new(e).context(format!("Reading file {:?}", rcfile))),
}?;
// Extend the argument list with all remaining command line arguments.
arguments.extend(args.into_iter());
let opts = Opts::from_iter(&arguments);
if opts.logging != logging {
// Try re-initializing the logger. Ignore errors.
let _ = env_logger::Builder::from_default_env()
.filter(None, opts.logging)
.try_init();
}
Ok(opts)
}
// Print the result of a command.
// If there is an error and `RUST_BACKTRACE=1`, print a backtrace.
fn print_command_result(opts: &Opts, result: Result<Option<Box<dyn Annotate>>>) -> Result<()> {
match result {
Ok(Some(value)) => {
log::info!("Command result: success.");
let profile = if atty::is(Stream::Stdout) && opts.color.unwrap_or(true) {
ColorProfile::basic()
} else {
ColorProfile::default()
};
let doc = serde_annotate::serialize(value.as_ref())?;
let string = match opts.format {
Format::Json => doc.to_json().color(profile).to_string(),
Format::Json5 => doc.to_json5().color(profile).to_string(),
Format::HJson => doc.to_hjson().color(profile).to_string(),
Format::Yaml => doc.to_yaml().color(profile).to_string(),
};
println!("{}", string);
Ok(())
}
Ok(None) => {
log::info!("Command result: success.");
Ok(())
}
Err(e) => {
log::info!("Command result: {:?}", e);
Err(e)
}
}
}
// Execute is a convenience function for taking a list of strings,
// parsing them into a command, executing the command and printing the result.
fn execute<I>(args: I, opts: &Opts, transport: &TransportWrapper) -> Result<()>
where
I: IntoIterator<Item = OsString>,
{
let command = RootCommandHierarchy::from_iter(
std::iter::once(OsString::from("opentitantool")).chain(args),
);
print_command_result(opts, command.run(opts, transport))?;
Ok(())
}
fn main() -> Result<()> {
let opts = parse_command_line(Opts::from_args(), args_os())?;
let transport = backend::create(&opts.backend_opts)?;
for command in &opts.exec {
execute(
shellwords::split(command)?.iter().map(OsString::from),
&opts,
&transport,
)?;
}
print_command_result(&opts, opts.command.run(&opts, &transport))?;
Ok(())
}