Anvil
Declarative Workstation Configuration Management
Anvil is a configuration management tool that lets you define your development environment in YAML. Describe the packages, configuration files, scripts, and environment variables your workstation needs, and Anvil takes care of the rest — installing software, deploying dotfiles, running setup scripts, and validating that everything is healthy.
Key Features
- Package management — install software via winget with version pinning (Homebrew and APT planned)
- File synchronization — deploy configuration files with automatic backup and integrity checks
- Script execution — run PowerShell setup and validation scripts with timeout and elevation support
- Workload inheritance — compose configurations with DRY principles using
extends - Health checks — validate that the live system matches your workload definition
- Multiple output formats — table, JSON, YAML, and HTML reports
- Backup and restore — snapshot and roll back system state
- Shell completions — tab completion for PowerShell, Bash, Zsh, and Fish
Quick Install
# From crates.io (requires Rust 1.75+)
cargo install anvil-cli
Or download a pre-built binary from the Releases page.
Quick Start
# List available workloads
anvil list
# Preview what an install would do
anvil install rust-developer --dry-run
# Install a workload
anvil install rust-developer
# Verify system health
anvil health rust-developer
Learn More
- Read the User Guide for complete usage instructions.
- See Workload Authoring to create your own workloads.
- Check the Troubleshooting guide if you run into issues.
- Review the Specification for the technical design and roadmap.
- Explore the Architecture docs to understand the codebase.
- Want to help? Read the Contributing guide.
User Guide
A comprehensive guide to using Anvil for workstation configuration management.
1. Introduction
What is Anvil?
Anvil is a declarative configuration management tool for developer workstations. It allows you to define your development environment in YAML files and automatically:
- Install software packages via package managers (currently winget; Homebrew and APT planned)
- Copy and manage configuration files
- Execute setup and validation scripts
- Verify system health against your defined configuration
Key Concepts
- Workload: A configuration bundle containing package definitions, files to deploy, and scripts to run
- Package: A software application to be installed via winget
- File: A configuration file to be copied to your system
- Script: A PowerShell or CMD script to execute during installation or health checks
- Inheritance: The ability to compose workloads by extending other workloads
System Requirements
Current platform: Windows
- Windows 10 (version 1809 or later) or Windows 11
- Windows Package Manager (winget) version 1.4 or later
- PowerShell 5.1 or later (included with Windows)
- Administrator access (for some operations)
Cross-platform support (macOS, Linux) is on the roadmap.
2. Installation
Install from crates.io
# Prerequisites: Rust 1.75+
cargo install anvil-cli
Download Pre-built Binary
-
Download the latest release from the Releases page
-
Extract the archive:
Expand-Archive anvil-v0.3.1-windows-x64.zip -DestinationPath C:\Tools\anvil -
Add to your PATH:
# Add to current session $env:PATH += ";C:\Tools\anvil" # Add permanently (User scope) [Environment]::SetEnvironmentVariable("PATH", $env:PATH + ";C:\Tools\anvil", "User")
Build from Source
# Prerequisites: Rust 1.75+ and Visual Studio Build Tools
# Clone the repository
git clone https://github.com/kafkade/anvil.git
cd anvil
# Build release binary
cargo build --release
# The binary is at target/release/anvil.exe
Verify Installation
anvil --version
# Output: anvil 0.3.1
anvil --help
# Shows available commands and options
Shell Completions Setup
Generate and install shell completions for better command-line experience:
PowerShell
# Generate completions
anvil completions powershell > $HOME\Documents\WindowsPowerShell\anvil.ps1
# Add to your PowerShell profile
Add-Content $PROFILE '. $HOME\Documents\WindowsPowerShell\anvil.ps1'
Bash (WSL/Git Bash)
# Generate completions
anvil completions bash > ~/.local/share/bash-completion/completions/anvil
# Or add to .bashrc
anvil completions bash >> ~/.bashrc
Zsh
# Generate completions
anvil completions zsh > ~/.zfunc/_anvil
# Add to .zshrc (before compinit)
fpath+=~/.zfunc
3. Quick Start
List Available Workloads
See what workloads are available:
anvil list
Output:
Available Workloads:
essentials Core development tools and productivity utilities
rust-developer Rust development environment (extends essentials)
python-developer Python development environment (extends essentials)
View Workload Details
Inspect what a workload will do:
anvil show rust-developer
Dry Run Installation
Preview what would happen without making changes:
anvil install rust-developer --dry-run
Install a Workload
Apply a workload configuration:
anvil install rust-developer
Check System Health
Verify your system matches the workload definition:
anvil health rust-developer
4. Command Reference
install
Apply a workload configuration to your system.
Synopsis:
anvil install <WORKLOAD> [OPTIONS]
Arguments:
<WORKLOAD>- Name of the workload to install
Options:
| Option | Description |
|---|---|
--dry-run | Preview actions without making changes |
--force | Force reinstallation of packages |
--skip-packages | Skip package installation |
--skip-files | Skip file operations |
--skip-scripts | Skip script execution |
--output <FORMAT> | Output format: table, json, yaml |
--path <DIR> | Custom workload search path |
Examples:
# Standard installation
anvil install rust-developer
# Preview only
anvil install rust-developer --dry-run
# Skip packages (only copy files and run scripts)
anvil install rust-developer --skip-packages
# Use JSON output for scripting
anvil install rust-developer --dry-run --output json
# Install from custom directory
anvil install my-workload --path C:\Workloads
Exit Codes:
0- Success1- General error2- Workload not found3- Package installation failed4- File operation failed5- Script execution failed
health
Validate system state against a workload definition.
Synopsis:
anvil health <WORKLOAD> [OPTIONS]
Arguments:
<WORKLOAD>- Name of the workload to check
Options:
| Option | Description |
|---|---|
--output <FORMAT> | Output format: table, json, yaml, html |
--file <PATH> | Write output to file |
--verbose | Show detailed check results |
--path <DIR> | Custom workload search path |
Examples:
# Basic health check
anvil health rust-developer
# Detailed output
anvil health rust-developer --verbose
# Generate JSON report
anvil health rust-developer --output json --file health-report.json
# Generate HTML report
anvil health rust-developer --output html --file report.html
Understanding Health Reports:
Health checks verify:
- Packages: Are required packages installed? Correct versions?
- Files: Do configuration files exist with expected content?
- Scripts: Do health check scripts pass?
Status indicators:
- ✓ (Green) - Check passed
- ✗ (Red) - Check failed
- ! (Yellow) - Warning or partial match
list
List available workloads.
Synopsis:
anvil list [OPTIONS]
Options:
| Option | Description |
|---|---|
--all | Include hidden/system workloads |
--long | Show detailed information |
--path <DIR> | Custom workload search path |
--output <FORMAT> | Output format: table, json, yaml |
Examples:
# Simple list
anvil list
# Detailed list with versions and descriptions
anvil list --long
# JSON output for scripting
anvil list --output json
# List from custom directory
anvil list --path C:\MyWorkloads
show
Display detailed information about a workload.
Synopsis:
anvil show <WORKLOAD> [OPTIONS]
Arguments:
<WORKLOAD>- Name of the workload to display
Options:
| Option | Description |
|---|---|
--inheritance-tree | Show inheritance hierarchy |
--resolved | Show fully resolved workload (after inheritance) |
--output <FORMAT> | Output format: table, json, yaml |
--path <DIR> | Custom workload search path |
Examples:
# Show workload details
anvil show rust-developer
# Show inheritance tree
anvil show rust-developer --inheritance-tree
# Export as YAML
anvil show rust-developer --output yaml
# Show resolved (merged) workload
anvil show rust-developer --resolved
validate
Validate workload syntax and structure.
Synopsis:
anvil validate <WORKLOAD> [OPTIONS]
Arguments:
<WORKLOAD>- Name of the workload to validate
Options:
| Option | Description |
|---|---|
--strict | Enable strict validation mode |
--output <FORMAT> | Output format: table, json, yaml |
--path <DIR> | Custom workload search path |
Examples:
# Basic validation
anvil validate my-workload
# Strict mode (treats warnings as errors)
anvil validate my-workload --strict
# Validate all bundled workloads
anvil list --output json | ConvertFrom-Json | ForEach-Object { anvil validate $_.name }
Common Validation Errors:
- Missing required fields (
name,version) - Invalid workload name format
- Circular inheritance dependencies
- Invalid package IDs
- Non-existent script paths
- Invalid file paths
init
Create a new workload from a template.
Synopsis:
anvil init <PATH> [OPTIONS]
Arguments:
<PATH>- Directory path for the new workload
Options:
| Option | Description |
|---|---|
--template <NAME> | Template to use: minimal, full, rust, python |
--force | Overwrite existing files |
--name <NAME> | Workload name (defaults to directory name) |
Examples:
# Create minimal workload
anvil init C:\Workloads\my-workload
# Create from template
anvil init C:\Workloads\my-rust-env --template rust
# Overwrite existing
anvil init C:\Workloads\existing --force
Available Templates:
minimal- Basic structure with required fields onlyfull- Complete example with all featuresrust- Rust development environment templatepython- Python development environment template
status
Show current installation status.
Synopsis:
anvil status [WORKLOAD] [OPTIONS]
Arguments:
[WORKLOAD]- Optional workload to check status for
Options:
| Option | Description |
|---|---|
--output <FORMAT> | Output format: table, json, yaml |
Examples:
# Overall status
anvil status
# Status for specific workload
anvil status rust-developer
backup
Manage system state backups.
Synopsis:
anvil backup <SUBCOMMAND>
Subcommands:
backup create
Create a new backup of current system state.
# Create backup before changes
anvil backup create
# Create named backup
anvil backup create --name "before-update"
# Create backup for specific workload
anvil backup create --workload rust-developer
backup list
List available backups.
anvil backup list
anvil backup list --output json
backup show
Show details of a specific backup.
anvil backup show <BACKUP_ID>
backup restore
Restore from a backup.
# Restore from backup
anvil backup restore <BACKUP_ID>
# Preview restore
anvil backup restore <BACKUP_ID> --dry-run
backup delete
Delete a backup.
anvil backup delete <BACKUP_ID>
anvil backup delete <BACKUP_ID> --force
config
Manage Anvil configuration.
Synopsis:
anvil config <SUBCOMMAND>
Subcommands:
config show
Display current configuration.
anvil config show
anvil config show --output json
config set
Set a configuration value.
anvil config set workload_paths "C:\Workloads;D:\MoreWorkloads"
anvil config set default_output json
anvil config set backup.enabled true
config reset
Reset configuration to defaults.
anvil config reset
anvil config reset --key workload_paths
config edit
Open configuration file in editor.
anvil config edit
completions
Generate shell completion scripts.
Synopsis:
anvil completions <SHELL>
Arguments:
<SHELL>- Target shell: powershell, bash, zsh, fish
Examples:
# Generate PowerShell completions
anvil completions powershell
# Generate and install bash completions
anvil completions bash > /etc/bash_completion.d/anvil
Global Options
These options work with all commands:
| Option | Short | Description |
|---|---|---|
--verbose | -v | Increase verbosity (use multiple times: -v, -vv, -vvv) |
--quiet | -q | Suppress non-essential output |
--no-color | Disable colored output | |
--help | -h | Show help information |
--version | -V | Show version information |
Examples:
# Verbose output
anvil -v install rust-developer
# Very verbose (debug level)
anvil -vvv health rust-developer
# Quiet mode for scripting
anvil -q install rust-developer
# No colors (for log files)
anvil --no-color list > workloads.txt
5. Configuration
Configuration File Location
Anvil stores its configuration at:
%APPDATA%\anvil\config.toml
Or if ANVIL_CONFIG is set:
$env:ANVIL_CONFIG
Configuration Options
# Global Anvil Configuration
# Default output format (table, json, yaml)
default_output = "table"
# Workload search paths (semicolon-separated)
workload_paths = "C:\\Workloads;D:\\MyWorkloads"
# Enable colored output
color = true
# Default verbosity level (0-3)
verbosity = 0
[backup]
# Enable automatic backups before changes
enabled = true
# Backup directory
path = "%APPDATA%\\anvil\\backups"
# Maximum number of backups to keep
max_count = 10
[packages]
# Default package source
default_source = "winget"
# Allow prerelease versions
allow_prerelease = false
[scripts]
# Default script timeout (seconds)
default_timeout = 300
# Shell for script execution
default_shell = "powershell"
View Current Configuration
anvil config show
Modify Configuration
# Set a value
anvil config set default_output json
# Reset to default
anvil config reset
6. Configuring Workload Search Paths
Anvil searches for workloads in multiple directories. You can add custom paths to include your own workloads alongside the built-in ones.
Adding a Search Path
anvil config set workloads.paths '["~/my-workloads", "/shared/team-workloads"]'
Or edit ~/.anvil/config.yaml directly:
workloads:
paths:
- "~/my-workloads"
- "/shared/team-workloads"
Search Order
Anvil resolves workloads in this priority order:
- Explicit path — passed via
--pathflag - User-configured — paths from
~/.anvil/config.yaml - Default locations — bundled workloads, local data directory, current directory
When the same workload name exists in multiple paths, the first match wins. Use anvil list --all-paths to see all discovered paths including shadowed duplicates.
Complete Config Example
# Anvil global configuration
# Location: ~/.anvil/config.yaml
workloads:
paths:
- "~/my-workloads" # Personal workloads
- "~/work/team-workloads" # Team-shared workloads
logging:
level: info
7. Working with Workloads
Discovering Workloads
Anvil searches for workloads in these locations (in priority order):
- Path specified with
--pathoption - User-configured paths (from
~/.anvil/config.yaml) - Default locations (bundled workloads, local data directory, current directory)
# List all available workloads
anvil list
# List with details
anvil list --long
# List from specific directory
anvil list --path C:\MyWorkloads
Understanding Workload Inheritance
Workloads can extend other workloads to inherit their configuration:
name: my-rust-env
version: "1.0.0"
extends:
- essentials # Inherits packages, files, scripts
- rust-developer # Adds Rust-specific config
View the inheritance tree:
anvil show my-rust-env --inheritance-tree
Output:
my-rust-env
├── essentials
└── rust-developer
└── essentials
Using Custom Workload Directories
# One-time use
anvil list --path C:\MyWorkloads
anvil install my-workload --path C:\MyWorkloads
# Configure permanently
anvil config set workload_paths "C:\MyWorkloads"
Validating Before Install
Always validate workloads before installation:
# Validate syntax
anvil validate my-workload
# Strict validation
anvil validate my-workload --strict
# Preview installation
anvil install my-workload --dry-run
8. Output Formats
Anvil supports multiple output formats for different use cases.
Table (Default)
Human-readable format for terminal display:
anvil list
┌──────────────────┬─────────┬────────────────────────────────────────────────────┐
│ Name │ Version │ Description │
├──────────────────┼─────────┼────────────────────────────────────────────────────┤
│ essentials │ 2.0.0 │ Core development tools and productivity utilities │
│ rust-developer │ 1.0.0 │ Rust development environment │
└──────────────────┴─────────┴────────────────────────────────────────────────────┘
JSON
Machine-readable format for scripting and automation:
anvil list --output json
[
{
"name": "essentials",
"version": "2.0.0",
"description": "Core development tools and productivity utilities"
},
{
"name": "rust-developer",
"version": "1.0.0",
"description": "Rust development environment"
}
]
YAML
Configuration-friendly format:
anvil show rust-developer --output yaml
name: rust-developer
version: "1.0.0"
description: Rust development environment
extends:
- essentials
packages:
winget:
- id: Rustlang.Rustup
HTML
Rich reports for documentation:
anvil health rust-developer --output html --file report.html
Generates a styled HTML document with:
- Summary statistics
- Detailed check results
- Pass/fail indicators
- Timestamp and system info
9. Environment Variables
| Variable | Description | Default |
|---|---|---|
ANVIL_CONFIG | Configuration file path | %APPDATA%\anvil\config.toml |
ANVIL_WORKLOADS | Additional workload search paths | (none) |
ANVIL_LOG | Log level: error, warn, info, debug, trace | warn |
NO_COLOR | Disable colored output (any value) | (unset) |
ANVIL_BACKUP_DIR | Backup storage directory | %APPDATA%\anvil\backups |
Examples:
# Use custom config file
$env:ANVIL_CONFIG = "C:\config\anvil.toml"
anvil list
# Add workload search paths
$env:ANVIL_WORKLOADS = "C:\Workloads;D:\MoreWorkloads"
anvil list
# Enable debug logging
$env:ANVIL_LOG = "debug"
anvil install rust-developer
# Disable colors
$env:NO_COLOR = "1"
anvil list
10. Best Practices
Always Dry-Run First
Before applying any workload, preview the changes:
anvil install my-workload --dry-run
This shows what will happen without making changes.
Use Health Checks Regularly
Verify your system state periodically:
# Quick check
anvil health rust-developer
# Detailed report
anvil health rust-developer --verbose --output html --file health.html
Keep Backups
Enable automatic backups in configuration:
anvil config set backup.enabled true
Or create manual backups before major changes:
anvil backup create --name "before-upgrade"
Version Your Workloads
Store your workloads in version control:
my-workloads/
├── .git/
├── team-base/
│ └── workload.yaml
├── frontend-dev/
│ └── workload.yaml
└── backend-dev/
└── workload.yaml
Use Inheritance Wisely
Create a base workload with common tools:
# team-base/workload.yaml
name: team-base
version: "1.0.0"
packages:
winget:
- id: Git.Git
- id: Microsoft.VisualStudioCode
Then extend it for specific roles:
# frontend-dev/workload.yaml
name: frontend-dev
extends:
- team-base
packages:
winget:
- id: OpenJS.NodeJS
Validate Before Committing
Add validation to your CI/CD pipeline:
# Validate all workloads
Get-ChildItem -Directory | ForEach-Object {
anvil validate $_.Name --strict
}
Use Verbose Output for Debugging
When things go wrong:
# Increase verbosity
anvil -vvv install my-workload
# Enable trace logging
$env:ANVIL_LOG = "trace"
anvil install my-workload
Script Error Handling
In your workload scripts, handle errors gracefully:
# health-check.ps1
try {
$rustVersion = rustc --version
if ($LASTEXITCODE -ne 0) {
Write-Error "Rust not installed"
exit 1
}
Write-Host "Rust installed: $rustVersion"
exit 0
}
catch {
Write-Error "Health check failed: $_"
exit 1
}
Getting Help
Built-in Help
# General help
anvil --help
# Command-specific help
anvil install --help
anvil health --help
Resources
Reporting Issues
When reporting issues, include:
- Anvil version:
anvil --version - Windows version:
winver - Command that failed
- Verbose output:
anvil -vvv <command> - Relevant workload files (sanitized)
This guide is for Anvil v0.3.1. For other versions, check the corresponding documentation.
Workload Authoring
A comprehensive guide to creating custom workloads for Anvil.
1. Workload Structure
A workload is a directory containing configuration files, scripts, and assets that define a system configuration.
Directory Structure
workload-name/
├── workload.yaml # Required: workload definition
├── files/ # Optional: files to deploy
│ ├── .config/
│ │ └── app.conf
│ └── settings.json
└── scripts/ # Optional: installation/health scripts
├── pre-install.ps1
├── post-install.ps1
└── health-check.ps1
Required Files
- workload.yaml: The main workload definition file (required)
Optional Directories
- files/: Contains configuration files to be copied to the target system
- scripts/: Contains PowerShell or CMD scripts for installation and validation
Naming Conventions
- Workload directory names should be lowercase with hyphens:
my-workload-name - Use descriptive names that indicate the workload's purpose
- Avoid spaces and special characters
2. Schema Reference
The workload.yaml file defines all aspects of your workload configuration.
Complete Schema
# Required fields
name: string # Workload identifier
version: string # Semantic version (e.g., "1.0.0")
# Optional fields
description: string # Human-readable description
extends: string[] # Parent workloads to inherit from
packages: object # Package definitions
files: array # File deployment definitions
scripts: object # Script execution definitions
environment: object # Environment variable configuration
assertions: array # Declarative health assertions
health: object # Health check configuration
Required Fields
name
Unique identifier for the workload. Must be alphanumeric with hyphens and underscores only.
name: my-workload # Valid
name: my_workload_v2 # Valid
name: "my workload" # Invalid (spaces)
name: my.workload # Invalid (dots)
version
Semantic version string. Recommended to follow SemVer.
version: "1.0.0"
version: "2.1.0-beta"
version: "0.1.0"
Optional Fields
description
Human-readable description displayed in listings.
description: "Development environment for Rust projects with debugging tools"
extends
List of parent workloads to inherit from.
extends:
- essentials
- rust-developer
packages
Package installation definitions (see Package Definitions).
files
File deployment definitions (see File Definitions).
scripts
Script execution definitions (see Script Definitions).
environment
Environment variable configuration (see Environment Configuration).
3. Package Definitions
Define software packages to install via Windows Package Manager (winget).
Basic Structure
packages:
winget:
- id: Publisher.PackageName
- id: Another.Package
Full Package Options
packages:
winget:
- id: Git.Git
version: "2.43.0" # Optional: pin to specific version
source: winget # Optional: winget, msstore
override: # Optional: additional winget arguments
- "--scope"
- "machine"
Package Fields
| Field | Required | Description |
|---|---|---|
id | Yes | Winget package identifier |
version | No | Specific version to install |
source | No | Package source: winget or msstore |
override | No | Additional arguments passed to winget |
Finding Package IDs
Use winget to search for packages:
# Search for packages
winget search vscode
# Get exact ID
winget search --exact "Visual Studio Code"
# Show package details
winget show Microsoft.VisualStudioCode
Common package IDs:
| Package | ID |
|---|---|
| Visual Studio Code | Microsoft.VisualStudioCode |
| Git | Git.Git |
| Windows Terminal | Microsoft.WindowsTerminal |
| Node.js | OpenJS.NodeJS |
| Python | Python.Python.3.12 |
| Rust | Rustlang.Rustup |
| PowerShell | Microsoft.PowerShell |
Version Pinning
Pin specific versions when compatibility matters:
packages:
winget:
# Pin exact version
- id: Python.Python.3.12
version: "3.12.0"
# Use latest (default)
- id: Git.Git
Check available versions:
winget show Python.Python.3.12 --versions
Override Arguments
Pass custom arguments to winget:
packages:
winget:
- id: Microsoft.VisualStudioCode
override:
- "--scope"
- "machine" # Install for all users
- "--override"
- "/SILENT"
4. File Definitions
Define configuration files to copy to the target system.
Basic Structure
files:
- source: config.json
destination: "~/.config/app/config.json"
Full File Options
files:
- source: relative/path/in/workload/file.conf
destination: "~/target/path/file.conf"
backup: true # Backup existing file
mode: "0644" # Optional: file permissions
template: false # Process as Handlebars template
create_dirs: true # Create parent directories
File Fields
| Field | Required | Default | Description |
|---|---|---|---|
source | Yes | - | Path relative to workload directory |
destination | Yes | - | Target path on system |
backup | No | true | Backup existing files before overwriting |
mode | No | - | File permissions (Unix-style, informational on Windows) |
template | No | false | Process file as Handlebars template |
create_dirs | No | true | Create parent directories if missing |
Path Variables
Use these variables in destination paths:
| Variable | Description | Example |
|---|---|---|
~ | User home directory | C:\Users\username |
${HOME} | User home directory | C:\Users\username |
${USERPROFILE} | User profile directory | C:\Users\username |
${APPDATA} | Application data | C:\Users\username\AppData\Roaming |
${LOCALAPPDATA} | Local app data | C:\Users\username\AppData\Local |
${WORKLOAD_DIR} | Workload directory | Path to current workload |
Examples:
files:
# Home directory
- source: .gitconfig
destination: "~/.gitconfig"
# AppData
- source: settings.json
destination: "${APPDATA}/MyApp/settings.json"
# Local AppData
- source: cache.db
destination: "${LOCALAPPDATA}/MyApp/cache.db"
Templating
Enable Handlebars templating for dynamic content:
files:
- source: config.toml.hbs
destination: "~/.config/app/config.toml"
template: true
Template file (config.toml.hbs):
# Configuration for {{username}}
# Generated on {{date}}
[user]
name = "{{username}}"
home = "{{home}}"
[paths]
workload = "{{workload_dir}}"
Available template variables:
| Variable | Description |
|---|---|
{{username}} | Current username |
{{home}} | Home directory path |
{{computername}} | Computer name |
{{workload_dir}} | Workload directory |
{{workload_name}} | Workload name |
{{date}} | Current date |
{{env.VAR_NAME}} | Environment variable |
Directory Copying
Copy entire directories:
files:
- source: config/
destination: "~/.config/myapp/"
5. Script Definitions
Define PowerShell or CMD scripts for installation steps and health checks.
Script Categories
scripts:
pre_install: # Run before package installation
- path: scripts/pre-install.ps1
post_install: # Run after package installation
- path: scripts/post-install.ps1
health_check: # Run during health checks
- path: scripts/health-check.ps1
Full Script Options
scripts:
post_install:
- path: scripts/setup.ps1
shell: powershell # powershell, pwsh, cmd
description: "Configure application settings"
elevated: false # Run as administrator
timeout: 300 # Timeout in seconds
continue_on_error: false # Continue if script fails
env: # Additional environment variables
MY_VAR: "value"
Script Fields
| Field | Required | Default | Description |
|---|---|---|---|
path | Yes | - | Path relative to workload directory |
shell | No | powershell | Shell: powershell, pwsh, cmd |
description | No | - | Human-readable description |
elevated | No | false | Run with administrator privileges |
timeout | No | 300 | Timeout in seconds |
continue_on_error | No | false | Continue installation if script fails |
env | No | - | Additional environment variables |
name | No | - | Display name (for health checks) |
Health Check Scripts
Health check scripts verify system state:
scripts:
health_check:
- path: scripts/check-rust.ps1
name: "Rust Toolchain"
description: "Verify Rust is installed and configured"
timeout: 30
Script Guidelines
Exit Codes
0- Success- Non-zero - Failure
# Good: explicit exit codes
if (Test-Path $requiredFile) {
exit 0
} else {
Write-Error "Required file not found"
exit 1
}
Output Handling
- Use
Write-Hostfor informational output - Use
Write-Errorfor error messages - Use
Write-Warningfor warnings
Write-Host "Installing components..."
Write-Warning "This may take a while"
Write-Error "Installation failed"
Idempotency
Scripts should be safe to run multiple times:
# Good: check before acting
if (-not (Test-Path "C:\Tools\mytool")) {
Write-Host "Installing mytool..."
# Install logic here
} else {
Write-Host "mytool already installed"
}
Error Handling
Use try-catch for robust error handling:
try {
# Risky operation
Invoke-WebRequest -Uri $url -OutFile $path
exit 0
}
catch {
Write-Error "Failed to download: $_"
exit 1
}
Sample Scripts
Pre-Install Check
# scripts/pre-install.ps1
# Check prerequisites before installation
$ErrorActionPreference = "Stop"
# Check Windows version
$version = [Environment]::OSVersion.Version
if ($version.Major -lt 10) {
Write-Error "Windows 10 or later required"
exit 1
}
# Check available disk space (need 1GB)
$drive = Get-PSDrive C
$freeGB = [math]::Round($drive.Free / 1GB, 2)
if ($freeGB -lt 1) {
Write-Error "Insufficient disk space. Need 1GB, have ${freeGB}GB"
exit 1
}
Write-Host "Prerequisites check passed"
exit 0
Post-Install Configuration
# scripts/post-install.ps1
# Configure application after installation
$ErrorActionPreference = "Stop"
try {
# Install Rust components
Write-Host "Installing Rust components..."
rustup component add rustfmt
rustup component add clippy
# Install common cargo tools
Write-Host "Installing cargo tools..."
cargo install cargo-watch
cargo install cargo-edit
Write-Host "Post-install configuration complete"
exit 0
}
catch {
Write-Error "Post-install failed: $_"
exit 1
}
Health Check
# scripts/health-check.ps1
# Verify Rust development environment
$ErrorActionPreference = "Stop"
$errors = @()
# Check rustc
try {
$rustVersion = rustc --version
Write-Host "✓ Rust compiler: $rustVersion"
}
catch {
$errors += "rustc not found"
}
# Check cargo
try {
$cargoVersion = cargo --version
Write-Host "✓ Cargo: $cargoVersion"
}
catch {
$errors += "cargo not found"
}
# Check rustfmt
try {
$null = rustfmt --version
Write-Host "✓ rustfmt installed"
}
catch {
$errors += "rustfmt not installed"
}
# Report results
if ($errors.Count -gt 0) {
Write-Error "Health check failed:"
$errors | ForEach-Object { Write-Error " - $_" }
exit 1
}
Write-Host "All health checks passed"
exit 0
6. Environment Configuration
Configure environment variables and PATH additions.
Basic Structure
environment:
variables:
- name: MY_VARIABLE
value: "my-value"
scope: user
path_additions:
- "C:\\Tools\\bin"
- "~\\.local\\bin"
Environment Variables
environment:
variables:
- name: RUST_BACKTRACE
value: "1"
scope: user # user or machine
- name: EDITOR
value: "code"
scope: user
| Field | Required | Default | Description |
|---|---|---|---|
name | Yes | - | Variable name |
value | Yes | - | Variable value |
scope | No | user | Scope: user or machine |
PATH Additions
Add directories to the system PATH:
environment:
path_additions:
- "C:\\Tools\\bin"
- "~\\.cargo\\bin"
- "${LOCALAPPDATA}\\Programs\\bin"
PATH additions are appended to the existing PATH for the specified scope.
Scope
| Scope | Description | Requires Admin |
|---|---|---|
user | Current user only | No |
machine | All users on system | Yes |
7. Assertions
Assertions are declarative health checks defined directly in workload.yaml. They let you validate system state — such as installed commands, existing files, environment variables, and PATH entries — without writing PowerShell scripts.
Use assertions when your checks are simple conditions (command exists, file exists, env var set). Use health check scripts for complex validation that requires multi-step logic or custom output.
Assertion Structure
Each assertion has a name and a check that specifies a condition:
assertions:
- name: Git is installed
check:
type: command_exists
command: git
Condition Types
command_exists
Checks whether a command is available on PATH.
- name: cargo is available
check:
type: command_exists
command: cargo
file_exists
Checks whether a file exists at the given path. Supports ~ expansion.
- name: Git config exists
check:
type: file_exists
path: "~/.gitconfig"
dir_exists
Checks whether a directory exists at the given path. Supports ~ expansion.
- name: Cargo directory exists
check:
type: dir_exists
path: "~/.cargo"
env_var
Checks whether an environment variable is set, optionally matching a specific value.
# Check existence only
- name: RUST_BACKTRACE is set
check:
type: env_var
name: RUST_BACKTRACE
# Check existence and value
- name: RUST_BACKTRACE is 1
check:
type: env_var
name: RUST_BACKTRACE
value: "1"
path_contains
Checks whether the system PATH contains a given substring.
- name: Cargo bin on PATH
check:
type: path_contains
substring: ".cargo/bin"
registry_value
Queries a Windows registry value under HKCU or HKLM. If expected is omitted, the check only asserts the value exists.
- name: Developer mode enabled
check:
type: registry_value
hive: HKLM
key: "SOFTWARE\\Microsoft\\Windows\\CurrentVersion\\AppModelUnlock"
name: AllowDevelopmentWithoutDevLicense
expected: "1"
shell
Runs an arbitrary shell command; the condition passes when the exit code is 0.
- name: Rust compiler responds
check:
type: shell
command: "rustc --version"
description: "Rust compiler version check"
Composition with all_of and any_of
Combine conditions with logical operators for complex checks.
all_of — all conditions must pass (AND)
- name: Full Rust toolchain
check:
type: all_of
conditions:
- type: command_exists
command: rustc
- type: command_exists
command: cargo
- type: dir_exists
path: "~/.cargo"
any_of — at least one must pass (OR)
- name: Python is available
check:
type: any_of
conditions:
- type: command_exists
command: python
- type: command_exists
command: python3
Enabling Assertion Checks
Assertions are evaluated during anvil health when assertion_check is enabled in the health config (it defaults to true):
health:
package_check: true
file_check: true
script_check: true
assertion_check: true # Evaluate declarative assertions
Complete Example
name: rust-developer
version: "1.0.0"
description: "Rust development environment"
packages:
winget:
- id: Rustlang.Rustup
assertions:
- name: cargo command exists
check:
type: command_exists
command: cargo
- name: rustc command exists
check:
type: command_exists
command: rustc
- name: Cargo directory exists
check:
type: dir_exists
path: "~/.cargo"
- name: Cargo bin on PATH
check:
type: path_contains
substring: ".cargo/bin"
- name: RUST_BACKTRACE is set
check:
type: env_var
name: RUST_BACKTRACE
value: "1"
health:
package_check: true
assertion_check: true
8. Inheritance
Workloads can extend other workloads to inherit their configuration.
Basic Inheritance
name: my-rust-dev
version: "1.0.0"
extends:
- essentials # Inherit base development tools
Multiple Inheritance
name: full-stack-dev
version: "1.0.0"
extends:
- essentials
- rust-developer
- python-developer
Merge Behavior
When a workload extends parents, configuration is merged:
| Section | Merge Behavior |
|---|---|
packages | Merged; child packages added to parent packages |
files | Merged; child files override parent files with same destination |
scripts | Merged; child scripts run after parent scripts |
environment | Merged; child variables override parent variables |
Override Example
Parent (base/workload.yaml):
name: base
version: "1.0.0"
packages:
winget:
- id: Git.Git
- id: Microsoft.VisualStudioCode
files:
- source: .gitconfig
destination: "~/.gitconfig"
Child (extended/workload.yaml):
name: extended
version: "1.0.0"
extends:
- base
packages:
winget:
# Adds to parent packages
- id: Rustlang.Rustup
files:
# Overrides parent's .gitconfig
- source: .gitconfig
destination: "~/.gitconfig"
Inheritance Chains
Anvil resolves inheritance chains automatically:
my-workload
└── rust-developer
└── essentials
Circular dependencies are detected and rejected:
# workload-a extends workload-b
# workload-b extends workload-a
# ERROR: Circular dependency detected
View Inheritance
# Show inheritance tree
anvil show my-workload --inheritance-tree
# Show fully resolved workload
anvil show my-workload --resolved
9. Variable Expansion
Use variables in paths and values for dynamic configuration.
Supported Variables
| Variable | Description | Example Value |
|---|---|---|
~ | User home directory | C:\Users\username |
${HOME} | User home directory | C:\Users\username |
${USERNAME} | Current username | username |
${COMPUTERNAME} | Machine name | WORKSTATION-01 |
${WORKLOAD_DIR} | Workload directory | C:\Workloads\my-workload |
${USERPROFILE} | User profile path | C:\Users\username |
${APPDATA} | Roaming AppData | C:\Users\username\AppData\Roaming |
${LOCALAPPDATA} | Local AppData | C:\Users\username\AppData\Local |
${env:VAR_NAME} | Any environment variable | (varies) |
Usage Examples
files:
# Home directory shorthand
- source: .bashrc
destination: "~/.bashrc"
# Explicit home variable
- source: config.json
destination: "${HOME}/.config/myapp/config.json"
# AppData paths
- source: settings.json
destination: "${APPDATA}/MyApp/settings.json"
# Environment variable
- source: custom.conf
destination: "${env:MY_CUSTOM_PATH}/config.conf"
environment:
variables:
- name: MY_APP_HOME
value: "${HOME}/.myapp"
path_additions:
- "${HOME}/.local/bin"
- "${LOCALAPPDATA}/Programs/bin"
10. Best Practices
Workload Design
-
Use Descriptive Names
name: rust-developer # Good name: workload1 # Bad -
Include Version Information
version: "1.2.0" # Good: semantic versioning version: "latest" # Bad: not meaningful -
Write Helpful Descriptions
description: "Complete Rust development environment with debugging tools and VS Code extensions" -
Use Inheritance for Common Bases
# Create a base workload for team-wide tools # Then extend it for role-specific setups extends: - team-base
Package Management
-
Pin Versions When Necessary
packages: winget: # Pin when compatibility matters - id: Python.Python.3.12 version: "3.12.0" # Use latest for frequently updated tools - id: Git.Git -
Use Machine Scope for Shared Tools
packages: winget: - id: Microsoft.VisualStudioCode override: - "--scope" - "machine"
File Management
-
Always Enable Backups for Important Files
files: - source: .gitconfig destination: "~/.gitconfig" backup: true -
Use Templates for Dynamic Content
files: - source: config.toml.hbs destination: "~/.config/app/config.toml" template: true
Script Safety
-
Make Scripts Idempotent
# Check before acting if (-not (Test-Path $target)) { # Create/install } -
Handle Errors Gracefully
try { # Risky operation } catch { Write-Error "Operation failed: $_" exit 1 } -
Include Health Checks
scripts: health_check: - path: scripts/verify.ps1 name: "Installation Verification"
Testing
-
Validate Before Committing
anvil validate my-workload --strict -
Test with Dry Run
anvil install my-workload --dry-run -
Test on Clean System
- Use a VM or container
- Document dependencies
11. Example Workloads
Minimal Workload
The simplest valid workload:
# minimal/workload.yaml
name: minimal
version: "1.0.0"
description: "A minimal workload example"
Package-Only Workload
Install software without files or scripts:
# dev-essentials/workload.yaml
name: dev-essentials
version: "1.0.0"
description: "Essential development tools"
packages:
winget:
- id: Git.Git
- id: Microsoft.VisualStudioCode
- id: Microsoft.WindowsTerminal
- id: JanDeDobbeleer.OhMyPosh
Full-Featured Workload
Complete example with all features:
# full-example/workload.yaml
name: full-example
version: "1.0.0"
description: "Complete workload demonstrating all features"
extends:
- essentials
packages:
winget:
- id: Rustlang.Rustup
- id: LLVM.LLVM
version: "17.0.6"
- id: Microsoft.VisualStudio.2022.BuildTools
override:
- "--add"
- "Microsoft.VisualStudio.Workload.VCTools"
files:
- source: files/.cargo/config.toml
destination: "~/.cargo/config.toml"
backup: true
- source: files/vscode/settings.json.hbs
destination: "${APPDATA}/Code/User/settings.json"
template: true
scripts:
pre_install:
- path: scripts/pre-check.ps1
description: "Check prerequisites"
post_install:
- path: scripts/setup-rust.ps1
description: "Configure Rust toolchain"
elevated: false
timeout: 600
health_check:
- path: scripts/health.ps1
name: "Rust Environment"
description: "Verify Rust development environment"
environment:
variables:
- name: RUST_BACKTRACE
value: "1"
scope: user
path_additions:
- "~/.cargo/bin"
Inherited Workload
Workload that builds on others:
# rust-advanced/workload.yaml
name: rust-advanced
version: "1.0.0"
description: "Advanced Rust development with WASM and embedded support"
extends:
- rust-developer
packages:
winget:
- id: Docker.DockerDesktop
- id: WasmEdge.WasmEdge
scripts:
post_install:
- path: scripts/setup-targets.ps1
description: "Add Rust compilation targets"
environment:
path_additions:
- "~/.wasmedge/bin"
With corresponding script:
# rust-advanced/scripts/setup-targets.ps1
# Add additional Rust compilation targets
$ErrorActionPreference = "Stop"
try {
Write-Host "Adding WASM target..."
rustup target add wasm32-unknown-unknown
Write-Host "Adding embedded targets..."
rustup target add thumbv7em-none-eabihf
Write-Host "Installing cargo tools for embedded..."
cargo install cargo-embed
cargo install probe-run
exit 0
}
catch {
Write-Error "Setup failed: $_"
exit 1
}
12. Private Workload Repositories
You can maintain your own workloads in a separate Git repository and configure Anvil to discover them.
Recommended Directory Layout
my-workloads/
├── my-dev-env/
│ ├── workload.yaml
│ ├── files/
│ │ └── .gitconfig
│ └── scripts/
│ └── setup.ps1
├── team-tools/
│ ├── workload.yaml
│ └── scripts/
│ └── post-install.ps1
└── README.md
Each subdirectory containing a workload.yaml is treated as a separate workload, following the same directory structure as bundled workloads.
Setup
-
Clone your workloads repository:
git clone https://github.com/your-org/workloads ~/my-workloads -
Configure Anvil to search this path:
anvil config set workloads.paths '["~/my-workloads"]'Or edit
~/.anvil/config.yamldirectly:workloads: paths: - "~/my-workloads" -
Verify discovery:
anvil list
Tips
- Inheritance: Private workloads can
extends:built-in workloads (e.g.,extends: [essentials]) - Version control: Keep workloads in Git for team sharing and history
- Multiple repos: Add multiple paths for team-shared and personal workloads
- Precedence: If your workload has the same name as a built-in one, yours takes priority
- Validation: Run
anvil validate <name> --strictto check your workloads before committing
Complete Config Example
# Anvil global configuration
# Location: ~/.anvil/config.yaml
workloads:
paths:
- "~/my-workloads" # Personal workloads
- "~/work/team-workloads" # Team-shared workloads
logging:
level: info
Resources
This guide is for Anvil v0.3.1. For other versions, check the corresponding documentation.
Troubleshooting
A guide to diagnosing and resolving common Anvil issues.
1. Installation Issues
Anvil won't run
Symptoms:
- "anvil" is not recognized as a command
- Application crashes immediately
Solutions:
-
Check PATH configuration
Windows:
where.exe anvil # If not found, add to PATH $env:PATH += ";C:\path\to\anvil"Linux/macOS:
which anvil # If not found, add to PATH export PATH="$PATH:/path/to/anvil" -
Verify platform compatibility
- Anvil currently requires Windows 10 (1809+) or Windows 11
- Cross-platform support is on the roadmap
-
Verify the download isn't corrupted
- Re-download from the releases page
- Verify the file hash if provided
Package manager not found
Symptoms:
- Error: "winget is not recognized"
- Error: "Package manager not available"
- Package installation fails before starting
Solutions (Windows — winget):
-
Install Windows Package Manager
# Open Microsoft Store to App Installer Start-Process "ms-windows-store://pdp/?ProductId=9NBLGGH4NNS1" -
Update App Installer
- Open Microsoft Store → search "App Installer" → click "Update"
-
Test availability
winget --version -
Restart your terminal after installing winget
2. Package Installation Issues
Package not found
Symptoms:
- Error: "No package found matching input criteria"
- Error: "Package 'X' not found in any source"
Solutions:
-
Verify package ID
winget search <package-name> winget search --exact "Package Name" -
Check package source in workload
packages: winget: - id: SomeApp.App source: msstore # For Microsoft Store apps -
Update package sources
winget source update
Installation hangs
Symptoms:
- Installation appears frozen
- No progress for extended time
Solutions:
-
Check for interactive prompts
- Some installers require user interaction
- Run anvil without
--quietto see prompts
-
Use silent install overrides
packages: winget: - id: SomeApp.App override: - "--silent" - "--accept-license" -
Check for pending system updates
- Pending OS updates can block installations
- Complete any pending updates and restart
Access denied during installation
Symptoms:
- Error: "Access is denied"
- Error: "Administrator privileges required"
Solutions:
-
Run with elevation (Windows)
Start-Process powershell -Verb RunAs anvil install my-workload -
Use user-scope installation
packages: winget: - id: Package.Name override: - "--scope" - "user"
3. File Operation Issues
Permission denied
Symptoms:
- Error: "Permission denied" when copying files
- File operations fail
Solutions:
-
Use user-writable paths
files: - source: config.json destination: "~/.config/app/config.json" # Expands to user home -
Check target directory permissions
Windows:
Get-Acl "C:\path\to\directory" | Format-ListLinux/macOS:
ls -la /path/to/directory -
Run as administrator for system-level paths
File not found
Symptoms:
- Error: "Source file not found"
Solutions:
-
Verify source path — paths are relative to the workload directory
files: - source: files/config.json # Relative to workload dir destination: "~/.config/app/config.json" -
Check workload directory structure
my-workload/ ├── workload.yaml └── files/ └── config.json
Backup failed
Symptoms:
- Error: "Failed to create backup"
Solutions:
- Check disk space
- Verify backup directory exists and is writable
$env:ANVIL_BACKUP_DIR = "D:\Backups\anvil"
4. Script Execution Issues
Script won't execute
Symptoms:
- Error: "Script execution failed"
- No output from script
Solutions:
-
Verify script path in workload.yaml — paths are relative to
scripts/scripts: post_install: - path: setup.ps1 # Relative to scripts/ directory -
Test script manually
& "C:\workloads\my-workload\scripts\setup.ps1"
Execution policy error (Windows)
Symptoms:
- Error: "Running scripts is disabled on this system"
Solutions:
-
Set execution policy
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser -
Check group policy — corporate environments may have stricter policies
Script timeout
Symptoms:
- Error: "Script execution timed out"
Solutions:
-
Increase timeout in workload
scripts: post_install: - path: scripts/long-running.ps1 timeout: 1800 # 30 minutes (default: 300) -
Check for infinite loops or slow network operations in the script
Elevated script fails (Windows)
Symptoms:
- Error: "Elevation required"
Solutions:
- Run anvil as administrator
- Set the elevated flag in workload.yaml
scripts: post_install: - path: scripts/admin-setup.ps1 elevated: true
5. Health Check Issues
False failures
Symptoms:
- Health check fails but software is working
- Intermittent failures
Solutions:
-
Run with verbose output
anvil health my-workload --verbose -
Review health check scripts — they may have outdated expectations
-
Update version expectations
winget list --id Package.Name
Partial results
Symptoms:
- Some checks don't run
- Health report incomplete
Solutions:
-
Check verbose output for errors in earlier checks
anvil health my-workload -vvv -
Verify all referenced scripts exist in the workload directory
6. Configuration Issues
Config file not loading
Symptoms:
- Settings not applied
- Default values used instead of custom
Solutions:
-
Check config location
anvil config path -
View current configuration
anvil config list -
Reset to defaults
anvil config reset
Workloads not found
Symptoms:
- Error: "Workload 'X' not found"
- Empty list from
anvil list
Solutions:
-
Check search paths
anvil config list -
Verify workload structure — needs a
workload.yamlfileworkload-name/ └── workload.yaml -
Use explicit path
anvil install my-workload --path /path/to/workloads
7. Error Reference
Common Error Codes
| Error | Description | Solution |
|---|---|---|
E001 | Workload not found | Check name and search paths |
E002 | Invalid workload schema | Run anvil validate for details |
E003 | Circular dependency | Review extends chain in workloads |
E004 | Package installation failed | Check package manager logs, verify package ID |
E005 | File operation failed | Check permissions, verify paths |
E006 | Script execution failed | Review script output, check syntax |
E007 | Health check failed | Review check results, update scripts |
E008 | Configuration error | Validate config file syntax |
E009 | Backup operation failed | Check disk space and permissions |
E010 | Restore operation failed | Verify backup exists and is valid |
8. Getting Help
Verbose Output
Get more detailed information about what Anvil is doing:
anvil -v <command> # Some detail
anvil -vv <command> # More detail
anvil -vvv <command> # Maximum detail (debug level)
Enable Logging
# Set log level via environment variable
ANVIL_LOG=debug anvil install my-workload
# Available levels: error, warn, info, debug, trace
System Information
Gather helpful information when reporting issues:
# Anvil version
anvil --version
# Operating system
# Windows: winver
# Linux/macOS: uname -a
# Package manager version
winget --version # Windows
Reporting Issues
When reporting issues, include:
- Anvil version:
anvil --version - Operating system and version
- Command that failed: exact command you ran
- Error message: complete error output
- Verbose output: run with
-vvvflag - Workload file: contents of
workload.yaml(sanitized if needed)
Resources
Quick Reference
Common Commands for Troubleshooting
# Check Anvil installation
anvil --version
# Validate workload
anvil validate my-workload --strict
# Preview installation (dry run)
anvil install my-workload --dry-run
# Verbose health check
anvil health my-workload -vvv
# View current configuration
anvil config list
# Reset configuration
anvil config reset
Environment Variables
| Variable | Purpose |
|---|---|
ANVIL_CONFIG | Custom config file path |
ANVIL_WORKLOADS | Additional workload search paths |
ANVIL_LOG | Log level (error, warn, info, debug, trace) |
ANVIL_BACKUP_DIR | Custom backup directory |
NO_COLOR | Disable colored output |
Specification
Version: 2.0.0
Status: Active
Last Updated: 2026-04-16
1. Executive Summary
1.1 Purpose
Anvil is a declarative workstation configuration management tool designed to automate the setup and validation of development environments. It provides a declarative approach to defining workstation configurations ("workloads") and offers both installation and health-check capabilities. Anvil currently targets Windows with winget as the primary package manager, with cross-platform support (Homebrew, APT) on the roadmap.
1.2 Core Modes of Operation
| Mode | Description |
|---|---|
| Install | Apply a workload configuration by installing packages, executing scripts, and copying files |
| Health Check | Validate the current system state against a workload definition |
1.3 Key Features
- Declarative Configuration: Define workloads in human-readable YAML files
- Package Management: Leverage winget for software installation
- File Synchronization: Copy configuration files to designated locations with integrity verification
- Script Execution: Run pre/post installation scripts and diagnostic validations
- Workload Composition: Inherit and extend base workloads for DRY configurations
- Detailed Reporting: Comprehensive logs and health reports
2. Technology Analysis
2.1 Evaluation Criteria
| Criterion | Weight | Description |
|---|---|---|
| Windows Integration | 25% | Native Windows APIs, winget interoperability, registry access |
| Maintainability | 20% | Code clarity, testing support, community ecosystem |
| Extensibility | 15% | Plugin architecture, workload composition |
| Dependencies | 15% | Target machine requirements, deployment complexity |
| Performance | 10% | Startup time, execution speed |
| Error Handling | 15% | Exception management, detailed reporting |
2.2 Technology Comparison
2.2.1 PowerShell Scripts
Strengths:
- Native Windows integration; first-class citizen on all modern Windows
- Direct winget command execution with native output parsing
- No additional runtime dependencies
- Excellent registry, file system, and Windows service access
- Built-in remoting capabilities
Weaknesses:
- Complex error handling patterns
- Limited testing frameworks compared to general-purpose languages
- Script organization becomes unwieldy at scale
- Type system limitations for complex data structures
- Inconsistent cross-version behavior (5.1 vs 7+)
Dependency Requirements: None (built into Windows)
Winget Integration: ★★★★★ Excellent - native command execution
Score: 72/100
2.2.2 Python Scripts
Strengths:
- Rich ecosystem with excellent YAML/JSON/TOML parsing libraries
- Strong testing frameworks (pytest, unittest)
- Good readability and maintainability
- Cross-platform potential
- Excellent error handling with exceptions
Weaknesses:
- Requires Python runtime installation on target machines
- Virtual environment complexity for dependencies
- Subprocess management for winget feels indirect
- Windows-specific operations require additional libraries
- Version compatibility issues (3.x versions)
Dependency Requirements: Python 3.8+ runtime, pip packages
Winget Integration: ★★★☆☆ Good - via subprocess
Score: 68/100
2.2.3 C# Script Files (dotnet-script)
Strengths:
- Strong typing with excellent IDE support
- Access to full .NET ecosystem
- Good Windows integration via .NET APIs
- Reasonable startup time for scripts
- NuGet package support
Weaknesses:
- Requires .NET SDK installation
- Less common tooling; smaller community
- Debugging experience inferior to full applications
- Script organization patterns less established
- Version pinning complexity
Dependency Requirements: .NET 6+ SDK, dotnet-script global tool
Winget Integration: ★★★☆☆ Good - via Process class
Score: 65/100
2.2.4 C# Console Application
Strengths:
- Excellent type safety and refactoring support
- Comprehensive .NET ecosystem (DI, logging, configuration)
- Strong testing capabilities (xUnit, NUnit, MSTest)
- Native Windows API access via P/Invoke
- Can produce single-file executables
- Mature error handling with structured exceptions
- Excellent async/await support for parallel operations
Weaknesses:
- Requires .NET runtime (can be self-contained but increases size)
- Longer development cycle than scripting
- Compilation step required
- Larger deployment artifact if self-contained (~60MB+)
Dependency Requirements: .NET 8 runtime (or self-contained)
Winget Integration: ★★★★☆ Very Good - Process class with structured parsing
Score: 82/100
2.2.5 Rust CLI Application
Strengths:
- Zero runtime dependencies; single static binary
- Excellent performance and minimal resource usage
- Strong type system with compile-time guarantees
- Superior error handling with Result<T, E> pattern
- Memory safety without garbage collection
- Excellent CLI frameworks (clap, structopt)
- Fast startup time
Weaknesses:
- Steeper learning curve
- Longer compilation times
- Smaller Windows-specific ecosystem
- Windows API interop more verbose than .NET
- Less familiar to typical Windows administrators
Dependency Requirements: None (static binary)
Winget Integration: ★★★★☆ Very Good - std::process::Command with structured parsing
Score: 85/100
2.3 Comparison Matrix
| Criterion | PowerShell | Python | C# Script | C# App | Rust |
|---|---|---|---|---|---|
| Windows Integration | ★★★★★ | ★★★☆☆ | ★★★★☆ | ★★★★★ | ★★★★☆ |
| Maintainability | ★★★☆☆ | ★★★★☆ | ★★★☆☆ | ★★★★★ | ★★★★☆ |
| Extensibility | ★★★☆☆ | ★★★★☆ | ★★★☆☆ | ★★★★★ | ★★★★★ |
| Dependencies | ★★★★★ | ★★☆☆☆ | ★★☆☆☆ | ★★★★☆ | ★★★★★ |
| Performance | ★★★☆☆ | ★★★☆☆ | ★★★☆☆ | ★★★★☆ | ★★★★★ |
| Error Handling | ★★★☆☆ | ★★★★☆ | ★★★★☆ | ★★★★★ | ★★★★★ |
| Weighted Score | 72 | 68 | 65 | 82 | 85 |
3. Technology Recommendation
3.1 Primary Recommendation: Rust CLI Application
Rationale:
-
Zero Dependencies: The single static binary eliminates runtime requirements, crucial for bootstrapping fresh Windows installations where development tools aren't yet installed.
-
Robust Error Handling: Rust's
Result<T, E>pattern enforces comprehensive error handling at compile time, preventing runtime surprises during critical system configuration. -
Performance: Fast startup (~5ms) and execution makes the tool feel responsive, encouraging frequent health checks.
-
Configuration Parsing: Excellent libraries for YAML (
serde_yaml), TOML (toml), and JSON (serde_json) with strong type safety. -
CLI Excellence: The
clapcrate provides industry-leading CLI parsing with automatic help generation, shell completions, and argument validation. -
Future-Proof: Rust's stability guarantees and backwards compatibility ensure long-term maintainability.
3.2 Configuration Format: YAML
Rationale:
- Human-readable and writable without tooling
- Supports comments for documentation
- Native multi-line string support for inline scripts
- Widely understood by developers and DevOps professionals
- Excellent library support across all evaluated languages
3.3 Secondary Components: PowerShell
For pre/post installation scripts and health check validators, PowerShell remains the optimal choice:
- Native Windows command execution
- No additional dependencies
- Familiar to Windows administrators
- Direct access to Windows-specific features
3.4 Architecture Decision Records (ADR)
| ADR | Decision | Rationale |
|---|---|---|
| ADR-001 | Use Rust for core CLI | Zero runtime deps, robust error handling |
| ADR-002 | Use YAML for workload definitions | Human-readable, supports comments |
| ADR-003 | Use PowerShell for scripts | Native Windows integration |
| ADR-004 | Use SHA-256 for file hashing | Industry standard, good performance |
| ADR-005 | Support workload inheritance | DRY principle, modular configurations |
4. Project Structure
4.1 Repository Layout
anvil/
├── .github/
│ └── workflows/
│ ├── ci.yml # Continuous integration
│ └── release.yml # Release automation
├── docs/
│ ├── SPECIFICATION.md # This document
│ ├── USER_GUIDE.md # End-user documentation
│ ├── WORKLOAD_AUTHORING.md # Guide for creating workloads
│ └── TROUBLESHOOTING.md # Common issues and solutions
├── src/
│ ├── main.rs # Application entry point
│ ├── cli/
│ │ ├── mod.rs # CLI module root
│ │ ├── commands.rs # Command definitions
│ │ └── output.rs # Output formatting (table, JSON)
│ ├── config/
│ │ ├── mod.rs # Configuration module root
│ │ ├── workload.rs # Workload struct definitions
│ │ ├── schema.rs # Schema validation
│ │ └── inheritance.rs # Workload composition logic
│ ├── operations/
│ │ ├── mod.rs # Operations module root
│ │ ├── install.rs # Installation operations
│ │ ├── health.rs # Health check operations
│ │ └── backup.rs # Backup/restore operations
│ ├── providers/
│ │ ├── mod.rs # Providers module root
│ │ ├── winget.rs # Winget package manager
│ │ ├── filesystem.rs # File operations
│ │ └── script.rs # Script execution
│ ├── hashing/
│ │ ├── mod.rs # Hashing module root
│ │ └── sha256.rs # SHA-256 implementation
│ └── reporting/
│ ├── mod.rs # Reporting module root
│ ├── console.rs # Console output
│ ├── json.rs # JSON report generation
│ └── html.rs # HTML report generation
├── workloads/
│ ├── essentials/
│ │ ├── workload.yaml # Core dev tools (VS Code, Git, Terminal) and utilities
│ │ ├── files/
│ │ │ └── user/ # User configuration files
│ │ └── scripts/
│ │ ├── health-check.ps1 # Essentials health validation
│ │ └── configure-sudo.ps1 # Windows sudo configuration
│ ├── rust-developer/
│ │ ├── workload.yaml # Rust toolchain workload
│ │ ├── files/
│ │ │ └── config.toml # Cargo config
│ │ └── scripts/
│ │ ├── post-install.ps1 # Rustup component installation
│ │ └── health-check.ps1 # Rust environment validation
│ └── python-developer/
│ ├── workload.yaml # Python development workload
│ ├── files/
│ │ └── pip.ini # Pip configuration
│ └── scripts/
│ ├── post-install.ps1 # Virtual environment setup
│ └── health-check.ps1 # Python environment validation
├── tests/
│ ├── integration/
│ │ ├── install_tests.rs # Installation integration tests
│ │ └── health_tests.rs # Health check integration tests
│ └── fixtures/
│ └── sample-workload/ # Test workload
├── Cargo.toml # Rust project manifest
├── Cargo.lock # Dependency lock file
├── README.md # Project overview
├── LICENSE # License file
└── .gitignore # Git ignore rules
4.2 Workload Directory Structure
Each workload follows a consistent structure:
<workload-name>/
├── workload.yaml # Workload definition (required)
├── files/ # Files to copy to target system (optional)
│ ├── .config/
│ │ └── app/
│ │ └── settings.json
│ └── .profile
└── scripts/ # Scripts for installation/validation (optional)
├── pre-install.ps1 # Runs before package installation
├── post-install.ps1 # Runs after package installation
└── health-check.ps1 # Validation script for health mode
5. Workload Definition Schema
5.1 Schema Overview (YAML)
# workload.yaml - Full Schema Reference
# Metadata (required)
name: string # Unique workload identifier
version: string # Semantic version (e.g., "1.0.0")
description: string # Human-readable description
# Inheritance (optional)
extends: # List of parent workloads
- string # Parent workload name
# Package Management (optional)
packages:
winget: # Winget package definitions
- id: string # Winget package ID (required)
version: string # Specific version (optional, default: latest)
source: string # Package source (optional, default: winget)
override: string[] # Additional winget arguments (optional)
# File Management (optional)
files:
- source: string # Relative path from workload's files/ directory
destination: string # Absolute path or path with variables
backup: boolean # Backup existing file (optional, default: true)
permissions: string # File permissions (optional, future use)
template: boolean # Process as template (optional, default: false)
# Script Execution (optional)
scripts:
pre_install: # Scripts to run before installation
- path: string # Relative path from workload's scripts/ directory
shell: string # Execution shell (optional, default: "powershell")
elevated: boolean # Require admin privileges (optional, default: false)
timeout: integer # Timeout in seconds (optional, default: 300)
post_install: # Scripts to run after installation
- path: string
shell: string
elevated: boolean
timeout: integer
health_check: # Scripts for health validation
- path: string
shell: string
name: string # Display name for the check
description: string # What this check validates
# Environment Configuration (optional)
environment:
variables: # Environment variables to set
- name: string # Variable name
value: string # Variable value
scope: string # "user" or "machine" (default: "user")
path_additions: # Additions to PATH
- string # Path to add
# Health Check Configuration (optional)
health:
package_check: boolean # Verify packages installed (default: true)
file_check: boolean # Verify files match (default: true)
script_check: boolean # Run health check scripts (default: true)
assertion_check: boolean # Evaluate declarative assertions (default: true)
5.1.1 Assertions and Legacy Health Check Coexistence
Anvil supports two mechanisms for health validation:
| Mechanism | Field | Introduced | Status |
|---|---|---|---|
| Declarative assertions | assertions: | v0.5 | Recommended |
| Health check scripts | scripts.health_check | v0.1 | Deprecated when used with assertions |
Execution order: When both exist, assertions run first, then legacy scripts. Results are merged into a single health report.
Deprecation timeline:
- v0.5: Warning emitted when
scripts.health_checkis used alongsideassertions - v0.6: Warning emitted for any use of
scripts.health_check(even without assertions) - v1.0:
scripts.health_checkremoved; use assertions exclusively
Migration: Convert health check scripts to declarative assertions:
# Before (legacy script)
scripts:
health_check:
- path: check-git.ps1
name: "Git installed"
# After (declarative assertion)
assertions:
- name: Git installed
check:
type: command_exists
command: git
5.1.2 Commands and Legacy Scripts Coexistence
Anvil supports two mechanisms for running actions during installation:
| Mechanism | Field | Introduced | Status |
|---|---|---|---|
| Inline commands | commands.pre_install / commands.post_install | v0.6 | Recommended |
| Script files | scripts.pre_install / scripts.post_install | v0.1 | Deprecated when used with commands |
Execution order: When both exist for the same phase, commands run first, then legacy scripts.
Deprecation timeline:
- v0.6: Warning emitted when
scripts.pre_install/scripts.post_installis used alongsidecommands.pre_install/commands.post_install - v0.8: Warning emitted for any use of
scripts.pre_install/scripts.post_install - v1.0:
scripts.pre_installandscripts.post_installremoved; usecommandsexclusively
Migration: Convert script entries to inline commands:
# Before (legacy script)
scripts:
post_install:
- path: setup.ps1
description: "Install Rust components"
# After (inline command)
commands:
post_install:
- run: rustup default stable
description: "Set stable as default toolchain"
- run: cargo install cargo-watch
description: "Install cargo-watch"
when:
type: command_exists
command: cargo
Note: scripts.health_check follows a separate deprecation path (see §5.1.1).
5.1.3 Command Execution Semantics
Commands defined in commands.pre_install and commands.post_install are executed inline during the install flow.
Execution rules:
- Commands run sequentially in the order defined
- Default behavior: stop on first failure (non-zero exit code)
continue_on_error: trueoverrides this per-command- Commands with
when:conditions are evaluated before execution; if the condition fails, the command is skipped
Output handling:
- stdout and stderr are captured and included in reports
- In verbose mode (
-v), output is streamed in real-time - In quiet mode (
-q), output is suppressed unless the command fails
Timeout behavior:
- Default timeout: 300 seconds
- On timeout: process is terminated (SIGTERM on Unix, TerminateProcess on Windows)
- Timed-out commands are reported as failures
Elevated commands:
- Commands with
elevated: truerequire admin privileges - On Windows: validated via
whoami /privor equivalent before attempting - If elevation is unavailable, the command fails with a clear error message
Exit codes:
- Exit code 0 = success
- Any non-zero exit code = failure (unless
continue_on_error: true)
5.2 Example Workloads
5.2.1 Essentials Workload
# workloads/essentials/workload.yaml
name: essentials
version: "2.0.0"
description: "Core development tools and productivity utilities for everyday Windows use"
packages:
winget:
- id: Git.Git
override:
- --override
- '/VERYSILENT /NORESTART /NOCANCEL /SP- /CLOSEAPPLICATIONS /RESTARTAPPLICATIONS /COMPONENTS="icons,ext\reg\shellhere,assoc,assoc_sh"'
- id: Microsoft.VisualStudioCode
override:
- --override
- '/VERYSILENT /NORESTART /MERGETASKS=!runcode,addcontextmenufiles,addcontextmenufolders,addtopath'
- id: Microsoft.WindowsTerminal
- id: JanDeDobbeleer.OhMyPosh
- id: GitHub.cli
files:
- source: user/.gitconfig
destination: "~/.gitconfig"
backup: true
- source: user/.config/pwsh
destination: "~/.config/pwsh"
backup: true
scripts:
post_install:
- path: post-install.ps1
shell: powershell
- path: configure-sudo.ps1
shell: powershell
elevated: true
health_check:
- path: health-check.ps1
name: "essentials Health Check"
description: "Verifies essentials is properly configured"
- path: verify-sudo.ps1
name: "Windows Sudo"
description: "Verifies Windows sudo is enabled and set to inline mode"
5.2.2 Rust Developer Workload
# workloads/rust-developer/workload.yaml
name: rust-developer
version: "1.0.0"
description: "Complete Rust development environment"
extends:
- essentials
packages:
winget:
- id: Rustlang.Rustup
- id: Microsoft.VisualStudio.2022.BuildTools
override:
- --override
- '--quiet --wait --add Microsoft.VisualStudio.Workload.VCTools --includeRecommended'
- id: LLVM.LLVM
files:
- source: config.toml
destination: "~/.cargo/config.toml"
backup: true
- source: rustfmt.toml
destination: "~/rustfmt.toml"
backup: false
scripts:
post_install:
- path: post-install.ps1
shell: powershell
description: "Install Rust components and common tools"
timeout: 600
health_check:
- path: health-check.ps1
name: "Rust Toolchain"
description: "Verifies rustc, cargo, and components are properly installed"
environment:
path_additions:
- "~/.cargo/bin"
5.2.3 Python Developer Workload
# workloads/python-developer/workload.yaml
name: python-developer
version: "1.0.0"
description: "Python development environment with common tools"
extends:
- essentials
packages:
winget:
- id: Python.Python.3.12
- id: astral-sh.uv
files:
- source: pip.ini
destination: "~/AppData/Roaming/pip/pip.ini"
backup: true
scripts:
post_install:
- path: post-install.ps1
shell: powershell
description: "Configure Python environment and install global tools"
health_check:
- path: health-check.ps1
name: "Python Environment"
description: "Verifies Python, pip, and uv are properly configured"
environment:
variables:
- name: PYTHONDONTWRITEBYTECODE
value: "1"
scope: user
5.3 Variable Expansion
Anvil supports variable expansion in destination paths and template files:
| Variable | Expansion |
|---|---|
~ | User's home directory (%USERPROFILE%) |
${HOME} | User's home directory |
${APPDATA} | Application data directory |
${LOCALAPPDATA} | Local application data |
${PROGRAMFILES} | Program Files directory |
${PROGRAMFILES_X86} | Program Files (x86) directory |
${ANVIL_WORKLOAD} | Current workload name |
${ANVIL_VERSION} | Anvil version |
6. CLI Interface Design
6.1 Command Overview
anvil <COMMAND> [OPTIONS]
Commands:
install Apply a workload configuration to the system
health Check system health against a workload definition
list List available workloads
show Display detailed workload information
validate Validate workload definition syntax
init Initialize a new workload template
backup Backup current system state
restore Restore from a backup
completions Generate shell completions
help Print help information
Options:
-v, --verbose Increase output verbosity (can be repeated: -vvv)
-q, --quiet Suppress non-essential output
-c, --config <PATH> Use custom configuration file
--no-color Disable colored output
-h, --help Print help
-V, --version Print version
6.2 Command Details
6.2.1 Install Command
anvil install [OPTIONS] <WORKLOAD>
Apply a workload configuration to the system
Arguments:
<WORKLOAD> Name of the workload to install (or path to workload.yaml)
Options:
-d, --dry-run Show what would be done without making changes
-f, --force Skip confirmation prompts
-p, --packages-only Only install packages, skip files and scripts
--skip-packages Skip package installation
--skip-files Skip file operations
--skip-scripts Skip script execution
--skip-pre-scripts Skip pre-installation scripts
--skip-post-scripts Skip post-installation scripts
--no-backup Don't backup existing files
-j, --jobs <N> Number of parallel package installations (default: 4)
--timeout <SECONDS> Global timeout for operations (default: 3600)
Examples:
anvil install rust-developer
anvil install ./custom-workload/workload.yaml
anvil install python-developer --dry-run
anvil install essentials --packages-only
6.2.2 Health Command
anvil health [OPTIONS] <WORKLOAD>
Check system health against a workload definition
Arguments:
<WORKLOAD> Name of the workload to check against
Options:
-o, --output <FORMAT> Output format: table, json, yaml, html (default: table)
-f, --file <PATH> Write report to file instead of stdout
--fail-fast Stop on first failure
--packages-only Only check packages
--files-only Only check files
--scripts-only Only run health check scripts
-s, --strict Treat warnings as errors
Exit Codes:
0 All checks passed
1 One or more checks failed
2 Configuration or runtime error
Examples:
anvil health rust-developer
anvil health python-developer --output json --file report.json
anvil health essentials --fail-fast
6.2.3 List Command
anvil list [OPTIONS]
List available workloads
Options:
-a, --all Include built-in and custom workloads
-l, --long Show detailed information
--path <PATH> Search for workloads in additional path
-o, --output <FORMAT> Output format: table, json, yaml (default: table)
Examples:
anvil list
anvil list --long
anvil list --output json
6.2.4 Show Command
anvil show <WORKLOAD>
Display detailed workload information
Arguments:
<WORKLOAD> Name of the workload to display
Options:
-r, --resolved Show resolved configuration (with inheritance applied)
-o, --output <FORMAT> Output format: yaml, json (default: yaml)
Examples:
anvil show rust-developer
anvil show python-developer --resolved
6.2.5 Validate Command
anvil validate [OPTIONS] <PATH>
Validate workload definition syntax
Arguments:
<PATH> Path to workload.yaml file or workload directory
Options:
--strict Enable strict validation mode
--schema Output JSON schema for workload definitions
Examples:
anvil validate workloads/rust-developer/
anvil validate ./custom-workload/workload.yaml --strict
6.2.6 Init Command
anvil init [OPTIONS] <NAME>
Initialize a new workload template
Arguments:
<NAME> Name for the new workload
Options:
-t, --template <NAME> Base template: minimal, standard, full (default: standard)
-e, --extends <PARENT> Parent workload to extend
-o, --output <PATH> Output directory (default: ./workloads/<NAME>)
Examples:
anvil init my-workload
anvil init frontend-dev --extends essentials
anvil init minimal-setup --template minimal
6.3 Output Formats
6.3.1 Table Format (Default)
$ anvil health rust-developer
╭─────────────────────────────────────────────────────────────────────╮
│ Anvil Health Check Report │
│ Workload: rust-developer │
├─────────────────────────────────────────────────────────────────────┤
│ Component │ Status │ Details │
├────────────────────┼─────────┼──────────────────────────────────────┤
│ Packages │
├────────────────────┼─────────┼──────────────────────────────────────┤
│ Git.Git │ ✓ OK │ 2.43.0 installed │
│ Microsoft.VSCode │ ✓ OK │ 1.85.0 installed │
│ Rustlang.Rustup │ ✓ OK │ 1.26.0 installed │
│ LLVM.LLVM │ ✗ FAIL │ Not installed │
├────────────────────┼─────────┼──────────────────────────────────────┤
│ Files │
├────────────────────┼─────────┼──────────────────────────────────────┤
│ ~/.cargo/config │ ✓ OK │ Hash matches │
│ ~/.gitconfig │ ⚠ WARN │ Modified locally │
├────────────────────┼─────────┼──────────────────────────────────────┤
│ Health Scripts │
├────────────────────┼─────────┼──────────────────────────────────────┤
│ Rust Toolchain │ ✓ OK │ All components verified │
│ Git Configuration │ ✓ OK │ User configured │
╰─────────────────────────────────────────────────────────────────────╯
Summary: 6 passed, 1 failed, 1 warning
6.3.2 JSON Format
{
"workload": "rust-developer",
"timestamp": "2025-01-15T10:30:00Z",
"overall_status": "FAILED",
"summary": {
"total": 8,
"passed": 6,
"failed": 1,
"warnings": 1
},
"checks": {
"packages": [
{
"id": "Git.Git",
"status": "OK",
"expected_version": null,
"installed_version": "2.43.0"
},
{
"id": "LLVM.LLVM",
"status": "FAILED",
"expected_version": null,
"installed_version": null,
"error": "Package not installed"
}
],
"files": [
{
"source": ".cargo/config.toml",
"destination": "C:\\Users\\dev\\.cargo\\config.toml",
"status": "OK",
"expected_hash": "sha256:abc123...",
"actual_hash": "sha256:abc123..."
}
],
"scripts": [
{
"name": "Rust Toolchain",
"status": "OK",
"output": "rustc 1.75.0, cargo 1.75.0"
}
]
}
}
7. Core Components
7.1 Component Architecture
┌─────────────────────────────────────────────────────────────────────┐
│ CLI Layer │
│ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────┐ ┌─────────────┐ │
│ │ install │ │ health │ │ list │ │ show │ │ validate │ │
│ └────┬────┘ └────┬────┘ └────┬────┘ └────┬────┘ └──────┬──────┘ │
└───────┼──────────┼────────────┼──────────┼─────────────┼───────────┘
│ │ │ │ │
┌───────┴──────────┴────────────┴──────────┴─────────────┴───────────┐
│ Operations Layer │
│ ┌──────────────────┐ ┌──────────────────┐ ┌────────────────┐ │
│ │ InstallOperation │ │ HealthOperation │ │ BackupOperation│ │
│ └────────┬─────────┘ └────────┬─────────┘ └───────┬────────┘ │
└───────────┼─────────────────────┼────────────────────┼─────────────┘
│ │ │
┌───────────┴─────────────────────┴────────────────────┴─────────────┐
│ Providers Layer │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ WingetProvider │ │ FilesystemProv. │ │ ScriptProvider │ │
│ │ │ │ │ │ │ │
│ │ - install() │ │ - copy() │ │ - execute() │ │
│ │ - uninstall() │ │ - compare() │ │ - validate() │ │
│ │ - list() │ │ - backup() │ │ │ │
│ │ - search() │ │ - restore() │ │ │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
└────────────────────────────────────────────────────────────────────┘
│ │ │
┌───────────┴─────────────────────┴────────────────────┴─────────────┐
│ Config Layer │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ WorkloadParser │ │ SchemaValidator │ │ InheritanceRes. │ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
└────────────────────────────────────────────────────────────────────┘
7.2 Winget Provider
The Winget Provider interfaces with Windows Package Manager:
Key Functions:
| Function | Description |
|---|---|
install(package) | Install a package with optional version/overrides |
uninstall(package) | Remove an installed package |
is_installed(package) | Check if a package is installed |
get_version(package) | Get installed version of a package |
list_installed() | List all installed packages |
search(query) | Search for packages |
Error Handling:
#![allow(unused)] fn main() { pub enum WingetError { PackageNotFound(String), InstallationFailed { package: String, exit_code: i32, stderr: String }, VersionMismatch { package: String, expected: String, actual: String }, NetworkError(String), AccessDenied(String), Timeout { package: String, timeout_seconds: u64 }, } }
7.3 Filesystem Provider
Handles file operations with integrity verification:
Key Functions:
| Function | Description |
|---|---|
copy_file(src, dest) | Copy file with optional backup |
compute_hash(path) | Calculate SHA-256 hash |
compare_files(a, b) | Compare files by hash |
backup_file(path) | Create timestamped backup |
restore_file(backup, dest) | Restore from backup |
expand_path(path) | Expand variables in path |
Hash Format:
sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
7.4 Script Provider
Executes PowerShell scripts with proper error handling:
Key Functions:
| Function | Description |
|---|---|
execute(script, config) | Run a script with configuration |
validate_syntax(script) | Check script for syntax errors |
run_elevated(script) | Execute with admin privileges |
Script Execution Model:
#![allow(unused)] fn main() { pub struct ScriptConfig { pub path: PathBuf, pub shell: Shell, // PowerShell, Cmd, Bash (WSL) pub elevated: bool, pub timeout: Duration, pub working_dir: Option<PathBuf>, pub environment: HashMap<String, String>, } pub struct ScriptResult { pub exit_code: i32, pub stdout: String, pub stderr: String, pub duration: Duration, pub success: bool, } }
7.5 Inheritance Resolution
Workloads can extend other workloads with the following merge rules:
| Field | Merge Strategy |
|---|---|
name | Child overwrites |
version | Child overwrites |
description | Child overwrites |
packages.winget | Append (child packages added after parent) |
files | Append (child files added, same destinations overwritten) |
scripts.pre_install | Parent first, then child |
scripts.post_install | Parent first, then child |
scripts.health_check | Combine all |
environment.variables | Child overwrites same-named variables |
environment.path_additions | Append |
Circular Dependency Detection:
The inheritance resolver maintains a visited set to detect and reject circular dependencies:
A extends B extends C extends A → ERROR: Circular dependency detected
8. Roadmap
Phases 1–6 of the original implementation are complete. This section describes the forward-looking roadmap for Anvil, organized by milestone.
8.1 Overview
v0.4 — Declarative Assertions
│
├── Reusable condition/predicate engine
├── assertions: YAML schema
├── Integration with health command
└── Backward compatibility with scripts.health_check
v0.5 — Package Manager Abstraction
│
├── PackageManager trait
├── WingetProvider adapter
└── Schema design for multi-manager support
v0.6 — Commands Block
│
├── commands: YAML schema
├── Conditional execution (when:)
├── Failure semantics
└── Backward compatibility with scripts.post_install
v0.7 — Workload Discovery & Separation
│
├── Wire search_paths through ConfigManager
├── Search precedence and conflict resolution
└── Private workload repository pattern
v1.0 — Polish & Distribution
│
├── crates.io publishing
├── Cross-platform CI
├── Remove scripts.health_check (use assertions exclusively)
└── Documentation review
8.2 v0.4 — Declarative Assertions
Goal: Replace ~70% of PowerShell health-check scripts with declarative YAML assertions, evaluated natively in Rust.
Before (current):
scripts:
health_check:
- path: health-check/check-rust.ps1
name: "Rust Toolchain"
Plus a 70-line PowerShell script.
After (proposed):
assertions:
- name: "Rust compiler"
check:
type: command_exists
command: rustc
min_version: "1.70"
- name: "Cargo config"
check:
type: file_exists
path: "~/.cargo/config.toml"
- name: "RUST_BACKTRACE set"
check:
type: env_var
name: RUST_BACKTRACE
value: "1"
Assertion types:
| Type | Parameters | Purpose |
|---|---|---|
command_exists | command, min_version? | Verify a CLI tool is installed |
file_exists | path | Verify a file exists |
dir_exists | path | Verify a directory exists |
env_var | name, value? | Verify an environment variable is set |
path_contains | entry | Verify PATH includes an entry |
json_property | path, property, value | Verify a JSON file property |
registry_value | key, name, value | Verify a Windows registry value |
shell | command, expected_exit_code? | Escape hatch — run a shell command |
all_of | checks: [...] | All sub-checks must pass (AND) |
any_of | checks: [...] | At least one sub-check must pass (OR) |
Implementation:
- New module:
src/conditions/— shared predicate engine reused by assertions and futurecommands.when - New module:
src/assertions/—Assertionenum (tagged serde),evaluate()function - Update
src/config/workload.rs— addassertionsfield toWorkload - Update
src/operations/health.rs— evaluate assertions alongside legacyscripts.health_check - Backward compatible — existing
scripts.health_checkcontinues to work
8.3 v0.5 — Package Manager Abstraction
Goal: Introduce a PackageManager trait so the install flow is decoupled from winget, enabling future cross-platform support.
Proposed trait:
#![allow(unused)] fn main() { pub trait PackageManager: Send + Sync { fn name(&self) -> &str; fn is_available(&self) -> bool; fn install(&self, package: &PackageSpec) -> Result<InstallResult>; fn is_installed(&self, package_id: &str) -> Result<Option<String>>; fn list_installed(&self) -> Result<Vec<InstalledPackage>>; } }
Schema direction:
packages:
winget:
- id: Microsoft.VisualStudioCode
brew:
- id: visual-studio-code
apt:
- id: code
Anvil selects the available manager for the current platform. The v0.5 milestone implements the trait and refactors WingetProvider — other manager implementations are deferred.
8.4 v0.6 — Commands Block
Goal: Replace post-install PowerShell scripts with inline commands that support conditional execution.
Proposed schema:
commands:
post_install:
- run: rustup default stable
description: "Set stable as default toolchain"
- run: cargo install cargo-watch
timeout: 900
- run: code --install-extension rust-lang.rust-analyzer
when:
command_exists: code
Failure semantics:
- Default: stop on first failure
continue_on_error: trueto continue- Non-zero exit codes are errors unless
expected_exit_codeis set - Commands produce structured output for reporting
The when: condition reuses the predicate engine from v0.4.
8.5 v0.7 — Workload Discovery & Separation
Goal: Support multiple workload directories so private workloads live outside the main repo.
Current state: GlobalConfig already has a workloads.paths field, but ConfigManager uses only default_workload_paths(). This milestone wires configured paths through all commands and defines precedence.
Search precedence:
- Explicit path argument (highest priority)
- User-configured search paths (
~/.anvil/config.yaml) - Default paths (exe-relative, LOCALAPPDATA, cwd)
Duplicate workload names produce a warning with the resolved path shown.
8.6 v1.0 — Polish & Distribution
Goal: Stable release with cross-platform CI and crates.io publishing.
- Decide crate name (
anvilis taken on crates.io — candidates:anvil-cli,devsmith,forgekit) - Cross-compilation CI for Windows, Linux, macOS
- Comprehensive documentation review
- First stable release
9. Appendices
9.1 Sample Health Check Script
# health-check.ps1 - Rust Developer Workload
$ErrorActionPreference = "Stop"
$exitCode = 0
function Test-Command {
param([string]$Command, [string]$Name)
try {
$null = Get-Command $Command -ErrorAction Stop
Write-Host "✓ $Name is available" -ForegroundColor Green
return $true
}
catch {
Write-Host "✗ $Name is not available" -ForegroundColor Red
return $false
}
}
function Test-RustComponent {
param([string]$Component)
$installed = rustup component list --installed 2>&1
if ($installed -match $Component) {
Write-Host "✓ Rust component '$Component' is installed" -ForegroundColor Green
return $true
}
else {
Write-Host "✗ Rust component '$Component' is missing" -ForegroundColor Red
return $false
}
}
# Check core tools
if (-not (Test-Command "rustc" "Rust Compiler")) { $exitCode = 1 }
if (-not (Test-Command "cargo" "Cargo")) { $exitCode = 1 }
if (-not (Test-Command "rustup" "Rustup")) { $exitCode = 1 }
# Check Rust version
$rustVersion = rustc --version
Write-Host "Rust version: $rustVersion"
# Check essential components
if (-not (Test-RustComponent "rust-src")) { $exitCode = 1 }
if (-not (Test-RustComponent "rust-analyzer")) { $exitCode = 1 }
if (-not (Test-RustComponent "clippy")) { $exitCode = 1 }
if (-not (Test-RustComponent "rustfmt")) { $exitCode = 1 }
# Check cargo tools
if (-not (Test-Command "cargo-watch" "cargo-watch")) {
Write-Host "⚠ cargo-watch not installed (optional)" -ForegroundColor Yellow
}
exit $exitCode
9.2 Sample Post-Install Script
# post-install.ps1 - Rust Developer Workload
$ErrorActionPreference = "Stop"
Write-Host "Configuring Rust development environment..." -ForegroundColor Cyan
# Update Rust toolchain
Write-Host "Updating Rust toolchain..."
rustup update
# Install stable toolchain
Write-Host "Installing stable toolchain..."
rustup default stable
# Install essential components
Write-Host "Installing Rust components..."
rustup component add rust-src
rustup component add rust-analyzer
rustup component add clippy
rustup component add rustfmt
# Install common cargo tools
Write-Host "Installing cargo tools..."
cargo install cargo-watch
cargo install cargo-edit
cargo install cargo-expand
# Configure VS Code (if installed)
$vscodePath = Get-Command code -ErrorAction SilentlyContinue
if ($vscodePath) {
Write-Host "Installing VS Code extensions for Rust..."
code --install-extension rust-lang.rust-analyzer
code --install-extension vadimcn.vscode-lldb
code --install-extension serayuzgur.crates
}
Write-Host "Rust development environment configured successfully!" -ForegroundColor Green
9.3 Error Codes Reference
| Code | Name | Description |
|---|---|---|
| 0 | SUCCESS | Operation completed successfully |
| 1 | HEALTH_CHECK_FAILED | One or more health checks failed |
| 2 | CONFIG_ERROR | Configuration file error |
| 3 | WORKLOAD_NOT_FOUND | Specified workload does not exist |
| 4 | WINGET_ERROR | Winget operation failed |
| 5 | FILE_ERROR | File operation failed |
| 6 | SCRIPT_ERROR | Script execution failed |
| 7 | PERMISSION_ERROR | Insufficient permissions |
| 8 | NETWORK_ERROR | Network connectivity issue |
| 9 | TIMEOUT | Operation timed out |
| 10 | CIRCULAR_DEPENDENCY | Circular workload inheritance |
9.4 Glossary
| Term | Definition |
|---|---|
| Workload | A named configuration bundle defining packages, files, and scripts |
| Health Check | Validation of system state against a workload definition |
| Provider | Component that interfaces with external systems (winget, filesystem, scripts) |
| Inheritance | Mechanism for workloads to extend and build upon other workloads |
| Variable Expansion | Replacement of placeholders like ~ or ${HOME} with actual paths |
Document History
| Version | Date | Author | Changes |
|---|---|---|---|
| 2.0.0 | 2026-04-16 | Anvil Team | Rename to Anvil, updated roadmap (v0.4–v1.0) |
| 1.0.0 | 2025-01-15 | Anvil Team | Initial specification |
This specification serves as the authoritative reference for implementing Anvil. Subsequent implementation prompts should reference this document for architecture decisions, schemas, and interfaces.
Architecture
This document describes Anvil's internal architecture for contributors. For user-facing documentation, see the User Guide. For the project vision and roadmap, see the Specification.
Overview
Anvil is a single-binary Rust CLI. The binary parses commands, loads YAML workload definitions, delegates work to providers, and tracks state on disk.
main.rs → cli/ → operations/ → providers/
↓ ↓
config/ state/
| Layer | Role |
|---|---|
| cli/ | Command parsing (clap), output formatting (table/JSON/YAML/HTML), progress bars |
| config/ | Workload YAML loading, schema validation, inheritance resolution, global config |
| operations/ | One module per CLI command — each exposes execute(args, cli) → Result<()> |
| providers/ | External system integrations: winget, filesystem, scripts, templates, backups |
| state/ | Tracks installation records, file hashes, and package cache at ~/.anvil/ |
Data Flow: anvil install <workload>
1. CLI parsing cli/mod.rs → InstallArgs
2. Workload discovery config/mod.rs → finds workload.yaml on search paths
3. YAML loading config/mod.rs → Workload struct (serde)
4. Inheritance config/inheritance.rs → resolved Workload (parents merged)
5. Variable expansion config/mod.rs → ~ and ${VAR} expanded
6. Plan & confirm operations/install.rs → dry-run plan, user confirmation
7. Package install providers/winget.rs → winget install per package
8. File copy providers/filesystem.rs → copy with backup and hash
9. Script execution providers/script.rs → PowerShell pre/post scripts
10. State persistence state/ → JSON files at ~/.anvil/state/
Key Types
Workload Schema (config/workload.rs)
#![allow(unused)] fn main() { pub struct Workload { pub name: String, pub version: String, pub description: String, pub extends: Option<Vec<String>>, pub packages: Option<Packages>, pub files: Option<Vec<FileEntry>>, pub scripts: Option<Scripts>, pub environment: Option<Environment>, pub health: Option<HealthConfig>, } }
The Workload struct is the central data type — deserialized from YAML via serde. All fields except name, version, and description are optional.
CLI (cli/mod.rs, cli/commands.rs)
10 top-level commands: install, health, list, show, validate, init, status, completions, backup, config.
The Cli struct (clap-derived) holds global flags (--verbose, --quiet, --no-color, --config). Each command has its own args struct.
Providers (providers/)
Providers wrap external systems. Each provider is a struct with methods — there is no shared trait yet (see roadmap).
| Provider | Responsibility |
|---|---|
WingetProvider | Package install/upgrade/query via winget.exe |
FilesystemProvider | File copy with backup, hash verification, glob expansion |
ScriptProvider | PowerShell script execution with timeout and output capture |
TemplateProcessor | Handlebars template rendering for config files |
BackupManager | System state backup and restore |
State (state/)
All state is persisted as JSON under ~/.anvil/:
~/.anvil/
├── state/
│ ├── <workload>.json # InstallationState per workload
│ └── files.json # FileStateIndex (all tracked files)
├── cache/
│ └── packages.json # PackageCache (winget query results)
└── config.yaml # GlobalConfig (user preferences)
| Type | File | Purpose |
|---|---|---|
InstallationState | state/<workload>.json | Package install records and status |
FileStateManager | state/files.json | File hashes for drift detection |
PackageCache | cache/packages.json | Cached winget query results |
GlobalConfig | config.yaml | User settings and search paths |
Config System
Workload Discovery
ConfigManager searches for workloads in this order:
- Direct path (if an absolute or relative path is given)
<exe_dir>/workloads/<name>/workload.yaml%LOCALAPPDATA%/anvil/workloads/<name>/workload.yaml./workloads/<name>/workload.yaml
Within each search path, it tries: <name>/workload.yaml, <name>/workload.yml, <name>.yaml.
Inheritance
Workloads can extend other workloads via extends: [parent]. Resolution in config/inheritance.rs:
- Build dependency graph from all
extendsreferences - Detect cycles (error) and enforce max depth of 10
- Topological sort determines merge order (parents first)
- Merge strategy:
- Packages: append, child overrides same ID
- Files: append, child overrides same destination
- Scripts: concatenate (parent first, child after)
- Environment variables: child overrides same name
- Path additions: append unique entries
Variable Expansion
Workload values support variable expansion:
| Variable | Expands to |
|---|---|
~ | $env:USERPROFILE |
${HOME} | $env:USERPROFILE |
${ANVIL_WORKLOAD} | Current workload name |
${ANVIL_VERSION} | Anvil version |
${ANVIL_WORKLOAD_PATH} | Path to workload directory |
${ENV_NAME} | Any environment variable |
Error Handling
Two-tier approach:
thiserrorfor domain-specific error enums in each module:WingetError,FilesystemError,ScriptError,BackupError,TemplateErrorInheritanceError(includessuggestion()for user-friendly hints)FileStateErrorProviderError(wraps all provider errors)
anyhowfor error propagation in operations and CLI code
Pattern: domain errors are created with thiserror and converted to anyhow::Error at operation boundaries using .with_context(|| ...).
Testing
Unit Tests
Inline #[cfg(test)] mod tests in the same file as the code under test. 168 unit tests covering providers, config parsing, inheritance, state management, and formatting.
Integration Tests
tests/cli_tests.rs uses assert_cmd + predicates for end-to-end CLI testing. 75 integration tests covering all commands with fixture workloads.
tests/common/mod.rs provides test fixture helpers:
create_test_workload()— minimal valid workloadcreate_inherited_workload()— parent + child workloadscreate_invalid_workload()— malformed YAMLcreate_circular_workloads()— cycle detection fixturescreate_full_workload()— workload with all featurescreate_template_workload()— workload with template files
Running Tests
cargo test # All tests (243 total)
cargo test --bin anvil # Unit tests only (168)
cargo test --test cli_tests # Integration tests only (75)
cargo test test_name # Single test by name
Output Formats
All commands that produce output support --format:
| Format | Module | Description |
|---|---|---|
table | cli/formats/table.rs | Human-readable terminal tables (default) |
json | cli/formats/json.rs | Machine-readable JSON |
yaml | cli/formats/yaml.rs | YAML output |
html | cli/formats/html.rs | Standalone HTML reports |
CI Pipeline
.github/workflows/ci.yml runs on every push/PR:
cargo fmt --all -- --checkcargo clippy --all-targets --all-features -- -D warningscargo checkcargo test --bin anvil(unit tests)cargo test --test cli_tests(integration tests)cargo build --release- Verify binary runs (
./anvil --version)
Changelog
All notable changes to this project will be documented in this file.
The format is based on Keep a Changelog, and this project adheres to Semantic Versioning.
Unreleased
Added
- Declarative assertions for workload health validation (
assertions:field in workload YAML) - Condition engine with 9 predicate types:
command_exists,file_exists,dir_exists,env_var,path_contains,registry_value,shell, plusall_of/any_ofcomposition --assertions-onlyflag foranvil healthto run only assertion checksassertion_checktoggle in workload health configuration- Assertion examples in
anvil init --template fullscaffold - Multi-manager workload schema:
packages.brew(Homebrew) andpackages.apt(APT) fields alongside existingpackages.winget - Platform-aware validation warns when workload references an unavailable package manager
- Inline
commands:block for workload command execution withpre_installandpost_installphases - Conditional command execution via
when:field using the predicate engine continue_on_erroroption for commands that should not block the install flow- Configurable workload search paths via
~/.anvil/config.yaml(user paths prepended to defaults) - Search precedence with conflict resolution: explicit path > user-configured > defaults; first match wins
anvil list --all-pathsto show all discovered paths including shadowed duplicates- Cross-platform release builds for Linux (x86_64, aarch64), macOS (x86_64, aarch64), and Windows (x86_64)
Changed
- Crate renamed to
anvil-clifor crates.io publishing (binary name staysanvil; install viacargo install anvil-cli)
Deprecated
scripts.health_checkwhen used alongsideassertions(migrate to declarative assertions; removal planned for v1.0)scripts.pre_installandscripts.post_installwhen used alongsidecommands(migrate to inline commands; removal planned for v1.0)
0.5.0 - 2026-04-17
This is the first release from the new repository home at kafkade/anvil. It marks a fresh start with modernized CI/CD, updated documentation, and a clear cross-platform direction. Prior changelog entries (v0.1.0–v0.3.1) are preserved below for historical context.
Added
- Architecture reference document for contributors (
docs/ARCHITECTURE.md) - Automated releases from CHANGELOG.md with SHA256 checksums
- Consolidated CI pipeline with formatting, linting, and testing in a single gate
Changed
- Project rebranded to "Declarative Workstation Configuration Management" to reflect cross-platform direction
- Streamlined CI from 3 separate jobs to a single
Validategate plus release build - Release workflow aligned with org-wide pattern (changelog-driven notes, semver pre-release detection)
- Resolved all clippy warnings and formatting issues across the codebase
0.3.1 - 2026-01-10
Added
- Comprehensive user documentation (USER_GUIDE.md)
- Workload authoring guide (WORKLOAD_AUTHORING.md)
- Troubleshooting guide (TROUBLESHOOTING.md)
- Integration test suite for CLI commands
- GitHub Actions CI/CD workflows
- Shell completion generation for PowerShell, Bash, Zsh, and Fish
- CONTRIBUTING.md with contribution guidelines
Changed
- Improved error messages throughout CLI
- Enhanced validation reporting with detailed messages
- Updated README with badges and clearer instructions
Fixed
- Various documentation typos and inconsistencies
0.3.0 - 2026-01-08
Added
- Shell completions command for multiple shells
- Global configuration management (
configcommand) - Backup management with create, list, show, restore, clean, and verify subcommands
- Status command to show installation state
- HTML output format for health reports
--strictflag for validation command- Environment variable configuration in workloads
- PATH additions support in workloads
Changed
- Improved inheritance resolution algorithm
- Enhanced output formatting for all commands
- Better progress indicators during installation
0.2.0 - 2026-01-05
Added
- Script execution support with PowerShell and CMD
- Pre-install and post-install script hooks
- Health check script execution
- Elevated privilege handling for scripts
- Configurable script timeouts
- Template processing with Handlebars
- File integrity verification with SHA-256 checksums
- Automatic backup creation before file overwrites
- Variable expansion in paths (
~,${HOME}, etc.)
Changed
- Improved file operation error handling
- Enhanced dry-run output with detailed plan
Fixed
- Path expansion on Windows with backslashes
- File permission handling on Windows
0.1.0 - 2026-01-01
Added
- Initial release of Anvil
- Core CLI commands: install, health, list, show, validate, init
- Package management via winget integration
- Install packages with version pinning
- Custom winget arguments support
- Package health verification
- File operations
- Copy files to target locations
- Path variable expansion
- Workload system
- YAML-based workload definitions
- Workload inheritance and composition
- Circular dependency detection
- Multiple output formats
- Table (default, human-readable)
- JSON (machine-readable)
- YAML
- Bundled workloads
- essentials: Core development tools (VS Code, Git, Windows Terminal, Oh My Posh) and productivity utilities
- rust-developer: Rust toolchain with cargo tools (extends essentials)
- python-developer: Python with uv package manager (extends essentials)
Documentation
- Initial specification document
- Phase development prompts
Contributing
Thank you for your interest in contributing to Anvil! This document provides guidelines and instructions for contributing to the project.
Code of Conduct
This project follows the Contributor Covenant Code of Conduct. Please be respectful and constructive in all interactions.
How to Contribute
Reporting Bugs
Before reporting a bug:
- Check existing issues to avoid duplicates
- Gather relevant information:
- Anvil version (
anvil --version) - Windows version
- Steps to reproduce
- Expected vs actual behavior
- Verbose output (
anvil -vvv <command>)
- Anvil version (
Create a bug report with this template:
## Environment
- Anvil version:
- Windows version:
- PowerShell version:
## Description
Brief description of the bug.
## Steps to Reproduce
1.
2.
3.
## Expected Behavior
What you expected to happen.
## Actual Behavior
What actually happened.
## Verbose Output
(paste -vvv output here)
## Workload (if applicable)
```yaml
(paste workload.yaml here)
### Suggesting Features
1. Check existing issues and discussions for similar suggestions
2. Open a feature request issue with:
- Clear description of the feature
- Use case and motivation
- Proposed implementation (if you have ideas)
### Submitting Code
1. Fork the repository
2. Create a feature branch (`git checkout -b feature/amazing-feature`)
3. Make your changes
4. Run tests (`cargo test`)
5. Run lints (`cargo clippy`)
6. Format code (`cargo fmt`)
7. Commit with a descriptive message
8. Push to your fork
9. Open a Pull Request
---
## Development Setup
### Prerequisites
- **Rust**: 1.75 or later ([rustup.rs](https://rustup.rs/))
- **Windows**: Visual Studio Build Tools (for linking)
- **Windows Package Manager (winget)**: For package operations (integration tests)
- **Linux/macOS**: Standard build toolchain (gcc/clang)
### Building
```powershell
# Clone your fork
git clone https://github.com/YOUR_USERNAME/anvil.git
cd anvil
# Build debug version
cargo build
# Build release version
cargo build --release
# Run tests
cargo test
# Run clippy lints
cargo clippy --all-targets --all-features -- -D warnings
# Format code
cargo fmt
# Run with debug output
cargo run -- -vvv list
Running Tests
# Run all tests
cargo test
# Run unit tests only
cargo test --lib
# Run integration tests only
cargo test --test '*'
# Run a specific test
cargo test test_name
# Run tests with output
cargo test -- --nocapture
Project Structure
anvil/
├── src/
│ ├── main.rs # Entry point
│ ├── cli/ # Command line interface
│ │ ├── mod.rs # CLI definitions (clap)
│ │ ├── commands.rs # Command argument structs
│ │ ├── completions.rs # Shell completions
│ │ ├── output.rs # Output handling
│ │ ├── progress.rs # Progress indicators
│ │ └── formats/ # Output formatters
│ │ ├── mod.rs
│ │ ├── table.rs # Table formatter
│ │ ├── json.rs # JSON formatter
│ │ ├── yaml.rs # YAML formatter
│ │ └── html.rs # HTML report generator
│ ├── config/ # Configuration handling
│ │ ├── mod.rs
│ │ ├── schema.rs # Workload schema validation
│ │ ├── workload.rs # Workload parsing
│ │ ├── inheritance.rs # Inheritance resolution
│ │ └── global.rs # Global configuration
│ ├── operations/ # Command implementations
│ │ ├── mod.rs
│ │ ├── install.rs # Install command
│ │ ├── health.rs # Health check command
│ │ ├── list.rs # List command
│ │ ├── show.rs # Show command
│ │ ├── validate.rs # Validate command
│ │ ├── init.rs # Init command
│ │ ├── status.rs # Status command
│ │ ├── backup.rs # Backup command
│ │ └── config.rs # Config command
│ ├── providers/ # External integrations
│ │ ├── mod.rs
│ │ ├── winget.rs # Winget package manager
│ │ ├── filesystem.rs # File operations
│ │ ├── script.rs # Script execution
│ │ ├── template.rs # Template processing
│ │ └── backup.rs # Backup provider
│ └── state/ # State management
│ ├── mod.rs
│ ├── installation.rs # Installation state
│ ├── cache.rs # Cache management
│ └── files.rs # File state tracking
├── examples/ # Example workloads
│ ├── minimal/
│ ├── rust-developer/
│ └── python-developer/
├── tests/ # Integration tests
│ ├── cli_tests.rs
│ └── common/
│ └── mod.rs
└── docs/ # Documentation
└── src/
├── SUMMARY.md
├── introduction.md
├── user-guide.md
├── workload-authoring.md
├── troubleshooting.md
├── specification.md
├── architecture.md
├── changelog.md
└── contributing.md
Key Modules
- cli: Handles command-line parsing with clap and output formatting
- config: Parses workload YAML files and handles inheritance
- operations: Implements each CLI command's business logic
- providers: Interfaces with external systems (winget, filesystem, PowerShell)
- state: Tracks installation state and manages caching
Coding Standards
Rust Style
- Follow standard Rust conventions and idioms
- Use
rustfmtfor formatting (default settings) - Address all
clippywarnings - Use
thiserrorfor error types - Use
anyhowfor error propagation in application code
Code Organization
- Keep functions focused and small
- Use descriptive names for functions and variables
- Add doc comments for public APIs
- Use modules to organize related functionality
Error Handling
#![allow(unused)] fn main() { // Define custom errors with thiserror #[derive(Debug, thiserror::Error)] pub enum WorkloadError { #[error("Workload '{name}' not found")] NotFound { name: String }, #[error("Invalid workload schema: {message}")] InvalidSchema { message: String }, } // Use anyhow for propagation pub fn load_workload(name: &str) -> anyhow::Result<Workload> { // ... } }
Documentation
#![allow(unused)] fn main() { /// Brief description of the function. /// /// More detailed description if needed. /// /// # Arguments /// /// * `name` - The workload name to load /// /// # Returns /// /// The loaded workload or an error /// /// # Errors /// /// Returns an error if the workload is not found /// /// # Examples /// /// ``` /// let workload = load_workload("my-workload")?; /// ``` pub fn load_workload(name: &str) -> Result<Workload> { // ... } }
Commit Messages
- Use present tense ("Add feature" not "Added feature")
- Use imperative mood ("Fix bug" not "Fixes bug")
- Keep the first line under 72 characters
- Reference issues when applicable
Examples:
Add shell completions for Fish
Implement Fish shell completion generation using clap_complete.
Closes #42
Fix file backup path on Windows
Handle UNC paths correctly when creating backups.
Fixes #55
Testing
Unit Tests
Add unit tests in the same file as the code being tested:
#![allow(unused)] fn main() { #[cfg(test)] mod tests { use super::*; #[test] fn test_function_name() { // Arrange let input = "test"; // Act let result = function_under_test(input); // Assert assert_eq!(result, expected); } } }
Integration Tests
Add integration tests in tests/cli_tests.rs:
#![allow(unused)] fn main() { #[test] fn new_command_works() { anvil() .args(["new-command", "arg"]) .assert() .success() .stdout(predicate::str::contains("expected output")); } }
Test Guidelines
- Each test should be independent
- Use descriptive test names
- Test both success and failure cases
- Use
tempfile::TempDirfor file operations - Don't rely on external system state where avoidable
Submitting Changes
Pull Request Process
- Update documentation if needed
- Add tests for new features
- Ensure CI passes (format, lint, test, build)
- Write a clear PR description explaining:
- What the change does
- Why it's needed
- How to test it
- Request review from maintainers
- Address feedback promptly
- Squash commits if requested
PR Title Format
feat: Add new featurefix: Fix specific bugdocs: Update documentationtest: Add testsrefactor: Restructure codechore: Update dependencies
Creating Workloads
Contributions of new bundled workloads are welcome! See the Workload Authoring guide for details.
Workload Guidelines
- Include meaningful health checks - Verify the workload achieves its purpose
- Document the workload purpose - Clear description and comments
- Test on clean Windows installation - Ensure it works from scratch
- Use inheritance for common bases (extend
essentialsif appropriate) - Keep packages minimal - Only include what's necessary
- Validate before submitting -
anvil validate your-workload --strict
Workload Structure
workload-name/
├── workload.yaml # Required: workload definition
├── files/ # Optional: configuration files
└── scripts/ # Optional: setup/health scripts
Example
name: my-workload
version: "1.0.0"
description: "Brief description of what this workload provides"
extends:
- essentials # If applicable
packages:
winget:
- id: Package.ID
scripts:
health_check:
- path: scripts/health.ps1
name: "Verification"
description: "Verify installation"
License
By contributing to Anvil, you agree that your contributions will be licensed under the MIT or Apache-2.0 license, at the user's choice.
Questions?
- Open a Discussion
- Check the Documentation
- Review existing Issues
Thank you for contributing to Anvil!