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.
It will then create the following directory structure with <hostname> being the hostname of your machine.
📁 traits/
│ 📂 base/
│ │ └ base.yml
📂 hosts/
│ 📂 <hostname>/
│ │ <hostname>.yml
│ │ └ vars.yml
└ bois.yml
How to Dotfiles
This chapter walks you through the typical bois workflow of managing a few dotfiles that are shared between some hosts.
It won’t go into too much detail, the idea is to get a feel for what working with bois looks like in practice.
Every topic that is touched in this guide also has its own chapter with more detailed documentation.
Note
The How to System Provisioning chapter covers the same workflow, but for provisioning your system.
Now let’s get to it. Let’s assume you have a laptop with whose hostname is milo, and you would like to manage your configuration files in ~/.config.
User mode
By default, bois determines its run mode from the user that calls it.
When run as a normal non-root user, it operates in user mode:
- Files are deployed to
~/.config. - No packages can be installed (yet).
- Service managers are run in user mode (e.g.
systemctlis called with--user)
Creating the bois directory
Everything bois manages lives in a single directory, the bois directory.
Let’s create this directory at a location where bois automatically discovers it (e.g. ~/.config/bois or ~/.config/dotfiles):
bois init ~/.config/bois
cd ~/.config/bois
Note
If you’d rather put it somewhere else, check out the Bois Config chapter.
All paths from here on are relative to this directory.
After running bois init, you’ll end up with this structure:
📁 traits/
│ 📂 base/
│ │ └ base.yml
📂 hosts/
│ 📂 milo/
│ │ milo.yml
│ │ └ vars.yml
└ bois.yml
The host’s config file lives inside the host’s directory at hosts/milo/milo.yml.
The important part for now is hosts/milo/.
Everything inside it (except the milo.yml config file and the vars.yml special file) is configuration that will be deployed for this host only.
Your first managed file
Let’s put a config file you already have under bois’ control like, for example, your alacritty config:
mkdir hosts/milo/alacritty
cp ~/.config/alacritty/alacritty.toml hosts/milo/alacritty/
That’s all there is to it.
Since you’re running as a normal user, bois runs in user mode and deploys to ~/.config by default (the target_directory).
Files in a host directory mirror their path relative to that target directory:
hosts/milo/alacritty/alacritty.toml will be deployed to ~/.config/alacritty/alacritty.toml.
Plan and deploy
bois plan is a dry-run that shows everything a deployment would do, without touching your system:
bois plan
Since we just copied the file, the source and the deployed file are identical and there’s nothing to do.
bois only ever touches things that actually changed.
So let’s change something!
Open hosts/milo/alacritty/alacritty.toml in the bois directory and tweak a value, for example the font size.
bois plan now shows a diff of exactly what would change on your system.
If you’re happy with it, apply the change:
bois deploy
That’s the basic workflow you’ll be using most of the time.
I.e. edit some files in the bois directory → bois plan → bois deploy
Safety features
In case some deployed (and thereby managed) file is edited on your host directly, it won’t just be overwritten.
bois compares the actual state of each managed file against the previous state from the last deployment.
If any changes are detected, bois warns you about non-adopted changes and only overwrites those changes if you explicitly say so:
These changes will be overwritten. Are you sure that's okay?
Nothing gets overwritten without you being notified about it. If you want to keep those changes, simply abort, adopt the changes into your bois directory and rerun the deployment.
Sharing config between hosts with traits
So far, everything lives in hosts/milo/ and is exclusive to your laptop.
But it would be nice to also have that alacritty config on your desktop.
That’s what traits are for: A trait is basically a bundle of configuration that any host can opt into.
The template generated by bois init already created a trait called base, and the generated hosts/milo/milo.yml already enables it:
# hosts/milo/milo.yml
traits:
- base
To move the alacritty config to the trait, simply run:
mv hosts/milo/alacritty traits/base/alacritty
If you were to now run bois init, nothing would change as milo opted into the base trait and as such, the file is still part of the configuration that’s active for milo.
Now, setting up configuration for your desktop with the name cleo boils down to:
- Adding a
hosts/cleo.yml, which also opts into thebasetrait. Sincecleobrings no files of its own (yet), a stand-alone config file without a host directory is all it needs:traits: - base - Cloning your bois directory on
cleoto a well-known path (e.g.~/.config/boisor~/.config/dotfiles). - Running
bois deploy.
Check the Trait Config chapter for everything else traits can do.
Removing files
To stop managing a file, just delete it from the bois directory.
The next bois deploy will notice the removal and delete the deployed file from your system.
Important
Directories will never be removed as long as they still contain files.
Also, directories aren’t removed by default. For that you have to set the following in your host/trait config:
cleanup:
directories: true
Managing hidden files
Some tools insist on dotfiles like ~/.bashrc, which are annoying to work with inside the bois directory, due to them being hidden.
To address this, files can carry their own configuration in a bois_config block inside the file itself. Most file format’s native comment syntaxes are supported:
hosts/milo/bashrc:
# bois_config
# target_path: ~/
# rename: .bashrc
# bois_config
alias ls='ls --color=auto'
target_path overrides the target directory, and rename adds that dot back to the filename, so that the file is deployed to ~/.bashrc.
The config block is also stripped during deployment.
Check out the File Configuration chapter to learn more and see all available options.
Templating
Sooner or later, shared configs may need small per-host adjustments, such as a bigger font size on a small laptop screen. For those cases, any file can be turned into a minijinja (Jinja2-style) template.
To enable templating, the bois_config block has a template parameter.
For example, the shared alacritty config from earlier could looks something like this:
# traits/base/alacritty/alacritty.toml
# bois_config
# template: true
# bois_config
[font]
{% if host == "milo" %}
size = 14
{% else %}
size = 11
{% endif %}
There are quite a few pre-set global variables and you can also define your own variables in the host’s vars.yml or use secrets from your password manager.
The delimiters {{/}}, {#/#} and {%/%} can also be individually overwritten so that they don’t interfere with your configuration file format.
See the Templating chapter for more information.
User service
In user mode, bois can also manage enabled services, such as systemd user services.
This is done via the services configuration in either a host config, a trait config or a dir.yml and look like this:
# hosts/milo/milo.yml
services:
systemd:
- ssh-agent
Important
By default, services are only enabled, not started.
Once this entry is removed from the host configuration, the ssh-agent service will be stopped and disabled.
For detailed info, check out the Service Management chapter.
I hope this gives you a good idea what you can do with bois :)
How to System Provisioning
This chapter walks you through the typical bois workflow of managing a system’s state.
This means managing files in /etc, installed packages and enabled services.
It won’t go into too much detail, the idea is to get a feel for what working with bois looks like in practice.
Every topic that is touched in this guide also has its own chapter with more detailed documentation.
Note
The How to Dotfiles chapter covers the same workflow, but for managing your user’s dotfiles.
Now, let’s get to it. Let’s assume you have a laptop with the hostname milo whose system configuration you’d like to manage.
System mode
By default, bois determines its run mode from the user that calls it.
Run as a normal user, it operates in user mode and manages the dotfiles in your home directory.
But when run as root, it switches to system mode:
- Files are deployed to
/etc. - Packages are installed and removed via your system’s package manager.
- Service managers are run in system mode (e.g.
systemctlis called without--user)
This whole chapter takes place in system mode, which is why we assume that all commands are executed as root.
Creating the bois directory
Everything bois manages lives in a single directory, the bois directory.
In system mode, bois expects it at /etc/bois:
bois init /etc/bois
cd /etc/bois
Note
Alternatively, you can also put your bois directory anywhere else and create a
/etc/bois/bois.ymlthat points to your desired directory location.
All paths from here on are relative to this directory.
After running bois init, you’ll end up with this structure:
📁 traits/
│ 📂 base/
│ │ └ base.yml
📂 hosts/
│ 📂 milo/
│ │ milo.yml
│ │ └ vars.yml
└ bois.yml
The host’s config file lives inside the host’s directory at hosts/milo/milo.yml.
The important part for now is hosts/milo/.
Everything inside it (except the milo.yml config file and the vars.yml special file) is configuration that will be deployed for this host only.
Your first managed file
Let’s put a config file you already have under bois’ control like, for example, your sysctl config:
cp /etc/sysctl.conf hosts/milo/
That’s all there is to it.
Files in a host directory mirror their path relative to the target directory, which is /etc in system mode:
hosts/milo/sysctl.conf will be deployed to /etc/sysctl.conf.
Plan and deploy
bois plan is a dry-run that shows everything a deployment would do, without touching your system:
bois plan
Since we just copied the file, the source and the deployed file are identical and there’s nothing to do.
bois only ever touches things that actually changed.
So let’s change something!
Open hosts/milo/sysctl.conf in the bois directory and tweak a value.
bois plan now shows a diff of exactly what would change on your system.
If you’re happy with it, apply the change:
bois deploy
That’s the basic workflow you’ll be using most of the time.
I.e. edit some files in the bois directory → bois plan → bois deploy
Safety features
In case some deployed (and thereby managed) file is edited on your host directly, it won’t just be overwritten.
bois compares the actual state of each managed file against the previous state from the last deployment.
If any changes are detected, bois warns you about non-adopted changes and only overwrites those changes if you explicitly say so:
These changes will be overwritten. Are you sure that's okay?
Nothing gets overwritten without you being notified about it. If you want to keep those changes, simply abort, adopt the changes into your bois directory and rerun the deployment.
Sharing config between hosts with traits
So far, everything lives in hosts/milo/ and is exclusive to your laptop.
But it would be nice to also have that sysctl config on your desktop.
That’s what traits are for: A trait is basically a bundle of configuration that any host can opt into.
The template generated by bois init already created a trait called base, and the generated hosts/milo/milo.yml already enables it:
# hosts/milo/milo.yml
traits:
- base
To move the sysctl config to the trait, simply run:
mv hosts/milo/sysctl.conf traits/base/sysctl.conf
If you were to now run bois plan, nothing would change as milo opted into the base trait and as such, the file is still part of the configuration that’s active for milo.
Now, setting up configuration for your desktop with the name cleo boils down to:
- Adding a
hosts/cleo.yml, which also opts into thebasetrait. Sincecleobrings no files of its own (yet), a stand-alone config file without a host directory is all it needs:traits: - base - Cloning your bois directory on
cleoto/etc/bois. - Running
bois deploy.
Check the Trait Config chapter for everything else traits can do.
Removing files
To stop managing a file, just delete it from the bois directory.
The next bois deploy will notice the removal and delete the deployed file from your system.
Important
Directories will never be removed as long as they still contain files.
Also, directories aren’t removed by default. For that you have to set the following in your host/trait config:
cleanup:
directories: true
Overriding target paths
Not every file lives in /etc though.
Let’s say you’re using systemd-boot and want to manage your boot entries, which live in /boot/loader/entries.
For cases like this, a folder’s target path can be overridden with a dir.yml file inside it:
📂 hosts/
└ 📂 milo/
└ 📂 boot_entries/
│ dir.yml
└ arch.conf
# hosts/milo/boot_entries/dir.yml
target_path: /boot/loader/entries
All files in that folder are now deployed to /boot/loader/entries/.
Ownership and permissions can be configured as well, if needed.
Check out the Folder Configuration and File Configuration chapters for all available options.
Managing packages
Bois can also be used to declare and synchronize packages on your machines. For this, you simply declare them in your host or trait config:
# hosts/milo/milo.yml
traits:
- base
packages:
pacman:
- tlp # Laptop battery management, only needed on milo.
Packages declared in a trait’s config are installed on every host that opts into that trait. This is useful for everyday tools you want want on multiple of your machines:
# traits/base/base.yml
packages:
pacman:
- git
- vim
- openssh
On the next bois deploy, everything that’s declared but missing will be installed.
If a package is removed from the list, it will be uninstalled on the next deploy.
Only packages that were part of the bois directory will be uninstalled. Packages that are not managed by bois won’t be touched.
Both bois plan and bois deploy show all installs and removals before anything actually happens.
Check the Package Management chapter for the list of supported package managers.
Tip
There’s also a way to see which installed packages on your system aren’t managed by bois yet:
bois diffThis is a really convenient way to incrementally sync your system with bois and to keep track of drift between hosts. Run
bois diffand then either add the packages to your config, uninstall them or simply just ignore them until you decide what to do with them.
System services
Installing a package is often only half the job, and installed services usually need to be enabled afterwards.
bois also manages system services, which are declared in host, trait or dir.yml configs like this:
# traits/base/base.yml
services:
systemd:
- systemd-timesyncd
- sshd
During deployment, bois installs packages first, then deploys files and only then enables services.
That way, unit files are guaranteed to exist by the time their services are enabled.
If a service is removed from your configuration, it is stopped and disabled during the next deploy.
Check the Service Management chapter for more info.
Important
By default, services are only enabled, not started.
If a service should be started immediately, you have to explicitly say so:
services: systemd: - name: docker start: trueEven then, services are only started when they are initially enabled.
boisdoes not manage the running state of services.
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 location (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 host 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, traits, 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 (default)
target_dir: ~/.config
# Cache directory for storing the deployed 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 host name, used to select which host directory to use. Defaults to the system hostname.bois_dir:PathBuf- The directory containing your bois configuration (hosts, traits, 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
- User mode defaults:
cache_dir:PathBuf- Cache directory for storing the deployed 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 operation. 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 - 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, hosts are configured on a hostname basis.
The configuration files for your hosts are located in the hosts directory.
Imagine having two hosts named milo and cleo (which are also their respective hostnames).
The directory structure might look something like this:
📁 traits/
📂 hosts/
│ 📂 cleo/
│ │ 📁 udev/
│ │ 📁 X11/
│ │ cleo.yml
│ │ pacman.conf
│ └ vars.yml
└ milo.yml
- Every host requires a config file.
The config file allows you to set host-specific configuration defaults and determines which traits are going to be included for this host.
If a directory exists for a host, the config is expected inside that directory at
hosts/<hostname>/<hostname>.yml, likecleoabove. If no directory is required, a host may only have a config file directly athosts/<hostname>.yml, likemiloabove. - 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.
Instead of aencrypt: false machine: threads: 8 is_laptop: truevars.ymlfile, the variables can also be defined in thevarsfield of the host’s config file. Only one of the two may be used, defining both is an error. - 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 the hostcleo.
Let’s look ahead to the next chapter real quick, which will be about traits. Traits are a tool to allow reuse of configuration files across multiple hosts.
In contrast to traits, 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 traits, and host specific configuration, which is located the host’s respective directory.
The host config file
The following is a full example of a host config file:
# Traits that're required by this host.
traits:
- base
- laptop
- games
# Packages that should always be installed for this host.
packages:
pacman:
- linux
- base-devel
- tuned
# Services that should be enabled for this host.
services:
systemd:
- systemd-timesyncd
- backup.timer
# Default permissions that should be applied to all files and directories.
permission_defaults:
owner: root
group: root
file_mode: 0o644
directory_mode: 0o755
# Controls what should be cleaned up once it's removed from this host's configuration.
cleanup:
directories: true
# Variables that're exposed to the templating engine.
# An alternative to a `vars.yml` file in the host directory.
vars:
editor: nvim
traits:List<String>The list of traits that’re enabled for this host. The trait names correspond to the trait’s directory names inside the top-leveltraitsdirectory.target_dir:PathBuf(optional) - Override the target directory for all configuration files in this host directory. Must be an absolute path (~is expanded). If not set, the global target directory is used.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.services: (optional) - A list of services sorted by service manager. Listed services are enabled during deployment. Once removed, they’re stopped and disabled. A service can either be a plain name, or an object with anameand astartflag to also start the service right away when it gets enabled. Look at Service Management to see the list of available service managers.permission_defaults: (optional) Set default permissions for all configuration files and directories that’re inside this host directory. Each field can be set on its own.owner:String(optional) - The default owner for all files and directories.group:String(optional) - The default group for all files and directories.file_mode:OctalInt(optional) - The default permissions that’ll be set for all files.directory_mode:OctalInt(optional) - The default permissions that’ll be set for all directories.
vars:Map(optional) - Variables that’re exposed to the templating engine. An alternative to thevars.ymlfile in the host directory, only one of the two may be used. Read the templating docs for detailed info.cleanup: (optional) Controls what should be cleaned up once it’s removed from this host’s configuration. Files and packages are always cleaned up; this only covers resources where cleanup is opt-in.directories:Boolean- Whether directories are removed once they leave the configuration. Defaults tofalse. Even if set totrue, directories are only removed if they’re empty. If a directory still contains unmanaged files, it’s never removed. This setting applies to the whole host directory and can be overridden per subtree via a folder’s dir.yml.
Trait Config
Traits are a tool for reusing configuration files across multiple hosts.
All configuration that’s shared between hosts should be placed into traits. For instance, all hosts might share the same base packages, shell configuration, or editor setup.
Traits are located in the top-level traits directory.
The directory structure might look something like this:
📂 traits/
│ 📂 base/
│ │ 📁 shell/
│ │ 📁 git/
│ └ base.yml
│ 📂 laptop/
│ │ 📁 upower/
│ └ laptop.yml
└ games.yml
📁 hosts/
- A trait’s config file is optional. A trait with its own directory keeps the config file inside
that directory at
traits/<traitname>/<traitname>.yml, likebaseandlaptopabove. A trait can also consist of only a config file without a directory; it then lives directly attraits/<traitname>.yml, likegamesabove. The config file allows you to set trait-specific configuration and specify packages that should be installed when this trait is included. - All other files that’re located in a trait’s directory are considered configuration files that should be deployed to the system.
In the example above, that would be the
shell,git, andupowerfolders.
Templating variables are defined in the host’s vars.yml and are available in all traits as well.
Read the templating docs for detailed info.
Traits are enabled per host by adding them to the traits list in the host config.
The trait config file
The following is a full example of a trait config file:
# Override the target directory for all files in this trait.
# If not set, the global target directory is used.
target_dir: /etc
# Packages that should be installed when this trait is enabled.
packages:
pacman:
- git
- vim
- neovim
# Services that should be enabled when this trait is enabled.
services:
systemd:
- systemd-timesyncd
- backup.timer
# Default permissions that should be applied to all files and directories in this trait.
permission_defaults:
owner: root
group: root
file_mode: 0o644
directory_mode: 0o755
# Controls what should be cleaned up once it's removed from this trait's configuration.
cleanup:
directories: true
target_dir:PathBuf(optional) - Override the target directory for all configuration files in this trait. Must be an absolute path (~is expanded). If not set, the global target directory 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.services:Map<String -> List<String|Object>>(optional) - A list of services sorted by service manager. Listed services are enabled during deployment. Once removed, they’re stopped and disabled. A service can either be a plain name, or an object with anameand astartflag to also start the service right away when it gets enabled. Look at Service Management to see the list of available service managers.permission_defaults: (optional) Set default permissions for all configuration files and directories that’re inside this trait directory. Each field can be set on its own.owner:String(optional) - The default owner for all files and directories.group:String(optional) - The default group for all files and directories.file_mode:OctalInt(optional) - The default permissions that’ll be set for all files.directory_mode:OctalInt(optional) - The default permissions that’ll be set for all directories.
cleanup: (optional) Controls what should be cleaned up once it’s removed from this trait’s configuration. Files and packages are always cleaned up; this only covers resources where cleanup is opt-in.directories:Boolean- Whether directories are removed once they leave the configuration. Defaults tofalse. Even if set totrue, directories are only removed if they’re empty. If a directory still contains unmanaged files, it’s never removed. This setting applies to the whole trait directory and can be overridden per subtree via a folder’s dir.yml.
File Config
By default, individual files will just be deployed to their the relative path to their host/trait with the identical permissions.
However, location, permissions, ownership and more can be configured by adding a bois_config YAML 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, and is pruned during deployment.
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 with native syntax.
Example
Here’s a bash script with a bois_config block:
#!/bin/bash
# bois_config
# template: true
# owner: root
# group: root
# mode: 0o755
# target_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 following comment prefixes are valid: #, //, --, /*, */, **, *, %.
This means you can use bois_config blocks in:
- Shell scripts, Python, Ruby, YAML (
#) - C, C++, JavaScript, Rust (
//or/* */) - SQL, Lua, Haskell (
--) - LaTeX (
%)
If anything is missing, please open a ticket.
Configuration Options
target_path:PathBuf(optional) - Override the destination path for this file.- If it’s a relative path, it’s treated as relative to the host’s/trait’s target directory
(the
target_diroverride if set, otherwise the global target directory). - If it’s an absolute path, that absolute path is used directly.
- If the path ends with a
/, the file is deployed into that directory under its own name. Otherwise, the path is used as the full destination path, including the file name. - Takes precedence over any folder-level
target_pathoverrides.
- If it’s a relative path, it’s treated as relative to the host’s/trait’s target directory
(the
rename:String(optional) - Override the filename when deploying. Useful for deploying dotfiles without having dots in your bois directory.
If# bois_config # rename: .bashrc # bois_configrenameand a non-dir-styletarget_path(does not end with/) is set, the filename intarget_pathwill be overwritten and a warning will be emitted.owner: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 opening delimiters with this string (e.g.,#{%to make templates behave 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 trait directory can have a dir.yml or dir.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
- Enabling services that belong to the configuration in that directory
Example
Imagine you have a udev folder in your host directory that should be deployed to /etc/udev/rules.d:
📂 hosts/
└ 📂 milo/
└ 📂 udev/
│ dir.yml
│ 10-network.rules
└ 20-usb.rules
The dir.yml might look like this:
# Deploy to an absolute path outside the default target directory
target_path: /etc/udev/rules.d
# Set ownership and permissions for the directory itself
owner: root
group: root
mode: 0o755
cleanup:
# If this directory is removed from the config, it will be cleaned up.
directories: true
# System services that should be enabled
services:
systemd:
- backup.timer
Now all files inside the udev folder will be deployed to /etc/udev/rules.d.
Note that owner, group, and mode only apply to the directory itself, not to the files inside it.
File ownership and permissions are set per file via its file config block, via the host’s/trait’s defaults, or fall back to the deploying user.
Configuration Options
target_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 host’s/trait’s target directory
(the
target_diroverride if set, otherwise the global 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
target_path.
- If it’s a relative path, it’s treated as relative to the host’s/trait’s target directory
(the
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.cleanup: (optional) - Override the cleanup behavior for this directory and all its contents.directories:Boolean- Whether directories are removed once they leave the configuration. Even if set totrue, directories are only removed if they’re empty. If a directory still contains unmanaged files, it’s never removed. If not specified, the value is inherited from the parent directory (ultimately the host’s/trait’scleanupsetting).
services: (optional) - A list of services sorted by service manager. This is identical to service sections on a trait or host level and allows service declarations close to the configuration files the services belong to.
Path Inheritance
When a folder has a target_path override, all files and subdirectories inside inherit that override:
📂 systemd/
│ dir.yml (target_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.
Symlinks on the System
It’s common to have symlinked directories on a system, such as /lib/ -> /usr/lib/.
Bois handles such symlinks in target paths as follows:
- If a directory in a target path is a symlink on the system and that directory has no declared permissions (
owner,groupormode), the symlink is accepted, as long as it points to a directory. All files are then simply deployed through the link. - As soon as any of
owner,groupormodeis declared for a directory, that directory is considered explicitly managed. Declared permissions can only be enforced on a real directory, so a symlink at its path is treated as a conflict: the link is deleted and replaced by a real directory. - Symlinks that point to a file, dangling symlinks and link loops are always treated as conflicts and are replaced.
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.
Alternatively, the variables can be defined in the vars field of the host’s config file.
Only one of the two may be used, defining both is an error.
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.traits:List<String>- A list with all traits that’re enabled for the host.USER:String- The name of the user that currently executes bois (from the$USERenvironment variable).USER_ID:Integer- The id of the user that currently executes bois.GROUP_ID:Integer- The group id of the user that currently executes bois.
The following example checks whether the encrypt trait is enabled for the current host.
If so, it adds the do_encryption=true flag to the configuration file.
{% if "encrypt" in traits %}
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 terminal-capable 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.- You can avoid having to copy and synchronize your passwordstore to root, by setting the following environment variable in your global
bois.yml. That way, you’ll simply use your normal user’s passwordstore.# /etc/bois/bois.yml envs: PASSWORD_STORE_DIR: /home/your_user/.local/share/password-store
- You can avoid having to copy and synchronize your passwordstore to root, by setting 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", "yml"](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", "yaml")["user"] }}
Would return: my@email.de
System management
bois is able to manage some of your system state:
- 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
- Specify the set of services that should be enabled.
- Services that leave the configuration are automatically stopped and disabled.
Package Management
Bois contains support for several package managers.
This allows you to un-/install and manage packages based on traits or per host.
Pacman
Configuration
Packages can be added by adding a packages.pacman section to either a trait config or the host config.
For example:
# Packages that should be installed when this trait is enabled.
packages:
pacman:
- git
All pacman packages that’re defined in the host config and the configs of all enabled traits 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 packages.paru section to either a trait config or the host config.
For example:
# Packages that should be installed when this trait is enabled.
packages:
paru:
- pueue-git
All paru packages that’re defined in the host config and the configs of all enabled traits will then be installed for the given host.
Service Management
Bois contains support for managing system services.
This allows you to enable services based on traits, folders or per host.
The rules are:
- Services are enabled, but not started, unless a service explicitly requests an immediate start via the
startflag. - Once a service leaves the configuration, it’s stopped and disabled during the cleanup phase of the next deployment.
- Bois doesn’t manage the running state of services. A service that’s already enabled but was stopped manually won’t be started again.
- The mode bois runs in determines which service manager instance is targeted: user mode manages the user’s own instance (e.g.
systemctl --user), system mode the system’s.
Services can be declared in a host config, a trait config or a folder’s dir.yml:
services:
systemd:
- systemd-timesyncd
- backup.timer
Supported service managers:
Systemd
Configuration
Services can be added by adding a services.systemd section to a host config, a trait config or a folder’s dir.yml.
For example:
# Services that should be enabled when this trait is enabled.
services:
systemd:
- systemd-timesyncd
- backup.timer
All systemd services that’re defined in the host config, in the configs of enabled traits and in dir.yml files will then be enabled for the given host.
Unit names without an explicit unit suffix (such as .service or .timer) refer to .service units, just like they do when calling systemctl.
I.e. systemd-timesyncd and systemd-timesyncd.service are treated as the same unit.
User and System Mode
Depending on the current mode bois runs in, different systemd units are targeted:
- In
Systemmode, the system’s systemd instance is managed. - In
Usermode, allsystemctlcalls are made with the--userflag, so the user’s own systemd instance is managed instead. Unit files for user services live in directories such as~/.config/systemd/user/.
Deployment
Services are enabled at the very end of a deployment, after packages have been installed and files have been deployed. That way, unit files that’re installed by packages or deployed by bois itself already exist by the time they’re enabled.
Services are only enabled (e.g. systemctl enable myunit), not started.
If a service should be started right away at the moment it gets enabled, use the explicit declaration syntax:
services:
systemd:
- name: docker
start: true
This enables the unit via systemctl enable --now.
Note that the start flag only takes effect at the moment the service gets enabled.
Bois doesn’t manage the running state of services.
This means that a service that’s already enabled but was stopped manually won’t be started again on the next deployment.
Cleanup
Once a service is removed from the configuration, it’s stopped and disabled (systemctl disable --now) during the cleanup phase of the next deployment.
Services are cleaned up before files and packages are removed, while the service’s unit files still exist on the system.