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.

Bois Config

The top-level bois.yml file configures global settings for bois. This file is optional - if it doesn’t exist, bois will use sensible defaults.

Location

Bois looks at the following locatin (in this order) for a bois.yml:

In User Mode:

  • ~/.config/dotfiles/bois.yml
  • ~/.config/bois/bois.yml
  • ~/.dotfiles/bois.yml
  • ~/.dots/bois.yml
  • ~/.bois/bois.yml

In System Mode:

  • /etc/bois/bois.yml

You can also specify a custom config file location using the --config flag.

Example

Here’s a full example of a bois.yml:

# The machine name used to select the host directory.
# If not set, the system hostname is used.
name: my-laptop

# The directory containing your bois configuration (hosts, groups, etc).
# Defaults to the directory where this bois.yml is located.
bois_dir: ~/dotfiles

# The target directory where configuration files are deployed.
# User mode: ~/.config (default)
# System mode: /etc/bois (default)
target_dir: ~/.config

# Cache directory for storing deployment state.
# User mode: ~/.cache/bois (default)
# System mode: /var/lib/bois (default)
cache_dir: ~/.cache/bois

# Runtime directory for temporary files.
# User mode: ~/run/user/$YOUR_USER_ID/bois (default)
# System mode: /var/lib/bois (default)
runtime_dir: /run/user/1000/bois

# Additional environment variables for password managers or other integrations.
envs:
  PASSWORD_STORE_DIR: ~/.password-store
  GOPASS_SESSION: some-session-token

# Operating mode: User or System
# User mode deploys to user directories (~/.config)
# System mode deploys to system directories (/etc)
# Defaults to System when running as root, User otherwise.
mode: User

Configuration Options

All fields are optional:

  • name: String - The machine name, used to select which host directory to use. Defaults to the system hostname.
  • bois_dir: PathBuf - The directory containing your bois configuration (hosts, groups, etc).
    • By default, it picks the first directory it finds at the following locations:
      • ~/.config/dotfiles
      • ~/.config/bois
      • ~/.dotfiles
      • ~/.dots
      • ~/.bois
    • System mode default:
      • /etc/bois
  • target_dir: PathBuf - The target directory where configuration files are deployed.
    • User mode defaults:
      • $XDG_CONFIG_DIR/
      • ~/.config (fallback)
    • System mode default:
      • /etc/bois
  • cache_dir: PathBuf - Cache directory for storing deployment 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 operatation. 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/bois
  • 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, your machines are configured on a hostname basis.

The configuration files for your machines are located in the host directory.
Imagine having two machines named strelok and artifact (which are also their respective hostnames). The directory structure might look something like this:

 📁 groups/
 📂 hosts/
 │ 📂 artifact/
 │ │ 📁 udev/
 │ │ 📁 X11/
 │ │ pacman.conf
 │ │ host.yml
 │ └ vars.yml
 └ 📂 strelok/
   │ host.yml
   └ vars.yml
  • The host.yml file is required to exist in every host directory. It allows you to set host-specific configuration defaults and determines which groups are going to be included for this host.
  • All variables inside the vars.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
    
  • 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 artifact host.

Let’s anticipate the next chapter a tiny bit, which will be about groups. Groups are a tool to allow reuse of configuration files across multiple hosts.

In contrast to groups, host configuration files are always exclusive for a specific host. This allows you have a strict distinction between reusable logic, which is kept inside of groups, and machine specific configuration, which is located the machine’s respective host directory.

host.yml

The following is a full example of a host.yml:

# Groups that're required by this host.
groups:
  - base
  - laptop
  - games

# Packages that should always be installed for this host.
packages:
  pacman:
    - linux
    - base-devel
    - tuned

# Defaults that should be applied to all files.
file_defaults:
  owner: root
  group: root
  file_mode: 0o644
  directory_mode: 0o755
  • groups: List<String> The list of groups that’re enabled for this host. The group names correspond to the group’s directory names inside the top-level groups directory.
  • packages: Map<String -> List<String>>: A list of packages sorted by package manager. Look at Package Management to see the list of available package managers.
  • file_defaults Set defaults file permissions for all configuration files that’re inside this host directory.
    • owner: String - The file’s owner
    • group: String - The file’s assigned group
    • file_mode: OctalInt - The default permissions that’ll be set for all files.
    • directory_mode: OctalInt - The default permissions that’ll be set for all directories.

Group Config

Groups are a tool for reusing configuration files across multiple hosts.

All configuration that’s shared between machines should be placed into groups. For instance, all machines might share the same base packages, shell configuration, or editor setup.

Groups are located in the top-level groups directory. The directory structure might look something like this:

 📂 groups/
 │ 📂 base/
 │ │ 📁 shell/
 │ │ 📁 git/
 │ │ group.yml
 │ └ vars.yml
 │ 📂 laptop/
 │ │ 📁 upower/
 │ │ group.yml
 │ └ vars.yml
 └ 📂 games/
   │ group.yml
   └ vars.yml
 📁 hosts/
  • The group.yml file is optional. It allows you to set group-specific configuration and specify packages that should be installed when this group is included.
  • All variables inside the vars.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.
  • All other files that’re located in a group’s directory are considered configuration files that should be deployed to the system. In the example above, that would be the shell, git, and upower folders.

Groups are enabled per host by adding them to the groups list in the host.yml.

group.yml

The following is a full example of a group.yml:

# Override the target directory for all files in this group.
# If not set, the global target directory is used.
target_directory: /etc

# Packages that should be installed when this group is enabled.
packages:
  pacman:
    - git
    - vim
    - neovim

# Defaults that should be applied to all files in this group.
defaults:
  owner: root
  group: root
  file_mode: 0o644
  directory_mode: 0o755
  • target_directory: PathBuf (optional) - Override the target directory for all configuration files in this group.
    • If it’s a relative path, it’s treated as relative to the global target directory.
    • If it’s an absolute path, that absolute path is used.
  • packages: Map<String -> List<String>> (optional) - A list of packages sorted by package manager. Look at Package Management to see the list of available package managers.
  • defaults: (optional) Set default file permissions for all configuration files that’re inside this group directory.
    • owner: String - The file’s owner
    • group: String - The file’s assigned group
    • file_mode: OctalInt - The default permissions that’ll be set for all files.
    • directory_mode: OctalInt - The default permissions that’ll be set for all directories.

File Config

Individual files can be configured by adding a bois_config block inside the file itself. The configuration block is commented out using the file’s native comment syntax, so it doesn’t interfere with the actual configuration.

This allows you to:

  • Override the destination path for a specific file
  • Rename files when deploying them
  • Set custom ownership and permissions
  • Enable templating for dynamic configuration
  • Customize template delimiters to avoid conflicts

Example

Here’s a bash script with a bois_config block:

#!/bin/bash
# bois_config
# template: true
# owner: root
# group: root
# mode: 0o755
# path: /usr/local/bin/
# bois_config

echo "Hello from {{ host }}"

The configuration is extracted from between the two # bois_config delimiter lines, and the actual file content (without the config block) is deployed.

Supported Comment Syntaxes

The parser supports multiple comment prefixes: #, //, --, /*, */, **, *, %

This means you can use bois_config blocks in:

  • Shell scripts, Python, Ruby, YAML (#)
  • C, C++, JavaScript, Rust (// or /* */)
  • SQL, Lua, Haskell (--)
  • LaTeX (%)

Configuration Options

  • path: PathBuf (optional) - Override the destination path for this file.
    • If it’s a relative path, it’s treated as relative to the target directory.
    • If it’s an absolute path, that absolute path is used directly.
    • Takes precedence over any folder-level path overrides.
  • rename: String (optional) - Override the filename when deploying. Useful for deploying dotfiles without having dots in your bois directory.
    # bois_config
    # rename: .bashrc
    # bois_config
    
  • 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 delimiters with this string (e.g., # to make templates look like comments).
    • block: [String, String] (optional) - Delimiters for logic blocks. Defaults to ["{%", "%}"].
    • variable: [String, String] (optional) - Delimiters for variables. Defaults to ["{{", "}}"].
    • comment: [String, String] (optional) - Delimiters for comments. Defaults to ["{#", "#}"].

Full Example with Custom Delimiters

When working with files that already use {{ }} syntax (like systemd service files or some shell scripts), you can prefix delimiters to avoid conflicts:

[Unit]
Description=Backup Service
# bois_config
# template: true
# delimiters:
#   prefix: "#"
# bois_config

#{% if host == "production" %}
ExecStart=/usr/bin/backup --important-data
#{% else %}
ExecStart=/usr/bin/backup --test-mode
#{% endif %}

With the # prefix, template blocks become #{% and #{{, making them valid comments while still being processed by the template engine.

Folder Config

Any folder inside a host or group directory can have a bois.yml or bois.yaml file to configure how that folder and its contents should be deployed.

This is useful for:

  • Overriding the destination path for a whole directory tree
  • Setting ownership and permissions for all files in that directory

Example

Imagine you have a udev folder in your host directory that should be deployed to /etc/udev/rules.d:

 📂 hosts/
 └ 📂 artifact/
   └ 📂 udev/
     │ bois.yml
     │ 10-network.rules
     └ 20-usb.rules

The bois.yml might look like this:

# Deploy to an absolute path outside the default target directory
path: /etc/udev/rules.d

# Set ownership and permissions
owner: root
group: root
mode: 0o755

Now all files inside the udev folder will be deployed to /etc/udev/rules.d with the specified ownership and permissions.

Configuration Options

  • path: PathBuf (optional) - Override the destination path for this directory and all its contents.
    • If it’s a relative path, it’s treated as relative to the target directory.
    • If it’s an absolute path, that absolute path is used directly.
    • This override cascades to all child files and directories, unless they specify their own path.
  • owner: String (optional) - The directory owner. Defaults to the current user.
  • group: String (optional) - The directory’s assigned group. Defaults to the current user’s group.
  • mode: OctalInt (optional) - The permissions for this directory (e.g., 0o755). Defaults to 0o755.

Path Inheritance

When a folder has a path override, all files and subdirectories inside inherit that override:

 📂 systemd/
 │ bois.yml (path: /etc/systemd/system)
 ├ 📂 timers/
 │ └ backup.timer
 └ 📁 services/
   └ backup.service

Both timers/backup.timer and services/backup.service will be deployed under /etc/systemd/system/ unless they specify their own path override.

Templating

bois uses the minijinja templating engine.

It is based on the syntax and behavior of the Jinja2 template engine for Python.

Documentation

Here’re some links to get started with how to write templates with minijinja.

How to use templating in bois

Templating functionality is opt-in in bois. To enable templating for a file, you must enable the template option in its File configuration block.

# bois_config
# template: true
# bois_config

Once this configuration flag is found, bois will treat the whole file as a template. If there’s a vars.yml file in the current host’s directory, it’ll be read and injected into the templating environment.

For example, consider the following vars.yml file in a host’s directory.

some_secret: "lorem"
some_secret_list:
  - "ipsum"
  - "dolor"
some_secret_dict:
  lorem: sit

These variables can then be used like this:

SECRET={{ some_secret }}

{% for item in some_secret_list %}
# Useless comment: {{ item }}
{% endfor %}

{% if 'lorem' in some_secret_dict %}
OTHER_SECRET={{ some_secret_dict['lorem'] }}
{% endif %}

Which results in the following output:

SECRET=lorem

# Useless comment: ipsum
# Useless comment: dolor

OTHER_SECRET=sit

Pre-defined variables

bois pre-populates the templating environment with a few variables for your convenience:

  • host: String - The name of the current host.
  • boi_groups: List<String> - A list with all groups that’re enabled for the host.

The following example checks whether the encrypt group is enabled for the current host. If so, it adds the do_encryption=true flag to the configuration file.

{% if "encrypt" in boi_groups %}
do_encryption=true
{% endif %}

Pre-defined functions

On top of minijinja’s native filters and functions, bois exposes some functions itself. Most of those functions are integrations with password managers, enabling you to inject secrets into your configuration files.

Custom delimiters

It’s possible to set custom delimiters for templating. This is sometimes useful for files that already have a Jinja2-style templating syntax themselves or for other formats that heavily use curly braces like Latex.

To change the syntax from ["{{", "}}", "{%", "%}", "{#", "#}"], the delimiters option can be used:

delimiters:
  block: ["{%", "%}"]
  variable: ["{{", "}}"]
  comment: ["{#", "#}"]

For example, the following is really handy to not interfere with a configuration format that uses # as a comment.
(The syntax highlighting is a bit off, as we’re now using a slightly different syntax. Just imagine the # prefix would be highlighted as well.)

# bois_config
# template: true
# delimiters:
#   block: ["#{%", "%}"]
#   variable: ["#{{", "}}"]
#   comment: ["#{#", "#}"]
# bois_config

...

#{# this is how a template comment now looks like #}

...

important_option=#{{ some_variable }}

Password Managers

bois provides a list of integrations with password managers to allow injecting sensitive data into your configuration files via templating.

For this purpose, each supported password manager exposes one or more functions, which might differ slighty based on the supported functionality of the respective manager.

For example, the passwordstore (pass) password manager can be used like this:

# bois_config
# template: true
# bois_config

MY_SECRET_KEY={{ pass("secrets/root_key") }}

...

Take a look at the documentation for the individual managers for more detail on how to use them.

Passwordstore (pass)

The pass() templating function can be used to interact with pass. There’re however a few requirements for this to go smoothly:

  1. If your key has a passphrase, you should have a working 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.
    • To avoid to also having to copy and synchronize your passwordstore to root, you can set the following environment variable in your global bois.yml to use your normal user’s.
      # /etc/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"] (feel free to contribute more formats).
    If this is provided, the first line of the password file is ignored and the remaining content is interpreted as said data format. The content of that data format is simply returned from the function and can be further used.

Examples

Consider the following pass file at service/kagi.com:

my super secret pass

user: my@email.de

Simple Usage

A simple call that returns the first line of said file.

{{ pass("service/kagi.com") }}

Would return: my super secret pass

With Data Format

Interpret the passwordstore file as a dataformat and returns the data for further usage. Note: The first line is always ignored.

{{ pass("service/kagi.com")["user"] }}

Would return: my@email.de

System management

bois has been designed to be used for system configuration from the very start.

On top of configuration file management, it also supports:

  • Package management
    • Specify the exact set of packages that should be installed via various system package managers.
    • Automatically un-/install packages when changes in the bois configuration have taken place.
  • Service management
    • Enable/Disable services via configuration

Package Management

Bois contains support for several package managers.

This allows you to un-/install and manage packages based on groups or per host.

Pacman

Configuration

Packages can be added by adding a package.pacman section to either a group.yml or the host.yml. For example:

# Packages that should be installed when this group is enabled.
packages:
  pacman:
    - git

All pacman packages that’re defined in the host.yml and of all enabled group.yml files will then be installed for the given host.

Tips and tricks

bois diff

If you encounter packages that’re listed as explicitly installed, but want them be handled as a dependency so they no longer show up in the diff, there’s a simple command for that:

sudo pacman -D --asdep $package_name

This command marks that package as a dependency and it’ll no longer show up in the diff.

Paru

Setting up paru

Installing AUR packages with paru is a bit tricky, as root isn’t allowed to build packages.

The current way to work around this is to create a dedicated user, which will run paru for root. It needs to be able to call pacman though, so there’s a bit of setup that needs to be done.

At this point of this writing, bois still expects this user to be named aur.

  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 package.paru section to either a group.yml or the host.yml. For example:

# Packages that should be installed when this group is enabled.
packages:
  paru:
    - pueue-git

All pacman packages that’re defined in the host.yml and of all enabled group.yml files will then be installed for the given host.