diff --git a/README.md b/README.md index 15835a6..ab5cc94 100644 --- a/README.md +++ b/README.md @@ -2,9 +2,13 @@ **Arduino project generator and build tool -- forges clean embedded projects.** -A single binary that scaffolds testable Arduino projects with hardware abstraction, -Google Mock infrastructure, and a streamlined build/upload/monitor workflow. Works on -Linux and Windows. +A single binary that scaffolds self-contained Arduino projects with hardware +abstraction, Google Mock infrastructure, and a streamlined build/upload/monitor +workflow. Works on Linux and Windows. + +Generated projects are fully standalone -- they only need `arduino-cli` in +PATH. The Anvil binary is a scaffolding and diagnostic tool, not a runtime +dependency. Anvil is a [Nexus Workshops](https://nxlearn.net) project. @@ -36,20 +40,37 @@ your system is ready. # Create a new project anvil new blink -# Check system health -anvil doctor +# Enter the project +cd blink -# Find your board -anvil devices +# Compile (verify only) +./build.sh + +# Compile and upload to board +./upload.sh # Compile, upload, and open serial monitor -cd blink -anvil build --monitor blink +./upload.sh --monitor # Run host-side tests (no board needed) -cd test && ./run_tests.sh +./test/run_tests.sh ``` +On Windows, use `build.bat`, `upload.bat`, `monitor.bat`, and +`test\run_tests.bat`. + +## What Anvil Does vs. What the Project Does + +| Need Anvil for | Don't need Anvil for | +|-------------------------------|-------------------------------| +| `anvil new` (create project) | `./build.sh` (compile) | +| `anvil doctor` (diagnose) | `./upload.sh` (flash) | +| `anvil setup` (install core) | `./monitor.sh` (serial) | +| `anvil devices` (port scan) | `./test/run_tests.sh` (test) | + +Once a project is created, Anvil is optional. Students clone the repo, +plug in a board, and run `./upload.sh`. + ## Commands | Command | Description | @@ -58,9 +79,12 @@ cd test && ./run_tests.sh | `anvil doctor` | Check system prerequisites | | `anvil setup` | Install arduino-cli and AVR core | | `anvil devices` | List connected boards and serial ports | -| `anvil build DIR` | Compile and upload a sketch | -| `anvil upload DIR`| Upload cached build (no recompile) | -| `anvil monitor` | Open serial monitor (`--watch` for persistent) | +| `anvil build DIR` | Compile and upload a sketch (convenience) | +| `anvil upload DIR`| Upload cached build (convenience) | +| `anvil monitor` | Open serial monitor (convenience) | + +The `build`, `upload`, and `monitor` commands are convenience wrappers. +They do the same thing as the generated scripts. ## Project Architecture @@ -74,6 +98,9 @@ your-project/ lib/app/your-project_app.h -- app logic (testable) test/mocks/mock_hal.h -- Google Mock HAL test/test_unit.cpp -- unit tests + build.sh / build.bat -- compile + upload.sh / upload.bat -- compile + flash + monitor.sh / monitor.bat -- serial monitor .anvil.toml -- project config ``` diff --git a/anvil_src.zip b/anvil_src.zip deleted file mode 100644 index 86df857..0000000 Binary files a/anvil_src.zip and /dev/null differ diff --git a/src/commands/new.rs b/src/commands/new.rs index 338a938..250168b 100644 --- a/src/commands/new.rs +++ b/src/commands/new.rs @@ -177,7 +177,12 @@ fn init_git(project_dir: &PathBuf, template_name: &str) { fn make_executable(project_dir: &PathBuf) { use std::os::unix::fs::PermissionsExt; - let scripts = ["test/run_tests.sh"]; + let scripts = [ + "build.sh", + "upload.sh", + "monitor.sh", + "test/run_tests.sh", + ]; for script in &scripts { let path = project_dir.join(script); if path.exists() { @@ -196,23 +201,36 @@ fn print_next_steps(project_name: &str) { " 1. {}", format!("cd {}", project_name).bright_cyan() ); - println!(" 2. Check your system: {}", "anvil doctor".bright_cyan()); println!( - " 3. Find your board: {}", - "anvil devices".bright_cyan() + " 2. Compile: {}", + "./build.sh".bright_cyan() ); println!( - " 4. Build and upload: {}", - format!("anvil build {}", project_name).bright_cyan() + " 3. Upload to board: {}", + "./upload.sh".bright_cyan() ); println!( - " 5. Build + monitor: {}", - format!("anvil build --monitor {}", project_name).bright_cyan() + " 4. Upload + monitor: {}", + "./upload.sh --monitor".bright_cyan() + ); + println!( + " 5. Serial monitor: {}", + "./monitor.sh".bright_cyan() + ); + println!( + " 6. Run host tests: {}", + "./test/run_tests.sh".bright_cyan() ); println!(); println!( - " Run host tests: {}", - "cd test && ./run_tests.sh".bright_cyan() + " {}", + "On Windows: build.bat, upload.bat, monitor.bat, test\\run_tests.bat" + .bright_black() + ); + println!( + " {}", + "System check: anvil doctor | Port scan: anvil devices" + .bright_black() ); println!(); } diff --git a/templates/basic/README.md.tmpl b/templates/basic/README.md.tmpl index 4bae2aa..ba591aa 100644 --- a/templates/basic/README.md.tmpl +++ b/templates/basic/README.md.tmpl @@ -1,29 +1,38 @@ # {{PROJECT_NAME}} -Arduino project generated by Anvil v{{ANVIL_VERSION}}. +Arduino project generated by [Anvil](https://github.com/nexusworkshops/anvil) v{{ANVIL_VERSION}}. + +This project is self-contained. After creation, it only needs `arduino-cli` +in PATH -- the Anvil binary is not required for day-to-day work. ## Quick Start ```bash -# Check your system -anvil doctor +# Compile only (verify) +./build.sh -# Find connected boards -anvil devices - -# Compile only (no upload) -anvil build --verify {{PROJECT_NAME}} - -# Compile and upload -anvil build {{PROJECT_NAME}} +# Compile and upload to board +./upload.sh # Compile, upload, and open serial monitor -anvil build --monitor {{PROJECT_NAME}} +./upload.sh --monitor + +# Open serial monitor (no compile) +./monitor.sh + +# Persistent monitor (reconnects after reset/replug) +./monitor.sh --watch # Run host-side unit tests (no board needed) -cd test && ./run_tests.sh +./test/run_tests.sh ``` +On Windows, use `build.bat`, `upload.bat`, `monitor.bat`, and +`test\run_tests.bat` instead. + +All scripts read settings from `.anvil.toml` -- edit it to change +the board, baud rate, include paths, or compiler flags. + ## Project Structure ``` @@ -44,6 +53,9 @@ cd test && ./run_tests.sh CMakeLists.txt Test build system run_tests.sh Test runner (Linux/Mac) run_tests.bat Test runner (Windows) + build.sh / build.bat Compile sketch + upload.sh / upload.bat Compile + upload to board + monitor.sh / monitor.bat Serial monitor .anvil.toml Project configuration ``` @@ -52,7 +64,7 @@ cd test && ./run_tests.sh All hardware access goes through the `Hal` interface. The app code (`lib/app/`) depends only on `Hal`, never on `Arduino.h` directly. This means the app can be compiled and tested on the host without -any Arduino SDK. +any Arduino hardware. Two HAL implementations: - `ArduinoHal` -- passthroughs to real hardware (used in the .ino) @@ -72,3 +84,9 @@ extra_flags = ["-Werror"] [monitor] baud = 115200 ``` + +## Prerequisites + +- `arduino-cli` in PATH with `arduino:avr` core installed +- For host tests: `cmake`, `g++` (or `clang++`), `git` +- Install everything at once: `anvil setup` diff --git a/templates/basic/_dot_gitignore b/templates/basic/_dot_gitignore index 36539fa..6032bd0 100644 --- a/templates/basic/_dot_gitignore +++ b/templates/basic/_dot_gitignore @@ -1,4 +1,5 @@ # Build artifacts +.build/ test/build/ # IDE diff --git a/templates/basic/build.bat b/templates/basic/build.bat new file mode 100644 index 0000000..4983f00 --- /dev/null +++ b/templates/basic/build.bat @@ -0,0 +1,126 @@ +@echo off +setlocal enabledelayedexpansion + +:: build.bat -- Compile the sketch using arduino-cli +:: +:: Reads all settings from .anvil.toml. No Anvil binary required. +:: +:: Usage: +:: build.bat Compile (verify only) +:: build.bat --clean Delete build cache first +:: build.bat --verbose Show full compiler output + +set "SCRIPT_DIR=%~dp0" +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 ---------------------------------------------------- +for /f "tokens=1,* delims==" %%a in ('findstr /b "name " "%CONFIG%"') do ( + set "SKETCH_NAME=%%b" +) +for /f "tokens=1,* delims==" %%a in ('findstr /b "fqbn " "%CONFIG%"') do ( + set "FQBN=%%b" +) +for /f "tokens=1,* delims==" %%a in ('findstr /b "warnings " "%CONFIG%"') do ( + set "WARNINGS=%%b" +) + +:: Strip quotes and whitespace +set "SKETCH_NAME=%SKETCH_NAME: =%" +set "SKETCH_NAME=%SKETCH_NAME:"=%" +set "FQBN=%FQBN: =%" +set "FQBN=%FQBN:"=%" +set "WARNINGS=%WARNINGS: =%" +set "WARNINGS=%WARNINGS:"=%" + +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=" + +:parse_args +if "%~1"=="" goto done_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 [--clean] [--verbose] +echo Compiles the sketch. Settings from .anvil.toml. +exit /b 0 + +:done_args + +:: -- 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" + ) +) +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%" + +set "COMPILE_CMD=arduino-cli compile --fqbn %FQBN% --build-path "%BUILD_DIR%" --warnings %WARNINGS%" + +if not "%BUILD_FLAGS%"=="" ( + set "COMPILE_CMD=%COMPILE_CMD% --build-property "build.extra_flags=%BUILD_FLAGS%"" +) + +if not "%VERBOSE%"=="" ( + set "COMPILE_CMD=%COMPILE_CMD% %VERBOSE%" +) + +set "COMPILE_CMD=%COMPILE_CMD% "%SKETCH_DIR%"" + +%COMPILE_CMD% +if errorlevel 1 ( + echo. + echo FAIL: Compilation failed. + exit /b 1 +) + +echo. +echo ok Compile succeeded. +echo. diff --git a/templates/basic/build.sh b/templates/basic/build.sh new file mode 100644 index 0000000..f0bbc0a --- /dev/null +++ b/templates/basic/build.sh @@ -0,0 +1,145 @@ +#!/usr/bin/env bash +# +# build.sh -- Compile the sketch using arduino-cli +# +# Reads all settings from .anvil.toml. No Anvil binary required. +# +# Usage: +# ./build.sh Compile (verify only) +# ./build.sh --clean Delete build cache first +# ./build.sh --verbose Show full compiler output +# +# Prerequisites: arduino-cli in PATH, arduino:avr core installed +# Install: anvil setup (or manually: arduino-cli core install arduino:avr) + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +CONFIG="$SCRIPT_DIR/.anvil.toml" + +# -- Colors ---------------------------------------------------------------- +if [[ -t 1 ]]; then + RED=$'\033[0;31m'; GRN=$'\033[0;32m'; YLW=$'\033[0;33m' + CYN=$'\033[0;36m'; BLD=$'\033[1m'; RST=$'\033[0m' +else + RED=''; GRN=''; YLW=''; CYN=''; BLD=''; RST='' +fi + +ok() { echo "${GRN}ok${RST} $*"; } +warn() { echo "${YLW}warn${RST} $*"; } +die() { echo "${RED}FAIL${RST} $*" >&2; exit 1; } + +# -- Parse .anvil.toml ----------------------------------------------------- +[[ -f "$CONFIG" ]] || die "No .anvil.toml found in $SCRIPT_DIR" + +# Extract a simple string value: toml_get "key" +# Searches the whole file; for sectioned keys, grep is specific enough +# given our small, flat schema. +toml_get() { + grep "^$1 " "$CONFIG" | head -1 | sed 's/.*= *"\{0,1\}\([^"]*\)"\{0,1\}/\1/' | tr -d ' ' +} + +# Extract a TOML array as space-separated values: toml_array "key" +toml_array() { + grep "^$1 " "$CONFIG" | head -1 \ + | sed 's/.*\[//; s/\].*//; s/"//g; s/,/ /g' | tr -s ' ' +} + +SKETCH_NAME="$(toml_get 'name')" +FQBN="$(toml_get 'fqbn')" +WARNINGS="$(toml_get 'warnings')" +INCLUDE_DIRS="$(toml_array 'include_dirs')" +EXTRA_FLAGS="$(toml_array 'extra_flags')" + +[[ -n "$SKETCH_NAME" ]] || die "Could not read project name from .anvil.toml" +[[ -n "$FQBN" ]] || die "Could not read fqbn from .anvil.toml" + +SKETCH_DIR="$SCRIPT_DIR/$SKETCH_NAME" +BUILD_DIR="$SCRIPT_DIR/.build" + +# -- Parse arguments ------------------------------------------------------- +DO_CLEAN=0 +VERBOSE="" + +for arg in "$@"; do + case "$arg" in + --clean) DO_CLEAN=1 ;; + --verbose) VERBOSE="--verbose" ;; + -h|--help) + echo "Usage: ./build.sh [--clean] [--verbose]" + echo " Compiles the sketch. Settings from .anvil.toml." + exit 0 + ;; + *) die "Unknown option: $arg" ;; + esac +done + +# -- Preflight ------------------------------------------------------------- +command -v arduino-cli &>/dev/null \ + || die "arduino-cli not found in PATH. Install it first." + +[[ -d "$SKETCH_DIR" ]] \ + || die "Sketch directory not found: $SKETCH_DIR" + +[[ -f "$SKETCH_DIR/$SKETCH_NAME.ino" ]] \ + || die "Sketch file not found: $SKETCH_DIR/$SKETCH_NAME.ino" + +# -- Clean ----------------------------------------------------------------- +if [[ $DO_CLEAN -eq 1 ]] && [[ -d "$BUILD_DIR" ]]; then + echo "${YLW}Cleaning build cache...${RST}" + rm -rf "$BUILD_DIR" + ok "Cache cleared." +fi + +# -- Build include flags --------------------------------------------------- +BUILD_FLAGS="" +for dir in $INCLUDE_DIRS; do + abs="$SCRIPT_DIR/$dir" + if [[ -d "$abs" ]]; then + BUILD_FLAGS="$BUILD_FLAGS -I$abs" + else + warn "Include directory not found: $dir" + fi +done +for flag in $EXTRA_FLAGS; do + BUILD_FLAGS="$BUILD_FLAGS $flag" +done + +# -- Compile --------------------------------------------------------------- +echo "${CYN}${BLD}Compiling ${SKETCH_NAME}...${RST}" +echo " Board: $FQBN" +echo " Sketch: $SKETCH_DIR" +echo "" + +mkdir -p "$BUILD_DIR" + +COMPILE_ARGS=( + compile + --fqbn "$FQBN" + --build-path "$BUILD_DIR" + --warnings "$WARNINGS" +) + +if [[ -n "$BUILD_FLAGS" ]]; then + COMPILE_ARGS+=(--build-property "build.extra_flags=$BUILD_FLAGS") +fi + +if [[ -n "$VERBOSE" ]]; then + COMPILE_ARGS+=("$VERBOSE") +fi + +COMPILE_ARGS+=("$SKETCH_DIR") + +arduino-cli "${COMPILE_ARGS[@]}" || die "Compilation failed." + +echo "" +ok "Compile succeeded." + +# -- Binary size ----------------------------------------------------------- +ELF="$BUILD_DIR/$SKETCH_NAME.ino.elf" +if [[ -f "$ELF" ]] && command -v avr-size &>/dev/null; then + echo "" + avr-size --mcu=atmega328p -C "$ELF" +fi + +echo "" diff --git a/templates/basic/monitor.bat b/templates/basic/monitor.bat new file mode 100644 index 0000000..2291038 --- /dev/null +++ b/templates/basic/monitor.bat @@ -0,0 +1,74 @@ +@echo off +setlocal enabledelayedexpansion + +:: monitor.bat -- Open the serial monitor +:: +:: Reads baud rate from .anvil.toml. No Anvil binary required. +:: +:: Usage: +:: monitor.bat Open monitor (auto-detect port) +:: monitor.bat -p COM3 Specify port +:: monitor.bat -b 9600 Override baud rate + +set "SCRIPT_DIR=%~dp0" +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 ---------------------------------------------------- +for /f "tokens=1,* delims==" %%a in ('findstr /b "baud " "%CONFIG%"') do ( + set "BAUD=%%b" +) +set "BAUD=%BAUD: =%" +set "BAUD=%BAUD:"=%" +if "%BAUD%"=="" set "BAUD=115200" + +:: -- Parse arguments ------------------------------------------------------ +set "PORT=" + +:parse_args +if "%~1"=="" goto done_args +if "%~1"=="-p" set "PORT=%~2" & shift & shift & goto parse_args +if "%~1"=="--port" set "PORT=%~2" & shift & shift & goto parse_args +if "%~1"=="-b" set "BAUD=%~2" & shift & shift & goto parse_args +if "%~1"=="--baud" set "BAUD=%~2" & shift & 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: monitor.bat [-p PORT] [-b BAUD] +echo Opens serial monitor. Baud rate from .anvil.toml. +exit /b 0 + +:done_args + +:: -- Preflight ------------------------------------------------------------ +where arduino-cli >nul 2>nul +if errorlevel 1 ( + echo FAIL: arduino-cli not found in PATH. + exit /b 1 +) + +:: -- Auto-detect port ----------------------------------------------------- +if "%PORT%"=="" ( + for /f "tokens=1" %%p in ('arduino-cli board list 2^>nul ^| findstr /i "serial" ^| findstr /n "." ^| findstr "^1:"') do ( + set "PORT=%%p" + ) + set "PORT=!PORT:1:=!" + if "!PORT!"=="" ( + echo FAIL: No serial port detected. Specify with: monitor.bat -p COM3 + exit /b 1 + ) + echo warn Auto-detected port: !PORT! (use -p to override) +) + +:: -- Monitor -------------------------------------------------------------- +echo Opening serial monitor on %PORT% at %BAUD% baud... +echo Press Ctrl+C to exit. +echo. +arduino-cli monitor -p %PORT% -c "baudrate=%BAUD%" diff --git a/templates/basic/monitor.sh b/templates/basic/monitor.sh new file mode 100644 index 0000000..2e037ee --- /dev/null +++ b/templates/basic/monitor.sh @@ -0,0 +1,107 @@ +#!/usr/bin/env bash +# +# monitor.sh -- Open the serial monitor +# +# Reads baud rate from .anvil.toml. No Anvil binary required. +# +# Usage: +# ./monitor.sh Auto-detect port +# ./monitor.sh -p /dev/ttyUSB0 Specify port +# ./monitor.sh -b 9600 Override baud rate +# ./monitor.sh --watch Reconnect after reset/replug +# +# Prerequisites: arduino-cli in PATH + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +CONFIG="$SCRIPT_DIR/.anvil.toml" + +# -- Colors ---------------------------------------------------------------- +if [[ -t 1 ]]; then + RED=$'\033[0;31m'; GRN=$'\033[0;32m'; YLW=$'\033[0;33m' + CYN=$'\033[0;36m'; BLD=$'\033[1m'; RST=$'\033[0m' +else + RED=''; GRN=''; YLW=''; CYN=''; BLD=''; RST='' +fi + +warn() { echo "${YLW}warn${RST} $*"; } +die() { echo "${RED}FAIL${RST} $*" >&2; exit 1; } + +# -- Parse .anvil.toml ----------------------------------------------------- +[[ -f "$CONFIG" ]] || die "No .anvil.toml found in $SCRIPT_DIR" + +toml_get() { + grep "^$1 " "$CONFIG" | head -1 | sed 's/.*= *"\{0,1\}\([^"]*\)"\{0,1\}/\1/' | tr -d ' ' +} + +BAUD="$(toml_get 'baud')" +BAUD="${BAUD:-115200}" + +# -- Parse arguments ------------------------------------------------------- +PORT="" +DO_WATCH=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + -p|--port) PORT="$2"; shift 2 ;; + -b|--baud) BAUD="$2"; shift 2 ;; + --watch) DO_WATCH=1; shift ;; + -h|--help) + echo "Usage: ./monitor.sh [-p PORT] [-b BAUD] [--watch]" + echo " Opens serial monitor. Baud rate from .anvil.toml." + exit 0 + ;; + *) die "Unknown option: $1" ;; + esac +done + +# -- Preflight ------------------------------------------------------------- +command -v arduino-cli &>/dev/null \ + || die "arduino-cli not found in PATH." + +# -- Auto-detect port ------------------------------------------------------ +auto_detect() { + arduino-cli board list 2>/dev/null \ + | grep -i "serial" \ + | head -1 \ + | awk '{print $1}' +} + +if [[ -z "$PORT" ]]; then + PORT="$(auto_detect)" + if [[ -z "$PORT" ]]; then + die "No serial port detected. Is the board plugged in?\n Specify manually: ./monitor.sh -p /dev/ttyUSB0" + fi + warn "Auto-detected port: $PORT (use -p to override)" +fi + +# -- Watch mode ------------------------------------------------------------ +if [[ $DO_WATCH -eq 1 ]]; then + echo "${CYN}${BLD}Persistent monitor on ${PORT} at ${BAUD} baud${RST}" + echo "Reconnects after upload / reset / replug." + echo "Press Ctrl+C to exit." + echo "" + + trap "echo ''; echo 'Monitor stopped.'; exit 0" INT + + while true; do + if [[ -e "$PORT" ]]; then + arduino-cli monitor -p "$PORT" -c "baudrate=$BAUD" 2>/dev/null || true + echo "${YLW}--- ${PORT} disconnected ---${RST}" + else + echo "${CYN}--- Waiting for ${PORT} ...${RST}" + while [[ ! -e "$PORT" ]]; do + sleep 0.5 + done + sleep 1 + echo "${GRN}--- ${PORT} connected ---${RST}" + fi + sleep 0.5 + done +else + echo "Opening serial monitor on $PORT at $BAUD baud..." + echo "Press Ctrl+C to exit." + echo "" + arduino-cli monitor -p "$PORT" -c "baudrate=$BAUD" +fi diff --git a/templates/basic/upload.bat b/templates/basic/upload.bat new file mode 100644 index 0000000..25bb8ef --- /dev/null +++ b/templates/basic/upload.bat @@ -0,0 +1,144 @@ +@echo off +setlocal enabledelayedexpansion + +:: upload.bat -- Compile and upload the sketch to the board +:: +:: Reads all settings from .anvil.toml. No Anvil binary required. +:: +:: Usage: +:: upload.bat Auto-detect port, compile + upload +:: upload.bat -p COM3 Specify port +:: upload.bat --monitor Open serial monitor after upload +:: upload.bat --clean Clean build cache first +:: upload.bat --verbose Full compiler + avrdude output + +set "SCRIPT_DIR=%~dp0" +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 ---------------------------------------------------- +for /f "tokens=1,* delims==" %%a in ('findstr /b "name " "%CONFIG%"') do ( + set "SKETCH_NAME=%%b" +) +for /f "tokens=1,* delims==" %%a in ('findstr /b "fqbn " "%CONFIG%"') do ( + set "FQBN=%%b" +) +for /f "tokens=1,* delims==" %%a in ('findstr /b "warnings " "%CONFIG%"') do ( + set "WARNINGS=%%b" +) +for /f "tokens=1,* delims==" %%a in ('findstr /b "baud " "%CONFIG%"') do ( + set "BAUD=%%b" +) + +set "SKETCH_NAME=%SKETCH_NAME: =%" +set "SKETCH_NAME=%SKETCH_NAME:"=%" +set "FQBN=%FQBN: =%" +set "FQBN=%FQBN:"=%" +set "WARNINGS=%WARNINGS: =%" +set "WARNINGS=%WARNINGS:"=%" +set "BAUD=%BAUD: =%" +set "BAUD=%BAUD:"=%" + +if "%SKETCH_NAME%"=="" ( + echo FAIL: Could not read project name from .anvil.toml + exit /b 1 +) +if "%BAUD%"=="" set "BAUD=115200" + +set "SKETCH_DIR=%SCRIPT_DIR%%SKETCH_NAME%" +set "BUILD_DIR=%SCRIPT_DIR%.build" + +:: -- Parse arguments ------------------------------------------------------ +set "PORT=" +set "DO_MONITOR=0" +set "DO_CLEAN=0" +set "VERBOSE=" + +:parse_args +if "%~1"=="" goto done_args +if "%~1"=="-p" set "PORT=%~2" & shift & shift & goto parse_args +if "%~1"=="--port" set "PORT=%~2" & shift & shift & goto parse_args +if "%~1"=="--monitor" set "DO_MONITOR=1" & 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: upload.bat [-p PORT] [--monitor] [--clean] [--verbose] +echo Compiles and uploads the sketch. Settings from .anvil.toml. +exit /b 0 + +:done_args + +:: -- Preflight ------------------------------------------------------------ +where arduino-cli >nul 2>nul +if errorlevel 1 ( + echo FAIL: arduino-cli not found in PATH. + exit /b 1 +) + +:: -- Auto-detect port ----------------------------------------------------- +if "%PORT%"=="" ( + for /f "tokens=1" %%p in ('arduino-cli board list 2^>nul ^| findstr /i "serial" ^| findstr /n "." ^| findstr "^1:"') do ( + set "PORT=%%p" + ) + :: Strip the line number prefix + set "PORT=!PORT:1:=!" + if "!PORT!"=="" ( + echo FAIL: No serial port detected. Specify with: upload.bat -p COM3 + exit /b 1 + ) + echo warn Auto-detected port: !PORT! (use -p to override) +) + +:: -- Clean ---------------------------------------------------------------- +if "%DO_CLEAN%"=="1" ( + if exist "%BUILD_DIR%" 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" + ) +) +set "BUILD_FLAGS=!BUILD_FLAGS! -Werror" + +:: -- Compile -------------------------------------------------------------- +echo Compiling %SKETCH_NAME%... +if not exist "%BUILD_DIR%" mkdir "%BUILD_DIR%" + +arduino-cli compile --fqbn %FQBN% --build-path "%BUILD_DIR%" --warnings %WARNINGS% --build-property "build.extra_flags=%BUILD_FLAGS%" %VERBOSE% "%SKETCH_DIR%" +if errorlevel 1 ( + echo FAIL: Compilation failed. + exit /b 1 +) +echo ok Compile succeeded. + +:: -- Upload --------------------------------------------------------------- +echo. +echo Uploading to %PORT%... + +arduino-cli upload --fqbn %FQBN% --port %PORT% --input-dir "%BUILD_DIR%" %VERBOSE% +if errorlevel 1 ( + echo FAIL: Upload failed. + exit /b 1 +) +echo ok Upload complete! + +:: -- Monitor -------------------------------------------------------------- +if "%DO_MONITOR%"=="1" ( + echo. + echo Opening serial monitor on %PORT% at %BAUD% baud... + echo Press Ctrl+C to exit. + echo. + arduino-cli monitor -p %PORT% -c "baudrate=%BAUD%" +) diff --git a/templates/basic/upload.sh b/templates/basic/upload.sh new file mode 100644 index 0000000..f0d8644 --- /dev/null +++ b/templates/basic/upload.sh @@ -0,0 +1,164 @@ +#!/usr/bin/env bash +# +# upload.sh -- Compile and upload the sketch to the board +# +# Reads all settings from .anvil.toml. No Anvil binary required. +# +# Usage: +# ./upload.sh Auto-detect port, compile + upload +# ./upload.sh -p /dev/ttyUSB0 Specify port +# ./upload.sh --monitor Open serial monitor after upload +# ./upload.sh --clean Clean build cache first +# ./upload.sh --verbose Full compiler + avrdude output +# +# Prerequisites: arduino-cli in PATH, arduino:avr core installed + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +CONFIG="$SCRIPT_DIR/.anvil.toml" + +# -- Colors ---------------------------------------------------------------- +if [[ -t 1 ]]; then + RED=$'\033[0;31m'; GRN=$'\033[0;32m'; YLW=$'\033[0;33m' + CYN=$'\033[0;36m'; BLD=$'\033[1m'; RST=$'\033[0m' +else + RED=''; GRN=''; YLW=''; CYN=''; BLD=''; RST='' +fi + +ok() { echo "${GRN}ok${RST} $*"; } +warn() { echo "${YLW}warn${RST} $*"; } +die() { echo "${RED}FAIL${RST} $*" >&2; exit 1; } + +# -- Parse .anvil.toml ----------------------------------------------------- +[[ -f "$CONFIG" ]] || die "No .anvil.toml found in $SCRIPT_DIR" + +toml_get() { + grep "^$1 " "$CONFIG" | head -1 | sed 's/.*= *"\{0,1\}\([^"]*\)"\{0,1\}/\1/' | tr -d ' ' +} + +toml_array() { + grep "^$1 " "$CONFIG" | head -1 \ + | sed 's/.*\[//; s/\].*//; s/"//g; s/,/ /g' | tr -s ' ' +} + +SKETCH_NAME="$(toml_get 'name')" +FQBN="$(toml_get 'fqbn')" +WARNINGS="$(toml_get 'warnings')" +INCLUDE_DIRS="$(toml_array 'include_dirs')" +EXTRA_FLAGS="$(toml_array 'extra_flags')" +BAUD="$(toml_get 'baud')" + +[[ -n "$SKETCH_NAME" ]] || die "Could not read project name from .anvil.toml" +[[ -n "$FQBN" ]] || die "Could not read fqbn from .anvil.toml" + +BAUD="${BAUD:-115200}" +SKETCH_DIR="$SCRIPT_DIR/$SKETCH_NAME" +BUILD_DIR="$SCRIPT_DIR/.build" + +# -- Parse arguments ------------------------------------------------------- +PORT="" +DO_MONITOR=0 +DO_CLEAN=0 +VERBOSE="" + +while [[ $# -gt 0 ]]; do + case "$1" in + -p|--port) PORT="$2"; shift 2 ;; + --monitor) DO_MONITOR=1; shift ;; + --clean) DO_CLEAN=1; shift ;; + --verbose) VERBOSE="--verbose"; shift ;; + -h|--help) + echo "Usage: ./upload.sh [-p PORT] [--monitor] [--clean] [--verbose]" + echo " Compiles and uploads the sketch. Settings from .anvil.toml." + exit 0 + ;; + *) die "Unknown option: $1" ;; + esac +done + +# -- Preflight ------------------------------------------------------------- +command -v arduino-cli &>/dev/null \ + || die "arduino-cli not found in PATH." + +[[ -d "$SKETCH_DIR" ]] \ + || die "Sketch directory not found: $SKETCH_DIR" + +# -- Auto-detect port ------------------------------------------------------ +if [[ -z "$PORT" ]]; then + # Look for the first serial port arduino-cli can see + PORT=$(arduino-cli board list 2>/dev/null \ + | grep -i "serial" \ + | head -1 \ + | awk '{print $1}') + + if [[ -z "$PORT" ]]; then + die "No serial port detected. Is the board plugged in?\n Specify manually: ./upload.sh -p /dev/ttyUSB0" + fi + + warn "Auto-detected port: $PORT (use -p to override)" +fi + +# -- Clean ----------------------------------------------------------------- +if [[ $DO_CLEAN -eq 1 ]] && [[ -d "$BUILD_DIR" ]]; then + echo "${YLW}Cleaning build cache...${RST}" + rm -rf "$BUILD_DIR" +fi + +# -- Build include flags --------------------------------------------------- +BUILD_FLAGS="" +for dir in $INCLUDE_DIRS; do + abs="$SCRIPT_DIR/$dir" + if [[ -d "$abs" ]]; then + BUILD_FLAGS="$BUILD_FLAGS -I$abs" + fi +done +for flag in $EXTRA_FLAGS; do + BUILD_FLAGS="$BUILD_FLAGS $flag" +done + +# -- Compile --------------------------------------------------------------- +echo "${CYN}${BLD}Compiling ${SKETCH_NAME}...${RST}" +mkdir -p "$BUILD_DIR" + +COMPILE_ARGS=( + compile + --fqbn "$FQBN" + --build-path "$BUILD_DIR" + --warnings "$WARNINGS" +) + +if [[ -n "$BUILD_FLAGS" ]]; then + COMPILE_ARGS+=(--build-property "build.extra_flags=$BUILD_FLAGS") +fi + +[[ -n "$VERBOSE" ]] && COMPILE_ARGS+=("$VERBOSE") +COMPILE_ARGS+=("$SKETCH_DIR") + +arduino-cli "${COMPILE_ARGS[@]}" || die "Compilation failed." +ok "Compile succeeded." + +# -- Upload ---------------------------------------------------------------- +echo "" +echo "${CYN}${BLD}Uploading to ${PORT}...${RST}" + +UPLOAD_ARGS=( + upload + --fqbn "$FQBN" + --port "$PORT" + --input-dir "$BUILD_DIR" +) + +[[ -n "$VERBOSE" ]] && UPLOAD_ARGS+=("$VERBOSE") + +arduino-cli "${UPLOAD_ARGS[@]}" || die "Upload failed." +ok "Upload complete!" + +# -- Monitor --------------------------------------------------------------- +if [[ $DO_MONITOR -eq 1 ]]; then + echo "" + echo "Opening serial monitor on $PORT at $BAUD baud..." + echo "Press Ctrl+C to exit." + echo "" + arduino-cli monitor -p "$PORT" -c "baudrate=$BAUD" +fi diff --git a/tests/integration_test.rs b/tests/integration_test.rs index 01076e8..fbd07d1 100644 --- a/tests/integration_test.rs +++ b/tests/integration_test.rs @@ -18,7 +18,7 @@ fn test_basic_template_extracts_all_expected_files() { }; let count = TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); - assert!(count >= 10, "Expected at least 10 files, got {}", count); + assert!(count >= 16, "Expected at least 16 files, got {}", count); } #[test] @@ -321,6 +321,12 @@ fn test_full_project_structure() { "lib/hal/hal.h", "lib/hal/hal_arduino.h", "lib/app/full_test_app.h", + "build.sh", + "build.bat", + "upload.sh", + "upload.bat", + "monitor.sh", + "monitor.bat", "test/CMakeLists.txt", "test/test_unit.cpp", "test/run_tests.sh", @@ -406,3 +412,228 @@ fn test_load_config_from_nonproject_fails() { let result = ProjectConfig::load(tmp.path()); assert!(result.is_err()); } + +// ============================================================================ +// Self-contained script tests +// ============================================================================ + +#[test] +fn test_template_creates_self_contained_scripts() { + let tmp = TempDir::new().unwrap(); + let ctx = TemplateContext { + project_name: "standalone".to_string(), + anvil_version: "1.0.0".to_string(), + }; + + TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); + + // All six scripts must exist + let scripts = vec![ + "build.sh", "build.bat", + "upload.sh", "upload.bat", + "monitor.sh", "monitor.bat", + ]; + for script in &scripts { + let p = tmp.path().join(script); + assert!(p.exists(), "Script missing: {}", script); + } +} + +#[test] +fn test_build_sh_reads_anvil_toml() { + let tmp = TempDir::new().unwrap(); + let ctx = TemplateContext { + project_name: "toml_reader".to_string(), + anvil_version: "1.0.0".to_string(), + }; + + TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); + + let content = fs::read_to_string(tmp.path().join("build.sh")).unwrap(); + assert!( + content.contains(".anvil.toml"), + "build.sh should reference .anvil.toml" + ); + assert!( + content.contains("arduino-cli"), + "build.sh should invoke arduino-cli" + ); + assert!( + !content.contains("anvil build"), + "build.sh must NOT depend on the anvil binary" + ); +} + +#[test] +fn test_upload_sh_reads_anvil_toml() { + let tmp = TempDir::new().unwrap(); + let ctx = TemplateContext { + project_name: "uploader".to_string(), + anvil_version: "1.0.0".to_string(), + }; + + TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); + + let content = fs::read_to_string(tmp.path().join("upload.sh")).unwrap(); + assert!( + content.contains(".anvil.toml"), + "upload.sh should reference .anvil.toml" + ); + assert!( + content.contains("arduino-cli"), + "upload.sh should invoke arduino-cli" + ); + assert!( + content.contains("upload"), + "upload.sh should contain upload command" + ); + assert!( + content.contains("--monitor"), + "upload.sh should support --monitor flag" + ); + assert!( + !content.contains("anvil upload"), + "upload.sh must NOT depend on the anvil binary" + ); +} + +#[test] +fn test_monitor_sh_reads_anvil_toml() { + let tmp = TempDir::new().unwrap(); + let ctx = TemplateContext { + project_name: "serial_mon".to_string(), + anvil_version: "1.0.0".to_string(), + }; + + TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); + + let content = fs::read_to_string(tmp.path().join("monitor.sh")).unwrap(); + assert!( + content.contains(".anvil.toml"), + "monitor.sh should reference .anvil.toml" + ); + assert!( + content.contains("--watch"), + "monitor.sh should support --watch flag" + ); + assert!( + !content.contains("anvil monitor"), + "monitor.sh must NOT depend on the anvil binary" + ); +} + +#[test] +fn test_scripts_have_shebangs() { + let tmp = TempDir::new().unwrap(); + let ctx = TemplateContext { + project_name: "shebangs".to_string(), + anvil_version: "1.0.0".to_string(), + }; + + TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); + + for script in &["build.sh", "upload.sh", "monitor.sh", "test/run_tests.sh"] { + let content = fs::read_to_string(tmp.path().join(script)).unwrap(); + assert!( + content.starts_with("#!/"), + "{} should start with a shebang line", + script + ); + } +} + +#[test] +fn test_scripts_no_anvil_binary_dependency() { + // Critical: generated projects must NOT require the anvil binary + // for build, upload, or monitor operations. + let tmp = TempDir::new().unwrap(); + let ctx = TemplateContext { + project_name: "no_anvil_dep".to_string(), + anvil_version: "1.0.0".to_string(), + }; + + TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); + + let scripts = vec![ + "build.sh", "build.bat", + "upload.sh", "upload.bat", + "monitor.sh", "monitor.bat", + "test/run_tests.sh", "test/run_tests.bat", + ]; + + for script in &scripts { + let content = fs::read_to_string(tmp.path().join(script)).unwrap(); + // None of these scripts should shell out to anvil + let has_anvil_cmd = content.lines().any(|line| { + let trimmed = line.trim(); + // Skip comments and echo/print lines + if trimmed.starts_with('#') + || trimmed.starts_with("::") + || trimmed.starts_with("echo") + || trimmed.starts_with("REM") + || trimmed.starts_with("rem") + { + return false; + } + // Check for "anvil " as a command invocation + trimmed.contains("anvil ") + && !trimmed.contains("anvil.toml") + && !trimmed.contains("Anvil") + }); + assert!( + !has_anvil_cmd, + "{} should not invoke the anvil binary (project must be self-contained)", + script + ); + } +} + +#[test] +fn test_gitignore_excludes_build_cache() { + let tmp = TempDir::new().unwrap(); + let ctx = TemplateContext { + project_name: "gitcheck".to_string(), + anvil_version: "1.0.0".to_string(), + }; + + TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); + + let content = fs::read_to_string(tmp.path().join(".gitignore")).unwrap(); + assert!( + content.contains(".build/"), + ".gitignore should exclude .build/ (arduino-cli build cache)" + ); + assert!( + content.contains("test/build/"), + ".gitignore should exclude test/build/ (cmake build cache)" + ); +} + +#[test] +fn test_readme_documents_self_contained_workflow() { + let tmp = TempDir::new().unwrap(); + let ctx = TemplateContext { + project_name: "docs_check".to_string(), + anvil_version: "1.0.0".to_string(), + }; + + TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); + + let readme = fs::read_to_string(tmp.path().join("README.md")).unwrap(); + assert!( + readme.contains("./build.sh"), + "README should document build.sh" + ); + assert!( + readme.contains("./upload.sh"), + "README should document upload.sh" + ); + assert!( + readme.contains("./monitor.sh"), + "README should document monitor.sh" + ); + assert!( + readme.contains("self-contained"), + "README should mention self-contained" + ); +}