From fc1fb73d5a32ceef63edf62f6f97fc0035083ca1 Mon Sep 17 00:00:00 2001 From: Eric Ratliff Date: Mon, 16 Feb 2026 08:29:33 -0600 Subject: [PATCH] Placing scripts in the generated project --- README.md | 53 ++++++-- anvil_src.zip | Bin 50136 -> 0 bytes src/commands/new.rs | 38 ++++-- templates/basic/README.md.tmpl | 46 +++++-- templates/basic/_dot_gitignore | 1 + templates/basic/build.bat | 126 ++++++++++++++++++ templates/basic/build.sh | 145 ++++++++++++++++++++ templates/basic/monitor.bat | 74 +++++++++++ templates/basic/monitor.sh | 107 +++++++++++++++ templates/basic/upload.bat | 144 ++++++++++++++++++++ templates/basic/upload.sh | 164 +++++++++++++++++++++++ tests/integration_test.rs | 233 ++++++++++++++++++++++++++++++++- 12 files changed, 1093 insertions(+), 38 deletions(-) delete mode 100644 anvil_src.zip create mode 100644 templates/basic/build.bat create mode 100644 templates/basic/build.sh create mode 100644 templates/basic/monitor.bat create mode 100644 templates/basic/monitor.sh create mode 100644 templates/basic/upload.bat create mode 100644 templates/basic/upload.sh 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 86df85723ef973824776367f95068c5336c52c47..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 50136 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 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" + ); +}