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:
252
src/commands/new.rs
Normal file
252
src/commands/new.rs
Normal file
@@ -0,0 +1,252 @@
|
||||
use anyhow::{Result, bail};
|
||||
use colored::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::templates::{TemplateManager, TemplateContext};
|
||||
use crate::version::ANVIL_VERSION;
|
||||
|
||||
pub fn list_templates() -> Result<()> {
|
||||
println!("{}", "Available templates:".bright_cyan().bold());
|
||||
println!();
|
||||
|
||||
for info in TemplateManager::list_templates() {
|
||||
let marker = if info.is_default { " (default)" } else { "" };
|
||||
println!(
|
||||
" {}{}",
|
||||
info.name.bright_white().bold(),
|
||||
marker.bright_cyan()
|
||||
);
|
||||
println!(" {}", info.description);
|
||||
println!();
|
||||
}
|
||||
|
||||
println!("{}", "Usage:".bright_yellow().bold());
|
||||
println!(" anvil new <project-name>");
|
||||
println!(" anvil new <project-name> --template basic");
|
||||
println!();
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn create_project(name: &str, template: Option<&str>) -> Result<()> {
|
||||
// Validate project name
|
||||
validate_project_name(name)?;
|
||||
|
||||
let project_path = PathBuf::from(name);
|
||||
if project_path.exists() {
|
||||
bail!(
|
||||
"Directory already exists: {}\n\
|
||||
Choose a different name or remove the existing directory.",
|
||||
project_path.display()
|
||||
);
|
||||
}
|
||||
|
||||
let template_name = template.unwrap_or("basic");
|
||||
if !TemplateManager::template_exists(template_name) {
|
||||
println!(
|
||||
"{}",
|
||||
format!("Template '{}' not found.", template_name).red().bold()
|
||||
);
|
||||
println!();
|
||||
list_templates()?;
|
||||
bail!("Invalid template");
|
||||
}
|
||||
|
||||
println!(
|
||||
"{}",
|
||||
format!("Creating Arduino project: {}", name)
|
||||
.bright_green()
|
||||
.bold()
|
||||
);
|
||||
println!("{}", format!("Template: {}", template_name).bright_cyan());
|
||||
println!();
|
||||
|
||||
// Create project directory
|
||||
std::fs::create_dir_all(&project_path)?;
|
||||
|
||||
// Extract template
|
||||
println!("{}", "Extracting template files...".bright_yellow());
|
||||
let context = TemplateContext {
|
||||
project_name: name.to_string(),
|
||||
anvil_version: ANVIL_VERSION.to_string(),
|
||||
};
|
||||
|
||||
let file_count = TemplateManager::extract(template_name, &project_path, &context)?;
|
||||
println!("{} Extracted {} files", "ok".green(), file_count);
|
||||
|
||||
// Make shell scripts executable on Unix
|
||||
#[cfg(unix)]
|
||||
{
|
||||
make_executable(&project_path);
|
||||
}
|
||||
|
||||
// Initialize git repository
|
||||
init_git(&project_path, template_name);
|
||||
|
||||
// Print success
|
||||
println!();
|
||||
println!(
|
||||
"{}",
|
||||
"================================================================"
|
||||
.bright_green()
|
||||
);
|
||||
println!(
|
||||
"{}",
|
||||
format!(" Project created: {}", name)
|
||||
.bright_green()
|
||||
.bold()
|
||||
);
|
||||
println!(
|
||||
"{}",
|
||||
"================================================================"
|
||||
.bright_green()
|
||||
);
|
||||
println!();
|
||||
|
||||
print_next_steps(name);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_project_name(name: &str) -> Result<()> {
|
||||
if name.is_empty() {
|
||||
bail!("Project name cannot be empty");
|
||||
}
|
||||
if name.len() > 50 {
|
||||
bail!("Project name must be 50 characters or less");
|
||||
}
|
||||
if !name.chars().next().unwrap().is_alphabetic() {
|
||||
bail!("Project name must start with a letter");
|
||||
}
|
||||
let valid = name
|
||||
.chars()
|
||||
.all(|c| c.is_alphanumeric() || c == '-' || c == '_');
|
||||
if !valid {
|
||||
bail!(
|
||||
"Invalid project name '{}'\n\
|
||||
Names may contain: letters, numbers, hyphens, underscores\n\
|
||||
Must start with a letter.\n\n\
|
||||
Valid examples:\n\
|
||||
\x20 blink\n\
|
||||
\x20 my-sensor\n\
|
||||
\x20 team1234_robot",
|
||||
name
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn init_git(project_dir: &PathBuf, template_name: &str) {
|
||||
println!("{}", "Initializing git repository...".bright_yellow());
|
||||
|
||||
// Check if git is available
|
||||
if which::which("git").is_err() {
|
||||
eprintln!(
|
||||
"{} git not found, skipping repository initialization.",
|
||||
"warn".yellow()
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let run = |args: &[&str]| -> bool {
|
||||
std::process::Command::new("git")
|
||||
.args(args)
|
||||
.current_dir(project_dir)
|
||||
.output()
|
||||
.map(|o| o.status.success())
|
||||
.unwrap_or(false)
|
||||
};
|
||||
|
||||
if !run(&["init"]) {
|
||||
eprintln!("{} Failed to initialize git.", "warn".yellow());
|
||||
return;
|
||||
}
|
||||
|
||||
run(&["add", "."]);
|
||||
|
||||
let msg = format!(
|
||||
"Initial commit from anvil new --template {}",
|
||||
template_name
|
||||
);
|
||||
run(&["commit", "-m", &msg]);
|
||||
|
||||
println!("{} Git repository initialized", "ok".green());
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
fn make_executable(project_dir: &PathBuf) {
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
|
||||
let scripts = ["test/run_tests.sh"];
|
||||
for script in &scripts {
|
||||
let path = project_dir.join(script);
|
||||
if path.exists() {
|
||||
if let Ok(meta) = std::fs::metadata(&path) {
|
||||
let mut perms = meta.permissions();
|
||||
perms.set_mode(0o755);
|
||||
let _ = std::fs::set_permissions(&path, perms);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn print_next_steps(project_name: &str) {
|
||||
println!("{}", "Next steps:".bright_yellow().bold());
|
||||
println!(
|
||||
" 1. {}",
|
||||
format!("cd {}", project_name).bright_cyan()
|
||||
);
|
||||
println!(" 2. Check your system: {}", "anvil doctor".bright_cyan());
|
||||
println!(
|
||||
" 3. Find your board: {}",
|
||||
"anvil devices".bright_cyan()
|
||||
);
|
||||
println!(
|
||||
" 4. Build and upload: {}",
|
||||
format!("anvil build {}", project_name).bright_cyan()
|
||||
);
|
||||
println!(
|
||||
" 5. Build + monitor: {}",
|
||||
format!("anvil build --monitor {}", project_name).bright_cyan()
|
||||
);
|
||||
println!();
|
||||
println!(
|
||||
" Run host tests: {}",
|
||||
"cd test && ./run_tests.sh".bright_cyan()
|
||||
);
|
||||
println!();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_validate_name_valid() {
|
||||
assert!(validate_project_name("blink").is_ok());
|
||||
assert!(validate_project_name("my-sensor").is_ok());
|
||||
assert!(validate_project_name("team1234_robot").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_name_empty() {
|
||||
assert!(validate_project_name("").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_name_starts_with_number() {
|
||||
assert!(validate_project_name("123abc").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_name_special_chars() {
|
||||
assert!(validate_project_name("my project").is_err());
|
||||
assert!(validate_project_name("my.project").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_name_too_long() {
|
||||
let long_name = "a".repeat(51);
|
||||
assert!(validate_project_name(&long_name).is_err());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user