Declare the packages and dotfiles your machines should have. Repeatability on any system
Why • Features • Install • Quick start • Guide
Every machine drifts. A tool goes in with cargo, another with pacman, an
editor plugin with npm, and a dotfile gets tweaked in ~/.config. Six months
later nobody can say what the setup actually is, let alone rebuild it on a new
laptop.
The usual fixes each cover part of the problem:
- Nix solves it completely, by replacing your package managers and your distro's way of doing things.
- stow and chezmoi keep dotfiles, but not the packages those dotfiles configure.
- Ansible can do both, but as a playbook of steps. Removing a line doesn't remove the package.
vow sits in between. You keep using the package managers you already have, whether that's 1 or 20 of them. vow keeps a list for each one, plus your dotfiles, in a folder you track in git. It makes the machine match in both directions: what's declared gets installed, and what's no longer declared gets removed.
- 📝 Plain TOML, no DSL. A list per manager and a table of links. A
package is name, orname versionto hold it at an exact version.
- 🧰 43 package managers. System (apt,pacman,brew,dnf), language (cargo,npm,pip,go), version managers (rustup,mise,pyenv), editor extensions and more, each driven through its own CLI.
- 🔄 Converges both ways. It installs what's declared and removes what
isn't. A manager with no list is left alone, and npm = []keeps npm at nothing installed.
- 🔗 Dotfiles as symlinks. Files stay in the graph, so editing
~/.config/nvim/init.luaedits the copy in git. vow only ever replaces symlinks it placed itself.
- 🧩 One file per machine, shared through includes. A machine applies
hosts/<hostname>.toml, orconfig.tomlif it has none, plus whatever that file includes. Files merge, and a disagreement between two files is an error, never a silent override.
- ⚡ Staged and parallel. System managers run first, then version
managers, then everything else. So aptcan installcargoand thecargolist is applied in the same switch. Within a stage, managers run side by side, each in a live box.
- 🎯 Compiler-style errors. Mistakes are reported at their file, line and column, with a note on how to fix them.
- 🛡️ Careful by default. It shows the plan before changing anything, checks afterwards that every change took effect, and writes every file atomically. A removal never takes a package that something else still needs.
Build from this repository with Cargo (Rust 1.88 or newer):
cargo install --locked --git https://github.com/Houdiee/vowOr from a clone:
git clone https://github.com/Houdiee/vow && cd vow
cargo build --release
install -Dm755 target/release/vow ~/.local/bin/vowWith Nix, nix develop in the clone gives you a shell with the Rust
toolchain, and cargo build --release works from there.
vow is developed and tested on Linux. It works with whichever of its
supported managers are on your PATH.
1. Declare this machine as it is. generate-config writes
~/.config/vow/config.toml, listing what every installed manager has
installed:
vow generate-config
vow status # nothing to change2. Bring in a dotfile. Move it into the graph:
mv ~/.config/nvim ~/.config/vow/nvimThen declare where it goes, in the [links] table at the end of
config.toml, and switch:
[links]
nvim = { source = "nvim", target = "~/.config/nvim" }vow switch # shows "+ nvim <- ~/.config/nvim", asks, links3. Track it in git.
cd ~/.config/vow && git init && git add -A && git commit -m "my machine"On another machine, clone the graph and converge:
git clone <your-repo> ~/.config/vow
vow status # what this machine is missing, or has extra
vow switch # install, remove and link to matchWhen machines start to differ, give each its own host file, found by hostname. It includes what the machines share:
~/.config/vow/
├── config.toml # applied by a machine with no host file
├── hosts/
│ ├── laptop.toml # applied by the machine named "laptop"
│ └── desktop.toml
├── common.toml # anything a host file includes, named as you like
└── dotfiles/ # your files, laid out however you like
# common.toml
[packages]
pacman = ["git", "neovim", "ripgrep"]
cargo = ["bat", "eza 0.20.0"] # a version holds it there
[links]
nvim = { source = "dotfiles/nvim", target = "~/.config/nvim" }
git = { source = "dotfiles/gitconfig", target = "~/.gitconfig" }# hosts/laptop.toml
includes = ["../common.toml"]
[packages]
pacman = ["tlp", "brightnessctl"] # merged with common.toml's listThe same package declared alike in two files merges. Declared differently,
it's an error, reported where it happens. Add cargo = ["bat 0.24.0"] to the
laptop's file and:
$ vow status
error: `bat` is already declared as `bat 0.24.0` in `hosts/laptop.toml`
--> ~/.config/vow/common.toml:3:10
|
3 | cargo = ["bat", "eza 0.20.0"] # a version holds it there
| ^^^^^ declared differently here
|
= note: first declared at ~/.config/vow/hosts/laptop.toml:5; files merge only what they declare alike, and none overrides another: make them agree, or declare the package in one fileThere's no overriding and no templating, by design. If something should apply
everywhere except one machine, put it in a file that machine doesn't include.
If a dotfile needs per-machine differences, use the tool's own include or
conditional (git's includeIf, ssh's Match, source in a shell). The
guide has the full rules.
Name managers first to act on just those: vow cargo,npm status.
--packages and --links restrict a command to one half. --host=NAME or
--file=PATH applies another machine's file, and --path points at a graph
somewhere else.
vow supports 43 package managers, applied in three stages:
Every manager except flatpak, code and codium is tested weekly against
the real tool in its own container: vow installs a package, reads it back,
searches, and removes it.
A manager is only added when its explicitly installed packages can be told apart from their dependencies. Otherwise vow would offer to remove dependencies. The guide explains why some obvious candidates are missing.
If you want every machine to be bit-for-bit reproducible, use Nix. If you want the tools you already have, with a machine you can describe in a few lists and rebuild with one command, use vow.
- The guide is the full reference: every rule, with examples.
- vow --helpand- vow <command> --helpcover every command and flag.
Bug reports and ideas are welcome in the issues. If vow misreads a manager's output, please paste what the tool printed. New managers are only added from real captured output.
Before opening a pull request, read AGENTS.md. It covers the
code layout, the design decisions already made, and how to run the tests:
cargo fmt --check && cargo clippy --all-targets && cargo test
VOW_CONTAINERS=1 cargo test container::apt # one manager, against the real tool (needs docker)MIT © Houdiee