diff --git a/src/commands/board.rs b/src/commands/board.rs index 6ca76ea..6b4b8d2 100644 --- a/src/commands/board.rs +++ b/src/commands/board.rs @@ -80,6 +80,15 @@ pub fn list_boards(project_dir: Option<&str>) -> Result<()> { "anvil board --add mega".bright_cyan() ); 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!( " Remove a board: {}", format!( @@ -446,7 +455,7 @@ pub fn remove_board(name: &str, project_dir: Option<&str>) -> Result<()> { if name == config.build.default { bail!( "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 ", name ); } @@ -495,6 +504,53 @@ pub fn remove_board(name: &str, project_dir: Option<&str>) -> Result<()> { 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::>().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 { let start = match project_dir { Some(dir) => std::path::PathBuf::from(dir), diff --git a/src/main.rs b/src/main.rs index c7f2d54..61a7a69 100644 --- a/src/main.rs +++ b/src/main.rs @@ -92,15 +92,19 @@ enum Commands { name: Option, /// Add a board to the project - #[arg(long, conflicts_with_all = ["remove", "listall"])] + #[arg(long, conflicts_with_all = ["remove", "listall", "default"])] add: bool, /// Remove a board from the project - #[arg(long, conflicts_with_all = ["add", "listall"])] + #[arg(long, conflicts_with_all = ["add", "listall", "default"])] remove: bool, + /// Set the default board + #[arg(long, conflicts_with_all = ["add", "remove", "listall"])] + default: bool, + /// Browse all available boards - #[arg(long, conflicts_with_all = ["add", "remove"])] + #[arg(long, conflicts_with_all = ["add", "remove", "default"])] listall: bool, /// Board identifier (from anvil board --listall) @@ -177,7 +181,7 @@ fn main() -> Result<()> { force, ) } - Commands::Board { name, add, remove, listall, id, baud, dir } => { + Commands::Board { name, add, remove, default, listall, id, baud, dir } => { if listall { commands::board::listall_boards(name.as_deref()) } else if add { @@ -205,6 +209,18 @@ fn main() -> Result<()> { board_name, 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 { commands::board::list_boards(dir.as_deref()) } diff --git a/src/project/config.rs b/src/project/config.rs index 69832fa..b9e2dd5 100644 --- a/src/project/config.rs +++ b/src/project/config.rs @@ -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 { 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 { + 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\"")); + } } \ No newline at end of file diff --git a/templates/basic/build.bat b/templates/basic/build.bat index aab7814..c9b14ab 100644 --- a/templates/basic/build.bat +++ b/templates/basic/build.bat @@ -70,6 +70,22 @@ exit /b 0 :: -- Resolve 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 "IN_SECTION=0" set "FQBN=" @@ -96,8 +112,15 @@ for /f "usebackq tokens=*" %%L in ("%CONFIG%") do ( ) if "!FQBN!"=="" ( - echo FAIL: No board '%BOARD_NAME%' in .anvil.toml. - echo Add it: anvil board --add %BOARD_NAME% + echo FAIL: No [boards.%BOARD_NAME%] section in .anvil.toml. + 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 ) diff --git a/templates/basic/build.sh b/templates/basic/build.sh index f940bee..5373e92 100644 --- a/templates/basic/build.sh +++ b/templates/basic/build.sh @@ -87,10 +87,35 @@ done # -- Resolve 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")" 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 if [[ -n "$BOARD_NAME" ]]; then diff --git a/templates/basic/monitor.bat b/templates/basic/monitor.bat index a1ee10a..5962377 100644 --- a/templates/basic/monitor.bat +++ b/templates/basic/monitor.bat @@ -84,6 +84,22 @@ exit /b 0 :: -- Resolve 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 "IN_SECTION=0" set "BOARD_BAUD=" diff --git a/templates/basic/monitor.sh b/templates/basic/monitor.sh index 6660bf2..353ab0d 100644 --- a/templates/basic/monitor.sh +++ b/templates/basic/monitor.sh @@ -82,6 +82,22 @@ done # -- Resolve 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")" if [[ -n "$BOARD_BAUD" ]]; then BAUD="$BOARD_BAUD" diff --git a/templates/basic/upload.bat b/templates/basic/upload.bat index 36e352f..3987ff4 100644 --- a/templates/basic/upload.bat +++ b/templates/basic/upload.bat @@ -99,6 +99,22 @@ exit /b 0 :: -- Resolve 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 "IN_SECTION=0" set "FQBN=" @@ -127,8 +143,15 @@ for /f "usebackq tokens=*" %%L in ("%CONFIG%") do ( ) if "!FQBN!"=="" ( - echo FAIL: No board '%BOARD_NAME%' in .anvil.toml. - echo Add it: anvil board --add %BOARD_NAME% + echo FAIL: No [boards.%BOARD_NAME%] section in .anvil.toml. + 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 ) if not "!BOARD_BAUD!"=="" set "BAUD=!BOARD_BAUD!" diff --git a/templates/basic/upload.sh b/templates/basic/upload.sh index c66c929..019f907 100644 --- a/templates/basic/upload.sh +++ b/templates/basic/upload.sh @@ -101,10 +101,35 @@ done # -- Resolve 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")" 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 BOARD_BAUD="$(toml_section_get "boards.$ACTIVE_BOARD" "baud")"