A blazing-fast, incremental build system with content-addressable caching.
Getting Started · Configuration · Architecture · Contributing
vex is a task runner and build orchestrator that knows which work to skip.
Instead of timestamping files (like Make), vex hashes the content of your inputs using SHA-256. A task only re-runs when its inputs actually change. This eliminates entire classes of stale-build bugs and makes CI significantly faster.
$ vex run --all
fmt [cached]
lint [running] → [ok] (1.2s)
test [cached]
build [ok] (4.8s)
── Summary ──────────────────────────
Tasks run: 4
Succeeded: 4
Total time: 6031 ms
─────────────────────────────────────
- Content-addressable cache — inputs are fingerprinted by file content, not mtime
- Parallel execution — independent tasks run concurrently across CPU cores
- Dependency graph — cyclic dependencies are detected at startup, not mid-run
- Persistent cache — SQLite-backed store survives restarts;
vex-servershares it across machines - Remote cache API — ship a
vex-serverinstance and every developer shares warm cache - Zero config escape hatches — every field in
vex.tomlis optional
cargo install vex-cliOr build from source:
git clone https://github.com/yourusername/vex
cd vex
cargo install --path crates/vex-cliCreate a vex.toml in your project root:
[project]
name = "my-app"
version = "1.0.0"
[tasks.fmt]
run = "cargo fmt --check"
description = "Check formatting"
cache = false
[tasks.lint]
run = "cargo clippy -- -D warnings"
inputs = ["src/**/*.rs", "Cargo.toml"]
description = "Lint with clippy"
depends_on = ["fmt"]
[tasks.test]
run = "cargo test"
inputs = ["src/**/*.rs", "tests/**/*.rs"]
description = "Run the test suite"
depends_on = ["lint"]
[tasks.build]
run = "cargo build --release"
inputs = ["src/**/*.rs", "Cargo.toml"]
outputs = ["target/release/my-app"]
description = "Compile release binary"
depends_on = ["test"]vex run --all # run all tasks in dependency order
vex run build # run a specific task
vex list # list all tasks
vex graph # print the dependency graph
vex cache clear # invalidate the local cache[project]
name = "string" # required
version = "string" # optional
description = "string" # optional
[tasks.<name>]
run = "shell command" # required — executed via sh -c / cmd /C
inputs = ["glob patterns"] # files that, when changed, invalidate the cache
outputs = ["paths"] # files this task produces
depends_on = ["task-names"] # tasks that must complete first
env = { KEY = "value" } # environment variables injected at runtime
working_dir = "path" # working directory (default: project root)
cache = true # set false to always re-run (default: true)
description = "string" # shown in `vex list`| Variable | Default | Description |
|---|---|---|
VEX_CACHE_DIR |
.vex/cache |
Local cache database location |
VEX_REMOTE |
— | URL of a vex-server instance |
VEX_JOBS |
# of CPUs | Max parallel task count |
RUST_LOG |
warn |
Log verbosity (use vex=debug) |
vex is structured as a Rust workspace with four crates, each with a single responsibility:
vex/
├── crates/
│ ├── vex-core # task graph, fingerprinting, execution engine
│ ├── vex-cache # SQLite-backed persistent cache (async)
│ ├── vex-cli # user-facing binary (`vex`)
│ └── vex-server # remote cache & API server (`vex-server`)
├── examples/ # example vex.toml configurations
└── tests/ # integration tests
vex.toml
│
▼
VexConfig::from_file()
│ parse & validate
▼
TaskGraph::build_edges()
│ topological sort, cycle detection (petgraph)
▼
Engine::run_all()
│
├── for each task (in dep order):
│ │
│ ├── Fingerprint::from_paths(task.inputs) ← SHA-256 of file contents
│ │
│ ├── cache.get(fingerprint)
│ │ hit → return cached result [cached]
│ │ miss → continue
│ │
│ ├── sh -c "task.run"
│ │
│ └── cache.put(fingerprint, result)
│
└── print summary
vex uses a two-level cache:
- In-process (
DashMap) — keyed by fingerprint, survives within a singlevexinvocation - Persistent (
SQLiteviasqlx) — survives restarts, lives in.vex/cache/vex.db - Remote (
vex-server) — shared across machines, keyed by the same fingerprints
Because cache keys are derived from file contents (not times), two developers who check out the same commit will always hit the same cache entries.
Start a server:
cargo install vex-server
vex-server --port 7878 --cache-dir /var/vex-cachePoint clients at it:
VEX_REMOTE=http://ci-server:7878 vex run --all| Method | Path | Description |
|---|---|---|
| GET | /health |
Health check |
| GET | /api/v1/cache/:fingerprint |
Retrieve a cached result |
| PUT | /api/v1/cache/:fingerprint |
Store a result |
| DELETE | /api/v1/cache/:fingerprint |
Invalidate an entry |
| GET | /api/v1/tasks |
List known tasks |
| Feature | vex | Make | Turborepo | Bazel |
|---|---|---|---|---|
| Content-addressed cache | ✅ | ❌ | ✅ | ✅ |
| Remote cache | ✅ | ❌ | ✅ | ✅ |
| Language agnostic | ✅ | ✅ | ✅ | ✅ |
| Cycle detection | ✅ | ❌ | ✅ | ✅ |
| Zero installation deps | ✅ | ✅ | ❌ | ❌ |
| Single binary | ✅ | ✅ | ❌ | ❌ |
| Config complexity | Low | Low | Medium | High |
Contributions are welcome. Please read CONTRIBUTING.md first.
# run the full check suite
cargo fmt --all && cargo clippy --all && cargo test --allLicensed under either of:
- MIT License (LICENSE-MIT)
- Apache License, Version 2.0 (LICENSE-APACHE)
at your option.