use std::{fmt, fs, io, path::Path, path::PathBuf};
use serde::Deserialize;
#[derive(Debug)]
pub enum Error {
NotAProject { start: String },
NoFirmware { root: String },
UnknownFirmware { name: String, known: String },
Ambiguous { known: String },
NoChip { firmware: String },
Read { path: String, source: io::Error },
Parse { path: String, message: String },
}
impl fmt::Display for Error {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NotAProject { start } => write!(
formatter,
"not inside a FerroForge project: no `firmware/` directory in \
{start} or any parent"
),
Self::NoFirmware { root } => write!(
formatter,
"{root}/firmware contains no applications; `ferroforge new` \
creates one"
),
Self::UnknownFirmware { name, known } => {
write!(
formatter,
"no firmware named `{name}`. This project has: {known}"
)
}
Self::Ambiguous { known } => write!(
formatter,
"this project has several applications, so name one or run from \
inside its directory: {known}"
),
Self::NoChip { firmware } => write!(
formatter,
"{firmware} does not say which chip it is for. Add to its \
Cargo.toml:\n\n [package.metadata.ferroforge]\n \
chip = \"stm32f401re\""
),
Self::Read { path, source } => write!(formatter, "cannot read {path}: {source}"),
Self::Parse { path, message } => write!(formatter, "{path}: {message}"),
}
}
}
impl std::error::Error for Error {}
#[derive(Debug, Deserialize)]
struct Manifest {
#[serde(default)]
package: Package,
}
#[derive(Debug, Default, Deserialize)]
struct Package {
#[serde(default)]
metadata: Metadata,
}
#[derive(Debug, Default, Deserialize)]
struct Metadata {
#[serde(default)]
ferroforge: FerroForge,
}
#[derive(Debug, Default, Deserialize)]
struct FerroForge {
chip: Option<String>,
#[serde(rename = "defmt-log")]
defmt_log: Option<String>,
}
pub struct Firmware {
pub name: String,
pub path: PathBuf,
}
impl Firmware {
pub fn defmt_log(&self) -> Result<String, Error> {
Ok(self
.metadata()?
.defmt_log
.unwrap_or_else(|| "info".to_owned()))
}
pub fn chip(&self) -> Result<String, Error> {
self.metadata()?.chip.ok_or_else(|| Error::NoChip {
firmware: self.path.display().to_string(),
})
}
fn metadata(&self) -> Result<FerroForge, Error> {
let manifest = self.path.join("Cargo.toml");
let display = manifest.display().to_string();
let text = fs::read_to_string(&manifest).map_err(|source| Error::Read {
path: display.clone(),
source,
})?;
let parsed: Manifest = toml::from_str(&text).map_err(|error| Error::Parse {
path: display,
message: error.to_string(),
})?;
Ok(parsed.package.metadata.ferroforge)
}
}
pub struct Project {
pub root: PathBuf,
}
impl Project {
pub fn containing(start: &Path) -> Result<Self, Error> {
let start = start
.canonicalize()
.map(|path| plain(&path))
.unwrap_or_else(|_| start.to_path_buf());
for directory in start.ancestors() {
if directory.join("firmware").is_dir() {
return Ok(Self {
root: directory.to_path_buf(),
});
}
}
Err(Error::NotAProject {
start: start.display().to_string(),
})
}
pub fn firmwares(&self) -> Result<Vec<Firmware>, Error> {
let directory = self.root.join("firmware");
let display = directory.display().to_string();
let mut found = Vec::new();
for entry in fs::read_dir(&directory).map_err(|source| Error::Read {
path: display,
source,
})? {
let path = match entry {
Ok(entry) => entry.path(),
Err(_) => continue,
};
if !path.join("Cargo.toml").is_file() {
continue;
}
let Some(name) = path
.file_name()
.map(|name| name.to_string_lossy().into_owned())
else {
continue;
};
found.push(Firmware { name, path });
}
found.sort_by(|a, b| a.name.cmp(&b.name));
if found.is_empty() {
return Err(Error::NoFirmware {
root: self.root.display().to_string(),
});
}
Ok(found)
}
pub fn select(&self, named: Option<&str>, from: &Path) -> Result<Firmware, Error> {
let mut candidates = self.firmwares()?;
let names = || candidates_names(&self.firmwares().unwrap_or_default());
if let Some(name) = named {
let position = candidates.iter().position(|firmware| firmware.name == name);
return match position {
Some(index) => Ok(candidates.swap_remove(index)),
None => Err(Error::UnknownFirmware {
name: name.to_owned(),
known: names(),
}),
};
}
let from = from
.canonicalize()
.map(|path| plain(&path))
.unwrap_or_else(|_| from.to_path_buf());
if let Some(index) = candidates.iter().position(|firmware| {
let path = firmware
.path
.canonicalize()
.map(|path| plain(&path))
.unwrap_or_else(|_| firmware.path.clone());
from.starts_with(&path)
}) {
return Ok(candidates.swap_remove(index));
}
if candidates.len() == 1 {
return Ok(candidates.remove(0));
}
Err(Error::Ambiguous { known: names() })
}
}
fn candidates_names(firmwares: &[Firmware]) -> String {
firmwares
.iter()
.map(|firmware| firmware.name.clone())
.collect::<Vec<_>>()
.join(", ")
}
fn plain(path: &Path) -> PathBuf {
const VERBATIM: &str = r#"\\?\"#;
let text = path.display().to_string();
match text.strip_prefix(VERBATIM) {
Some(rest) => PathBuf::from(rest),
None => path.to_path_buf(),
}
}