Board presets: - anvil new --board mega (uno, mega, nano, nano-old, leonardo, micro) - anvil new --list-boards shows presets with compatible clones - FQBN and baud rate flow into .anvil.toml via template variables - Defaults to uno when --board is omitted Devices --clear: - anvil devices --clear deletes .anvil.local, reverts to auto-detect
193 lines
5.4 KiB
Rust
193 lines
5.4 KiB
Rust
use anyhow::{Result, Context};
|
|
use colored::*;
|
|
use std::path::PathBuf;
|
|
use std::fs;
|
|
|
|
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",
|
|
"_detect_port.ps1",
|
|
"test/run_tests.sh",
|
|
"test/run_tests.bat",
|
|
];
|
|
|
|
pub fn run_refresh(project_dir: Option<&str>, force: bool) -> Result<()> {
|
|
// Resolve project directory
|
|
let project_path = match project_dir {
|
|
Some(dir) => PathBuf::from(dir),
|
|
None => std::env::current_dir()
|
|
.context("Could not determine current directory")?,
|
|
};
|
|
|
|
let project_root = ProjectConfig::find_project_root(&project_path)?;
|
|
let config = ProjectConfig::load(&project_root)?;
|
|
|
|
println!(
|
|
"Refreshing project: {}",
|
|
config.project.name.bright_white().bold()
|
|
);
|
|
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";
|
|
let context = TemplateContext {
|
|
project_name: config.project.name.clone(),
|
|
anvil_version: ANVIL_VERSION.to_string(),
|
|
fqbn: config.build.fqbn.clone(),
|
|
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")?;
|
|
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();
|
|
|
|
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;
|
|
}
|
|
|
|
let fresh_content = fs::read(&fresh)
|
|
.context(format!("Failed to read template file: {}", filename))?;
|
|
|
|
if !existing.exists() {
|
|
will_create.push(filename);
|
|
continue;
|
|
}
|
|
|
|
let existing_content = fs::read(&existing)
|
|
.context(format!("Failed to read project file: {}", filename))?;
|
|
|
|
if existing_content == fresh_content {
|
|
up_to_date.push(filename);
|
|
} else {
|
|
has_changes.push(filename);
|
|
}
|
|
}
|
|
|
|
// Report status
|
|
if !up_to_date.is_empty() {
|
|
println!(
|
|
"{} {} file(s) already up to date",
|
|
"ok".green(),
|
|
up_to_date.len()
|
|
);
|
|
}
|
|
|
|
if !will_create.is_empty() {
|
|
for f in &will_create {
|
|
println!(" {} {} (new)", "+".bright_green(), f.bright_white());
|
|
}
|
|
}
|
|
|
|
if !has_changes.is_empty() {
|
|
for f in &has_changes {
|
|
println!(
|
|
" {} {} (differs from latest)",
|
|
"~".bright_yellow(),
|
|
f.bright_white()
|
|
);
|
|
}
|
|
}
|
|
|
|
// Decide what to do
|
|
if has_changes.is_empty() && will_create.is_empty() {
|
|
println!();
|
|
println!(
|
|
"{}",
|
|
"All scripts are up to date. Nothing to do."
|
|
.bright_green()
|
|
.bold()
|
|
);
|
|
return Ok(());
|
|
}
|
|
|
|
if !has_changes.is_empty() && !force {
|
|
println!();
|
|
println!(
|
|
"{} {} script(s) differ from the latest Anvil templates.",
|
|
"!".bright_yellow(),
|
|
has_changes.len()
|
|
);
|
|
println!(
|
|
"This is normal after upgrading Anvil. To update them, run:"
|
|
);
|
|
println!();
|
|
println!(" {}", "anvil refresh --force".bright_cyan());
|
|
println!();
|
|
println!(
|
|
" {}",
|
|
"Only build scripts are replaced. Your .anvil.toml and source code 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)?;
|
|
}
|
|
|
|
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 &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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
println!();
|
|
println!(
|
|
"{} Updated {} file(s).",
|
|
"ok".green(),
|
|
files_to_write.len()
|
|
);
|
|
|
|
Ok(())
|
|
} |