use std::path::{Path, PathBuf};
use miette::Diagnostic;
use thiserror::Error;
pub use crate::commands::source::{find_fallback_circuit_json, load_circuit_source};
use crate::commands::target::{resolve_boards, BoardTarget, TargetError};
use crate::linter::{FuseBlown, LintContext, Linter, Severity};
use crate::netlist::Netlist;
use crate::parser;
use crate::tscircuit::RunnerError;
#[derive(Debug, Error, Diagnostic)]
#[error("cannot read the Circuit JSON at \"{path}\"")]
#[diagnostic(help("check that the file exists and is readable"))]
pub struct ReadError {
pub path: String,
#[source]
pub source: std::io::Error,
}
#[derive(Debug, Error, Diagnostic)]
pub enum CheckError {
#[error(transparent)]
#[diagnostic(transparent)]
Target(#[from] TargetError),
#[error(transparent)]
#[diagnostic(transparent)]
Runner(#[from] RunnerError),
#[error(transparent)]
#[diagnostic(transparent)]
Read(#[from] ReadError),
#[error(transparent)]
#[diagnostic(transparent)]
Parse(#[from] parser::JsonDiagnostic),
#[error(transparent)]
#[diagnostic(transparent)]
FuseBlown(#[from] FuseBlown),
}
pub fn lint_source(
entry: &Path,
target_mcu: Option<String>,
) -> Result<(PathBuf, String), CheckError> {
let (json_path, source) = load_circuit_source(entry)?;
let circuit = parser::parse(&source, &json_path)?;
let netlist = Netlist::from_circuit(&circuit);
let ctx = LintContext::new(&circuit, &netlist).with_mcu(target_mcu.as_deref());
let (warnings, violations): (Vec<_>, Vec<_>) = Linter::default()
.run_with_context(&ctx)
.into_iter()
.partition(|d| d.severity == Severity::Warning);
for warning in warnings {
eprintln!("{:?}", miette::Report::new(warning));
}
if !violations.is_empty() {
return Err(CheckError::FuseBlown(FuseBlown::new(violations)));
}
println!("✅ Circuit validated (fuse intact)");
if let Some(mcu) = target_mcu {
println!(" silicon target: {mcu}");
}
Ok((json_path, source))
}
pub fn run(
file: Option<PathBuf>,
mcu: Option<String>,
pins_rs: Option<PathBuf>,
) -> Result<(), CheckError> {
run_boards(&resolve_boards(file)?, mcu, pins_rs)
}
pub fn run_boards(
boards: &[BoardTarget],
mcu: Option<String>,
pins_rs: Option<PathBuf>,
) -> Result<(), CheckError> {
let mut failure = None;
for board in boards {
if let (true, Some(name)) = (boards.len() > 1, &board.name) {
println!("\n▸ board {name}");
}
let result = check_board(board, mcu.clone(), pins_rs.clone());
failure = merge_failures(failure, result.err());
}
failure.map_or(Ok(()), Err)
}
fn check_board(
board: &BoardTarget,
mcu: Option<String>,
pins_rs: Option<PathBuf>,
) -> Result<(), CheckError> {
let target_mcu = mcu.or_else(|| board.mcu.clone());
let (json_path, source) = lint_source(&board.entry, target_mcu.clone())?;
let pins = pins_rs.map_or_else(|| board.pins_rs.clone(), |path| vec![path]);
if pins.is_empty() {
return Ok(());
}
let circuit = parser::parse(&source, &json_path)?;
let netlist = Netlist::from_circuit(&circuit);
let ctx = LintContext::new(&circuit, &netlist).with_mcu(target_mcu.as_deref());
pins.iter()
.try_for_each(|path| write_pins_rs(&ctx, path, &board.title))
}
fn merge_failures(first: Option<CheckError>, next: Option<CheckError>) -> Option<CheckError> {
match (first, next) {
(Some(CheckError::FuseBlown(mut a)), Some(CheckError::FuseBlown(b))) => {
a.violations.extend(b.violations);
Some(CheckError::FuseBlown(a))
}
(Some(CheckError::FuseBlown(_)), Some(tool_error)) => Some(tool_error),
(first, next) => first.or(next),
}
}
fn write_pins_rs(ctx: &LintContext, pins_path: &Path, title: &str) -> Result<(), CheckError> {
let Some(mcu_info) = crate::codegen::extract_mcu_pins(ctx.circuit, ctx.netlist, ctx.mcu) else {
return Ok(());
};
let code = crate::codegen::generate_pins_rs(&mcu_info, title);
if let Some(parent) = pins_path.parent() {
std::fs::create_dir_all(parent).ok();
}
std::fs::write(pins_path, code).map_err(|source| ReadError {
path: pins_path.display().to_string(),
source,
})?;
println!(
"🦀 Generated firmware pin bindings -> {}",
pins_path.display()
);
Ok(())
}