A Cargo-inspired build tool for C β configured via a clean YAML subset.
No Makefiles. No CMake. No indentation-based YAML, no tags.
Just a readable config file, a streaming parser built from scratch, and clang.
package: "myapp"
version: "0.1"
description: "A #{C} project"- Multi-target builds β
exec, with cross-compilation support (x86_64,aarch64) - Release & debug profiles β flags defined once, applied everywhere
- Memoized build arguments β run
git rev-parse HEADonce, cache it - Hook scripts β bash hooks for dynamic values at build time
- Dependency management β
pkg-configfor system libs,githubfor single-file headers - YAML anchor support β define a profile once, reuse it with
*alias - Builds itself β anvil's own
anvil.yamlis the reference config
package: "myapp"
version: "0.1"
author: "you"
description: "A #{C} project"
# #{AWD} = Anvil Work Dir (project root)
workspace: {
libs: "#{AWD}/src/libs",
build: "#{AWD}/build"
}
# build targets β index 0 is default for `anvil build` / `anvil run`
# use `anvil build --target <n>` for others
targets: [
{
name: 'myapp',
type: 'exec',
main: '#{AWD}/src/main.c',
macros: {},
for: [
'x86_64-linux-gnu',
'aarch64-linux-gnu'
]
}
]
build: {
compiler: 'clang',
cstd: 'c23',
jobs: 0, # 0 -> auto
# #{arg:name} -> from arguments below
# #{hook:name} -> from bash hook scripts in hooks/
macros: {
GIT_HASH: '#{arg:git_hash}',
GIT_INFO: '#{hook:git_info}'
},
arguments: {
git_hash: {
validation: 'none', # none | status | content | all
cache_policy: 'memoize', # never | memoize | always
command: ['git', 'rev-parse', 'HEAD']
}
},
deps: [
{
name: 'gtk4',
type: 'pkg-config' # uses system pkg-config
},
{
name: 'stb_image',
type: 'github',
repo: 'nothings/stb',
path: 'stb_image.h' # single file download
# path: 'include/' # trailing / clones the folder as -Iinclude/
}
]
}
# release and debug profiles are required
# tip: define release as an anchor, alias it in debug to share flags
profiles: {
release: [
'-O3', '-Wall', '-Wextra', '-Werror', '-pedantic', '-DNDEBUG'
],
debug: [
'-ggdb', '-O1', '-Werror',
'-fstack-protector-strong',
'-D_FORTIFY_SOURCE=2',
'-fsanitize=address,undefined',
'-fno-omit-frame-pointer'
]
}anvil gives precise, context-aware errors pointing at the exact problem in your config (removed, to be added again):
YamlError:: UNEXPECTED_TOKEN
18 | here: 62
19 | other: "Hi"
^^^^^
20 | }
Expected TOKEN_COMMA, found TOKEN_KEY
anvil uses a custom streaming YAML parser. Supported:
- Flow maps
{ key: value }and flow sequences[ a, b, c ] - Quoted strings (single
'...'and double"...") - Numbers, booleans
- Anchors
&nameand aliases*name(including<<merge) - Comments
# ...
Not supported (by design):
- Indentation-based blocks
- Tags (
!!type) - Multiline scalars (
|,>)
anvil is built on a small set of C23 single-header libraries:
| Library | Description |
|---|---|
z3_string.h |
Growable heap strings, interpolation, escape/unescape, scoped cleanup |
z3_hashmap.h |
FNV-1a HashMap, bit-packed occupation tracking, linear probing, iterator |
z3_vector.h |
Generic growable vector |
z3_toys.h |
Shared utilities, next_power_of2, die, debug helpers |
All are single-header with #define Z3_*_IMPL for the implementation, and Valgrind-clean on the happy path.
- Occupation tracking 87.5% smaller β
bool[]replaced with packeduint64_tbitflags - Per-entry size reduced 32 β 24 bytes (~25%)
- Iteration cache locality improved β ~9% faster
- Streaming chunk-based reads via raw fd β no
fgets, no full file load - String pool replaces per-node allocations
- Heap: 14,181 β 7,220 bytes (-48.9%)
- Allocations: 151 β 107 (-29.1%)
Requires clang and C23. Bootstrap without anvil:
clang -std=c23 -O2 -o anvil src/main.cThen use anvil to build itself:
./anvil build # default target, release
./anvil build --profile debug # debug profile
./anvil build --target 1 # yaml test binary
./anvil run # build + run default targetAGPL-3.0-or-later β Β© 2025-present Klapptnot