Supports switching default board

This commit is contained in:
Eric Ratliff
2026-02-19 10:23:16 -06:00
parent b909da298e
commit 6cacc07109
9 changed files with 456 additions and 16 deletions

View File

@@ -150,16 +150,23 @@ impl ProjectConfig {
// Migrate old format: fqbn in [build] -> [boards.X] + default
if config.build.default.is_empty() {
if let Some(legacy_fqbn) = config.build.fqbn.take() {
if let Some(ref legacy_fqbn) = config.build.fqbn {
let board_name = crate::board::presets::PRESETS.iter()
.find(|p| p.fqbn == legacy_fqbn)
.find(|p| p.fqbn == *legacy_fqbn)
.map(|p| p.name.to_string())
.unwrap_or_else(|| "default".to_string());
config.boards.entry(board_name.clone()).or_insert(BoardProfile {
fqbn: legacy_fqbn,
// Text-based migration of the actual file
migrate_config_file(&config_path, &board_name, legacy_fqbn)?;
// Update in-memory config to match
let fqbn_clone = legacy_fqbn.clone();
config.build.fqbn = None;
config.build.default = board_name.clone();
config.boards.entry(board_name).or_insert(BoardProfile {
fqbn: fqbn_clone,
baud: None,
});
config.build.default = board_name;
}
}
@@ -239,6 +246,138 @@ pub fn anvil_home() -> Result<PathBuf> {
Ok(anvil_dir)
}
// -- Text-based .anvil.toml editing -----------------------------------------
// These operate on the raw file text to preserve comments and formatting.
/// Migrate an old-format config: add default = "X" to [build] and add
/// [boards.X] section if needed. Keeps old fqbn line for backward
/// compatibility with scripts that haven't been refreshed yet.
fn migrate_config_file(config_path: &Path, board_name: &str, fqbn: &str) -> Result<()> {
let content = fs::read_to_string(config_path)
.context("Failed to read .anvil.toml for migration")?;
let mut output = String::new();
let mut in_build = false;
let mut added_default = false;
let mut has_board_section = false;
// Check if [boards.NAME] already exists
let section_header = format!("[boards.{}]", board_name);
for line in content.lines() {
if line.trim() == section_header {
has_board_section = true;
break;
}
}
for line in content.lines() {
let trimmed = line.trim();
// Track which section we're in
if trimmed == "[build]" {
in_build = true;
output.push_str(line);
output.push('\n');
continue;
}
if trimmed.starts_with('[') && trimmed != "[build]" {
// Leaving [build] -- add default before leaving if we haven't yet
if in_build && !added_default {
output.push_str(&format!("default = \"{}\"\n", board_name));
added_default = true;
}
in_build = false;
}
// In [build]: add default right before fqbn (natural reading order)
if in_build && !added_default && trimmed.starts_with("fqbn") && trimmed.contains('=') {
output.push_str(&format!("default = \"{}\"\n", board_name));
added_default = true;
}
output.push_str(line);
output.push('\n');
}
// Add [boards.NAME] section if it doesn't already exist
if !has_board_section {
while output.ends_with("\n\n") {
output.pop();
}
output.push('\n');
output.push_str(&format!("\n[boards.{}]\n", board_name));
output.push_str(&format!("fqbn = \"{}\"\n", fqbn));
}
fs::write(config_path, &output)
.context("Failed to write migrated .anvil.toml")?;
eprintln!(
"\x1b[33minfo\x1b[0m Migrated .anvil.toml: default = \"{}\"{}",
board_name,
if has_board_section { "" } else { ", added [boards] section" }
);
eprintln!(
"\x1b[33minfo\x1b[0m Run \x1b[36manvil refresh --force\x1b[0m to update scripts."
);
Ok(())
}
/// Set or change the default board in .anvil.toml (text-based edit).
/// Returns the old default name (empty string if there wasn't one).
pub fn set_default_in_file(config_path: &Path, board_name: &str) -> Result<String> {
let content = fs::read_to_string(config_path)
.context("Failed to read .anvil.toml")?;
let mut output = String::new();
let mut in_build = false;
let mut replaced = false;
let mut old_default = String::new();
for line in content.lines() {
let trimmed = line.trim();
if trimmed == "[build]" {
in_build = true;
output.push_str(line);
output.push('\n');
continue;
}
if trimmed.starts_with('[') && trimmed != "[build]" {
// Leaving [build] without having found default -- add it
if in_build && !replaced {
output.push_str(&format!("default = \"{}\"\n", board_name));
replaced = true;
}
in_build = false;
}
if in_build && trimmed.starts_with("default") && trimmed.contains('=') {
// Extract old value
if let Some(val) = trimmed.split('=').nth(1) {
old_default = val.trim().trim_matches('"').to_string();
}
output.push_str(&format!("default = \"{}\"\n", board_name));
replaced = true;
continue;
}
output.push_str(line);
output.push('\n');
}
// Edge case: [build] was the last section and had no default
if in_build && !replaced {
output.push_str(&format!("default = \"{}\"\n", board_name));
}
fs::write(config_path, &output)
.context("Failed to write .anvil.toml")?;
Ok(old_default)
}
#[cfg(test)]
mod tests {
use super::*;
@@ -382,4 +521,105 @@ mod tests {
assert_eq!(mega.fqbn, "arduino:avr:mega:cpu=atmega2560");
assert_eq!(mega.baud, Some(57600));
}
#[test]
fn test_migrate_old_format() {
let tmp = TempDir::new().unwrap();
let old_config = r#"[project]
name = "hand"
anvil_version = "1.0.0"
[build]
fqbn = "arduino:avr:uno"
warnings = "more"
include_dirs = ["lib/hal", "lib/app"]
extra_flags = ["-Werror"]
[monitor]
baud = 115200
[boards.micro]
fqbn = "arduino:avr:micro"
"#;
fs::write(tmp.path().join(CONFIG_FILENAME), old_config).unwrap();
let config = ProjectConfig::load(tmp.path()).unwrap();
assert_eq!(config.build.default, "uno");
assert!(config.boards.contains_key("uno"));
assert_eq!(config.boards["uno"].fqbn, "arduino:avr:uno");
assert!(config.boards.contains_key("micro"));
// Verify the file was rewritten
let content = fs::read_to_string(tmp.path().join(CONFIG_FILENAME)).unwrap();
assert!(content.contains("default = \"uno\""));
assert!(content.contains("[boards.uno]"));
// Old fqbn stays in [build] for backward compat with old scripts
assert!(content.contains("fqbn = \"arduino:avr:uno\""));
}
#[test]
fn test_migrate_preserves_existing_boards() {
let tmp = TempDir::new().unwrap();
let old_config = r#"[project]
name = "test"
anvil_version = "1.0.0"
[build]
fqbn = "arduino:avr:mega:cpu=atmega2560"
warnings = "more"
include_dirs = ["lib/hal", "lib/app"]
extra_flags = ["-Werror"]
[monitor]
baud = 115200
"#;
fs::write(tmp.path().join(CONFIG_FILENAME), old_config).unwrap();
let config = ProjectConfig::load(tmp.path()).unwrap();
assert_eq!(config.build.default, "mega");
assert!(config.boards.contains_key("mega"));
}
#[test]
fn test_set_default_in_file() {
let tmp = TempDir::new().unwrap();
let config = ProjectConfig::new("test");
config.save(tmp.path()).unwrap();
let config_path = tmp.path().join(CONFIG_FILENAME);
let old = set_default_in_file(&config_path, "mega").unwrap();
assert_eq!(old, "uno");
let content = fs::read_to_string(&config_path).unwrap();
assert!(content.contains("default = \"mega\""));
assert!(!content.contains("default = \"uno\""));
}
#[test]
fn test_set_default_adds_when_missing() {
let tmp = TempDir::new().unwrap();
let content = r#"[project]
name = "test"
anvil_version = "1.0.0"
[build]
warnings = "more"
include_dirs = ["lib/hal", "lib/app"]
extra_flags = ["-Werror"]
[monitor]
baud = 115200
[boards.uno]
fqbn = "arduino:avr:uno"
"#;
let config_path = tmp.path().join(CONFIG_FILENAME);
fs::write(&config_path, content).unwrap();
let old = set_default_in_file(&config_path, "uno").unwrap();
assert_eq!(old, "");
let result = fs::read_to_string(&config_path).unwrap();
assert!(result.contains("default = \"uno\""));
}
}