Anvil v1.0.0 -- Arduino build tool with HAL and test scaffolding

Single-binary CLI that scaffolds testable Arduino projects, compiles,
uploads, and monitors serial output. Templates embed a hardware
abstraction layer, Google Mock infrastructure, and CMake-based host
tests so application logic can be verified without hardware.

Commands: new, doctor, setup, devices, build, upload, monitor
39 Rust tests (21 unit, 18 integration)
Cross-platform: Linux and Windows
This commit is contained in:
Eric Ratliff
2026-02-15 11:16:17 -06:00
commit 3298844399
41 changed files with 4866 additions and 0 deletions

237
src/commands/doctor.rs Normal file
View File

@@ -0,0 +1,237 @@
use anyhow::Result;
use colored::*;
use crate::board;
#[derive(Debug)]
pub struct SystemHealth {
pub arduino_cli_ok: bool,
pub arduino_cli_path: Option<String>,
pub avr_core_ok: bool,
pub avr_size_ok: bool,
pub dialout_ok: bool,
pub cmake_ok: bool,
pub cpp_compiler_ok: bool,
pub git_ok: bool,
pub ports_found: usize,
}
impl SystemHealth {
pub fn is_healthy(&self) -> bool {
self.arduino_cli_ok && self.avr_core_ok
}
}
pub fn run_diagnostics() -> Result<()> {
println!(
"{}",
"Checking system health...".bright_yellow().bold()
);
println!();
let health = check_system_health();
print_diagnostics(&health);
println!();
if health.is_healthy() {
println!(
"{}",
"System is ready for Arduino development."
.bright_green()
.bold()
);
} else {
println!(
"{}",
"Issues found. Run 'anvil setup' to fix."
.bright_yellow()
.bold()
);
}
println!();
Ok(())
}
pub fn check_system_health() -> SystemHealth {
// arduino-cli
let (arduino_cli_ok, arduino_cli_path) = match board::find_arduino_cli() {
Some(path) => (true, Some(path.display().to_string())),
None => (false, None),
};
// AVR core
let avr_core_ok = if let Some(ref path_str) = arduino_cli_path {
let path = std::path::Path::new(path_str);
board::is_avr_core_installed(path)
} else {
false
};
// avr-size (optional)
let avr_size_ok = which::which("avr-size").is_ok();
// dialout group (Linux only)
let dialout_ok = check_dialout();
// cmake (optional -- for host tests)
let cmake_ok = which::which("cmake").is_ok();
// C++ compiler (optional -- for host tests)
let cpp_compiler_ok = which::which("g++").is_ok() || which::which("clang++").is_ok();
// git
let git_ok = which::which("git").is_ok();
// Serial ports
let ports_found = board::list_ports().len();
SystemHealth {
arduino_cli_ok,
arduino_cli_path,
avr_core_ok,
avr_size_ok,
dialout_ok,
cmake_ok,
cpp_compiler_ok,
git_ok,
ports_found,
}
}
fn print_diagnostics(health: &SystemHealth) {
println!("{}", "Required:".bright_yellow().bold());
println!();
// arduino-cli
if health.arduino_cli_ok {
println!(
" {} arduino-cli {}",
"ok".green(),
health
.arduino_cli_path
.as_ref()
.unwrap_or(&String::new())
.bright_black()
);
} else {
println!(" {} arduino-cli {}", "MISSING".red(), "not found in PATH".red());
}
// AVR core
if health.avr_core_ok {
println!(" {} arduino:avr core installed", "ok".green());
} else if health.arduino_cli_ok {
println!(
" {} arduino:avr core {}",
"MISSING".red(),
"run: anvil setup".red()
);
} else {
println!(
" {} arduino:avr core {}",
"MISSING".red(),
"(needs arduino-cli first)".bright_black()
);
}
println!();
println!("{}", "Optional:".bright_yellow().bold());
println!();
// avr-size
if health.avr_size_ok {
println!(" {} avr-size (binary size reporting)", "ok".green());
} else {
println!(
" {} avr-size {}",
"--".bright_black(),
"install: sudo apt install gcc-avr".bright_black()
);
}
// dialout
#[cfg(unix)]
{
if health.dialout_ok {
println!(" {} user in dialout group", "ok".green());
} else {
println!(
" {} dialout group {}",
"WARN".yellow(),
"run: sudo usermod -aG dialout $USER".yellow()
);
}
}
// cmake
if health.cmake_ok {
println!(" {} cmake (for host-side tests)", "ok".green());
} else {
println!(
" {} cmake {}",
"--".bright_black(),
"install: sudo apt install cmake".bright_black()
);
}
// C++ compiler
if health.cpp_compiler_ok {
println!(" {} C++ compiler (g++/clang++)", "ok".green());
} else {
println!(
" {} C++ compiler {}",
"--".bright_black(),
"install: sudo apt install g++".bright_black()
);
}
// git
if health.git_ok {
println!(" {} git", "ok".green());
} else {
println!(
" {} git {}",
"--".bright_black(),
"install: sudo apt install git".bright_black()
);
}
println!();
println!("{}", "Hardware:".bright_yellow().bold());
println!();
if health.ports_found > 0 {
println!(
" {} {} serial port(s) detected",
"ok".green(),
health.ports_found
);
} else {
println!(
" {} no serial ports {}",
"--".bright_black(),
"(plug in a board to detect)".bright_black()
);
}
}
fn check_dialout() -> bool {
#[cfg(unix)]
{
let output = std::process::Command::new("groups")
.output();
match output {
Ok(out) => {
let groups = String::from_utf8_lossy(&out.stdout);
groups.contains("dialout")
}
Err(_) => false,
}
}
#[cfg(not(unix))]
{
true // Not applicable on Windows
}
}