use std::{
fs,
path::PathBuf,
process::{exit, Command, ExitStatus, Stdio},
};
use cargo_metadata::{Message, MetadataCommand};
use clap::{Args, CommandFactory, Parser, Subcommand};
use espflash::{
cli::{
self, board_info, checksum_md5, completions, config::Config, connect, erase_flash,
erase_partitions, erase_region, flash_elf_image, make_flash_data, monitor::monitor,
partition_table, print_board_info, read_flash, save_elf_as_image, serial_monitor,
ChecksumMd5Args, CompletionsArgs, ConnectArgs, EraseFlashArgs, EraseRegionArgs,
EspflashProgress, FlashConfigArgs, MonitorArgs, PartitionTableArgs, ReadFlashArgs,
},
error::Error as EspflashError,
flasher::parse_partition_table,
logging::initialize_logger,
targets::{Chip, XtalFrequency},
update::check_for_update,
};
use log::{debug, info, LevelFilter};
use miette::{IntoDiagnostic, Result, WrapErr};
use crate::{
cargo_config::CargoConfig,
error::{Error, NoTargetError, UnsupportedTargetError},
package_metadata::PackageMetadata,
};
mod cargo_config;
mod error;
mod package_metadata;
#[derive(Debug, Parser)]
#[clap(
bin_name = "cargo",
max_term_width = 100,
propagate_version = true,
version
)]
struct Cli {
#[clap(subcommand)]
subcommand: CargoSubcommand,
}
#[derive(Debug, Subcommand)]
enum CargoSubcommand {
#[clap(about)]
Espflash {
#[clap(subcommand)]
subcommand: Commands,
#[clap(short, long, global = true, action)]
skip_update_check: bool,
},
}
#[derive(Debug, Subcommand)]
enum Commands {
BoardInfo(ConnectArgs),
Completions(CompletionsArgs),
EraseFlash(EraseFlashArgs),
EraseParts(ErasePartsArgs),
EraseRegion(EraseRegionArgs),
Flash(FlashArgs),
HoldInReset(ConnectArgs),
Monitor(MonitorArgs),
PartitionTable(PartitionTableArgs),
ReadFlash(ReadFlashArgs),
Reset(ConnectArgs),
SaveImage(SaveImageArgs),
ChecksumMd5(ChecksumMd5Args),
}
#[derive(Debug, Args)]
#[non_exhaustive]
struct BuildArgs {
#[arg(long)]
pub bin: Option<String>,
#[arg(long)]
pub example: Option<String>,
#[arg(long, use_value_delimiter = true)]
pub features: Option<Vec<String>>,
#[arg(long)]
pub no_default_features: bool,
#[arg(long)]
pub frozen: bool,
#[arg(long)]
pub locked: bool,
#[arg(long)]
pub package: Option<String>,
#[arg(long)]
pub release: bool,
#[arg(long)]
pub target: Option<String>,
#[arg(long)]
pub target_dir: Option<String>,
#[arg(short = 'Z')]
pub unstable: Option<Vec<String>>,
#[clap(flatten)]
pub flash_config_args: FlashConfigArgs,
}
#[derive(Debug, Args)]
#[non_exhaustive]
pub struct ErasePartsArgs {
#[clap(flatten)]
pub connect_args: ConnectArgs,
#[arg(value_name = "LABELS", value_delimiter = ',')]
pub erase_parts: Vec<String>,
#[arg(long, value_name = "FILE")]
pub partition_table: Option<PathBuf>,
#[arg(long)]
pub package: Option<String>,
}
#[derive(Debug, Args)]
#[non_exhaustive]
struct FlashArgs {
#[clap(flatten)]
build_args: BuildArgs,
#[clap(flatten)]
connect_args: ConnectArgs,
#[clap(flatten)]
flash_args: cli::FlashArgs,
}
#[derive(Debug, Args)]
#[non_exhaustive]
struct SaveImageArgs {
#[clap(flatten)]
build_args: BuildArgs,
#[clap(flatten)]
save_image_args: cli::SaveImageArgs,
}
fn main() -> Result<()> {
miette::set_panic_hook();
initialize_logger(LevelFilter::Info);
let cli = Cli::parse();
let CargoSubcommand::Espflash {
subcommand: args,
skip_update_check,
} = cli.subcommand;
debug!("{:#?}, {:#?}", args, skip_update_check);
if !skip_update_check {
check_for_update(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
}
let config = Config::load()?;
match args {
Commands::BoardInfo(args) => board_info(&args, &config),
Commands::Completions(args) => completions(&args, &mut Cli::command(), "cargo"),
Commands::EraseFlash(args) => erase_flash(args, &config),
Commands::EraseParts(args) => erase_parts(args, &config),
Commands::EraseRegion(args) => erase_region(args, &config),
Commands::Flash(args) => flash(args, &config),
Commands::HoldInReset(args) => hold_in_reset(args, &config),
Commands::Monitor(args) => serial_monitor(args, &config),
Commands::PartitionTable(args) => partition_table(args),
Commands::ReadFlash(args) => read_flash(args, &config),
Commands::Reset(args) => reset(args, &config),
Commands::SaveImage(args) => save_image(args, &config),
Commands::ChecksumMd5(args) => checksum_md5(&args, &config),
}
}
#[derive(Debug, Clone)]
struct BuildContext {
pub artifact_path: PathBuf,
pub bootloader_path: Option<PathBuf>,
pub partition_table_path: Option<PathBuf>,
}
pub fn erase_parts(args: ErasePartsArgs, config: &Config) -> Result<()> {
if args.connect_args.no_stub {
return Err(EspflashError::StubRequired).into_diagnostic();
}
let partition_table = args
.partition_table
.as_deref()
.or(config.partition_table.as_deref());
let mut flasher = connect(&args.connect_args, config, false, false)?;
let partition_table = match partition_table {
Some(path) => Some(parse_partition_table(path)?),
None => None,
};
info!("Erasing the following partitions: {:?}", args.erase_parts);
erase_partitions(&mut flasher, partition_table, Some(args.erase_parts), None)?;
flasher
.connection()
.reset_after(!args.connect_args.no_stub)?;
Ok(())
}
fn reset(args: ConnectArgs, config: &Config) -> Result<()> {
let mut args = args.clone();
args.no_stub = true;
let mut flash = connect(&args, config, true, true)?;
info!("Resetting target device");
flash.connection().reset()?;
Ok(())
}
fn hold_in_reset(args: ConnectArgs, config: &Config) -> Result<()> {
connect(&args, config, true, true)?;
info!("Holding target device in reset");
Ok(())
}
fn flash(args: FlashArgs, config: &Config) -> Result<()> {
let metadata = PackageMetadata::load(&args.build_args.package)?;
let cargo_config = CargoConfig::load(&metadata.workspace_root, &metadata.package_root);
let mut flasher = connect(
&args.connect_args,
config,
args.flash_args.no_verify,
args.flash_args.no_skip,
)?;
flasher.verify_minimum_revision(args.flash_args.image.min_chip_rev)?;
if let Some(flash_size) = args.build_args.flash_config_args.flash_size {
flasher.set_flash_size(flash_size);
} else if let Some(flash_size) = config.flash.size {
flasher.set_flash_size(flash_size);
}
let chip = flasher.chip();
let target = chip.into_target();
let target_xtal_freq = target.crystal_freq(flasher.connection())?;
flasher.disable_watchdog()?;
let build_ctx =
build(&args.build_args, &cargo_config, chip).wrap_err("Failed to build project")?;
let elf_data = fs::read(build_ctx.artifact_path.clone()).into_diagnostic()?;
print_board_info(&mut flasher)?;
if args.flash_args.ram {
flasher.load_elf_to_ram(&elf_data, Some(&mut EspflashProgress::default()))?;
} else {
let flash_data = make_flash_data(
args.flash_args.image,
&args.build_args.flash_config_args,
config,
build_ctx.bootloader_path.as_deref(),
build_ctx.partition_table_path.as_deref(),
)?;
if args.flash_args.erase_parts.is_some() || args.flash_args.erase_data_parts.is_some() {
erase_partitions(
&mut flasher,
flash_data.partition_table.clone(),
args.flash_args.erase_parts,
args.flash_args.erase_data_parts,
)?;
}
flash_elf_image(&mut flasher, &elf_data, flash_data, target_xtal_freq)?;
}
if args.flash_args.monitor {
let pid = flasher.get_usb_pid()?;
let default_baud = if chip == Chip::Esp32c2 && target_xtal_freq == XtalFrequency::_26Mhz {
74_880
} else {
115_200
};
monitor(
flasher.into_serial(),
Some(&elf_data),
pid,
args.flash_args.monitor_baud.unwrap_or(default_baud),
args.flash_args.log_format,
true,
args.flash_args.processors,
Some(build_ctx.artifact_path),
)
} else {
Ok(())
}
}
fn build(
build_options: &BuildArgs,
cargo_config: &CargoConfig,
chip: Chip,
) -> Result<BuildContext> {
let target = build_options
.target
.as_deref()
.or_else(|| cargo_config.target())
.ok_or_else(|| NoTargetError::new(Some(chip)))?;
let mut metadata_cmd = MetadataCommand::new();
if build_options.no_default_features {
metadata_cmd.features(cargo_metadata::CargoOpt::NoDefaultFeatures);
}
if let Some(features) = &build_options.features {
metadata_cmd.features(cargo_metadata::CargoOpt::SomeFeatures(features.clone()));
}
let metadata = metadata_cmd.exec().into_diagnostic()?;
if !chip.into_target().supports_build_target(target) {
return Err(UnsupportedTargetError::new(target, chip).into());
}
let cfg_has_build_std = cargo_config.has_build_std();
let opts_has_build_std = build_options
.unstable
.clone()
.map(|ref v| v.iter().any(|s| s.contains("build-std")))
.unwrap_or_default();
let xtensa_target = target.starts_with("xtensa-");
if xtensa_target && !(cfg_has_build_std || opts_has_build_std) {
return Err(Error::NoBuildStd.into());
};
let mut args = vec!["--target".to_string(), target.to_string()];
if let Some(target_dir) = &build_options.target_dir {
args.push("--target-dir".to_string());
args.push(target_dir.to_string());
}
if build_options.release {
args.push("--release".to_string());
}
if build_options.locked {
args.push("--locked".to_string());
}
if build_options.frozen {
args.push("--frozen".to_string());
}
if let Some(example) = &build_options.example {
args.push("--example".to_string());
args.push(example.to_string());
}
if let Some(bin) = &build_options.bin {
args.push("--bin".to_string());
args.push(bin.to_string());
}
if let Some(package) = &build_options.package {
args.push("--package".to_string());
args.push(package.to_string());
}
if build_options.no_default_features {
args.push("--no-default-features".to_string());
}
if let Some(features) = &build_options.features {
args.push("--features".to_string());
args.push(features.join(","));
}
if let Some(unstable) = &build_options.unstable {
for item in unstable.iter() {
args.push("-Z".to_string());
args.push(item.to_string());
}
}
let output = Command::new("cargo")
.arg("build")
.args(args)
.args(["--message-format", "json-diagnostic-rendered-ansi"])
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.into_diagnostic()?
.wait_with_output()
.into_diagnostic()?;
let messages = Message::parse_stream(&output.stdout[..]);
let mut target_artifact = None;
let mut bootloader_path = None;
let mut partition_table_path = None;
for message in messages {
match message.into_diagnostic()? {
Message::BuildScriptExecuted(script) => {
let Some(package) = metadata.packages.iter().find(|p| p.id == script.package_id)
else {
continue;
};
if package.name != "esp-idf-sys" {
continue;
}
let build_path = PathBuf::from(script.out_dir).join("build");
let bl_path = build_path.join("bootloader").join("bootloader.bin");
let pt_path = build_path
.join("partition_table")
.join("partition-table.bin");
if bl_path.exists() && bl_path.is_file() {
bootloader_path = Some(bl_path);
}
if pt_path.exists() && pt_path.is_file() {
partition_table_path = Some(pt_path);
}
}
Message::CompilerArtifact(artifact) => {
if artifact.executable.is_some() {
if target_artifact.is_some() {
return Err(Error::MultipleArtifacts.into());
} else {
target_artifact = Some(artifact);
}
}
}
Message::CompilerMessage(message) => {
if let Some(rendered) = message.message.rendered {
print!("{}", rendered);
}
}
_ => (),
}
}
if !output.status.success() {
exit_with_process_status(output.status);
}
let target_artifact = target_artifact.ok_or(Error::NoArtifact)?;
let artifact_path = target_artifact.executable.unwrap().into();
let build_ctx = BuildContext {
artifact_path,
bootloader_path,
partition_table_path,
};
Ok(build_ctx)
}
fn save_image(args: SaveImageArgs, config: &Config) -> Result<()> {
let metadata = PackageMetadata::load(&args.build_args.package)?;
let cargo_config = CargoConfig::load(&metadata.workspace_root, &metadata.package_root);
let build_ctx = build(&args.build_args, &cargo_config, args.save_image_args.chip)?;
let elf_data = fs::read(build_ctx.artifact_path).into_diagnostic()?;
println!("Chip type: {}", args.save_image_args.chip);
println!("Merge: {}", args.save_image_args.merge);
println!("Skip padding: {}", args.save_image_args.skip_padding);
let flash_data = make_flash_data(
args.save_image_args.image,
&args.build_args.flash_config_args,
config,
build_ctx.bootloader_path.as_deref(),
build_ctx.partition_table_path.as_deref(),
)?;
let xtal_freq = args
.save_image_args
.xtal_freq
.unwrap_or(XtalFrequency::default(args.save_image_args.chip));
save_elf_as_image(
&elf_data,
args.save_image_args.chip,
args.save_image_args.file,
flash_data,
args.save_image_args.merge,
args.save_image_args.skip_padding,
xtal_freq,
)?;
Ok(())
}
#[cfg(unix)]
fn exit_with_process_status(status: ExitStatus) -> ! {
use std::os::unix::process::ExitStatusExt;
let code = status.code().or_else(|| status.signal()).unwrap_or(1);
exit(code)
}
#[cfg(not(unix))]
fn exit_with_process_status(status: ExitStatus) -> ! {
let code = status.code().unwrap_or(1);
exit(code)
}