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

@@ -80,6 +80,15 @@ pub fn list_boards(project_dir: Option<&str>) -> Result<()> {
"anvil board --add mega".bright_cyan() "anvil board --add mega".bright_cyan()
); );
if config.boards.len() > 1 { if config.boards.len() > 1 {
println!(
" Set default: {}",
format!(
"anvil board --default {}",
config.boards.keys()
.find(|k| *k != &config.build.default)
.unwrap_or(&config.build.default)
).bright_cyan()
);
println!( println!(
" Remove a board: {}", " Remove a board: {}",
format!( format!(
@@ -446,7 +455,7 @@ pub fn remove_board(name: &str, project_dir: Option<&str>) -> Result<()> {
if name == config.build.default { if name == config.build.default {
bail!( bail!(
"Cannot remove '{}' because it is the default board.\n \ "Cannot remove '{}' because it is the default board.\n \
Change the default in .anvil.toml first, or remove a different board.", Change the default first: anvil board --default <other-board>",
name name
); );
} }
@@ -495,6 +504,53 @@ pub fn remove_board(name: &str, project_dir: Option<&str>) -> Result<()> {
Ok(()) Ok(())
} }
/// Set the default board in .anvil.toml.
pub fn set_default_board(name: &str, project_dir: Option<&str>) -> Result<()> {
let project_path = resolve_project_dir(project_dir)?;
let config = ProjectConfig::load(&project_path)?;
// Verify the board exists
if !config.boards.contains_key(name) {
let available: Vec<&String> = config.boards.keys().collect();
if available.is_empty() {
bail!(
"No board '{}' found.\n \
Add it first: anvil board --add {}",
name, name
);
} else {
bail!(
"No board '{}' found.\n \
Available: {}\n \
Add it first: anvil board --add {}",
name,
available.iter().map(|s| s.as_str()).collect::<Vec<_>>().join(", "),
name
);
}
}
let config_path = project_path.join(CONFIG_FILENAME);
let old = crate::project::config::set_default_in_file(&config_path, name)?;
let label = board_label(&config.boards[name].fqbn);
println!(
"{} Default board: {} ({})",
"ok".green(),
name.bright_white().bold(),
label.bright_cyan()
);
if !old.is_empty() && old != name {
println!(
" {}",
format!("Changed from: {}", old).bright_black()
);
}
println!();
Ok(())
}
fn resolve_project_dir(project_dir: Option<&str>) -> Result<std::path::PathBuf> { fn resolve_project_dir(project_dir: Option<&str>) -> Result<std::path::PathBuf> {
let start = match project_dir { let start = match project_dir {
Some(dir) => std::path::PathBuf::from(dir), Some(dir) => std::path::PathBuf::from(dir),

View File

@@ -92,15 +92,19 @@ enum Commands {
name: Option<String>, name: Option<String>,
/// Add a board to the project /// Add a board to the project
#[arg(long, conflicts_with_all = ["remove", "listall"])] #[arg(long, conflicts_with_all = ["remove", "listall", "default"])]
add: bool, add: bool,
/// Remove a board from the project /// Remove a board from the project
#[arg(long, conflicts_with_all = ["add", "listall"])] #[arg(long, conflicts_with_all = ["add", "listall", "default"])]
remove: bool, remove: bool,
/// Set the default board
#[arg(long, conflicts_with_all = ["add", "remove", "listall"])]
default: bool,
/// Browse all available boards /// Browse all available boards
#[arg(long, conflicts_with_all = ["add", "remove"])] #[arg(long, conflicts_with_all = ["add", "remove", "default"])]
listall: bool, listall: bool,
/// Board identifier (from anvil board --listall) /// Board identifier (from anvil board --listall)
@@ -177,7 +181,7 @@ fn main() -> Result<()> {
force, force,
) )
} }
Commands::Board { name, add, remove, listall, id, baud, dir } => { Commands::Board { name, add, remove, default, listall, id, baud, dir } => {
if listall { if listall {
commands::board::listall_boards(name.as_deref()) commands::board::listall_boards(name.as_deref())
} else if add { } else if add {
@@ -205,6 +209,18 @@ fn main() -> Result<()> {
board_name, board_name,
dir.as_deref(), dir.as_deref(),
) )
} else if default {
let board_name = name.as_deref().ok_or_else(|| {
anyhow::anyhow!(
"Board name required.\n\
Usage: anvil board --default uno\n\
List boards: anvil board"
)
})?;
commands::board::set_default_board(
board_name,
dir.as_deref(),
)
} else { } else {
commands::board::list_boards(dir.as_deref()) commands::board::list_boards(dir.as_deref())
} }

View File

@@ -150,16 +150,23 @@ impl ProjectConfig {
// Migrate old format: fqbn in [build] -> [boards.X] + default // Migrate old format: fqbn in [build] -> [boards.X] + default
if config.build.default.is_empty() { 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() 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()) .map(|p| p.name.to_string())
.unwrap_or_else(|| "default".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, baud: None,
}); });
config.build.default = board_name;
} }
} }
@@ -239,6 +246,138 @@ pub fn anvil_home() -> Result<PathBuf> {
Ok(anvil_dir) 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -382,4 +521,105 @@ mod tests {
assert_eq!(mega.fqbn, "arduino:avr:mega:cpu=atmega2560"); assert_eq!(mega.fqbn, "arduino:avr:mega:cpu=atmega2560");
assert_eq!(mega.baud, Some(57600)); 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\""));
}
} }

View File

@@ -70,6 +70,22 @@ exit /b 0
:: -- Resolve board -------------------------------------------------------- :: -- Resolve board --------------------------------------------------------
if "%BOARD_NAME%"=="" set "BOARD_NAME=%DEFAULT_BOARD%" if "%BOARD_NAME%"=="" set "BOARD_NAME=%DEFAULT_BOARD%"
if "%BOARD_NAME%"=="" (
echo FAIL: No default board set in .anvil.toml.
echo.
echo Add a default to the [build] section of .anvil.toml:
echo default = "uno"
echo.
echo And make sure a matching [boards.uno] section exists:
echo [boards.uno]
echo fqbn = "arduino:avr:uno"
echo.
echo Or with Anvil: anvil board --default uno
echo List boards: anvil board --listall
echo arduino-cli board listall
exit /b 1
)
set "BOARD_SECTION=[boards.%BOARD_NAME%]" set "BOARD_SECTION=[boards.%BOARD_NAME%]"
set "IN_SECTION=0" set "IN_SECTION=0"
set "FQBN=" set "FQBN="
@@ -96,8 +112,15 @@ for /f "usebackq tokens=*" %%L in ("%CONFIG%") do (
) )
if "!FQBN!"=="" ( if "!FQBN!"=="" (
echo FAIL: No board '%BOARD_NAME%' in .anvil.toml. echo FAIL: No [boards.%BOARD_NAME%] section in .anvil.toml.
echo Add it: anvil board --add %BOARD_NAME% echo.
echo Add it to .anvil.toml:
echo [boards.%BOARD_NAME%]
echo fqbn = "arduino:avr:uno" ^(replace with your board^)
echo.
echo Or with Anvil: anvil board --add %BOARD_NAME%
echo List boards: anvil board --listall
echo arduino-cli board listall
exit /b 1 exit /b 1
) )

View File

@@ -87,10 +87,35 @@ done
# -- Resolve board --------------------------------------------------------- # -- Resolve board ---------------------------------------------------------
ACTIVE_BOARD="${BOARD_NAME:-$DEFAULT_BOARD}" ACTIVE_BOARD="${BOARD_NAME:-$DEFAULT_BOARD}"
if [[ -z "$ACTIVE_BOARD" ]]; then
echo "${RED}FAIL${RST} No default board set in .anvil.toml." >&2
echo "" >&2
echo " Add a default to the [build] section of .anvil.toml:" >&2
echo " default = \"uno\"" >&2
echo "" >&2
echo " And make sure a matching [boards.uno] section exists:" >&2
echo " [boards.uno]" >&2
echo " fqbn = \"arduino:avr:uno\"" >&2
echo "" >&2
echo " Or with Anvil: anvil board --default uno" >&2
echo " List boards: anvil board --listall" >&2
echo " arduino-cli board listall" >&2
exit 1
fi
FQBN="$(toml_section_get "boards.$ACTIVE_BOARD" "fqbn")" FQBN="$(toml_section_get "boards.$ACTIVE_BOARD" "fqbn")"
if [[ -z "$FQBN" ]]; then if [[ -z "$FQBN" ]]; then
die "No board '$ACTIVE_BOARD' in .anvil.toml.\n Add it: anvil board --add $ACTIVE_BOARD" echo "${RED}FAIL${RST} No [boards.$ACTIVE_BOARD] section in .anvil.toml." >&2
echo "" >&2
echo " Add it to .anvil.toml:" >&2
echo " [boards.$ACTIVE_BOARD]" >&2
echo " fqbn = \"arduino:avr:uno\" # replace with your board" >&2
echo "" >&2
echo " Or with Anvil: anvil board --add $ACTIVE_BOARD" >&2
echo " List boards: anvil board --listall" >&2
echo " arduino-cli board listall" >&2
exit 1
fi fi
if [[ -n "$BOARD_NAME" ]]; then if [[ -n "$BOARD_NAME" ]]; then

View File

@@ -84,6 +84,22 @@ exit /b 0
:: -- Resolve board -------------------------------------------------------- :: -- Resolve board --------------------------------------------------------
if "%BOARD_NAME%"=="" set "BOARD_NAME=%DEFAULT_BOARD%" if "%BOARD_NAME%"=="" set "BOARD_NAME=%DEFAULT_BOARD%"
if "%BOARD_NAME%"=="" (
echo FAIL: No default board set in .anvil.toml.
echo.
echo Add a default to the [build] section of .anvil.toml:
echo default = "uno"
echo.
echo And make sure a matching [boards.uno] section exists:
echo [boards.uno]
echo fqbn = "arduino:avr:uno"
echo.
echo Or with Anvil: anvil board --default uno
echo List boards: anvil board --listall
echo arduino-cli board listall
exit /b 1
)
set "BOARD_SECTION=[boards.%BOARD_NAME%]" set "BOARD_SECTION=[boards.%BOARD_NAME%]"
set "IN_SECTION=0" set "IN_SECTION=0"
set "BOARD_BAUD=" set "BOARD_BAUD="

View File

@@ -82,6 +82,22 @@ done
# -- Resolve board --------------------------------------------------------- # -- Resolve board ---------------------------------------------------------
ACTIVE_BOARD="${BOARD_NAME:-$DEFAULT_BOARD}" ACTIVE_BOARD="${BOARD_NAME:-$DEFAULT_BOARD}"
if [[ -z "$ACTIVE_BOARD" ]]; then
echo "${RED}FAIL${RST} No default board set in .anvil.toml." >&2
echo "" >&2
echo " Add a default to the [build] section of .anvil.toml:" >&2
echo " default = \"uno\"" >&2
echo "" >&2
echo " And make sure a matching [boards.uno] section exists:" >&2
echo " [boards.uno]" >&2
echo " fqbn = \"arduino:avr:uno\"" >&2
echo "" >&2
echo " Or with Anvil: anvil board --default uno" >&2
echo " List boards: anvil board --listall" >&2
echo " arduino-cli board listall" >&2
exit 1
fi
BOARD_BAUD="$(toml_section_get "boards.$ACTIVE_BOARD" "baud")" BOARD_BAUD="$(toml_section_get "boards.$ACTIVE_BOARD" "baud")"
if [[ -n "$BOARD_BAUD" ]]; then if [[ -n "$BOARD_BAUD" ]]; then
BAUD="$BOARD_BAUD" BAUD="$BOARD_BAUD"

View File

@@ -99,6 +99,22 @@ exit /b 0
:: -- Resolve board -------------------------------------------------------- :: -- Resolve board --------------------------------------------------------
if "%BOARD_NAME%"=="" set "BOARD_NAME=%DEFAULT_BOARD%" if "%BOARD_NAME%"=="" set "BOARD_NAME=%DEFAULT_BOARD%"
if "%BOARD_NAME%"=="" (
echo FAIL: No default board set in .anvil.toml.
echo.
echo Add a default to the [build] section of .anvil.toml:
echo default = "uno"
echo.
echo And make sure a matching [boards.uno] section exists:
echo [boards.uno]
echo fqbn = "arduino:avr:uno"
echo.
echo Or with Anvil: anvil board --default uno
echo List boards: anvil board --listall
echo arduino-cli board listall
exit /b 1
)
set "BOARD_SECTION=[boards.%BOARD_NAME%]" set "BOARD_SECTION=[boards.%BOARD_NAME%]"
set "IN_SECTION=0" set "IN_SECTION=0"
set "FQBN=" set "FQBN="
@@ -127,8 +143,15 @@ for /f "usebackq tokens=*" %%L in ("%CONFIG%") do (
) )
if "!FQBN!"=="" ( if "!FQBN!"=="" (
echo FAIL: No board '%BOARD_NAME%' in .anvil.toml. echo FAIL: No [boards.%BOARD_NAME%] section in .anvil.toml.
echo Add it: anvil board --add %BOARD_NAME% echo.
echo Add it to .anvil.toml:
echo [boards.%BOARD_NAME%]
echo fqbn = "arduino:avr:uno" ^(replace with your board^)
echo.
echo Or with Anvil: anvil board --add %BOARD_NAME%
echo List boards: anvil board --listall
echo arduino-cli board listall
exit /b 1 exit /b 1
) )
if not "!BOARD_BAUD!"=="" set "BAUD=!BOARD_BAUD!" if not "!BOARD_BAUD!"=="" set "BAUD=!BOARD_BAUD!"

View File

@@ -101,10 +101,35 @@ done
# -- Resolve board --------------------------------------------------------- # -- Resolve board ---------------------------------------------------------
ACTIVE_BOARD="${BOARD_NAME:-$DEFAULT_BOARD}" ACTIVE_BOARD="${BOARD_NAME:-$DEFAULT_BOARD}"
if [[ -z "$ACTIVE_BOARD" ]]; then
echo "${RED}FAIL${RST} No default board set in .anvil.toml." >&2
echo "" >&2
echo " Add a default to the [build] section of .anvil.toml:" >&2
echo " default = \"uno\"" >&2
echo "" >&2
echo " And make sure a matching [boards.uno] section exists:" >&2
echo " [boards.uno]" >&2
echo " fqbn = \"arduino:avr:uno\"" >&2
echo "" >&2
echo " Or with Anvil: anvil board --default uno" >&2
echo " List boards: anvil board --listall" >&2
echo " arduino-cli board listall" >&2
exit 1
fi
FQBN="$(toml_section_get "boards.$ACTIVE_BOARD" "fqbn")" FQBN="$(toml_section_get "boards.$ACTIVE_BOARD" "fqbn")"
if [[ -z "$FQBN" ]]; then if [[ -z "$FQBN" ]]; then
die "No board '$ACTIVE_BOARD' in .anvil.toml.\n Add it: anvil board --add $ACTIVE_BOARD" echo "${RED}FAIL${RST} No [boards.$ACTIVE_BOARD] section in .anvil.toml." >&2
echo "" >&2
echo " Add it to .anvil.toml:" >&2
echo " [boards.$ACTIVE_BOARD]" >&2
echo " fqbn = \"arduino:avr:uno\" # replace with your board" >&2
echo "" >&2
echo " Or with Anvil: anvil board --add $ACTIVE_BOARD" >&2
echo " List boards: anvil board --listall" >&2
echo " arduino-cli board listall" >&2
exit 1
fi fi
BOARD_BAUD="$(toml_section_get "boards.$ACTIVE_BOARD" "baud")" BOARD_BAUD="$(toml_section_get "boards.$ACTIVE_BOARD" "baud")"