Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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.

Packaging status

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. systemctl is 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 planbois 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:

  1. Adding a hosts/cleo.yml, which also opts into the base trait. Since cleo brings no files of its own (yet), a stand-alone config file without a host directory is all it needs:
    traits:
      - base
    
  2. Cloning your bois directory on cleo to a well-known path (e.g. ~/.config/bois or ~/.config/dotfiles).
  3. 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. systemctl is 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.yml that 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 planbois 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:

  1. Adding a hosts/cleo.yml, which also opts into the base trait. Since cleo brings no files of its own (yet), a stand-alone config file without a host directory is all it needs:
    traits:
      - base
    
  2. Cloning your bois directory on cleo to /etc/bois.
  3. 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 diff

This is a really convenient way to incrementally sync your system with bois and to keep track of drift between hosts. Run bois diff and 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: true

Even then, services are only started when they are initially enabled. bois does 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
  • target_dir: PathBuf - The target directory where configuration files are deployed.
    • User mode defaults:
      • $XDG_CONFIG_DIR/
      • ~/.config (fallback)
    • System mode default:
      • /etc
  • 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
  • runtime_dir: PathBuf - Runtime directory for temporary files.
    • User mode defaults:
      • $XDG_RUNTIME_DIR/bois
      • ~/.cache/bois (fallback)
    • System mode default:
      • /var/lib/bois
  • 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 | system The mode of operation. By default, this is detected based on the current user: root users run in system mode while non-root users run in user mode.
    • user: Deploy to user directories and perform actions as user, such as running systemctl with --user flag
    • system: Deploy to system directories and perform actions as root, such as installing packages as root or running systemctl as 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 --user flag
  • Use case: Managing personal dotfiles

System Mode

  • Target directory: /etc
  • Cache directory: /var/lib/bois
  • Runtime directory: /var/lib/bois
  • Systemctl: Called without --user flag
  • 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, like cleo above. If no directory is required, a host may only have a config file directly at hosts/<hostname>.yml, like milo above.
  • All variables inside the vars.yml are exposed to the templating engine. Read the templating docs for detailed info. The top level of the vars.yml is expected to be an object. I.e.
    encrypt: false
    machine:
      threads: 8
      is_laptop: true
    
    Instead of a vars.yml file, the variables can also 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.
  • 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 X11 and udev folders, as well as the pacman.conf for the host cleo .

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-level traits directory.
  • 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 a name and a start flag 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 the vars.yml file 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 to false. Even if set to true, 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, like base and laptop above. A trait can also consist of only a config file without a directory; it then lives directly at traits/<traitname>.yml, like games above. 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, and upower folders.

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 a name and a start flag 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 to false. Even if set to true, 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_dir override 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_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_config
    
    If rename and a non-dir-style target_path (does not end with /) is set, the filename in target_path will 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 to false. 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_config
    
    • prefix: 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_dir override 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.
  • 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 to 0o755.
  • 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 to true, 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’s cleanup setting).
  • 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.

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, group or mode), 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, group or mode is 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 $USER environment 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:

  1. If your key has a passphrase, you should have a working terminal-capable gpg-agent setup. Otherwise, pass won’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.
  2. When you’re running bois as root to configure your system, you must have a working passwordstore and gpg setup for root as 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
      

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:

  • key is the path you would specify when calling pass directly from the cli. If only the key is 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 bois configuration 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.

  1. Create an aur user.
    useradd --home-dir /var/lib/aur --create-home aur
    
  2. Allow aur to call pacman as with root permissions 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 start flag.
  • 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 System mode, the system’s systemd instance is managed.
  • In User mode, all systemctl calls are made with the --user flag, 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.