Introduction
Bois
Bois is an opinionated system provisioning tool for your personal machines.
I call my own machines jokingly my bois, hence the name.
It enables you to manage configuration files while being straight forward to use, making it easy to share them with other devices. This means re-usability of your configuration via templating and optional deployment on a per-host basis.
This effectively means that bois can be used for both system configuration (as root) and as a manager for your dotfiles.
On top of handling system config files, it’s also able to manage your installed packages and enabled services.
You could say that it aims to strike a balance between Chezmoi and Ansible/Saltstack, but on-host and for your bois.
Short Overview of the Most Prominent Features
- System configuration file management
- Allow editing of deployed files
- Diffing/Merging of deployed files vs. changed files in bois directory.
- Safety first. Don’t overwrite changes without a prompt.
- System package management (via package managers)
- System service management (e.g. Systemd).
- Dis-/enable services based on deployed files.
- Cleanup
- Remove deployed files/directories if removed from bois.
- Uninstall packages if removed from bois.
- Disable services if removed from bois.
- Also designed for usage as user dotfile manager.
Installation
There’re bunch of ways to install bois:
System Package Manager
The recommended and most convenient way to install bois is via your distribution’s package manager.
You can check whether bois is available for your package manager with the following table. For more detail, just click on the image.
Pre-built Static Binaries
Statically linked executables for ARM/Linux are built on each release.
You can find the binary for your system on the release page.
Just download it, rename it to bois and place it somewhere in your $PATH/program folder.
Install via cargo
If you have the rust toolchain installed, you can build the latest release or directly from the Git repository
Latest release:
cargo install --locked bois
Latest commit on the git repository:
cargo install --locked --git https://github.com/Nukesor/bois.git bois
Setup
To get started, just run bois init inside of an empty directory, or run bois init $dir_name to let bois create the directory for you.
Bois Config
The top-level bois.yml file configures global settings for bois.
This file is optional - if it doesn’t exist, bois will use sensible defaults.
Location
Bois looks at the following locatin (in this order) for a bois.yml:
In User Mode:
~/.config/dotfiles/bois.yml~/.config/bois/bois.yml~/.dotfiles/bois.yml~/.dots/bois.yml~/.bois/bois.yml
In System Mode:
/etc/bois/bois.yml
You can also specify a custom config file location using the --config flag.
Example
Here’s a full example of a bois.yml:
# The machine name used to select the host directory.
# If not set, the system hostname is used.
name: my-laptop
# The directory containing your bois configuration (hosts, groups, etc).
# Defaults to the directory where this bois.yml is located.
bois_dir: ~/dotfiles
# The target directory where configuration files are deployed.
# User mode: ~/.config (default)
# System mode: /etc/bois (default)
target_dir: ~/.config
# Cache directory for storing deployment state.
# User mode: ~/.cache/bois (default)
# System mode: /var/lib/bois (default)
cache_dir: ~/.cache/bois
# Runtime directory for temporary files.
# User mode: ~/run/user/$YOUR_USER_ID/bois (default)
# System mode: /var/lib/bois (default)
runtime_dir: /run/user/1000/bois
# Additional environment variables for password managers or other integrations.
envs:
PASSWORD_STORE_DIR: ~/.password-store
GOPASS_SESSION: some-session-token
# Operating mode: User or System
# User mode deploys to user directories (~/.config)
# System mode deploys to system directories (/etc)
# Defaults to System when running as root, User otherwise.
mode: User
Configuration Options
All fields are optional:
name:String- The machine name, used to select which host directory to use. Defaults to the system hostname.bois_dir:PathBuf- The directory containing your bois configuration (hosts, groups, etc).- By default, it picks the first directory it finds at the following locations:
~/.config/dotfiles~/.config/bois~/.dotfiles~/.dots~/.bois
- System mode default:
/etc/bois
- By default, it picks the first directory it finds at the following locations:
target_dir:PathBuf- The target directory where configuration files are deployed.- User mode defaults:
$XDG_CONFIG_DIR/~/.config(fallback)
- System mode default:
/etc/bois
- User mode defaults:
cache_dir:PathBuf- Cache directory for storing deployment state.- User mode default:
XDG_CACHE_DIR/bois~/.cache/bois
- System mode default:
/var/lib/bois
- User mode default:
runtime_dir:PathBuf- Runtime directory for temporary files.- User mode defaults:
$XDG_RUNTIME_DIR/bois~/.cache/bois(fallback)
- System mode default:
/var/lib/bois
- User mode defaults:
envs:Map<String -> String>This can be used to set additional environment variables that should be loaded into bois environment. That’s useful for password manager integration which often requires special configuration or session variables.mode:User | SystemThe mode of operatation. By default, this is detected based on the current user:rootusers run inSystemmode while non-root users run inUsermode.User: Deploy to user directories and perform actions as user, such as runningsystemctlwith--userflagSystem: Deploy to system directories and perform actions as root, such as installing packages as root or runningsystemctlas root.
Modes
Bois operates in two modes that determine default directories and behavior:
User Mode
- Target directory:
~/.config - Cache directory:
~/.cache/bois - Runtime directory:
$XDG_RUNTIME_DIR/bois - Systemctl: Called with
--userflag - Use case: Managing personal dotfiles
System Mode
- Target directory:
/etc/bois - Cache directory:
/var/lib/bois - Runtime directory:
/var/lib/bois - Systemctl: Called without
--userflag - Use case: Managing system-wide configuration (requires root)
Hosts
Hosts are an important concept in bois.
Since bois is designed for your personal computers, your machines are configured on a hostname basis.
The configuration files for your machines are located in the host directory.
Imagine having two machines named strelok and artifact (which are also their respective hostnames).
The directory structure might look something like this:
📁 groups/
📂 hosts/
│ 📂 artifact/
│ │ 📁 udev/
│ │ 📁 X11/
│ │ pacman.conf
│ │ host.yml
│ └ vars.yml
└ 📂 strelok/
│ host.yml
└ vars.yml
- The
host.ymlfile is required to exist in every host directory. It allows you to set host-specific configuration defaults and determines which groups are going to be included for this host. - All variables inside the
vars.ymlare exposed to the templating engine. Read the templating docs for detailed info. The top level of thevars.ymlis expected to be an object. I.e.encrypt: false machine: threads: 8 is_laptop: true - All other files that’re located in a host’s directory are considered configuration files that should be deployed to the system.
In the example above, that would be the
X11andudevfolders, as well as thepacman.conffor theartifacthost.
Let’s anticipate the next chapter a tiny bit, which will be about groups. Groups are a tool to allow reuse of configuration files across multiple hosts.
In contrast to groups, host configuration files are always exclusive for a specific host. This allows you have a strict distinction between reusable logic, which is kept inside of groups, and machine specific configuration, which is located the machine’s respective host directory.
host.yml
The following is a full example of a host.yml:
# Groups that're required by this host.
groups:
- base
- laptop
- games
# Packages that should always be installed for this host.
packages:
pacman:
- linux
- base-devel
- tuned
# Defaults that should be applied to all files.
file_defaults:
owner: root
group: root
file_mode: 0o644
directory_mode: 0o755
groups:List<String>The list of groups that’re enabled for this host. The group names correspond to the group’s directory names inside the top-levelgroupsdirectory.packages:Map<String -> List<String>>: A list of packages sorted by package manager. Look at Package Management to see the list of available package managers.file_defaultsSet defaults file permissions for all configuration files that’re inside this host directory.owner:String- The file’s ownergroup:String- The file’s assigned groupfile_mode:OctalInt- The default permissions that’ll be set for all files.directory_mode:OctalInt- The default permissions that’ll be set for all directories.
Group Config
Groups are a tool for reusing configuration files across multiple hosts.
All configuration that’s shared between machines should be placed into groups. For instance, all machines might share the same base packages, shell configuration, or editor setup.
Groups are located in the top-level groups directory.
The directory structure might look something like this:
📂 groups/
│ 📂 base/
│ │ 📁 shell/
│ │ 📁 git/
│ │ group.yml
│ └ vars.yml
│ 📂 laptop/
│ │ 📁 upower/
│ │ group.yml
│ └ vars.yml
└ 📂 games/
│ group.yml
└ vars.yml
📁 hosts/
- The
group.ymlfile is optional. It allows you to set group-specific configuration and specify packages that should be installed when this group is included. - All variables inside the
vars.ymlare exposed to the templating engine. Read the templating docs for detailed info. The top level of thevars.ymlis expected to be an object. - All other files that’re located in a group’s directory are considered configuration files that should be deployed to the system.
In the example above, that would be the
shell,git, andupowerfolders.
Groups are enabled per host by adding them to the groups list in the host.yml.
group.yml
The following is a full example of a group.yml:
# Override the target directory for all files in this group.
# If not set, the global target directory is used.
target_directory: /etc
# Packages that should be installed when this group is enabled.
packages:
pacman:
- git
- vim
- neovim
# Defaults that should be applied to all files in this group.
defaults:
owner: root
group: root
file_mode: 0o644
directory_mode: 0o755
target_directory:PathBuf(optional) - Override the target directory for all configuration files in this group.- If it’s a relative path, it’s treated as relative to the global target directory.
- If it’s an absolute path, that absolute path is used.
packages:Map<String -> List<String>>(optional) - A list of packages sorted by package manager. Look at Package Management to see the list of available package managers.defaults: (optional) Set default file permissions for all configuration files that’re inside this group directory.owner:String- The file’s ownergroup:String- The file’s assigned groupfile_mode:OctalInt- The default permissions that’ll be set for all files.directory_mode:OctalInt- The default permissions that’ll be set for all directories.
File Config
Individual files can be configured by adding a bois_config block inside the file itself.
The configuration block is commented out using the file’s native comment syntax, so it doesn’t interfere with the actual configuration.
This allows you to:
- Override the destination path for a specific file
- Rename files when deploying them
- Set custom ownership and permissions
- Enable templating for dynamic configuration
- Customize template delimiters to avoid conflicts
Example
Here’s a bash script with a bois_config block:
#!/bin/bash
# bois_config
# template: true
# owner: root
# group: root
# mode: 0o755
# path: /usr/local/bin/
# bois_config
echo "Hello from {{ host }}"
The configuration is extracted from between the two # bois_config delimiter lines, and the actual file content (without the config block) is deployed.
Supported Comment Syntaxes
The parser supports multiple comment prefixes: #, //, --, /*, */, **, *, %
This means you can use bois_config blocks in:
- Shell scripts, Python, Ruby, YAML (
#) - C, C++, JavaScript, Rust (
//or/* */) - SQL, Lua, Haskell (
--) - LaTeX (
%)
Configuration Options
path:PathBuf(optional) - Override the destination path for this file.- If it’s a relative path, it’s treated as relative to the target directory.
- If it’s an absolute path, that absolute path is used directly.
- Takes precedence over any folder-level path overrides.
rename:String(optional) - Override the filename when deploying. Useful for deploying dotfiles without having dots in your bois directory.# bois_config # rename: .bashrc # bois_configowner:String(optional) - The file owner. Defaults to the current user.group:String(optional) - The file’s assigned group. Defaults to the current user’s group.mode:OctalInt(optional) - File permissions (e.g.,0o644). If not set, the source file’s permissions are preserved.template:Boolean(optional) - Enable Jinja2 templating for this file. Defaults tofalse. Read the templating docs for detailed info.delimiters:Object(optional) - Customize Jinja2 template delimiters. Useful when the default{{ }}/{% %}syntax conflicts with the file’s content.# bois_config # template: true # delimiters: # prefix: "#" # block: ["{%", "%}"] # variable: ["{{", "}}"] # comment: ["{#", "#}"] # bois_configprefix:String(optional) - Prefix all delimiters with this string (e.g.,#to make templates look like comments).block:[String, String](optional) - Delimiters for logic blocks. Defaults to["{%", "%}"].variable:[String, String](optional) - Delimiters for variables. Defaults to["{{", "}}"].comment:[String, String](optional) - Delimiters for comments. Defaults to["{#", "#}"].
Full Example with Custom Delimiters
When working with files that already use {{ }} syntax (like systemd service files or some shell scripts), you can prefix delimiters to avoid conflicts:
[Unit]
Description=Backup Service
# bois_config
# template: true
# delimiters:
# prefix: "#"
# bois_config
#{% if host == "production" %}
ExecStart=/usr/bin/backup --important-data
#{% else %}
ExecStart=/usr/bin/backup --test-mode
#{% endif %}
With the # prefix, template blocks become #{% and #{{, making them valid comments while still being processed by the template engine.
Folder Config
Any folder inside a host or group directory can have a bois.yml or bois.yaml file to configure how that folder and its contents should be deployed.
This is useful for:
- Overriding the destination path for a whole directory tree
- Setting ownership and permissions for all files in that directory
Example
Imagine you have a udev folder in your host directory that should be deployed to /etc/udev/rules.d:
📂 hosts/
└ 📂 artifact/
└ 📂 udev/
│ bois.yml
│ 10-network.rules
└ 20-usb.rules
The bois.yml might look like this:
# Deploy to an absolute path outside the default target directory
path: /etc/udev/rules.d
# Set ownership and permissions
owner: root
group: root
mode: 0o755
Now all files inside the udev folder will be deployed to /etc/udev/rules.d with the specified ownership and permissions.
Configuration Options
path:PathBuf(optional) - Override the destination path for this directory and all its contents.- If it’s a relative path, it’s treated as relative to the target directory.
- If it’s an absolute path, that absolute path is used directly.
- This override cascades to all child files and directories, unless they specify their own
path.
owner:String(optional) - The directory owner. Defaults to the current user.group:String(optional) - The directory’s assigned group. Defaults to the current user’s group.mode:OctalInt(optional) - The permissions for this directory (e.g.,0o755). Defaults to0o755.
Path Inheritance
When a folder has a path override, all files and subdirectories inside inherit that override:
📂 systemd/
│ bois.yml (path: /etc/systemd/system)
├ 📂 timers/
│ └ backup.timer
└ 📁 services/
└ backup.service
Both timers/backup.timer and services/backup.service will be deployed under /etc/systemd/system/ unless they specify their own path override.
Templating
bois uses the minijinja templating engine.
It is based on the syntax and behavior of the Jinja2 template engine for Python.
Documentation
Here’re some links to get started with how to write templates with minijinja.
How to use templating in bois
Templating functionality is opt-in in bois.
To enable templating for a file, you must enable the template option in its File configuration block.
# bois_config
# template: true
# bois_config
Once this configuration flag is found, bois will treat the whole file as a template.
If there’s a vars.yml file in the current host’s directory, it’ll be read and injected into the templating environment.
For example, consider the following vars.yml file in a host’s directory.
some_secret: "lorem"
some_secret_list:
- "ipsum"
- "dolor"
some_secret_dict:
lorem: sit
These variables can then be used like this:
SECRET={{ some_secret }}
{% for item in some_secret_list %}
# Useless comment: {{ item }}
{% endfor %}
{% if 'lorem' in some_secret_dict %}
OTHER_SECRET={{ some_secret_dict['lorem'] }}
{% endif %}
Which results in the following output:
SECRET=lorem
# Useless comment: ipsum
# Useless comment: dolor
OTHER_SECRET=sit
Pre-defined variables
bois pre-populates the templating environment with a few variables for your convenience:
host: String - The name of the current host.boi_groups:List<String>- A list with all groups that’re enabled for the host.
The following example checks whether the encrypt group is enabled for the current host.
If so, it adds the do_encryption=true flag to the configuration file.
{% if "encrypt" in boi_groups %}
do_encryption=true
{% endif %}
Pre-defined functions
On top of minijinja’s native filters and functions, bois exposes some functions itself.
Most of those functions are integrations with password managers, enabling you to inject secrets into your configuration files.
Custom delimiters
It’s possible to set custom delimiters for templating. This is sometimes useful for files that already have a Jinja2-style templating syntax themselves or for other formats that heavily use curly braces like Latex.
To change the syntax from ["{{", "}}", "{%", "%}", "{#", "#}"], the delimiters option can be used:
delimiters:
block: ["{%", "%}"]
variable: ["{{", "}}"]
comment: ["{#", "#}"]
For example, the following is really handy to not interfere with a configuration format that uses # as a comment.
(The syntax highlighting is a bit off, as we’re now using a slightly different syntax. Just imagine the # prefix would be highlighted as well.)
# bois_config
# template: true
# delimiters:
# block: ["#{%", "%}"]
# variable: ["#{{", "}}"]
# comment: ["#{#", "#}"]
# bois_config
...
#{# this is how a template comment now looks like #}
...
important_option=#{{ some_variable }}
Password Managers
bois provides a list of integrations with password managers to allow injecting sensitive data into your configuration files via templating.
For this purpose, each supported password manager exposes one or more functions, which might differ slighty based on the supported functionality of the respective manager.
For example, the passwordstore (pass) password manager can be used like this:
# bois_config
# template: true
# bois_config
MY_SECRET_KEY={{ pass("secrets/root_key") }}
...
Take a look at the documentation for the individual managers for more detail on how to use them.
Passwordstore (pass)
The pass() templating function can be used to interact with pass.
There’re however a few requirements for this to go smoothly:
- If your key has a passphrase, you should have a working gpg-agent setup.
Otherwise,
passwon’t work as there’s no way to provide the password to decrypt your gpg key via a CLI option. Your key needs to be, at least temporarily, added to the gpg-agent for bois to be able to access keys. - When you’re running
boisasrootto configure your system, you must have a working passwordstore and gpg setup forrootas well.- To avoid to also having to copy and synchronize your passwordstore to root, you can set the following environment variable in your global
bois.ymlto use your normal user’s.# /etc/bois.yml envs: PASSWORD_STORE_DIR: /home/your_user/.local/share/password-store
- To avoid to also having to copy and synchronize your passwordstore to root, you can set the following environment variable in your global
How to use
To get data stored in pass, there exists the pass template function.
For normal password retrieval, it can be used like this in any file with activated templating: {{ pass("service/kagi.com") }} \
This will read the first line of the service/kagi.com file and return it.
On top of this, the function also supports deserialization of extra data.
Function
{{ pass(key, parse_mode) }}
The pass function accepts two parameters, the second being optional:
keyis the path you would specify when callingpassdirectly from the cli. If only thekeyis provided, the first line of the password file is returned.parse_mode(optional): Can be one of["yaml"](feel free to contribute more formats).
If this is provided, the first line of the password file is ignored and the remaining content is interpreted as said data format. The content of that data format is simply returned from the function and can be further used.
Examples
Consider the following pass file at service/kagi.com:
my super secret pass
user: my@email.de
Simple Usage
A simple call that returns the first line of said file.
{{ pass("service/kagi.com") }}
Would return: my super secret pass
With Data Format
Interpret the passwordstore file as a dataformat and returns the data for further usage. Note: The first line is always ignored.
{{ pass("service/kagi.com")["user"] }}
Would return: my@email.de
System management
bois has been designed to be used for system configuration from the very start.
On top of configuration file management, it also supports:
- Package management
- Specify the exact set of packages that should be installed via various system package managers.
- Automatically un-/install packages when changes in the
boisconfiguration have taken place.
- Service management
- Enable/Disable services via configuration
Package Management
Bois contains support for several package managers.
This allows you to un-/install and manage packages based on groups or per host.
Pacman
Configuration
Packages can be added by adding a package.pacman section to either a group.yml or the host.yml.
For example:
# Packages that should be installed when this group is enabled.
packages:
pacman:
- git
All pacman packages that’re defined in the host.yml and of all enabled group.yml files will then be installed for the given host.
Tips and tricks
bois diff
If you encounter packages that’re listed as explicitly installed, but want them be handled as a dependency so they no longer show up in the diff, there’s a simple command for that:
sudo pacman -D --asdep $package_name
This command marks that package as a dependency and it’ll no longer show up in the diff.
Paru
Setting up paru
Installing AUR packages with paru is a bit tricky, as root isn’t allowed to build packages.
The current way to work around this is to create a dedicated user, which will run paru for root.
It needs to be able to call pacman though, so there’s a bit of setup that needs to be done.
At this point of this writing, bois still expects this user to be named aur.
- Create an
auruser.useradd --home-dir /var/lib/aur --create-home aur - Allow
aurto call pacman as withrootpermissions to install packages.aur ALL=(ALL) NOPASSWD: /usr/bin/pacman
Configuration
Packages can be added by adding a package.paru section to either a group.yml or the host.yml.
For example:
# Packages that should be installed when this group is enabled.
packages:
paru:
- pueue-git
All pacman packages that’re defined in the host.yml and of all enabled group.yml files will then be installed for the given host.