diff --git a/templates/basic/build.bat b/templates/basic/build.bat index a7526f2..22c6881 100644 --- a/templates/basic/build.bat +++ b/templates/basic/build.bat @@ -1,9 +1,11 @@ @echo off -setlocal enabledelayedexpansion - -:: build.bat -- Compile the sketch using arduino-cli +:: build.bat -- Thin wrapper that invokes build.ps1 :: -:: Reads all settings from .anvil.toml. No Anvil binary required. +:: Students can type "build" at a command prompt or double-click this file. +:: All logic lives in build.ps1. Requires PowerShell 5.1+ (ships with +:: Windows 10/11). +:: +:: Settings are read from .anvil.toml. No Anvil binary required. :: :: Usage: :: build.bat Compile (verify only) @@ -11,173 +13,5 @@ setlocal enabledelayedexpansion :: build.bat --clean Delete build cache first :: build.bat --verbose Show full compiler output -set "SCRIPT_DIR=%~dp0" -set "SCRIPT_DIR=%SCRIPT_DIR:~0,-1%" -set "CONFIG=%SCRIPT_DIR%\.anvil.toml" - -if not exist "%CONFIG%" ( - echo FAIL: No .anvil.toml found in %SCRIPT_DIR% - exit /b 1 -) - -:: -- Parse .anvil.toml (flat keys) ---------------------------------------- -for /f "usebackq tokens=1,* delims==" %%a in ("%CONFIG%") do ( - set "_K=%%a" - if not "!_K:~0,1!"=="#" if not "!_K:~0,1!"=="[" ( - set "_K=!_K: =!" - set "_V=%%b" - if defined _V ( - set "_V=!_V: =!" - set "_V=!_V:"=!" - ) - if "!_K!"=="name" set "SKETCH_NAME=!_V!" - if "!_K!"=="default" set "DEFAULT_BOARD=!_V!" - if "!_K!"=="warnings" set "WARNINGS=!_V!" - ) -) - -if "%SKETCH_NAME%"=="" ( - echo FAIL: Could not read project name from .anvil.toml - exit /b 1 -) - -set "SKETCH_DIR=%SCRIPT_DIR%\%SKETCH_NAME%" -set "BUILD_DIR=%SCRIPT_DIR%\.build" - -:: -- Parse arguments ------------------------------------------------------ -set "DO_CLEAN=0" -set "VERBOSE=" -set "BOARD_NAME=" - -:parse_args -if "%~1"=="" goto done_args -if "%~1"=="--board" set "BOARD_NAME=%~2" & shift & shift & goto parse_args -if "%~1"=="--clean" set "DO_CLEAN=1" & shift & goto parse_args -if "%~1"=="--verbose" set "VERBOSE=--verbose" & shift & goto parse_args -if "%~1"=="--help" goto show_help -if "%~1"=="-h" goto show_help -echo FAIL: Unknown option: %~1 -exit /b 1 - -:show_help -echo Usage: build.bat [--board NAME] [--clean] [--verbose] -echo Compiles the sketch. Settings from .anvil.toml. -echo --board NAME selects a board from [boards.NAME]. -exit /b 0 - -:done_args - -:: -- 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=" -for /f "usebackq tokens=*" %%L in ("%CONFIG%") do ( - set "_LINE=%%L" - if "!_LINE!"=="!BOARD_SECTION!" ( - set "IN_SECTION=1" - ) else if "!IN_SECTION!"=="1" ( - if "!_LINE:~0,1!"=="[" ( - set "IN_SECTION=0" - ) else if not "!_LINE:~0,1!"=="#" ( - for /f "tokens=1,* delims==" %%a in ("!_LINE!") do ( - set "_BK=%%a" - set "_BK=!_BK: =!" - set "_BV=%%b" - if defined _BV ( - set "_BV=!_BV: =!" - set "_BV=!_BV:"=!" - ) - if "!_BK!"=="fqbn" set "FQBN=!_BV!" - ) - ) - ) -) - -if "!FQBN!"=="" ( - 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_NAME%"=="%DEFAULT_BOARD%" ( - echo ok Using board: %BOARD_NAME% -- %FQBN% -) - -:: -- Preflight ------------------------------------------------------------ -where arduino-cli >nul 2>nul -if errorlevel 1 ( - echo FAIL: arduino-cli not found in PATH. - exit /b 1 -) - -if not exist "%SKETCH_DIR%" ( - echo FAIL: Sketch directory not found: %SKETCH_DIR% - exit /b 1 -) - -:: -- Clean ---------------------------------------------------------------- -if "%DO_CLEAN%"=="1" ( - if exist "%BUILD_DIR%" ( - echo Cleaning build cache... - rmdir /s /q "%BUILD_DIR%" - ) -) - -:: -- Build include flags -------------------------------------------------- -set "BUILD_FLAGS=" -for %%d in (lib\hal lib\app) do ( - if exist "%SCRIPT_DIR%\%%d" ( - set "BUILD_FLAGS=!BUILD_FLAGS! -I%SCRIPT_DIR%\%%d" - ) -) -:: Auto-discover driver libraries (added by: anvil add ) -if exist "%SCRIPT_DIR%\lib\drivers" ( - for /d %%d in ("%SCRIPT_DIR%\lib\drivers\*") do ( - set "BUILD_FLAGS=!BUILD_FLAGS! -I%%d" - ) -) -set "BUILD_FLAGS=!BUILD_FLAGS! -Werror" - -:: -- Compile -------------------------------------------------------------- -echo Compiling %SKETCH_NAME%... -echo Board: %FQBN% -echo Sketch: %SKETCH_DIR% -echo. - -if not exist "%BUILD_DIR%" mkdir "%BUILD_DIR%" - -arduino-cli compile --fqbn %FQBN% --build-path "%BUILD_DIR%" --warnings %WARNINGS% --build-property "compiler.cpp.extra_flags=%BUILD_FLAGS%" --build-property "compiler.c.extra_flags=%BUILD_FLAGS%" %VERBOSE% "%SKETCH_DIR%" -if errorlevel 1 ( - echo. - echo FAIL: Compilation failed. - exit /b 1 -) - -echo. -echo ok Compile succeeded. -echo. \ No newline at end of file +powershell.exe -NoProfile -ExecutionPolicy Bypass -File "%~dp0build.ps1" %* +exit /b %ERRORLEVEL% \ No newline at end of file diff --git a/templates/basic/build.ps1 b/templates/basic/build.ps1 new file mode 100644 index 0000000..921ec2f --- /dev/null +++ b/templates/basic/build.ps1 @@ -0,0 +1,212 @@ +# build.ps1 -- Compile the sketch using arduino-cli +# +# Reads all settings from .anvil.toml. No Anvil binary required. +# Called by build.bat (thin wrapper) or directly: +# powershell -File build.ps1 [--board NAME] [--clean] [--verbose] +# +# Exit codes: 0 = success, 1 = error + +param( + [string]$board = "", + [switch]$clean, + [switch]$verbose, + [switch]$help +) + +$ErrorActionPreference = "Stop" + +# -- Helpers --------------------------------------------------------------- + +function Fail($msg) { + Write-Host "FAIL: $msg" -ForegroundColor Red + exit 1 +} + +function Ok($msg) { + Write-Host "ok $msg" -ForegroundColor Green +} + +# -- Locate config --------------------------------------------------------- + +$ScriptDir = Split-Path -Parent $MyInvocation.MyCommand.Definition +$Config = Join-Path $ScriptDir ".anvil.toml" + +if (-not (Test-Path $Config)) { + Fail "No .anvil.toml found in $ScriptDir" +} + +# -- Parse .anvil.toml ----------------------------------------------------- +# Simple line-by-line parser. Tracks current section header to support +# [project], [build], [boards.NAME], etc. + +$tomlData = @{} +$currentSection = "" + +foreach ($rawLine in Get-Content $Config) { + $line = $rawLine.Trim() + + # Skip blank lines and comments + if ($line -eq "" -or $line.StartsWith("#")) { continue } + + # Section header: [project], [boards.uno], etc. + if ($line -match '^\[(.+)\]$') { + $currentSection = $Matches[1] + continue + } + + # Key = value (only process lines with =) + if ($line -match '^([^=]+?)\s*=\s*(.+)$') { + $key = $Matches[1].Trim() + $val = $Matches[2].Trim() + + # Strip surrounding quotes + if ($val.StartsWith('"') -and $val.EndsWith('"')) { + $val = $val.Substring(1, $val.Length - 2) + } + + # Skip array values (multi-line or inline [...]) + if ($val.StartsWith("[")) { continue } + + $fullKey = if ($currentSection) { "$currentSection.$key" } else { $key } + $tomlData[$fullKey] = $val + } +} + +# -- Extract settings ------------------------------------------------------ + +$SketchName = $tomlData["project.name"] +$DefaultBoard = $tomlData["build.default"] +$Warnings = $tomlData["build.warnings"] + +if (-not $SketchName) { + Fail "Could not read project name from .anvil.toml" +} + +$SketchDir = Join-Path $ScriptDir $SketchName +$BuildDir = Join-Path $ScriptDir ".build" + +# -- Help ------------------------------------------------------------------ + +if ($help) { + Write-Host "Usage: build.bat [--board NAME] [--clean] [--verbose]" + Write-Host " Compiles the sketch. Settings from .anvil.toml." + Write-Host " --board NAME selects a board from [boards.NAME]." + exit 0 +} + +# -- Resolve board --------------------------------------------------------- + +$BoardName = if ($board) { $board } else { $DefaultBoard } + +if (-not $BoardName) { + Fail @" +No default board set in .anvil.toml. + + Add a default to the [build] section of .anvil.toml: + default = "uno" + + And make sure a matching [boards.uno] section exists: + [boards.uno] + fqbn = "arduino:avr:uno" + + Or with Anvil: Anvil board --default uno + List boards: Anvil board --listall + arduino-cli board listall +"@ +} + +$Fqbn = $tomlData["boards.$BoardName.fqbn"] + +if (-not $Fqbn) { + Fail @" +No [boards.$BoardName] section in .anvil.toml. + + Add it to .anvil.toml: + [boards.$BoardName] + fqbn = "arduino:avr:uno" (replace with your board) + + Or with Anvil: Anvil board --add $BoardName + List boards: Anvil board --listall + arduino-cli board listall +"@ +} + +if ($BoardName -ne $DefaultBoard) { + Ok "Using board: $BoardName -- $Fqbn" +} + +# -- Preflight ------------------------------------------------------------- + +$arduinoCli = Get-Command "arduino-cli" -ErrorAction SilentlyContinue +if (-not $arduinoCli) { + Fail "arduino-cli not found in PATH." +} + +if (-not (Test-Path $SketchDir)) { + Fail "Sketch directory not found: $SketchDir" +} + +# -- Clean ----------------------------------------------------------------- + +if ($clean -and (Test-Path $BuildDir)) { + Write-Host "Cleaning build cache..." + Remove-Item -Recurse -Force $BuildDir +} + +# -- Build include flags --------------------------------------------------- + +$buildFlags = @() +foreach ($sub in @("lib\hal", "lib\app")) { + $dir = Join-Path $ScriptDir $sub + if (Test-Path $dir) { + $buildFlags += "-I$dir" + } +} + +# Auto-discover driver libraries (added by: anvil add ) +$driversDir = Join-Path $ScriptDir "lib\drivers" +if (Test-Path $driversDir) { + foreach ($d in Get-ChildItem -Path $driversDir -Directory) { + $buildFlags += "-I$($d.FullName)" + } +} + +$buildFlags += "-Werror" +$flagsStr = $buildFlags -join " " + +# -- Compile --------------------------------------------------------------- + +Write-Host "Compiling $SketchName..." +Write-Host " Board: $Fqbn" +Write-Host " Sketch: $SketchDir" +Write-Host "" + +if (-not (Test-Path $BuildDir)) { + New-Item -ItemType Directory -Path $BuildDir | Out-Null +} + +$compileArgs = @( + "compile" + "--fqbn", $Fqbn + "--build-path", $BuildDir + "--warnings", $Warnings + "--build-property", "compiler.cpp.extra_flags=$flagsStr" + "--build-property", "compiler.c.extra_flags=$flagsStr" +) + +if ($verbose) { + $compileArgs += "--verbose" +} + +$compileArgs += $SketchDir + +& arduino-cli @compileArgs +if ($LASTEXITCODE -ne 0) { + Write-Host "" + Fail "Compilation failed." +} + +Write-Host "" +Ok "Compile succeeded." +Write-Host "" +exit 0 \ No newline at end of file diff --git a/tests/script_execution_test.rs.disable b/tests/script_execution_test.rs similarity index 98% rename from tests/script_execution_test.rs.disable rename to tests/script_execution_test.rs index ca8d4b0..9fe9378 100644 --- a/tests/script_execution_test.rs.disable +++ b/tests/script_execution_test.rs @@ -749,10 +749,12 @@ fn test_cmake_lists_fetches_google_test() { fn test_scripts_all_reference_anvil_toml() { let tmp = extract_project("toml_refs"); - // Build and upload scripts must read .anvil.toml for configuration + // Build and upload scripts must read .anvil.toml for configuration. + // On Windows, build.bat is a thin wrapper that calls build.ps1, + // so we check the .ps1 file for content. let config_scripts = vec![ "build.sh", - "build.bat", + "build.ps1", "upload.sh", "upload.bat", ]; @@ -777,9 +779,11 @@ fn test_scripts_all_reference_anvil_toml() { fn test_scripts_invoke_arduino_cli_not_anvil() { let tmp = extract_project("no_anvil_dep"); - // Build/upload/monitor scripts must invoke arduino-cli directly + // Build/upload/monitor scripts must invoke arduino-cli directly. + // On Windows, build.bat is a thin wrapper calling build.ps1, + // so we check the .ps1 file for content. let scripts = vec![ - "build.sh", "build.bat", + "build.sh", "build.ps1", "upload.sh", "upload.bat", "monitor.sh", "monitor.bat", ]; @@ -829,6 +833,10 @@ fn test_scripts_invoke_arduino_cli_not_anvil() { || trimmed.starts_with("Write-Host") || trimmed.starts_with("Write-Error") || trimmed.starts_with("Write-Warning") + || trimmed.starts_with("Fail ") + || trimmed.starts_with("Fail(") + || trimmed.starts_with("Fail \"") + || trimmed.starts_with("Fail @") { return false; } @@ -860,6 +868,7 @@ fn test_all_expected_scripts_exist() { let expected = vec![ "build.sh", "build.bat", + "build.ps1", "upload.sh", "upload.bat", "monitor.sh", diff --git a/tests/test_config.rs b/tests/test_config.rs index d803c59..eda7e6c 100644 --- a/tests/test_config.rs +++ b/tests/test_config.rs @@ -574,7 +574,9 @@ fn test_sh_scripts_have_toml_section_get() { #[test] fn test_bat_scripts_have_section_parser() { - // Batch scripts need section-aware TOML parsing for board profiles + // Windows scripts need section-aware TOML parsing for board profiles. + // build.bat delegates to build.ps1; upload.bat and monitor.bat may + // still use batch-native parsing or their own .ps1 backends. let tmp = TempDir::new().unwrap(); let ctx = TemplateContext { project_name: "bat_section".to_string(), @@ -585,12 +587,33 @@ fn test_bat_scripts_have_section_parser() { }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); - for bat in &["build.bat", "upload.bat", "monitor.bat"] { - let content = fs::read_to_string(tmp.path().join(bat)).unwrap(); + // Check each Windows script OR its PowerShell backend for section parsing + let pairs: &[(&str, &str)] = &[ + ("build.bat", "build.ps1"), + ("upload.bat", "upload.ps1"), + ("monitor.bat", "monitor.ps1"), + ]; + + for (bat, ps1) in pairs { + let bat_path = tmp.path().join(bat); + let ps1_path = tmp.path().join(ps1); + + let has_parser = if ps1_path.exists() { + // PowerShell backend handles TOML parsing + let content = fs::read_to_string(&ps1_path).unwrap(); + content.contains("boards.") || content.contains("currentSection") + } else if bat_path.exists() { + // Batch does its own section parsing + let content = fs::read_to_string(&bat_path).unwrap(); + content.contains("BOARD_SECTION") || content.contains("IN_SECTION") + } else { + false + }; + assert!( - content.contains("BOARD_SECTION") || content.contains("IN_SECTION"), - "{} should have section parser for board profiles", - bat + has_parser, + "{} (or {}) should have section parser for board profiles", + bat, ps1 ); } } diff --git a/tests/test_scripts.rs b/tests/test_scripts.rs index 64bcd36..46619a6 100644 --- a/tests/test_scripts.rs +++ b/tests/test_scripts.rs @@ -489,7 +489,7 @@ fn test_refresh_freshly_extracted_is_up_to_date() { TemplateManager::extract("basic", reference.path(), &ctx).unwrap(); let refreshable = vec![ - "build.sh", "build.bat", + "build.sh", "build.bat", "build.ps1", "upload.sh", "upload.bat", "monitor.sh", "monitor.bat", "test.sh", "test.bat", @@ -561,7 +561,7 @@ fn test_refresh_does_not_list_user_files() { ]; let refreshable = vec![ - "build.sh", "build.bat", + "build.sh", "build.bat", "build.ps1", "upload.sh", "upload.bat", "monitor.sh", "monitor.bat", "test.sh", "test.bat", @@ -644,12 +644,14 @@ fn test_scripts_read_default_board() { ); } - for bat in &["build.bat", "upload.bat", "monitor.bat"] { - let content = fs::read_to_string(tmp.path().join(bat)).unwrap(); + // build.bat is now a thin wrapper; build.ps1 has the real logic. + // upload.bat and monitor.bat still have batch-native parsing. + for script in &["build.ps1", "upload.bat", "monitor.bat"] { + let content = fs::read_to_string(tmp.path().join(script)).unwrap(); assert!( - content.contains("DEFAULT_BOARD"), - "{} should read default field into DEFAULT_BOARD", - bat + content.contains("DEFAULT_BOARD") || content.contains("DefaultBoard") || content.contains("default"), + "{} should read default field for board selection", + script ); } } @@ -674,8 +676,9 @@ fn test_scripts_use_compiler_extra_flags_not_build() { }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); + // build.bat is a thin wrapper; check build.ps1 for content let compile_scripts = vec![ - "build.sh", "build.bat", + "build.sh", "build.ps1", "upload.sh", "upload.bat", ]; @@ -743,8 +746,9 @@ fn test_script_errors_show_manual_fix() { }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); + // build.bat is a thin wrapper; check build.ps1 for content let all_scripts = vec![ - "build.sh", "build.bat", + "build.sh", "build.ps1", "upload.sh", "upload.bat", "monitor.sh", "monitor.bat", ]; @@ -752,7 +756,7 @@ fn test_script_errors_show_manual_fix() { for script in &all_scripts { let content = fs::read_to_string(tmp.path().join(script)).unwrap(); assert!( - content.contains("default = "), + content.contains("default = ") || content.contains("default ="), "{} error messages should show the manual fix (default = \"...\")", script ); @@ -773,8 +777,9 @@ fn test_script_errors_mention_arduino_cli() { }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); + // build.bat is a thin wrapper; check build.ps1 for content let all_scripts = vec![ - "build.sh", "build.bat", + "build.sh", "build.ps1", "upload.sh", "upload.bat", "monitor.sh", "monitor.bat", ]; @@ -803,8 +808,8 @@ fn test_script_errors_mention_toml_section_syntax() { }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); - // build and upload scripts have both no-default and board-not-found errors - for script in &["build.sh", "build.bat", "upload.sh", "upload.bat"] { + // build.bat is a thin wrapper; check build.ps1 for content + for script in &["build.sh", "build.ps1", "upload.sh", "upload.bat"] { let content = fs::read_to_string(tmp.path().join(script)).unwrap(); assert!( content.contains("[boards."), @@ -932,11 +937,12 @@ fn test_build_scripts_autodiscover_driver_includes() { }; TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); + // build.bat is a thin wrapper; check build.ps1 for content. // All four compile scripts must auto-discover lib/drivers/* - for script in &["build.sh", "upload.sh", "build.bat", "upload.bat"] { + for script in &["build.sh", "upload.sh", "build.ps1", "upload.bat"] { let content = fs::read_to_string(tmp.path().join(script)).unwrap(); assert!( - content.contains("lib/drivers") || content.contains("lib\\drivers"), + content.contains("lib/drivers") || content.contains("lib\\drivers") || content.contains("lib\\\\drivers"), "{} must auto-discover lib/drivers/* for library include paths", script );