Layer 3: Templates as pure data, weather template, .anvilignore refresh system
Templates are now composed declaratively via template.toml -- no Rust code changes needed to add new templates. The weather station is the first composed template, demonstrating the full pattern. Template engine: - Composed templates declare base, required libraries, and per-board pins - Overlay mechanism replaces base files (app, sketch, tests) cleanly - Generic orchestration: extract base, apply overlay, install libs, assign pins - Template name tracked in .anvil.toml for refresh awareness Weather template (--template weather): - WeatherApp with 2-second polling, C/F conversion, serial output - TMP36 driver: TempSensor interface, Tmp36 impl, Tmp36Mock, Tmp36Sim - Managed example tests in test_weather.cpp (unit + system) - Minimal student starters in test_unit.cpp and test_system.cpp - Per-board pin defaults (A0 for uno, A0 for mega, A0 for nano) .anvilignore system: - Glob pattern matching (*, ?) with comments and backslash normalization - Default patterns protect student tests, app code, sketch, config - anvil refresh --force respects .anvilignore - anvil refresh --force --file <path> overrides ignore for one file - anvil refresh --ignore/--unignore manages patterns from CLI - Missing managed files always recreated even without --force - .anvilignore itself is in NEVER_REFRESH (cannot be overwritten) Refresh rewrite: - Discovers all template-produced files dynamically (no hardcoded list) - Extracts fresh template + libraries into temp dir for byte comparison - Config template field drives which files are managed - Separated missing-file creation from changed-file updates 428 tests passing on Windows MSVC, 0 warnings.
This commit is contained in:
@@ -5,6 +5,41 @@ use std::path::PathBuf;
|
||||
use crate::library;
|
||||
use crate::project::config::ProjectConfig;
|
||||
|
||||
/// Install a library quietly -- extract files and update config only.
|
||||
/// Used by the template orchestrator during `anvil new`.
|
||||
/// Returns the list of written file paths relative to project root.
|
||||
pub fn install_library(
|
||||
name: &str,
|
||||
project_dir: &std::path::Path,
|
||||
) -> Result<Vec<String>> {
|
||||
let meta = library::find_library(name).ok_or_else(|| {
|
||||
anyhow::anyhow!("Unknown library: '{}'", name)
|
||||
})?;
|
||||
|
||||
let project_root = ProjectConfig::find_project_root(project_dir)?;
|
||||
let mut config = ProjectConfig::load(&project_root)?;
|
||||
|
||||
// Skip if already installed
|
||||
if config.libraries.contains_key(name) {
|
||||
return Ok(vec![]);
|
||||
}
|
||||
|
||||
// Extract files
|
||||
let written = library::extract_library(name, &project_root)?;
|
||||
|
||||
// Add to include_dirs if not present
|
||||
let driver_include = format!("lib/drivers/{}", name);
|
||||
if !config.build.include_dirs.contains(&driver_include) {
|
||||
config.build.include_dirs.push(driver_include);
|
||||
}
|
||||
|
||||
// Track in config
|
||||
config.libraries.insert(name.to_string(), meta.version.clone());
|
||||
config.save(&project_root)?;
|
||||
|
||||
Ok(written)
|
||||
}
|
||||
|
||||
/// Add a library to the current project.
|
||||
pub fn add_library(name: &str, pin: Option<&str>, project_dir: Option<&str>) -> Result<()> {
|
||||
let meta = library::find_library(name)
|
||||
|
||||
@@ -2,6 +2,7 @@ use anyhow::{Result, bail};
|
||||
use colored::*;
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::board::pinmap;
|
||||
use crate::board::presets::{self, BoardPreset};
|
||||
use crate::templates::{TemplateManager, TemplateContext};
|
||||
use crate::version::ANVIL_VERSION;
|
||||
@@ -18,12 +19,43 @@ pub fn list_templates() -> Result<()> {
|
||||
marker.bright_cyan()
|
||||
);
|
||||
println!(" {}", info.description);
|
||||
|
||||
if !info.libraries.is_empty() {
|
||||
// Show what drivers are included and their wiring needs
|
||||
let mut wiring_parts: Vec<String> = Vec::new();
|
||||
for lib_name in &info.libraries {
|
||||
if let Some(meta) = crate::library::find_library(lib_name) {
|
||||
wiring_parts.push(format!(
|
||||
"{} ({})",
|
||||
meta.name, meta.wiring_summary()
|
||||
));
|
||||
}
|
||||
}
|
||||
println!(
|
||||
" Includes: {}",
|
||||
wiring_parts.join(", ").bright_yellow()
|
||||
);
|
||||
}
|
||||
|
||||
// Show the create command for non-default templates
|
||||
if !info.is_default {
|
||||
println!(
|
||||
" Create: {}",
|
||||
format!(
|
||||
"anvil new <project-name> --template {}",
|
||||
info.name
|
||||
)
|
||||
.bright_cyan()
|
||||
);
|
||||
}
|
||||
println!();
|
||||
}
|
||||
|
||||
println!("{}", "Usage:".bright_yellow().bold());
|
||||
println!(" anvil new <project-name>");
|
||||
println!(" anvil new <project-name> --template basic");
|
||||
println!(
|
||||
" anvil new <project-name> --template <name> --board <board>"
|
||||
);
|
||||
println!();
|
||||
|
||||
Ok(())
|
||||
@@ -58,12 +90,10 @@ pub fn list_boards() -> Result<()> {
|
||||
println!();
|
||||
println!(
|
||||
" {}",
|
||||
"For boards not listed here, create a project and then:".bright_black()
|
||||
);
|
||||
println!(
|
||||
" {}",
|
||||
" anvil board --listall".bright_black()
|
||||
"For boards not listed here, create a project and then:"
|
||||
.bright_black()
|
||||
);
|
||||
println!(" {}", " anvil board --listall".bright_black());
|
||||
println!();
|
||||
|
||||
Ok(())
|
||||
@@ -90,7 +120,9 @@ pub fn create_project(
|
||||
if !TemplateManager::template_exists(template_name) {
|
||||
println!(
|
||||
"{}",
|
||||
format!("Template '{}' not found.", template_name).red().bold()
|
||||
format!("Template '{}' not found.", template_name)
|
||||
.red()
|
||||
.bold()
|
||||
);
|
||||
println!();
|
||||
list_templates()?;
|
||||
@@ -99,40 +131,47 @@ pub fn create_project(
|
||||
|
||||
// Resolve board preset
|
||||
let preset: &BoardPreset = match board {
|
||||
Some(b) => {
|
||||
match presets::find_preset(b) {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
println!(
|
||||
"{}",
|
||||
format!("Unknown board preset: '{}'", b).red().bold()
|
||||
);
|
||||
println!();
|
||||
list_boards()?;
|
||||
bail!("Invalid board preset");
|
||||
}
|
||||
Some(b) => match presets::find_preset(b) {
|
||||
Some(p) => p,
|
||||
None => {
|
||||
println!(
|
||||
"{}",
|
||||
format!("Unknown board preset: '{}'", b).red().bold()
|
||||
);
|
||||
println!();
|
||||
list_boards()?;
|
||||
bail!("Invalid board preset");
|
||||
}
|
||||
}
|
||||
},
|
||||
None => presets::find_preset(presets::DEFAULT_PRESET).unwrap(),
|
||||
};
|
||||
|
||||
// Check board compatibility for composed templates (warn, never block)
|
||||
if let Some(meta) = TemplateManager::composed_meta(template_name) {
|
||||
check_board_compatibility(preset, &meta.board_capabilities);
|
||||
}
|
||||
|
||||
println!(
|
||||
"{}",
|
||||
format!("Creating Arduino project: {}", name)
|
||||
.bright_green()
|
||||
.bold()
|
||||
);
|
||||
println!("{}", format!("Template: {}", template_name).bright_cyan());
|
||||
println!(
|
||||
"{}",
|
||||
format!("Board: {} ({})", preset.name, preset.description).bright_cyan()
|
||||
format!("Template: {}", template_name).bright_cyan()
|
||||
);
|
||||
println!(
|
||||
"{}",
|
||||
format!("Board: {} ({})", preset.name, preset.description)
|
||||
.bright_cyan()
|
||||
);
|
||||
println!();
|
||||
|
||||
// Create project directory
|
||||
std::fs::create_dir_all(&project_path)?;
|
||||
|
||||
// Extract template
|
||||
// Extract template (basic scaffold + overlay for composed templates)
|
||||
println!("{}", "Extracting template files...".bright_yellow());
|
||||
let context = TemplateContext {
|
||||
project_name: name.to_string(),
|
||||
@@ -142,9 +181,85 @@ pub fn create_project(
|
||||
baud: preset.baud,
|
||||
};
|
||||
|
||||
let file_count = TemplateManager::extract(template_name, &project_path, &context)?;
|
||||
let file_count =
|
||||
TemplateManager::extract(template_name, &project_path, &context)?;
|
||||
println!("{} Extracted {} files", "ok".green(), file_count);
|
||||
|
||||
// Record which template was used (for refresh to know what to manage)
|
||||
{
|
||||
let mut config =
|
||||
crate::project::config::ProjectConfig::load(&project_path)?;
|
||||
config.project.template = template_name.to_string();
|
||||
config.save(&project_path)?;
|
||||
}
|
||||
|
||||
// Generate .anvilignore with sensible defaults
|
||||
crate::ignore::generate_default(&project_path, template_name)?;
|
||||
|
||||
// For composed templates: install libraries and assign pins
|
||||
if let Some(meta) = TemplateManager::composed_meta(template_name) {
|
||||
// Install required libraries
|
||||
for lib_name in &meta.libraries {
|
||||
println!();
|
||||
println!(
|
||||
"{}",
|
||||
format!("Installing library: {}...", lib_name)
|
||||
.bright_yellow()
|
||||
);
|
||||
let written =
|
||||
crate::commands::lib::install_library(lib_name, &project_path)?;
|
||||
for f in &written {
|
||||
println!(" {} {}", "+".bright_green(), f.bright_white());
|
||||
}
|
||||
println!("{} {} installed", "ok".green(), lib_name);
|
||||
}
|
||||
|
||||
// Assign default pins for this board
|
||||
let pin_defaults = meta.pins_for_board(preset.name);
|
||||
if !pin_defaults.is_empty() {
|
||||
println!();
|
||||
println!(
|
||||
"{}",
|
||||
"Assigning default pin mappings...".bright_yellow()
|
||||
);
|
||||
for pin_def in &pin_defaults {
|
||||
match crate::commands::pin::install_pin_assignment(
|
||||
&pin_def.name,
|
||||
&pin_def.pin,
|
||||
&pin_def.mode,
|
||||
preset.name,
|
||||
&project_path,
|
||||
) {
|
||||
Ok(()) => {
|
||||
println!(
|
||||
" {} {} -> {} [{}]",
|
||||
"ok".green(),
|
||||
pin_def.name.bold(),
|
||||
pin_def.pin,
|
||||
pin_def.mode
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
println!(
|
||||
" {} {}: {}",
|
||||
"!!".red(),
|
||||
pin_def.name,
|
||||
e
|
||||
);
|
||||
println!(
|
||||
" Assign manually: {}",
|
||||
format!(
|
||||
"anvil pin --assign {} {} --mode {}",
|
||||
pin_def.name, pin_def.pin, pin_def.mode
|
||||
)
|
||||
.bright_cyan()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Make shell scripts executable on Unix
|
||||
#[cfg(unix)]
|
||||
{
|
||||
@@ -174,11 +289,52 @@ pub fn create_project(
|
||||
);
|
||||
println!();
|
||||
|
||||
print_next_steps(name);
|
||||
print_next_steps(name, template_name);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Check if a board supports the required capabilities and warn if not.
|
||||
fn check_board_compatibility(
|
||||
preset: &BoardPreset,
|
||||
required: &[String],
|
||||
) {
|
||||
if required.is_empty() {
|
||||
return;
|
||||
}
|
||||
let pm = pinmap::find_pinmap(preset.name);
|
||||
if pm.is_none() {
|
||||
// No pinmap data for this board -- can't check
|
||||
return;
|
||||
}
|
||||
let pm = pm.unwrap();
|
||||
let caps = pinmap::board_capabilities(pm);
|
||||
for req in required {
|
||||
if !caps.contains(&req.as_str()) {
|
||||
println!(
|
||||
"{}",
|
||||
format!(
|
||||
"note: This template needs '{}' pins, which {} \
|
||||
may not support.",
|
||||
req, preset.name
|
||||
)
|
||||
.bright_yellow()
|
||||
);
|
||||
println!(
|
||||
" {}",
|
||||
"Your tests will still work (mocks don't need hardware)."
|
||||
.bright_black()
|
||||
);
|
||||
println!(
|
||||
" {}",
|
||||
"You may need to adjust pin assignments for your board."
|
||||
.bright_black()
|
||||
);
|
||||
println!();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn validate_project_name(name: &str) -> Result<()> {
|
||||
if name.is_empty() {
|
||||
bail!("Project name cannot be empty");
|
||||
@@ -266,67 +422,103 @@ fn make_executable(project_dir: &PathBuf) {
|
||||
}
|
||||
}
|
||||
|
||||
fn print_next_steps(project_name: &str) {
|
||||
fn print_next_steps(project_name: &str, template_name: &str) {
|
||||
println!("{}", "Next steps:".bright_yellow().bold());
|
||||
println!(
|
||||
" 1. {}",
|
||||
format!("cd {}", project_name).bright_cyan()
|
||||
);
|
||||
|
||||
if cfg!(target_os = "windows") {
|
||||
// For composed templates, wiring is step 2
|
||||
if template_name != "basic" {
|
||||
println!(
|
||||
" 2. Compile: {}",
|
||||
"build.bat".bright_cyan()
|
||||
);
|
||||
println!(
|
||||
" 3. Upload to board: {}",
|
||||
"upload.bat".bright_cyan()
|
||||
);
|
||||
println!(
|
||||
" 4. Upload + monitor: {}",
|
||||
"upload.bat --monitor".bright_cyan()
|
||||
);
|
||||
println!(
|
||||
" 5. Serial monitor: {}",
|
||||
"monitor.bat".bright_cyan()
|
||||
);
|
||||
println!(
|
||||
" 6. Run host tests: {}",
|
||||
"test\\run_tests.bat".bright_cyan()
|
||||
);
|
||||
println!();
|
||||
println!(
|
||||
" {}",
|
||||
"On Linux/macOS: ./build.sh, ./upload.sh, ./monitor.sh"
|
||||
.bright_black()
|
||||
" 2. Review wiring: {}",
|
||||
"anvil pin --audit".bright_cyan()
|
||||
);
|
||||
if cfg!(target_os = "windows") {
|
||||
println!(
|
||||
" 3. Run host tests: {}",
|
||||
"test\\run_tests.bat --clean".bright_cyan()
|
||||
);
|
||||
println!(
|
||||
" 4. Compile: {}",
|
||||
"build.bat".bright_cyan()
|
||||
);
|
||||
println!(
|
||||
" 5. Upload: {}",
|
||||
"upload.bat --monitor".bright_cyan()
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
" 3. Run host tests: {}",
|
||||
"./test/run_tests.sh --clean".bright_cyan()
|
||||
);
|
||||
println!(
|
||||
" 4. Compile: {}",
|
||||
"./build.sh".bright_cyan()
|
||||
);
|
||||
println!(
|
||||
" 5. Upload: {}",
|
||||
"./upload.sh --monitor".bright_cyan()
|
||||
);
|
||||
}
|
||||
} else {
|
||||
println!(
|
||||
" 2. Compile: {}",
|
||||
"./build.sh".bright_cyan()
|
||||
);
|
||||
println!(
|
||||
" 3. Upload to board: {}",
|
||||
"./upload.sh".bright_cyan()
|
||||
);
|
||||
println!(
|
||||
" 4. Upload + monitor: {}",
|
||||
"./upload.sh --monitor".bright_cyan()
|
||||
);
|
||||
println!(
|
||||
" 5. Serial monitor: {}",
|
||||
"./monitor.sh".bright_cyan()
|
||||
);
|
||||
println!(
|
||||
" 6. Run host tests: {}",
|
||||
"./test/run_tests.sh".bright_cyan()
|
||||
);
|
||||
println!();
|
||||
println!(
|
||||
" {}",
|
||||
"On Windows: build.bat, upload.bat, monitor.bat, test\\run_tests.bat"
|
||||
.bright_black()
|
||||
);
|
||||
if cfg!(target_os = "windows") {
|
||||
println!(
|
||||
" 2. Compile: {}",
|
||||
"build.bat".bright_cyan()
|
||||
);
|
||||
println!(
|
||||
" 3. Upload to board: {}",
|
||||
"upload.bat".bright_cyan()
|
||||
);
|
||||
println!(
|
||||
" 4. Upload + monitor: {}",
|
||||
"upload.bat --monitor".bright_cyan()
|
||||
);
|
||||
println!(
|
||||
" 5. Serial monitor: {}",
|
||||
"monitor.bat".bright_cyan()
|
||||
);
|
||||
println!(
|
||||
" 6. Run host tests: {}",
|
||||
"test\\run_tests.bat".bright_cyan()
|
||||
);
|
||||
println!();
|
||||
println!(
|
||||
" {}",
|
||||
"On Linux/macOS: ./build.sh, ./upload.sh, ./monitor.sh"
|
||||
.bright_black()
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
" 2. Compile: {}",
|
||||
"./build.sh".bright_cyan()
|
||||
);
|
||||
println!(
|
||||
" 3. Upload to board: {}",
|
||||
"./upload.sh".bright_cyan()
|
||||
);
|
||||
println!(
|
||||
" 4. Upload + monitor: {}",
|
||||
"./upload.sh --monitor".bright_cyan()
|
||||
);
|
||||
println!(
|
||||
" 5. Serial monitor: {}",
|
||||
"./monitor.sh".bright_cyan()
|
||||
);
|
||||
println!(
|
||||
" 6. Run host tests: {}",
|
||||
"./test/run_tests.sh".bright_cyan()
|
||||
);
|
||||
println!();
|
||||
println!(
|
||||
" {}",
|
||||
"On Windows: build.bat, upload.bat, monitor.bat, \
|
||||
test\\run_tests.bat"
|
||||
.bright_black()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
println!(
|
||||
|
||||
@@ -237,6 +237,43 @@ pub fn assign_pin(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Assign a pin quietly -- validate, write config, no printing.
|
||||
/// Used by the template orchestrator during `anvil new`.
|
||||
pub fn install_pin_assignment(
|
||||
name: &str,
|
||||
pin_str: &str,
|
||||
mode: &str,
|
||||
board_name: &str,
|
||||
project_dir: &std::path::Path,
|
||||
) -> Result<()> {
|
||||
let project_root = ProjectConfig::find_project_root(project_dir)?;
|
||||
let pinmap = require_pinmap(board_name)?;
|
||||
|
||||
let pin_num = pinmap::resolve_alias(pinmap, pin_str).ok_or_else(|| {
|
||||
anyhow::anyhow!(
|
||||
"Pin '{}' not found on board '{}'",
|
||||
pin_str,
|
||||
board_name
|
||||
)
|
||||
})?;
|
||||
|
||||
if !ALL_MODES.contains(&mode) {
|
||||
bail!("Unknown pin mode: '{}'", mode);
|
||||
}
|
||||
|
||||
let pin_info = pinmap::get_pin(pinmap, pin_num).unwrap();
|
||||
validate_mode_for_pin(pin_info, mode)?;
|
||||
validate_pin_name(name)?;
|
||||
|
||||
let assignment = PinAssignment {
|
||||
pin: pin_num,
|
||||
mode: mode.to_string(),
|
||||
};
|
||||
write_pin_assignment(&project_root, board_name, name, &assignment)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// =========================================================================
|
||||
// Assign a bus group
|
||||
// =========================================================================
|
||||
|
||||
@@ -1,39 +1,46 @@
|
||||
use anyhow::{Result, Context};
|
||||
use colored::*;
|
||||
use std::path::PathBuf;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::fs;
|
||||
|
||||
use crate::ignore::{self, AnvilIgnore};
|
||||
use crate::project::config::ProjectConfig;
|
||||
use crate::templates::{TemplateManager, TemplateContext};
|
||||
use crate::version::ANVIL_VERSION;
|
||||
|
||||
/// Files that anvil owns and can safely refresh.
|
||||
/// These are build/deploy infrastructure -- not user source code.
|
||||
const REFRESHABLE_FILES: &[&str] = &[
|
||||
"build.sh",
|
||||
"build.bat",
|
||||
"upload.sh",
|
||||
"upload.bat",
|
||||
"monitor.sh",
|
||||
"monitor.bat",
|
||||
"test.sh",
|
||||
"test.bat",
|
||||
"_detect_port.ps1",
|
||||
"_monitor_filter.ps1",
|
||||
"test/run_tests.sh",
|
||||
"test/run_tests.bat",
|
||||
"test/CMakeLists.txt",
|
||||
"test/mocks/mock_arduino.h",
|
||||
"test/mocks/mock_arduino.cpp",
|
||||
"test/mocks/Arduino.h",
|
||||
"test/mocks/Wire.h",
|
||||
"test/mocks/SPI.h",
|
||||
"test/mocks/mock_hal.h",
|
||||
"test/mocks/sim_hal.h",
|
||||
/// Files that are never refreshable, even if they appear in the template.
|
||||
/// .anvilignore itself is the user's protection config -- never overwrite it.
|
||||
const NEVER_REFRESH: &[&str] = &[
|
||||
".anvilignore",
|
||||
];
|
||||
|
||||
pub fn run_refresh(project_dir: Option<&str>, force: bool) -> Result<()> {
|
||||
// Resolve project directory
|
||||
/// Recursively walk a directory and return all file paths relative to `base`.
|
||||
/// Uses forward-slash separators regardless of platform.
|
||||
fn discover_files(base: &Path, dir: &Path) -> Vec<String> {
|
||||
let mut files = Vec::new();
|
||||
let entries = match fs::read_dir(dir) {
|
||||
Ok(e) => e,
|
||||
Err(_) => return files,
|
||||
};
|
||||
for entry in entries.flatten() {
|
||||
let path = entry.path();
|
||||
if path.is_dir() {
|
||||
files.extend(discover_files(base, &path));
|
||||
} else if let Ok(rel) = path.strip_prefix(base) {
|
||||
let rel_str = rel.to_string_lossy().replace('\\', "/");
|
||||
files.push(rel_str);
|
||||
}
|
||||
}
|
||||
files.sort();
|
||||
files
|
||||
}
|
||||
|
||||
/// Main refresh entry point.
|
||||
pub fn run_refresh(
|
||||
project_dir: Option<&str>,
|
||||
force: bool,
|
||||
file_override: Option<&str>,
|
||||
) -> Result<()> {
|
||||
let project_path = match project_dir {
|
||||
Some(dir) => PathBuf::from(dir),
|
||||
None => std::env::current_dir()
|
||||
@@ -42,61 +49,101 @@ pub fn run_refresh(project_dir: Option<&str>, force: bool) -> Result<()> {
|
||||
|
||||
let project_root = ProjectConfig::find_project_root(&project_path)?;
|
||||
let config = ProjectConfig::load(&project_root)?;
|
||||
let anvilignore = AnvilIgnore::load(&project_root)?;
|
||||
|
||||
let template_name = &config.project.template;
|
||||
|
||||
println!(
|
||||
"Refreshing project: {}",
|
||||
config.project.name.bright_white().bold()
|
||||
);
|
||||
println!(
|
||||
"Template: {}",
|
||||
template_name.bright_cyan()
|
||||
);
|
||||
println!(
|
||||
"Project directory: {}",
|
||||
project_root.display().to_string().bright_black()
|
||||
);
|
||||
println!();
|
||||
|
||||
// Generate fresh copies of all refreshable files from the template
|
||||
let template_name = "basic";
|
||||
// Build template context for extraction
|
||||
let context = TemplateContext {
|
||||
project_name: config.project.name.clone(),
|
||||
anvil_version: ANVIL_VERSION.to_string(),
|
||||
board_name: config.build.default.clone(),
|
||||
fqbn: config.default_fqbn().unwrap_or_else(|_| "arduino:avr:uno".to_string()),
|
||||
fqbn: config
|
||||
.default_fqbn()
|
||||
.unwrap_or_else(|_| "arduino:avr:uno".to_string()),
|
||||
baud: config.monitor.baud,
|
||||
};
|
||||
|
||||
// Extract template into a temp directory so we can compare
|
||||
let temp_dir = tempfile::tempdir()
|
||||
.context("Failed to create temp directory")?;
|
||||
// Extract fresh template + libraries into temp directory
|
||||
let temp_dir =
|
||||
tempfile::tempdir().context("Failed to create temp directory")?;
|
||||
TemplateManager::extract(template_name, temp_dir.path(), &context)?;
|
||||
|
||||
// Compare each refreshable file
|
||||
let mut up_to_date = Vec::new();
|
||||
let mut will_create = Vec::new();
|
||||
let mut has_changes = Vec::new();
|
||||
// Also extract library files into temp (for composed templates)
|
||||
for lib_name in config.libraries.keys() {
|
||||
let _ = crate::library::extract_library(lib_name, temp_dir.path());
|
||||
}
|
||||
|
||||
// Discover ALL files the template produces
|
||||
let all_template_files = discover_files(temp_dir.path(), temp_dir.path());
|
||||
|
||||
// Normalize the --file override
|
||||
let file_override_norm = file_override.map(|f| f.replace('\\', "/"));
|
||||
|
||||
// Compare each template-produced file
|
||||
let mut up_to_date = Vec::new();
|
||||
let mut will_create: Vec<String> = Vec::new();
|
||||
let mut has_changes: Vec<String> = Vec::new();
|
||||
let mut ignored: Vec<(String, String)> = Vec::new(); // (file, pattern)
|
||||
|
||||
for filename in &all_template_files {
|
||||
// Skip files that are never refreshable
|
||||
if NEVER_REFRESH.iter().any(|&nr| nr == filename) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for &filename in REFRESHABLE_FILES {
|
||||
let existing = project_root.join(filename);
|
||||
let fresh = temp_dir.path().join(filename);
|
||||
|
||||
if !fresh.exists() {
|
||||
// Template doesn't produce this file (shouldn't happen)
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check .anvilignore (unless this file is the --file override)
|
||||
let is_override = file_override_norm
|
||||
.as_ref()
|
||||
.map(|f| f == filename)
|
||||
.unwrap_or(false);
|
||||
|
||||
if !is_override {
|
||||
if let Some(pattern) = anvilignore.matching_pattern(filename) {
|
||||
ignored.push((
|
||||
filename.clone(),
|
||||
pattern.to_string(),
|
||||
));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
let fresh_content = fs::read(&fresh)
|
||||
.context(format!("Failed to read template file: {}", filename))?;
|
||||
.context(format!("Failed to read template: {}", filename))?;
|
||||
|
||||
if !existing.exists() {
|
||||
will_create.push(filename);
|
||||
will_create.push(filename.clone());
|
||||
continue;
|
||||
}
|
||||
|
||||
let existing_content = fs::read(&existing)
|
||||
.context(format!("Failed to read project file: {}", filename))?;
|
||||
.context(format!("Failed to read project: {}", filename))?;
|
||||
|
||||
if existing_content == fresh_content {
|
||||
up_to_date.push(filename);
|
||||
up_to_date.push(filename.clone());
|
||||
} else {
|
||||
has_changes.push(filename);
|
||||
has_changes.push(filename.clone());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -111,7 +158,11 @@ pub fn run_refresh(project_dir: Option<&str>, force: bool) -> Result<()> {
|
||||
|
||||
if !will_create.is_empty() {
|
||||
for f in &will_create {
|
||||
println!(" {} {} (new)", "+".bright_green(), f.bright_white());
|
||||
println!(
|
||||
" {} {} (new)",
|
||||
"+".bright_green(),
|
||||
f.bright_white()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -125,22 +176,90 @@ pub fn run_refresh(project_dir: Option<&str>, force: bool) -> Result<()> {
|
||||
}
|
||||
}
|
||||
|
||||
// Report ignored files -- always, so the user knows
|
||||
if !ignored.is_empty() {
|
||||
println!();
|
||||
println!(
|
||||
"{} {} file(s) protected by .anvilignore:",
|
||||
"skip".bright_black(),
|
||||
ignored.len()
|
||||
);
|
||||
for (file, pattern) in &ignored {
|
||||
if file == pattern {
|
||||
println!(
|
||||
" {} {} (ignored)",
|
||||
"-".bright_black(),
|
||||
file.bright_black()
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
" {} {} (matches: {})",
|
||||
"-".bright_black(),
|
||||
file.bright_black(),
|
||||
pattern.bright_black()
|
||||
);
|
||||
}
|
||||
}
|
||||
if file_override.is_none() {
|
||||
println!(
|
||||
" {}",
|
||||
"Override one: anvil refresh --force --file <path>"
|
||||
.bright_black()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Decide what to do
|
||||
if has_changes.is_empty() && will_create.is_empty() {
|
||||
println!();
|
||||
println!(
|
||||
"{}",
|
||||
"All scripts are up to date. Nothing to do."
|
||||
"All managed files are up to date. Nothing to do."
|
||||
.bright_green()
|
||||
.bold()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Always create missing files (they are not conflicting, just absent)
|
||||
if !will_create.is_empty() {
|
||||
for filename in &will_create {
|
||||
let fresh = temp_dir.path().join(filename);
|
||||
let dest = project_root.join(filename);
|
||||
if let Some(parent) = dest.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
fs::copy(&fresh, &dest)
|
||||
.context(format!("Failed to write: {}", filename))?;
|
||||
}
|
||||
|
||||
// Make new shell scripts executable on Unix
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
for filename in &will_create {
|
||||
if filename.ends_with(".sh") {
|
||||
let path = project_root.join(filename);
|
||||
if let Ok(meta) = fs::metadata(&path) {
|
||||
let mut perms = meta.permissions();
|
||||
perms.set_mode(0o755);
|
||||
let _ = fs::set_permissions(&path, perms);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!(
|
||||
"{} Created {} missing file(s).",
|
||||
"ok".green(),
|
||||
will_create.len()
|
||||
);
|
||||
}
|
||||
|
||||
if !has_changes.is_empty() && !force {
|
||||
println!();
|
||||
println!(
|
||||
"{} {} script(s) differ from the latest Anvil templates.",
|
||||
"{} {} file(s) differ from the latest Anvil templates.",
|
||||
"!".bright_yellow(),
|
||||
has_changes.len()
|
||||
);
|
||||
@@ -152,54 +271,120 @@ pub fn run_refresh(project_dir: Option<&str>, force: bool) -> Result<()> {
|
||||
println!();
|
||||
println!(
|
||||
" {}",
|
||||
"Only build scripts are replaced. Your .anvil.toml and source code are never touched."
|
||||
"Only managed files are replaced. Your source code \
|
||||
and .anvilignore files are never touched."
|
||||
.bright_black()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Apply updates
|
||||
let files_to_write: Vec<&str> = if force {
|
||||
will_create.iter().chain(has_changes.iter()).copied().collect()
|
||||
} else {
|
||||
will_create.to_vec()
|
||||
};
|
||||
|
||||
for filename in &files_to_write {
|
||||
let fresh = temp_dir.path().join(filename);
|
||||
let dest = project_root.join(filename);
|
||||
|
||||
// Ensure parent directory exists
|
||||
if let Some(parent) = dest.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
// Apply changed file updates (requires --force)
|
||||
if !has_changes.is_empty() {
|
||||
for filename in &has_changes {
|
||||
let fresh = temp_dir.path().join(filename);
|
||||
let dest = project_root.join(filename);
|
||||
if let Some(parent) = dest.parent() {
|
||||
fs::create_dir_all(parent)?;
|
||||
}
|
||||
fs::copy(&fresh, &dest)
|
||||
.context(format!("Failed to write: {}", filename))?;
|
||||
}
|
||||
|
||||
fs::copy(&fresh, &dest)
|
||||
.context(format!("Failed to write: {}", filename))?;
|
||||
// Make shell scripts executable on Unix
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
for filename in &has_changes {
|
||||
if filename.ends_with(".sh") {
|
||||
let path = project_root.join(filename);
|
||||
if let Ok(meta) = fs::metadata(&path) {
|
||||
let mut perms = meta.permissions();
|
||||
perms.set_mode(0o755);
|
||||
let _ = fs::set_permissions(&path, perms);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!(
|
||||
"{} Updated {} file(s).",
|
||||
"ok".green(),
|
||||
has_changes.len()
|
||||
);
|
||||
}
|
||||
|
||||
// Make shell scripts executable on Unix
|
||||
#[cfg(unix)]
|
||||
{
|
||||
use std::os::unix::fs::PermissionsExt;
|
||||
for filename in &files_to_write {
|
||||
if filename.ends_with(".sh") {
|
||||
let path = project_root.join(filename);
|
||||
if let Ok(meta) = fs::metadata(&path) {
|
||||
let mut perms = meta.permissions();
|
||||
perms.set_mode(0o755);
|
||||
let _ = fs::set_permissions(&path, perms);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Add a pattern to .anvilignore.
|
||||
pub fn add_ignore(project_dir: Option<&str>, pattern: &str) -> Result<()> {
|
||||
let project_path = match project_dir {
|
||||
Some(dir) => PathBuf::from(dir),
|
||||
None => std::env::current_dir()?,
|
||||
};
|
||||
let project_root = ProjectConfig::find_project_root(&project_path)?;
|
||||
|
||||
// Check if already present
|
||||
let anvilignore = AnvilIgnore::load(&project_root)?;
|
||||
let normalized = pattern.replace('\\', "/");
|
||||
if anvilignore.has_pattern(&normalized) {
|
||||
println!(
|
||||
"{} Pattern already in .anvilignore: {}",
|
||||
"ok".green(),
|
||||
normalized.bright_white()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
ignore::add_pattern(&project_root, pattern)?;
|
||||
println!(
|
||||
"{} Added to .anvilignore: {}",
|
||||
"ok".green(),
|
||||
normalized.bright_white().bold()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Remove a pattern from .anvilignore.
|
||||
pub fn remove_ignore(
|
||||
project_dir: Option<&str>,
|
||||
pattern: &str,
|
||||
) -> Result<()> {
|
||||
let project_path = match project_dir {
|
||||
Some(dir) => PathBuf::from(dir),
|
||||
None => std::env::current_dir()?,
|
||||
};
|
||||
let project_root = ProjectConfig::find_project_root(&project_path)?;
|
||||
|
||||
let removed = ignore::remove_pattern(&project_root, pattern)?;
|
||||
let normalized = pattern.replace('\\', "/");
|
||||
if removed {
|
||||
println!(
|
||||
"{} Removed from .anvilignore: {}",
|
||||
"ok".green(),
|
||||
normalized.bright_white().bold()
|
||||
);
|
||||
println!(
|
||||
" {}",
|
||||
"This file will now be updated by anvil refresh --force."
|
||||
.bright_black()
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"{} Pattern not found in .anvilignore: {}",
|
||||
"!".bright_yellow(),
|
||||
normalized.bright_white()
|
||||
);
|
||||
// Show current patterns
|
||||
let anvilignore = AnvilIgnore::load(&project_root)?;
|
||||
if !anvilignore.patterns().is_empty() {
|
||||
println!(" Current patterns:");
|
||||
for p in anvilignore.patterns() {
|
||||
println!(" {}", p.bright_black());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
println!();
|
||||
println!(
|
||||
"{} Updated {} file(s).",
|
||||
"ok".green(),
|
||||
files_to_write.len()
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user