mirror of
https://github.com/esp-rs/espflash.git
synced 2026-03-27 09:41:37 +00:00
Add support for using custom cargo metadata when in a workspace
This commit is contained in:
committed by
Jesse Braham
parent
654240596e
commit
3a6abd3f85
1005
Cargo.lock
generated
1005
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -29,12 +29,12 @@ bin-dir = "{ bin }{ binary-ext }"
|
||||
pkg-fmt = "zip"
|
||||
|
||||
[dependencies]
|
||||
cargo = { version = "0.66.0", features = ["vendored-openssl"] }
|
||||
cargo_metadata = "0.15.1"
|
||||
cargo_toml = "0.13.0"
|
||||
clap = { version = "4.0.22", features = ["derive"] }
|
||||
clap = { version = "4.0.25", features = ["derive"] }
|
||||
env_logger = "0.9.3"
|
||||
esp-idf-part = "0.1.2"
|
||||
espflash = { version = "=2.0.0-rc.1", path = "../espflash" }
|
||||
esp-idf-part = "0.1.1"
|
||||
log = "0.4.17"
|
||||
miette = { version = "5.4.1", features = ["fancy"] }
|
||||
serde = { version = "1.0.147", features = ["derive"] }
|
||||
|
||||
@@ -1,26 +1,12 @@
|
||||
use crate::error::TomlError;
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use miette::{Result, WrapErr};
|
||||
use serde::Deserialize;
|
||||
use std::fs;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
pub struct CargoConfig {
|
||||
#[serde(default)]
|
||||
unstable: Unstable,
|
||||
#[serde(default)]
|
||||
build: Build,
|
||||
}
|
||||
|
||||
impl CargoConfig {
|
||||
pub fn has_build_std(&self) -> bool {
|
||||
!self.unstable.build_std.is_empty()
|
||||
}
|
||||
|
||||
pub fn target(&self) -> Option<&str> {
|
||||
self.build.target.as_deref()
|
||||
}
|
||||
}
|
||||
use crate::error::TomlError;
|
||||
|
||||
#[derive(Debug, Default, Deserialize)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
@@ -34,33 +20,75 @@ pub struct Build {
|
||||
target: Option<String>,
|
||||
}
|
||||
|
||||
pub fn parse_cargo_config<P: AsRef<Path>>(project_path: P) -> Result<CargoConfig> {
|
||||
let config_path = match config_path(project_path.as_ref()) {
|
||||
#[derive(Debug, Deserialize, Default)]
|
||||
pub struct CargoConfig {
|
||||
#[serde(default)]
|
||||
unstable: Unstable,
|
||||
#[serde(default)]
|
||||
build: Build,
|
||||
}
|
||||
|
||||
impl CargoConfig {
|
||||
pub fn load(workspace_root: &Path, package_root: &Path) -> Self {
|
||||
// If there is a Cargo configuration file in the current package, we will
|
||||
// deserialize and return it.
|
||||
// If the package is in a workspace and a Cargo configuration file is present in
|
||||
// that workspace we will deserialize and return that one instead.
|
||||
// Otherwise, there is no configuration present so we will return `None`.
|
||||
if let Ok(Some(package_config)) = load_cargo_config(package_root) {
|
||||
package_config
|
||||
} else if let Ok(Some(workspace_config)) = load_cargo_config(workspace_root) {
|
||||
workspace_config
|
||||
} else {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_build_std(&self) -> bool {
|
||||
!self.unstable.build_std.is_empty()
|
||||
}
|
||||
|
||||
pub fn target(&self) -> Option<&str> {
|
||||
self.build.target.as_deref()
|
||||
}
|
||||
}
|
||||
|
||||
fn load_cargo_config(path: &Path) -> Result<Option<CargoConfig>> {
|
||||
let config_path = match config_path(path) {
|
||||
Some(path) => path,
|
||||
None => {
|
||||
return Ok(CargoConfig::default());
|
||||
return Ok(None);
|
||||
}
|
||||
};
|
||||
|
||||
let content = match fs::read_to_string(&config_path) {
|
||||
Ok(content) => content,
|
||||
Err(_) => return Ok(CargoConfig::default()),
|
||||
Err(_) => return Ok(None),
|
||||
};
|
||||
toml::from_str(&content)
|
||||
|
||||
let config = toml::from_str(&content)
|
||||
.map_err(move |e| TomlError::new(e, content))
|
||||
.wrap_err_with(|| {
|
||||
format!(
|
||||
"Failed to parse {}",
|
||||
&config_path.as_path().to_string_lossy()
|
||||
)
|
||||
})
|
||||
})?;
|
||||
|
||||
Ok(Some(config))
|
||||
}
|
||||
|
||||
fn config_path(project_path: &Path) -> Option<PathBuf> {
|
||||
let bare = project_path.join(".cargo/config");
|
||||
fn config_path(path: &Path) -> Option<PathBuf> {
|
||||
// Support for the .toml extension was added in version 1.39 and is the
|
||||
// preferred form. If both files exist, Cargo will use the file without the
|
||||
// extension.
|
||||
// https://doc.rust-lang.org/cargo/reference/config.html
|
||||
let bare = path.join(".cargo/config");
|
||||
if bare.exists() {
|
||||
return Some(bare);
|
||||
}
|
||||
let toml = project_path.join(".cargo/config.toml");
|
||||
|
||||
let toml = path.join(".cargo/config.toml");
|
||||
if toml.exists() {
|
||||
Some(toml)
|
||||
} else {
|
||||
|
||||
@@ -7,9 +7,31 @@ use espflash::targets::Chip;
|
||||
use miette::{Diagnostic, LabeledSpan, SourceCode, SourceOffset};
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug, Diagnostic)]
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
#[non_exhaustive]
|
||||
pub enum Error {
|
||||
#[error("Specified bootloader table is not a bin file")]
|
||||
#[diagnostic(code(cargo_espflash::invalid_bootloader_path))]
|
||||
InvalidBootloaderPath,
|
||||
|
||||
#[error("Specified partition table is not a csv file")]
|
||||
#[diagnostic(code(cargo_espflash::invalid_partition_table_path))]
|
||||
InvalidPartitionTablePath,
|
||||
|
||||
#[error("The current workspace is invalid, and could not be loaded")]
|
||||
#[diagnostic(
|
||||
code(cargo_espflash::invalid_workspace),
|
||||
help("Ensure that a valid Cargo.toml file is in the executing directory")
|
||||
)]
|
||||
InvalidWorkspace,
|
||||
|
||||
#[error("Multiple build artifacts found")]
|
||||
#[diagnostic(
|
||||
code(cargo_espflash::multiple_artifacts),
|
||||
help("Please specify which artifact to flash using --bin")
|
||||
)]
|
||||
MultipleArtifacts,
|
||||
|
||||
#[error("No executable artifact found")]
|
||||
#[diagnostic(
|
||||
code(cargo_espflash::no_artifact),
|
||||
@@ -17,9 +39,10 @@ pub enum Error {
|
||||
or if you're in a cargo workspace, specify the binary package with `--package`.")
|
||||
)]
|
||||
NoArtifact,
|
||||
|
||||
#[error("'build-std' not configured")]
|
||||
#[diagnostic(
|
||||
code(cargo_espflash::build_std),
|
||||
code(cargo_espflash::no_build_std),
|
||||
help(
|
||||
"cargo currently requires the unstable 'build-std' feature, ensure \
|
||||
that .cargo/config{{.toml}} has the appropriate options.\n \
|
||||
@@ -27,68 +50,40 @@ pub enum Error {
|
||||
)
|
||||
)]
|
||||
NoBuildStd,
|
||||
#[error("Multiple build artifacts found")]
|
||||
|
||||
#[error("No package could be located in the current workspace")]
|
||||
#[diagnostic(
|
||||
code(cargo_espflash::multiple_artifacts),
|
||||
help("Please specify which artifact to flash using --bin")
|
||||
code(cargo_espflash::no_package),
|
||||
help("Ensure that you are executing from a valid package, or that the specified package name\
|
||||
exists in the current workspace.")
|
||||
)]
|
||||
MultipleArtifacts,
|
||||
#[error("Specified partition table is not a csv file")]
|
||||
#[diagnostic(code(cargo_espflash::partition_table_path))]
|
||||
InvalidPartitionTablePath,
|
||||
#[error("Specified bootloader table is not a bin file")]
|
||||
#[diagnostic(code(cargo_espflash::bootloader_path))]
|
||||
InvalidBootloaderPath,
|
||||
NoPackage,
|
||||
|
||||
#[error("No Cargo.toml found in the current directory")]
|
||||
#[diagnostic(
|
||||
code(cargo_espflash::no_project),
|
||||
help("Ensure that you're running the command from within a cargo project")
|
||||
)]
|
||||
NoProject,
|
||||
#[error(transparent)]
|
||||
#[diagnostic(transparent)]
|
||||
UnsupportedTarget(UnsupportedTargetError),
|
||||
|
||||
#[error(transparent)]
|
||||
#[diagnostic(transparent)]
|
||||
NoTarget(#[from] NoTargetError),
|
||||
|
||||
#[error(transparent)]
|
||||
#[diagnostic(transparent)]
|
||||
UnsupportedTarget(UnsupportedTargetError),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TomlError {
|
||||
err: MaybeTomlError,
|
||||
err: toml::de::Error,
|
||||
source: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum MaybeTomlError {
|
||||
Toml(toml::de::Error),
|
||||
Io(std::io::Error),
|
||||
Other(&'static str),
|
||||
}
|
||||
|
||||
impl From<cargo_toml::Error> for MaybeTomlError {
|
||||
fn from(e: cargo_toml::Error) -> Self {
|
||||
match e {
|
||||
cargo_toml::Error::Parse(e) => MaybeTomlError::Toml(e),
|
||||
cargo_toml::Error::Io(e) => MaybeTomlError::Io(e),
|
||||
cargo_toml::Error::Other(e) => MaybeTomlError::Other(e),
|
||||
_ => todo!(), // `cargo_toml::Error` is marked as non-exhaustive
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<toml::de::Error> for MaybeTomlError {
|
||||
fn from(e: toml::de::Error) -> Self {
|
||||
MaybeTomlError::Toml(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl TomlError {
|
||||
pub fn new(err: impl Into<MaybeTomlError>, source: String) -> Self {
|
||||
TomlError {
|
||||
err: err.into(),
|
||||
source,
|
||||
}
|
||||
pub fn new(err: toml::de::Error, source: String) -> Self {
|
||||
Self { err, source }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,7 +93,7 @@ impl Display for TomlError {
|
||||
}
|
||||
}
|
||||
|
||||
// no `source` on purpose to prevent duplicating the message
|
||||
// NOTE: no `source` on purpose to prevent duplicating the message
|
||||
impl std::error::Error for TomlError {}
|
||||
|
||||
impl Diagnostic for TomlError {
|
||||
@@ -107,22 +102,18 @@ impl Diagnostic for TomlError {
|
||||
}
|
||||
|
||||
fn labels(&self) -> Option<Box<dyn Iterator<Item = LabeledSpan> + '_>> {
|
||||
match &self.err {
|
||||
MaybeTomlError::Toml(err) => {
|
||||
let (line, col) = err.line_col()?;
|
||||
let offset = SourceOffset::from_location(&self.source, line + 1, col + 1);
|
||||
Some(Box::new(once(LabeledSpan::new(
|
||||
Some(err.to_string()),
|
||||
offset.offset(),
|
||||
0,
|
||||
))))
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
let (line, col) = self.err.line_col()?;
|
||||
let offset = SourceOffset::from_location(&self.source, line + 1, col + 1);
|
||||
|
||||
Some(Box::new(once(LabeledSpan::new(
|
||||
Some(self.err.to_string()),
|
||||
offset.offset(),
|
||||
0,
|
||||
))))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug, Diagnostic)]
|
||||
#[derive(Debug, Diagnostic, Error)]
|
||||
#[error("Target {target} is not supported by the {chip}")]
|
||||
#[diagnostic(
|
||||
code(cargo_espflash::unsupported_target),
|
||||
@@ -134,8 +125,8 @@ pub struct UnsupportedTargetError {
|
||||
}
|
||||
|
||||
impl UnsupportedTargetError {
|
||||
pub fn new(target: &str, chip: Chip) -> UnsupportedTargetError {
|
||||
UnsupportedTargetError {
|
||||
pub fn new(target: &str, chip: Chip) -> Self {
|
||||
Self {
|
||||
target: target.into(),
|
||||
chip,
|
||||
}
|
||||
@@ -156,7 +147,7 @@ pub struct NoTargetError {
|
||||
|
||||
impl NoTargetError {
|
||||
pub fn new(chip: Option<Chip>) -> Self {
|
||||
NoTargetError { chip }
|
||||
Self { chip }
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,9 +23,9 @@ use miette::{IntoDiagnostic, Result, WrapErr};
|
||||
use strum::VariantNames;
|
||||
|
||||
use crate::{
|
||||
cargo_config::{parse_cargo_config, CargoConfig},
|
||||
cargo_config::CargoConfig,
|
||||
error::{Error, NoTargetError, UnsupportedTargetError},
|
||||
package_metadata::CargoEspFlashMeta,
|
||||
package_metadata::PackageMetadata,
|
||||
};
|
||||
|
||||
mod cargo_config;
|
||||
@@ -133,19 +133,17 @@ fn main() -> Result<()> {
|
||||
// displayed.
|
||||
check_for_update(env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION"));
|
||||
|
||||
// Load any user configuraiton and/or package metadata, if present.
|
||||
// Load any user configuration, if present.
|
||||
let config = Config::load().unwrap();
|
||||
let cargo_config = parse_cargo_config(".")?;
|
||||
let metadata = CargoEspFlashMeta::load("Cargo.toml")?;
|
||||
|
||||
// Execute the correct action based on the provided subcommand and its
|
||||
// associated arguments.
|
||||
match args {
|
||||
Commands::BoardInfo(args) => board_info(args, &config),
|
||||
Commands::Flash(args) => flash(args, &config, &cargo_config, &metadata),
|
||||
Commands::Flash(args) => flash(args, &config),
|
||||
Commands::Monitor(args) => serial_monitor(args, &config),
|
||||
Commands::PartitionTable(args) => partition_table(args),
|
||||
Commands::SaveImage(args) => save_image(args, &cargo_config, &metadata),
|
||||
Commands::SaveImage(args) => save_image(args),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -156,15 +154,12 @@ struct BuildContext {
|
||||
pub partition_table_path: Option<PathBuf>,
|
||||
}
|
||||
|
||||
fn flash(
|
||||
args: FlashArgs,
|
||||
config: &Config,
|
||||
cargo_config: &CargoConfig,
|
||||
metadata: &CargoEspFlashMeta,
|
||||
) -> Result<()> {
|
||||
let mut flasher = connect(&args.connect_args, config)?;
|
||||
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 build_ctx = build(&args.build_args, cargo_config, flasher.chip())
|
||||
let mut flasher = connect(&args.connect_args, config)?;
|
||||
let build_ctx = build(&args.build_args, &cargo_config, flasher.chip())
|
||||
.wrap_err("Failed to build project")?;
|
||||
|
||||
// Print the board information once the project has successfully built. We do
|
||||
@@ -393,12 +388,11 @@ fn build(
|
||||
Ok(build_ctx)
|
||||
}
|
||||
|
||||
fn save_image(
|
||||
args: SaveImageArgs,
|
||||
cargo_config: &CargoConfig,
|
||||
metadata: &CargoEspFlashMeta,
|
||||
) -> Result<()> {
|
||||
let build_ctx = build(&args.build_args, cargo_config, args.save_image_args.chip)?;
|
||||
fn save_image(args: SaveImageArgs) -> 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()?;
|
||||
|
||||
let bootloader = args
|
||||
|
||||
@@ -1,65 +1,105 @@
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
fs::read_to_string,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
use std::{ffi::OsStr, path::PathBuf, str::FromStr};
|
||||
|
||||
use cargo_toml::Manifest;
|
||||
use cargo::{
|
||||
core::{Package, Workspace},
|
||||
util::Config,
|
||||
};
|
||||
use espflash::image_format::ImageFormatKind;
|
||||
use miette::{IntoDiagnostic, Result, WrapErr};
|
||||
use miette::{IntoDiagnostic, Result};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::error::{Error, TomlError};
|
||||
use crate::error::Error;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Default)]
|
||||
pub struct CargoEspFlashMeta {
|
||||
pub partition_table: Option<PathBuf>,
|
||||
#[derive(Debug, Default, Clone, Deserialize)]
|
||||
pub struct PackageMetadata {
|
||||
pub workspace_root: PathBuf,
|
||||
pub package_root: PathBuf,
|
||||
pub bootloader: Option<PathBuf>,
|
||||
pub format: Option<ImageFormatKind>,
|
||||
pub partition_table: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Deserialize)]
|
||||
pub struct Meta {
|
||||
pub espflash: Option<CargoEspFlashMeta>,
|
||||
}
|
||||
|
||||
impl CargoEspFlashMeta {
|
||||
pub fn load<P>(manifest: P) -> Result<CargoEspFlashMeta>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let manifest = manifest.as_ref();
|
||||
if !manifest.exists() {
|
||||
impl PackageMetadata {
|
||||
pub fn load(package_name: &Option<String>) -> Result<PackageMetadata> {
|
||||
// There MUST be a cargo manifest in the executing directory, regardless of
|
||||
// whether or not we are in a workspace.
|
||||
let manifest_path = PathBuf::from("Cargo.toml");
|
||||
if !manifest_path.exists() {
|
||||
return Err(Error::NoProject.into());
|
||||
}
|
||||
|
||||
let toml = read_to_string(manifest)
|
||||
.into_diagnostic()
|
||||
.wrap_err("Failed to read Cargo.toml")?;
|
||||
let manifest_path = manifest_path.canonicalize().into_diagnostic()?;
|
||||
let config = Config::default().map_err(|_| Error::InvalidWorkspace)?;
|
||||
|
||||
let manifest = Manifest::<Meta>::from_slice_with_metadata(toml.as_bytes())
|
||||
.map_err(move |e| TomlError::new(e, toml))
|
||||
.wrap_err("Failed to parse Cargo.toml")?;
|
||||
let workspace =
|
||||
Workspace::new(&manifest_path, &config).map_err(|_| Error::InvalidWorkspace)?;
|
||||
|
||||
let meta = manifest
|
||||
.package
|
||||
.and_then(|pkg| pkg.metadata)
|
||||
.unwrap_or_default()
|
||||
.espflash
|
||||
.unwrap_or_default();
|
||||
let package = Self::load_package(&workspace, package_name)?;
|
||||
let metadata = Self::load_metadata(&workspace, &package)?;
|
||||
|
||||
if let Some(table) = &meta.partition_table {
|
||||
if let Some(table) = &metadata.partition_table {
|
||||
if table.extension() != Some(OsStr::new("csv")) {
|
||||
return Err(Error::InvalidPartitionTablePath.into());
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(bootloader) = &meta.bootloader {
|
||||
if let Some(bootloader) = &metadata.bootloader {
|
||||
if bootloader.extension() != Some(OsStr::new("bin")) {
|
||||
return Err(Error::InvalidBootloaderPath.into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(meta)
|
||||
Ok(metadata)
|
||||
}
|
||||
|
||||
fn load_package(workspace: &Workspace, package_name: &Option<String>) -> Result<Package> {
|
||||
// If we are currently in a package (ie. *not* in a workspace) then we can just
|
||||
// use this package; otherwise we must try to find a package with the correct
|
||||
// name within the current workspace.
|
||||
let maybe_package = if let Ok(package) = workspace.current() {
|
||||
Some(package)
|
||||
} else {
|
||||
workspace
|
||||
.members()
|
||||
.find(|pkg| Some(pkg.name().to_string()) == *package_name)
|
||||
};
|
||||
|
||||
match maybe_package {
|
||||
Some(package) => Ok(package.to_owned()),
|
||||
None => Err(Error::NoPackage.into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn load_metadata(workspace: &Workspace, package: &Package) -> Result<PackageMetadata> {
|
||||
let mut espflash_meta = PackageMetadata {
|
||||
workspace_root: workspace.root_manifest().parent().unwrap().to_path_buf(),
|
||||
package_root: package.root().to_path_buf(),
|
||||
|
||||
..PackageMetadata::default()
|
||||
};
|
||||
|
||||
match package.manifest().custom_metadata() {
|
||||
Some(meta) if meta.is_table() => match meta.as_table().unwrap().get("espflash") {
|
||||
Some(meta) if meta.is_table() => {
|
||||
let meta = meta.as_table().unwrap();
|
||||
|
||||
espflash_meta.bootloader = meta
|
||||
.get("bootloader")
|
||||
.map(|bl| package.root().join(bl.as_str().unwrap()));
|
||||
|
||||
espflash_meta.format = meta
|
||||
.get("format")
|
||||
.map(|fmt| ImageFormatKind::from_str(fmt.as_str().unwrap()).unwrap());
|
||||
|
||||
espflash_meta.partition_table = meta
|
||||
.get("partition_table")
|
||||
.map(|pt| package.root().join(pt.as_str().unwrap()));
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok(espflash_meta)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user