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:
226
src/project/config.rs
Normal file
226
src/project/config.rs
Normal file
@@ -0,0 +1,226 @@
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::fs;
|
||||
use anyhow::{Result, Context, bail};
|
||||
|
||||
use crate::version::ANVIL_VERSION;
|
||||
|
||||
pub const CONFIG_FILENAME: &str = ".anvil.toml";
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct ProjectConfig {
|
||||
pub project: ProjectMeta,
|
||||
pub build: BuildConfig,
|
||||
pub monitor: MonitorConfig,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct ProjectMeta {
|
||||
pub name: String,
|
||||
pub anvil_version: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct BuildConfig {
|
||||
pub fqbn: String,
|
||||
pub warnings: String,
|
||||
pub include_dirs: Vec<String>,
|
||||
pub extra_flags: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
pub struct MonitorConfig {
|
||||
pub baud: u32,
|
||||
}
|
||||
|
||||
impl ProjectConfig {
|
||||
/// Create a new project config with sensible defaults.
|
||||
pub fn new(name: &str) -> Self {
|
||||
Self {
|
||||
project: ProjectMeta {
|
||||
name: name.to_string(),
|
||||
anvil_version: ANVIL_VERSION.to_string(),
|
||||
},
|
||||
build: BuildConfig {
|
||||
fqbn: "arduino:avr:uno".to_string(),
|
||||
warnings: "more".to_string(),
|
||||
include_dirs: vec!["lib/hal".to_string(), "lib/app".to_string()],
|
||||
extra_flags: vec!["-Werror".to_string()],
|
||||
},
|
||||
monitor: MonitorConfig {
|
||||
baud: 115200,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Load config from a project directory.
|
||||
pub fn load(project_root: &Path) -> Result<Self> {
|
||||
let config_path = project_root.join(CONFIG_FILENAME);
|
||||
if !config_path.exists() {
|
||||
bail!(
|
||||
"Not an Anvil project (missing {}).\n\
|
||||
Create one with: anvil new <name>",
|
||||
CONFIG_FILENAME
|
||||
);
|
||||
}
|
||||
let contents = fs::read_to_string(&config_path)
|
||||
.context(format!("Failed to read {}", config_path.display()))?;
|
||||
let config: ProjectConfig = toml::from_str(&contents)
|
||||
.context(format!("Failed to parse {}", config_path.display()))?;
|
||||
Ok(config)
|
||||
}
|
||||
|
||||
/// Save config to a project directory.
|
||||
pub fn save(&self, project_root: &Path) -> Result<()> {
|
||||
let config_path = project_root.join(CONFIG_FILENAME);
|
||||
let contents = toml::to_string_pretty(self)
|
||||
.context("Failed to serialize config")?;
|
||||
fs::write(&config_path, contents)
|
||||
.context(format!("Failed to write {}", config_path.display()))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Walk up from a directory to find the project root containing .anvil.toml.
|
||||
pub fn find_project_root(start: &Path) -> Result<PathBuf> {
|
||||
let mut dir = if start.is_absolute() {
|
||||
start.to_path_buf()
|
||||
} else {
|
||||
std::env::current_dir()?.join(start)
|
||||
};
|
||||
|
||||
for _ in 0..10 {
|
||||
if dir.join(CONFIG_FILENAME).exists() {
|
||||
return Ok(dir);
|
||||
}
|
||||
match dir.parent() {
|
||||
Some(parent) => dir = parent.to_path_buf(),
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
|
||||
bail!(
|
||||
"No {} found in {} or any parent directory.\n\
|
||||
Create a project with: anvil new <name>",
|
||||
CONFIG_FILENAME,
|
||||
start.display()
|
||||
);
|
||||
}
|
||||
|
||||
/// Resolve include directories to absolute paths relative to project root.
|
||||
pub fn resolve_include_flags(&self, project_root: &Path) -> Vec<String> {
|
||||
let mut flags = Vec::new();
|
||||
for dir in &self.build.include_dirs {
|
||||
let abs = project_root.join(dir);
|
||||
if abs.is_dir() {
|
||||
flags.push(format!("-I{}", abs.display()));
|
||||
}
|
||||
}
|
||||
flags
|
||||
}
|
||||
|
||||
/// Build the full extra_flags string for arduino-cli.
|
||||
pub fn extra_flags_string(&self, project_root: &Path) -> String {
|
||||
let mut parts = self.resolve_include_flags(project_root);
|
||||
for flag in &self.build.extra_flags {
|
||||
parts.push(flag.clone());
|
||||
}
|
||||
parts.join(" ")
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ProjectConfig {
|
||||
fn default() -> Self {
|
||||
Self::new("untitled")
|
||||
}
|
||||
}
|
||||
|
||||
/// Return the Anvil home directory (~/.anvil).
|
||||
pub fn anvil_home() -> Result<PathBuf> {
|
||||
let home = dirs::home_dir()
|
||||
.context("Could not determine home directory")?;
|
||||
let anvil_dir = home.join(".anvil");
|
||||
fs::create_dir_all(&anvil_dir)?;
|
||||
Ok(anvil_dir)
|
||||
}
|
||||
|
||||
/// Return the build cache directory (~/.anvil/builds).
|
||||
pub fn build_cache_dir() -> Result<PathBuf> {
|
||||
let dir = anvil_home()?.join("builds");
|
||||
fs::create_dir_all(&dir)?;
|
||||
Ok(dir)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::TempDir;
|
||||
|
||||
#[test]
|
||||
fn test_new_config_defaults() {
|
||||
let config = ProjectConfig::new("test_project");
|
||||
assert_eq!(config.project.name, "test_project");
|
||||
assert_eq!(config.build.fqbn, "arduino:avr:uno");
|
||||
assert_eq!(config.monitor.baud, 115200);
|
||||
assert!(config.build.include_dirs.contains(&"lib/hal".to_string()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_save_and_load() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let config = ProjectConfig::new("roundtrip");
|
||||
config.save(tmp.path()).unwrap();
|
||||
|
||||
let loaded = ProjectConfig::load(tmp.path()).unwrap();
|
||||
assert_eq!(loaded.project.name, "roundtrip");
|
||||
assert_eq!(loaded.build.fqbn, config.build.fqbn);
|
||||
assert_eq!(loaded.monitor.baud, config.monitor.baud);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_project_root() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let config = ProjectConfig::new("finder");
|
||||
config.save(tmp.path()).unwrap();
|
||||
|
||||
// Create a subdirectory and search from there
|
||||
let sub = tmp.path().join("sketch").join("deep");
|
||||
fs::create_dir_all(&sub).unwrap();
|
||||
|
||||
let found = ProjectConfig::find_project_root(&sub).unwrap();
|
||||
assert_eq!(found, tmp.path());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_find_project_root_not_found() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
let result = ProjectConfig::find_project_root(tmp.path());
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_resolve_include_flags() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
fs::create_dir_all(tmp.path().join("lib/hal")).unwrap();
|
||||
fs::create_dir_all(tmp.path().join("lib/app")).unwrap();
|
||||
|
||||
let config = ProjectConfig::new("includes");
|
||||
let flags = config.resolve_include_flags(tmp.path());
|
||||
|
||||
assert_eq!(flags.len(), 2);
|
||||
assert!(flags[0].starts_with("-I"));
|
||||
assert!(flags[0].contains("lib"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extra_flags_string() {
|
||||
let tmp = TempDir::new().unwrap();
|
||||
fs::create_dir_all(tmp.path().join("lib/hal")).unwrap();
|
||||
fs::create_dir_all(tmp.path().join("lib/app")).unwrap();
|
||||
|
||||
let config = ProjectConfig::new("flags");
|
||||
let flags = config.extra_flags_string(tmp.path());
|
||||
|
||||
assert!(flags.contains("-Werror"));
|
||||
assert!(flags.contains("-I"));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user