Replace build-all.sh shell script with a Rust xtask workspace member. Uses cargo-zigbuild + zig as a universal cross-linker -- no VMs, no containers, no root required. Produces all three release binaries from a single FreeBSD machine: anvil-X.Y.Z-freebsd-x86_64.tar.gz (native cargo build) anvil-X.Y.Z-linux-x86_64.tar.gz (cargo zigbuild) anvil-X.Y.Z-windows-x86_64.zip (cargo zigbuild) Commands: cargo xtask --fix install zig, zip, cargo-zigbuild, rustup targets cargo xtask --check verify all dependencies cargo xtask build all three binaries + SHA256SUMS cargo xtask --clean remove cross-compile artifacts cargo xtask --suffix rc1 build with version suffix Also converts Cargo.toml to a workspace (members: anvil, xtask). build-all.sh retained as a thin wrapper around cargo xtask.
485 lines
14 KiB
Markdown
485 lines
14 KiB
Markdown
# Anvil
|
|
|
|
**Forge clean, testable Arduino projects from a single command.**
|
|
|
|
Anvil generates self-contained Arduino projects with hardware abstraction,
|
|
test infrastructure, sensor libraries, and a complete build/upload/test
|
|
workflow. Once generated, the project stands alone -- Anvil is a scaffolding
|
|
tool, not a runtime dependency.
|
|
|
|
<p align="center">
|
|
<img src="docs/terminal-demo.svg" alt="Anvil terminal demo" width="720"/>
|
|
</p>
|
|
|
|
Anvil is a [Nexus Workshops](https://nxlearn.net) project, built for FTC
|
|
robotics teams and embedded systems students.
|
|
|
|
---
|
|
|
|
## Getting Started
|
|
|
|
### Install
|
|
|
|
Download the release binary for your platform and add it to your PATH.
|
|
Then run first-time setup:
|
|
|
|
```bash
|
|
anvil setup
|
|
```
|
|
|
|
This installs `arduino-cli` and the `arduino:avr` core. If something is
|
|
already installed, Anvil skips it. Run `anvil doctor` at any time to
|
|
check your environment.
|
|
|
|
If there is no pre-built binary for your platform, or you want to hack on
|
|
Anvil itself, see [Building from Source](#building-from-source) below.
|
|
|
|
### Create a project
|
|
|
|
```bash
|
|
anvil new blink
|
|
cd blink
|
|
```
|
|
|
|
That's it. You have a complete project with build scripts, a HAL interface,
|
|
mock infrastructure, and a starter test file. Plug in your board and run:
|
|
|
|
```bash
|
|
./build.sh # compile (verify it builds)
|
|
./upload.sh # compile + upload to board
|
|
./monitor.sh # serial monitor
|
|
./test.sh # host-side tests (no board needed)
|
|
```
|
|
|
|
On Windows, use `build.bat`, `upload.bat`, `monitor.bat`, `test.bat`.
|
|
Every script reads settings from `.anvil.toml` -- no Anvil binary required.
|
|
|
|
---
|
|
|
|
## Templates
|
|
|
|
The default `basic` template gives you a blank canvas. For a richer starting
|
|
point, use a composed template:
|
|
|
|
```bash
|
|
anvil new weather_station --template weather --board uno
|
|
anvil new clicker --template button --board uno
|
|
```
|
|
|
|
The **weather** template adds a `WeatherApp` with a TMP36 temperature sensor,
|
|
managed example tests, and student test starters. The **button** template adds
|
|
a `ButtonApp` with edge detection that prints "Button pressed!" to the serial
|
|
monitor each time you press a button -- no repeated messages from holding it
|
|
down. Both templates include mock and simulator patterns. To see all options:
|
|
|
|
```bash
|
|
anvil new --list-templates
|
|
```
|
|
|
|
Templates are pure data -- each is a directory with a `template.toml`
|
|
declaring its base, required libraries, and per-board pin defaults. Adding
|
|
a new template requires zero Rust code changes.
|
|
|
|
---
|
|
|
|
## Libraries
|
|
|
|
Anvil ships sensor and actuator libraries, each with four files: an abstract
|
|
interface, a hardware implementation, a test mock, and a deterministic
|
|
simulator.
|
|
|
|
```bash
|
|
anvil add tmp36 --pin A0 # analog temperature sensor
|
|
anvil add button --pin 2 # digital pushbutton with debounce sim
|
|
```
|
|
|
|
See what's available and what's installed:
|
|
|
|
```bash
|
|
anvil lib --available # all libraries in the registry
|
|
anvil lib # libraries installed in this project
|
|
```
|
|
|
|
Remove a library:
|
|
|
|
```bash
|
|
anvil remove tmp36
|
|
```
|
|
|
|
Each library installs to `lib/drivers/<name>/` with its test file in `test/`.
|
|
The `CMakeLists.txt` auto-discovers driver directories, so adding a library
|
|
immediately makes it available to your tests.
|
|
|
|
### The mock/sim split
|
|
|
|
Every library provides two test doubles:
|
|
|
|
- **Mock** -- Returns exact values you set. Use in unit tests to verify your
|
|
application logic calls the sensor correctly and responds to specific values.
|
|
- **Simulator** -- Returns realistic values with configurable noise, bounce,
|
|
or drift. Use in system tests to verify your code handles real-world
|
|
sensor behavior (jitter, debounce timing, averaging).
|
|
|
|
This split teaches the difference between interaction testing and behavioral
|
|
testing -- a concept that transfers directly to professional software
|
|
development.
|
|
|
|
---
|
|
|
|
## Pin Management
|
|
|
|
Anvil knows the pinout of every supported board. Assignments are validated
|
|
at the command line, not when you discover a wiring bug at 9 PM.
|
|
|
|
```bash
|
|
anvil pin --assign led 13 --mode output
|
|
anvil pin --assign tmp36_data A0 --mode analog
|
|
anvil pin --assign spi --cs 10 # SPI bus with chip-select
|
|
anvil pin --assign i2c # I2C (pins auto-resolved)
|
|
```
|
|
|
|
Generate a `pins.h` header with `#define` constants:
|
|
|
|
```bash
|
|
anvil pin --generate
|
|
```
|
|
|
|
Audit your wiring against library requirements:
|
|
|
|
```bash
|
|
anvil pin --audit
|
|
```
|
|
|
|
Pin assignments are stored per-board in `.anvil.toml`, so switching between
|
|
an Uno and a Mega doesn't lose your wiring for either.
|
|
|
|
---
|
|
|
|
## Board Profiles
|
|
|
|
A single project can target multiple boards:
|
|
|
|
```bash
|
|
anvil board --add mega
|
|
anvil board --add nano --baud 57600
|
|
anvil board --default mega
|
|
```
|
|
|
|
Each board gets its own `[boards.<name>]` section in `.anvil.toml` with FQBN,
|
|
baud rate, and independent pin assignments. Scripts use the default board
|
|
unless you pass `--board`:
|
|
|
|
```bash
|
|
./upload.sh --board nano
|
|
```
|
|
|
|
List available presets:
|
|
|
|
```bash
|
|
anvil new --list-boards
|
|
```
|
|
|
|
---
|
|
|
|
## Device Detection
|
|
|
|
Anvil's scripts auto-detect your board, but you can pin a specific device:
|
|
|
|
```bash
|
|
anvil devices # list connected boards
|
|
anvil devices --set # auto-detect and save to .anvil.local
|
|
anvil devices --set COM3 # save a specific port
|
|
```
|
|
|
|
The `.anvil.local` file stores both the port name and the USB VID:PID. If
|
|
your board moves to a different port (common on Windows), the scripts find
|
|
it by VID:PID automatically.
|
|
|
|
---
|
|
|
|
## Refresh and .anvilignore
|
|
|
|
When you upgrade Anvil, existing projects still have old infrastructure.
|
|
Refresh updates managed files without touching your code:
|
|
|
|
```bash
|
|
anvil refresh # dry run -- shows what would change
|
|
anvil refresh --force # update managed files
|
|
```
|
|
|
|
### What's protected
|
|
|
|
An `.anvilignore` file (generated automatically) protects student-authored
|
|
files from refresh. The defaults protect:
|
|
|
|
- Your test files (`test/test_unit.cpp`, `test/test_system.cpp`)
|
|
- Your application code (`lib/app/*`)
|
|
- Your sketch (`*/*.ino`)
|
|
- Your config (`.anvil.toml`)
|
|
- Your project files (`.gitignore`, `README.md`, `.editorconfig`, etc.)
|
|
|
|
Managed infrastructure (build scripts, mock headers, CMakeLists.txt, library
|
|
drivers, template example tests) gets updated. Missing files are always
|
|
recreated, even without `--force`.
|
|
|
|
### Fine-grained control
|
|
|
|
```bash
|
|
anvil refresh --ignore "test/my_helper.h" # protect a custom file
|
|
anvil refresh --unignore "test/test_unit.cpp" # allow refresh to update it
|
|
anvil refresh --force --file test/test_unit.cpp # one-time override
|
|
```
|
|
|
|
Patterns support globs: `test/*.cpp`, `lib/app/*`, `*.h`.
|
|
|
|
---
|
|
|
|
## Project Structure
|
|
|
|
```
|
|
your-project/
|
|
your-project/your-project.ino Sketch (thin shell, no logic)
|
|
lib/
|
|
hal/
|
|
hal.h Hardware abstraction (pure virtual)
|
|
hal_arduino.h Real Arduino implementation
|
|
app/
|
|
your-project_app.h Your application logic (testable)
|
|
drivers/
|
|
tmp36/ Sensor driver (interface/impl/mock/sim)
|
|
button/ Actuator driver (same pattern)
|
|
test/
|
|
mocks/
|
|
mock_arduino.h Arduino API shims for host compile
|
|
mock_hal.h Google Mock HAL
|
|
sim_hal.h Stateful simulator HAL
|
|
test_weather.cpp Managed example tests (refreshable)
|
|
test_unit.cpp Your unit tests (protected)
|
|
test_system.cpp Your system tests (protected)
|
|
test_tmp36.cpp Library driver tests
|
|
CMakeLists.txt Fetches Google Test, compiles tests
|
|
build.sh / build.bat Compile sketch
|
|
build.sh / build.bat / build.ps1 Compile sketch (bat wraps ps1)
|
|
upload.sh / upload.bat Compile + upload to board
|
|
monitor.sh / monitor.bat Serial monitor
|
|
test.sh / test.bat Run host-side tests
|
|
.anvil.toml Project config (tracked by git)
|
|
.anvil.local Machine-specific port (gitignored)
|
|
.anvilignore File protection rules for refresh
|
|
```
|
|
|
|
The key architectural rule: application code in `lib/app/` depends only on
|
|
the `Hal` interface, never on `Arduino.h`. The sketch creates the real HAL
|
|
and passes it in. Tests create a mock or simulator HAL instead. This is
|
|
constructor injection -- the simplest form of dependency inversion.
|
|
|
|
---
|
|
|
|
## Commands
|
|
|
|
| Command | Description |
|
|
|---|---|
|
|
| `anvil new NAME [--template T] [--board B]` | Create a new project |
|
|
| `anvil new --list-templates` | Show available templates |
|
|
| `anvil new --list-boards` | Show available board presets |
|
|
| `anvil setup` | Install arduino-cli and AVR core |
|
|
| `anvil doctor [--fix]` | Check system prerequisites |
|
|
| `anvil devices [--set] [--get] [--clear]` | Manage serial port assignment |
|
|
| `anvil add NAME [--pin P]` | Install a device library |
|
|
| `anvil remove NAME` | Remove a device library |
|
|
| `anvil lib [--available]` | List installed or available libraries |
|
|
| `anvil pin --assign NAME PIN [--mode M]` | Assign a pin |
|
|
| `anvil pin --generate` | Generate pins.h header |
|
|
| `anvil pin --audit [--brief]` | Check wiring against library requirements |
|
|
| `anvil pin --capabilities` | Show board pin capabilities |
|
|
| `anvil pin --init-from BOARD` | Copy pin assignments from another board |
|
|
| `anvil board --add NAME [--id FQBN] [--baud N]` | Add a board profile |
|
|
| `anvil board --remove NAME` | Remove a board profile |
|
|
| `anvil board --default NAME` | Set the default board |
|
|
| `anvil refresh [--force] [--file P] [--ignore P] [--unignore P]` | Update project infrastructure |
|
|
| `anvil completions SHELL` | Generate tab-completion script (bash, zsh, fish, powershell) |
|
|
|
|
---
|
|
|
|
## Tab Completion
|
|
|
|
Anvil can generate shell completion scripts so that pressing Tab completes
|
|
commands, flags, and arguments:
|
|
|
|
```bash
|
|
# Bash (add to ~/.bashrc)
|
|
eval "$(anvil completions bash)"
|
|
|
|
# Zsh (add to ~/.zshrc)
|
|
eval "$(anvil completions zsh)"
|
|
|
|
# Fish
|
|
anvil completions fish > ~/.config/fish/completions/anvil.fish
|
|
|
|
# PowerShell (add to $PROFILE)
|
|
anvil completions powershell | Out-String | Invoke-Expression
|
|
```
|
|
|
|
After setup, `anvil ref<Tab>` completes to `anvil refresh`, and
|
|
`anvil pin --<Tab>` shows all available flags.
|
|
|
|
---
|
|
|
|
## Configuration
|
|
|
|
### .anvil.toml (tracked by git)
|
|
|
|
```toml
|
|
[project]
|
|
name = "weather_station"
|
|
anvil_version = "1.0.0"
|
|
template = "weather"
|
|
|
|
[build]
|
|
default = "uno"
|
|
warnings = "more"
|
|
include_dirs = ["lib/hal", "lib/app", "lib/drivers/tmp36"]
|
|
extra_flags = ["-Werror"]
|
|
|
|
[boards.uno]
|
|
fqbn = "arduino:avr:uno"
|
|
baud = 115200
|
|
|
|
[boards.mega]
|
|
fqbn = "arduino:avr:mega:cpu=atmega2560"
|
|
baud = 115200
|
|
|
|
[libraries]
|
|
tmp36 = "0.1.0"
|
|
|
|
[pins.uno]
|
|
tmp36_data = { pin = 14, mode = "analog" }
|
|
```
|
|
|
|
### .anvil.local (gitignored)
|
|
|
|
```toml
|
|
port = "COM3"
|
|
vid_pid = "0403:6001"
|
|
```
|
|
|
|
---
|
|
|
|
## Building from Source
|
|
|
|
If you want to build Anvil yourself -- either because there is no pre-built
|
|
binary for your platform, or because you want to contribute -- here is what
|
|
you need.
|
|
|
|
### Prerequisites
|
|
|
|
Anvil is written in Rust and compiles to a single static binary. You need:
|
|
|
|
- **Rust toolchain** (stable, 2021 edition or later)
|
|
- **A C linker** (`gcc` or equivalent -- Rust uses it under the hood)
|
|
|
|
### Build
|
|
|
|
```bash
|
|
cargo build --release
|
|
```
|
|
|
|
The release binary lands at `target/release/anvil` (or `target\release\anvil.exe`
|
|
on Windows). Copy it somewhere in your PATH.
|
|
|
|
### Running the test suite
|
|
|
|
```bash
|
|
cargo test
|
|
```
|
|
|
|
The test suite covers unit, integration, and end-to-end scenarios. The e2e
|
|
tests generate real projects and compile their C++ test suites, catching
|
|
build-system issues like missing linker flags and include paths. They require
|
|
cmake and a C++ compiler; if those tools are not installed, the compile tests
|
|
skip gracefully and everything else still passes.
|
|
|
|
#### Full test suite on Linux / WSL
|
|
|
|
The e2e tests need `cmake`, `g++`, and `arduino-cli` with the AVR core:
|
|
|
|
```bash
|
|
sudo apt install cmake g++
|
|
curl -fsSL https://raw.githubusercontent.com/arduino/arduino-cli/master/install.sh | sh
|
|
sudo mv bin/arduino-cli /usr/local/bin/
|
|
arduino-cli core install arduino:avr
|
|
```
|
|
|
|
#### Full test suite on Windows
|
|
|
|
Install [arduino-cli](https://arduino.github.io/arduino-cli/installation/)
|
|
and add it to your PATH, then install the AVR core:
|
|
|
|
```
|
|
arduino-cli core install arduino:avr
|
|
```
|
|
|
|
CMake and a C++ compiler are needed for the host-side test compilation.
|
|
Install [CMake](https://cmake.org/download/) and either MinGW-w64 or open
|
|
a Visual Studio Developer Command Prompt (which provides `cl.exe`).
|
|
|
|
---
|
|
|
|
## Building Release Binaries
|
|
|
|
Anvil uses a Rust `xtask` workspace member to build release binaries for all
|
|
platforms from a single machine. No VMs, no containers, no root required.
|
|
|
|
### How it works
|
|
|
|
The xtask build system uses [cargo-zigbuild](https://github.com/rust-cross/cargo-zigbuild)
|
|
with the [Zig](https://ziglang.org) compiler as a universal cross-linker.
|
|
From a single FreeBSD machine you get all three platform binaries:
|
|
|
|
```
|
|
release-artifacts/
|
|
anvil-X.Y.Z-freebsd-x86_64.tar.gz native FreeBSD build
|
|
anvil-X.Y.Z-linux-x86_64.tar.gz cross-compiled via zig
|
|
anvil-X.Y.Z-windows-x86_64.zip cross-compiled via zig
|
|
SHA256SUMS
|
|
```
|
|
|
|
### First-time setup
|
|
|
|
```bash
|
|
cargo xtask --fix
|
|
```
|
|
|
|
This installs zig (via pkg), zip, cargo-zigbuild, and the required rustup
|
|
cross-compile targets. Only the zig and zip installation steps require sudo.
|
|
|
|
### Building a release
|
|
|
|
```bash
|
|
cargo xtask # build all platforms, version from Cargo.toml
|
|
cargo xtask --suffix rc1 # pre-release (e.g. 1.0.0-rc1)
|
|
```
|
|
|
|
### Other commands
|
|
|
|
```bash
|
|
cargo xtask --check # verify all dependencies are installed
|
|
cargo xtask --clean # remove cross-compile artifacts
|
|
```
|
|
|
|
### Platform support
|
|
|
|
| Host | FreeBSD binary | Linux binary | Windows binary |
|
|
|---|---|---|---|
|
|
| FreeBSD | native | cargo-zigbuild | cargo-zigbuild |
|
|
| Linux | not possible | native | cargo-zigbuild |
|
|
| Windows | not possible | not tested | native |
|
|
|
|
FreeBSD is the recommended build host -- it produces all three binaries in
|
|
a single run.
|
|
|
|
---
|
|
|
|
## License
|
|
|
|
MIT -- see [LICENSE](LICENSE). |