pave ==== pave converges a machine onto goals written in plain bash. Goals live in a goals directory (usually its own repo), sorted by **phase** -- when they run, and as whom -- and optionally by **hostname**. Triggers at boot, graphical session start, and shell login all call the same thing: ``` pave run GOALS_DIR PHASE ``` Goals are expected to be idempotent: running them again on a converged machine changes nothing, and says nothing. That's what makes it fine to run them often, from several triggers, and by hand. pave needs bash (4.4 or later) and coreutils. It's Linux-first. Quick start ----------- ``` git clone ... pave && make -C pave install # into ~/made/pave/; or run pave/bin/pave in place mkdir -p ~/goals cat > ~/goals/boot.bash <<'EOF' ( chain lru_gen write_knob /sys/kernel/mm/lru_gen/enabled y write_knob /sys/kernel/mm/lru_gen/min_ttl_ms 1000 ) EOF pave install ~/goals ``` `pave install` sets up every trigger this machine supports, asks sudo for the root-owned one, and runs the boot phase right away. The goals directory ------------------- ``` GOALS_DIR/ boot.bash session.bash login.bash manual.bash hosts/ HOSTNAME/ boot.bash session.bash ``` `pave run GOALS_DIR PHASE` runs `PHASE.bash`, then `hosts/$HOSTNAME/PHASE.bash`, each if it exists. Every file is optional. The hostname is matched literally, as bash's `$HOSTNAME` reports it. Any `*.bash` file in the top dir or a host dir that isn't named for a phase is an error, so a misspelled phase can't silently never run. Other files and directories (helpers, config files to install) are yours to arrange. ### phases | phase | runs as | triggered by | | --- | --- | --- | | `boot` | root | once per boot: a systemd unit, an OpenRC `local.d` script, `/etc/rc.local`, or a cron `@reboot` line | | `session` | you | graphical session start: an XDG autostart entry | | `login` | you | login shells: a block in `~/.bash_profile`, `~/.bash_login`, or `~/.profile` (whichever bash reads) | | `manual` | either | nothing; it runs when you run it | Any phase can also be run by hand at any time. Writing goals ------------- Goal files are bash, sourced by pave, with the goals dir as the working directory and an empty stdin. They run under `set -uo pipefail`, but **not** `set -e`. ### every top-level command stands alone A failing command is recorded, with its file and line, and the file carries on with the next one. Independent things need no ceremony: ```bash xinput set-prop 'SynPS/2 Synaptics TouchPad' 'libinput Accel Speed' 0.4 ensure_file "$HOME/.config/git/ignore" 644 "$(