Placing scripts in the generated project

This commit is contained in:
Eric Ratliff
2026-02-16 08:29:33 -06:00
parent 3298844399
commit fc1fb73d5a
12 changed files with 1093 additions and 38 deletions

View File

@@ -2,9 +2,13 @@
**Arduino project generator and build tool -- forges clean embedded projects.** **Arduino project generator and build tool -- forges clean embedded projects.**
A single binary that scaffolds testable Arduino projects with hardware abstraction, A single binary that scaffolds self-contained Arduino projects with hardware
Google Mock infrastructure, and a streamlined build/upload/monitor workflow. Works on abstraction, Google Mock infrastructure, and a streamlined build/upload/monitor
Linux and Windows. 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. Anvil is a [Nexus Workshops](https://nxlearn.net) project.
@@ -36,20 +40,37 @@ your system is ready.
# Create a new project # Create a new project
anvil new blink anvil new blink
# Check system health # Enter the project
anvil doctor cd blink
# Find your board # Compile (verify only)
anvil devices ./build.sh
# Compile and upload to board
./upload.sh
# Compile, upload, and open serial monitor # Compile, upload, and open serial monitor
cd blink ./upload.sh --monitor
anvil build --monitor blink
# Run host-side tests (no board needed) # 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 ## Commands
| Command | Description | | Command | Description |
@@ -58,9 +79,12 @@ cd test && ./run_tests.sh
| `anvil doctor` | Check system prerequisites | | `anvil doctor` | Check system prerequisites |
| `anvil setup` | Install arduino-cli and AVR core | | `anvil setup` | Install arduino-cli and AVR core |
| `anvil devices` | List connected boards and serial ports | | `anvil devices` | List connected boards and serial ports |
| `anvil build DIR` | Compile and upload a sketch | | `anvil build DIR` | Compile and upload a sketch (convenience) |
| `anvil upload DIR`| Upload cached build (no recompile) | | `anvil upload DIR`| Upload cached build (convenience) |
| `anvil monitor` | Open serial monitor (`--watch` for persistent) | | `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 ## Project Architecture
@@ -74,6 +98,9 @@ your-project/
lib/app/your-project_app.h -- app logic (testable) lib/app/your-project_app.h -- app logic (testable)
test/mocks/mock_hal.h -- Google Mock HAL test/mocks/mock_hal.h -- Google Mock HAL
test/test_unit.cpp -- unit tests 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 .anvil.toml -- project config
``` ```

Binary file not shown.

View File

@@ -177,7 +177,12 @@ fn init_git(project_dir: &PathBuf, template_name: &str) {
fn make_executable(project_dir: &PathBuf) { fn make_executable(project_dir: &PathBuf) {
use std::os::unix::fs::PermissionsExt; 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 { for script in &scripts {
let path = project_dir.join(script); let path = project_dir.join(script);
if path.exists() { if path.exists() {
@@ -196,23 +201,36 @@ fn print_next_steps(project_name: &str) {
" 1. {}", " 1. {}",
format!("cd {}", project_name).bright_cyan() format!("cd {}", project_name).bright_cyan()
); );
println!(" 2. Check your system: {}", "anvil doctor".bright_cyan());
println!( println!(
" 3. Find your board: {}", " 2. Compile: {}",
"anvil devices".bright_cyan() "./build.sh".bright_cyan()
); );
println!( println!(
" 4. Build and upload: {}", " 3. Upload to board: {}",
format!("anvil build {}", project_name).bright_cyan() "./upload.sh".bright_cyan()
); );
println!( println!(
" 5. Build + monitor: {}", " 4. Upload + monitor: {}",
format!("anvil build --monitor {}", project_name).bright_cyan() "./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!();
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!(); println!();
} }

View File

@@ -1,29 +1,38 @@
# {{PROJECT_NAME}} # {{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 ## Quick Start
```bash ```bash
# Check your system # Compile only (verify)
anvil doctor ./build.sh
# Find connected boards # Compile and upload to board
anvil devices ./upload.sh
# Compile only (no upload)
anvil build --verify {{PROJECT_NAME}}
# Compile and upload
anvil build {{PROJECT_NAME}}
# Compile, upload, and open serial monitor # 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) # 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 ## Project Structure
``` ```
@@ -44,6 +53,9 @@ cd test && ./run_tests.sh
CMakeLists.txt Test build system CMakeLists.txt Test build system
run_tests.sh Test runner (Linux/Mac) run_tests.sh Test runner (Linux/Mac)
run_tests.bat Test runner (Windows) 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 .anvil.toml Project configuration
``` ```
@@ -52,7 +64,7 @@ cd test && ./run_tests.sh
All hardware access goes through the `Hal` interface. The app code All hardware access goes through the `Hal` interface. The app code
(`lib/app/`) depends only on `Hal`, never on `Arduino.h` directly. (`lib/app/`) depends only on `Hal`, never on `Arduino.h` directly.
This means the app can be compiled and tested on the host without This means the app can be compiled and tested on the host without
any Arduino SDK. any Arduino hardware.
Two HAL implementations: Two HAL implementations:
- `ArduinoHal` -- passthroughs to real hardware (used in the .ino) - `ArduinoHal` -- passthroughs to real hardware (used in the .ino)
@@ -72,3 +84,9 @@ extra_flags = ["-Werror"]
[monitor] [monitor]
baud = 115200 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`

View File

@@ -1,4 +1,5 @@
# Build artifacts # Build artifacts
.build/
test/build/ test/build/
# IDE # IDE

126
templates/basic/build.bat Normal file
View File

@@ -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.

145
templates/basic/build.sh Normal file
View File

@@ -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 ""

View File

@@ -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%"

107
templates/basic/monitor.sh Normal file
View File

@@ -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

144
templates/basic/upload.bat Normal file
View File

@@ -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%"
)

164
templates/basic/upload.sh Normal file
View File

@@ -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

View File

@@ -18,7 +18,7 @@ fn test_basic_template_extracts_all_expected_files() {
}; };
let count = TemplateManager::extract("basic", tmp.path(), &ctx).unwrap(); 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] #[test]
@@ -321,6 +321,12 @@ fn test_full_project_structure() {
"lib/hal/hal.h", "lib/hal/hal.h",
"lib/hal/hal_arduino.h", "lib/hal/hal_arduino.h",
"lib/app/full_test_app.h", "lib/app/full_test_app.h",
"build.sh",
"build.bat",
"upload.sh",
"upload.bat",
"monitor.sh",
"monitor.bat",
"test/CMakeLists.txt", "test/CMakeLists.txt",
"test/test_unit.cpp", "test/test_unit.cpp",
"test/run_tests.sh", "test/run_tests.sh",
@@ -406,3 +412,228 @@ fn test_load_config_from_nonproject_fails() {
let result = ProjectConfig::load(tmp.path()); let result = ProjectConfig::load(tmp.path());
assert!(result.is_err()); 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"
);
}