CAIFS is a tool to handle installing software across various unix-like operating systems. If you work with multiple flavours of linux, build docker containers and work with macs, then this tool will help you consistently install software and matching configuration across all of them.
CAIFS takes inspiration from Stow and especially Tuckr, in that it is a dotfile manager, with the ability to run scripts. Unlike Tuckr though, CAIFS takes it a step further and allows you to define different installs for different operating systems and even architectures. This is done via hook scripts and defining custom functions per os-flavour
For example, the following hook script (a pre.sh in this case), demonstrates how a single hook script can be defined
to work across operating systems, in this case installing curl in three different ways.
fedora() {
rootdo dnf install -y curl
}
macos() {
brew install curl
}
debian() {
rootdo apt-get install -y curl
}Running the hooks on your Fedora, MacOS or Debian host in this case would be performed by
caifs add curl --hooks
Running the equivalent in a docker file, after a bootstrap gives you consistency
FROM debian:trixie-slim
RUN curl -sL https://raw.githubusercontent.com/caifs-org/caifs/refs/heads/main/install.sh | sh && \
caifs add docker-cli --hooks
# Your other docker image build
...
Alternatively, use the github image to simplify your docker builds
FROM debian:trixie-slim
COPY --from=ghcr.io/caifs-org/caifs:latest /caifs/ /usr/local/
RUN caifs --versionSimplified dependency management in a GitHub Pipeline
...
steps:
- name: Add the dependencies to the runner
run: |
curl -sL https://raw.githubusercontent.com/caifs-org/caifs/refs/heads/main/install.sh | sh
caifs add uv ruff pre-commit rumdl docker-cli trivy just
- name: Run pre-commit checks
run: |
pre-commit run --all- 100% pure POSIX compliant shell. So it should run just about everywhere
- less than 60kb in size, so it won't take up precious space in your Docker builds
- It has zero dependencies, besides coreutils functions such as find,
sed,grep,dirname,realpath,pathchk...
Note
This CAIFS repo itself is a valid caifs collection, containing a single target, caifs! See a curated library of scores of more installers at https://github.com/caifs-org/caifs-common/
YOLO it onto your system to install locally within ~/.local/
curl -sL https://raw.githubusercontent.com/caifs-org/caifs/refs/heads/main/install.sh | sh
OR
Install globally by using env var INSTALL_PREFIX=/usr/local/ and root privileges
INSTALL_PREFIX=/usr/local/ curl -sOL https://raw.githubusercontent.com/caifs-org/caifs/refs/heads/main/install.sh | sudo sh -c
Check it's working and on your path with -
caifs --version or caifs --help
OR
Clone the repository and install CAIFS, using CAIFS
git clone https://github.com/caifs-org/caifs/caifs.git
./caifs/config/bin/caifs add caifs -d . --link-root "$HOME/.local"https://github.com/caifs-org/caifs-common is a collection of curated installs of commonly used developer software that can be enabled via the caifs library.
If you have installed caifs via the install.sh script, then you already have caifs-common installed
If are installing caifs via the git clone method, then you can simply run the caifs-common target
caifs add caifs -d .
caifs add caifs-common -d .This will grab the latest caifs-common release and place it into ~/.local/share/caifs-collections/caifs-common CAIFS
automatically looks for this library so there is no need to add it to the $CAIFS_COLLECTIONS environment variable or
specify it directly with the caifs add --directory <switch> switch.
![TIP] Running
caifs add caifs-commonperiodically will grab the latest version and keep it up to date
A CAIFS collection is a directory containing targets. Each target has a config/ directory for files to symlink and an
optional hooks/ directory for install scripts.
my-dotfiles/
├── git/
│ ├── config/
│ │ ├── .gitconfig
│ │ └── .gitconfig.d/
│ │ └── aliases.config
│ └── hooks/
│ └── pre.sh
├── bash/
│ └── config/
│ ├── .bashrc
│ └── .bashrc.d/
│ └── aliases.bash
└── nvim/
└── config/
└── .config/
└── nvim/
└── init.lua
Config files mirror their destination path relative to $HOME (or $CAIFS_LINK_ROOT):
git/config/.gitconfig→~/.gitconfignvim/config/.config/nvim/init.lua→~/.config/nvim/init.lua
Three types of hooks exist, pre.sh, post.sh and rm.sh. Following on from the above example, if you wanted to do a
pre.sh hook that installed git, before the configuration was symlinked across, then this would like like:
git/hooks/pre.sh
Hook scripts define functions named after OS identifiers. CAIFS detects the OS and calls the matching function:
# git/hooks/pre.sh
fedora() {
rootdo dnf install -y git-core
}
ubuntu() {
rootdo apt-get install -y git
}
arch() {
rootdo pacman -S --noconfirm git
}
macos() {
brew install git
}
linux() {
# Runs on any Linux after the distro-specific function
echo "Git installed on Linux"
}
generic() {
# Runs on all platforms last
echo "Git setup complete"
}Available function names:
- Distro-specific:
fedora,ubuntu,arch,debian, etc. (from/etc/os-releaseID) linux- any Linux systemmacos- macOS/Darwingeneric- all platformscontainer- runs when inside a container (Docker, Podman, LXC, etc.)portable- runs when on a portable device (laptop, notebook, etc.)
A hook script occurs within it's own sub-shell, meaning that any variables declared and any temporary generated files are deleted once the script exits. This is useful, as there is no manual cleanup required within a hook script.
If you want to install files manually, to say the default $HOME/.local/ folder, which is the default for CAIFS, then
another temporary folder is provided via an environment variable, ${CAIFS_INSTALL_DIR}. This folder has the following
structure of ${CAIFS_INSTALL_DIR}/{bin,lib,share}, which when you run the utility function caifs_install at the end
of a hook function, will merge the contents of ${CAIFS_INSTALL_DIR} into the $CAIFS_LINK_ROOT or equivalent CLI
flag --link-root <value>.
This also works for root installs, that wish to target directories like /usr/local/ or /usr/
It's often common in enterprise setups to require a custom certificate to be installed to maintain the certificate trust chain. For these scenarios, any given target should create a certificate file within the following structure:
<target>/config/.local/share/certificates/my_cert.crt
Of course, no OS updates their trust chain in the same way, so CAIFS provides a series of OS identifier wrapper functions to manage the various OS specific tasks to get that cert into the system wide cert trust.
From a post.sh hook script (because we need it to run after the linking), call the install_certs() function, from
either of the handlers or as a fail safe, within the more generic linux() handler, like so:
# enterprise-certs/hooks/post.sh
linux() {
install_certs
}
# bootstrap your system the caifs-common library, which contains everything below
caifs add caifs-common -d .
# does symlinking and pre/post hooks for target uv
caifs add uv
# does only symlinking for target uv
caifs add uv --links
# does only hooks for target uv
caifs add uv --hooks
# run multiple hooks for targets uv, ruff and poetry in that order
caifs add uv ruff poetry --hooks
# force an override of bash config files if the links exist already
caifs add bash --links --force
# run over multiple collections, with a first-link wins scenario
caifs add git -d ~/my-personal-collection -d ~/my-work-collection
# same as above, but using the environment variable to replace the -d|--directory option
CAIFS_COLLECTIONS="~/my-personal-collection:~/my-work-collection" \
caifs add git
# remove symlinks for a target
caifs rm git -d ~/my-dotfiles --links
# run remove hook script
caifs rm git -d ~/my-dotfiles --hooks
# run a target from a specific collection within your installed collections in ~/.local/share/caifs-collections/
caifs add git@caifs-common fzf curl custom@my-dotfiles| Variable | Default | Description |
|---|---|---|
CAIFS_COLLECTIONS |
$PWD |
Colon-separated list of collection paths to search for targets |
CAIFS_LINK_ROOT |
$HOME |
Destination root for symlinks (e.g., set to / for system-wide configs) |
CAIFS_VERBOSE |
1 |
Set to 0 to enable debug output |
CAIFS_RUN_FORCE |
1 |
Set to 0 to force overwrite existing files/links |
CAIFS_RUN_LINKS |
0 |
Set to 1 to skip symlinking (equivalent to --hooks) |
CAIFS_RUN_HOOKS |
0 |
Set to 1 to skip hooks (equivalent to --links) |
CAIFS_DRY_RUN |
1 |
Set to 0 to show what would run without making changes |
CAIFS_IN_CONTAINER |
unset | Set to 0 to set container config to run + triggers container() hooks). |
Set to 1 to specify not in container, regardless of if in a container or not |
||
CAIFS_IN_WSL |
unset | Set to 0 to set WSL config to run. Set to 1 to force to run |
CAIFS_LOCAL_COLLECTIONS |
~/.local/share/caifs-collections | A central store for collections that is automatically checked. |
CAIFS_USER |
$USER |
Override the user that the links will be owned by |
Configuring sudo is generally recommended for ease of use, especially when working with docker containers. The default
in WSL2 is something akin to echo '%sudo ALL=(ALL) NOPASSWD:ALL' >> /etc/sudoers
This is fine for a dedicated local dev machine, but if you working with containers then you might want to restrict sudo
or better, yet make use of the CAIFS_LINK_ROOT and CAIFS_USER variables to install within another users home
directory, or, provide the correct permissions.
Depending on your distro of choice, CAIFS requires the following for sudo access
| command | distro | justification |
|---|---|---|
| cp | all | Moving files into root locations |
| ln | all | creating links to root locations |
| mkdir | all | creating parent directories for symlinks |
| update-ca-certificates | debian/ubuntu | adding certificates to the trust store |
| update-ca-trust | fedora/rhel/arch | adding certificates to the trust store |
| apt-get/apt | debian/ubuntu | install packages |
| pacman/yay | arch/steamos | install packages |
| dnf/rpm | rhel/fedora | install packages |
A simple entry for the set of these on debian might look something like:
echo '%sudo ALL=(ALL) NOPASSWD:/usr/bin/apt, \
/usr/bin/apt-get, \
/usr/bin/cp, \
/usr/bin/ln, \
/usr/bin/update-ca-certificates' >> /etc/sudoersOr using a sudoers.d file
# /etc/sudoers.d/caifs
%sudo ALL=(ALL) NOPASSWD:/usr/bin/apt, \
/usr/bin/apt-get, \
/usr/bin/mkdir, \
/usr/bin/cp, \
/usr/bin/ln, \
/usr/bin/update-ca-certificatesEnabling multiple collections allows you to separate out your personal (and preferred) configuration into one collection
,then for instance, a work-specific collection defined, followed by the standard caifs-common library.
When you runs CAIFS, it will search all the collections, with the order you specify the collections in being the order of operations.
There are a few options to support this.
Using the -d|--directory arguments will override any $CAIFS_COLLECTIONS variable set, allowing you to work with a
collection in isolation.
The environment variable, $CAIFS_COLLECTIONS, can be set with multiple :-delimited directory paths. Much like the
standard $PATH variable. Setting this variable is the equivalent of supplying multiple -d|--directory
arguments to the caifs add|rm command itself.
When caifs is run with no -d|--directory arguments and the $CAIFS_COLLECTIONS variable is empty, then CAIFS will
internally look to an XDG area of ~/.local/share/caifs-collections/ for collections to process.
CAIFS will look only 1 level deep in that directory and then attempt to validate that they are in-fact, caifs
compatible directories. It adds each collection it finds to the back of the queue (internally the queue is just the
$CAIFS_COLLECTIONS variable), that is to say, the order of the collections in ~/.local/share/caifs-collections/ is
important and is dictated by the find defaults.
The one exception to this, is the caifs-common library. If present, then this collection will always be at the back
of the line. Allowing people to override configuration if they wish.
By default, CAIFS configuration will be linked to the current $HOME variable. This is desirable for most use cases
where you want to manage personal dotfiles.
If you need to manage files beyond the $HOME area, perhaps you have some custom networking that is required to be
added underneath / - then CAIFS has two options.
A config file under <target>/config/ with a leading ^ will be interpreted as being a / or root level file. For
example, my_sudo/config/^etc/sudoers.d/01-mysudo.conf will be attempted to be linked to
/etc/sudoers.d/01-mysudo.conf
Attempted, because CAIFS will attempt to escalate privileges
- CAIFS is currently running as root, i.e. uid=0, run
<the command>as is. - sudo is available and run
sudo <the command> - fallback to
su -c <the command>to issue the command
Note
Some of these options may prompt for passwords, depending on your setup
It may be useful in certain situations, particularly in docker builds which generally run as root, to set an alternative
to the default $HOME destination for links.
You can specify this with the -r|--link-root flags for the add|rm commands or use the $CAIFS_LINK_ROOT environment
variable
In typical docker builds, or perhaps escalated automation scenarios where you are running as root, but want the configuration to be placed into another users home directory.
FROM debian:trixie-slim
# Add an app user with a home directory at /app
RUN useradd \
--create-home \
--home-dir /app \
--uid 1000 \
--shell /bin/sh \
appuser
# Copy over a collection, or perhaps curl one on from github
COPY my-docker-collection /usr/local/share/my-docker-collection
# install some software and add the config from a custom collection, but
# create the links at the link-root of /app/
RUN curl -sL https://github.com/caifs-org/caifs/install.sh | sh && \
caifs add uv git pre-commit ruff \
--link-root /app \
--user appuser:appuser \
-d /usr/local/share/my-docker-collectionBesides the standard <target>/config directory, CAIFS caters for environment-specific config. To enable a specific set
of configuration that should only be linked in a particular environment, provide an alternative directory:
<target>/config_wsl- for WSL environments<target>/config_container- for container environments (Docker, Podman, LXC, etc.)<target>/config_portable- for portable devices (laptops, notebooks, convertibles, etc.)
Note
The order of precedence for multiple config directories is config_portable/, config_container/, config_wsl/, config/.
This effectively allows you to prevent environment-specific configuration from being clobbered by similarly named configuration
within the main config/ directory.
| Option | Env Variable | Description |
|---|---|---|
--verbose, -v |
CAIFS_VERBOSE=0 |
Show debug logs |
--force, -f |
CAIFS_RUN_FORCE=0 |
Remove existing links/files on conflict |
--links, -l |
CAIFS_RUN_LINKS=0 |
Run only links, disable hooks |
--hooks, -h |
CAIFS_RUN_HOOKS=0 |
Run only hooks, disable links |
--dry-run, -n |
CAIFS_DRY_RUN=0 |
Show what would run without making changes |
--collection, -c |
- | Constrain the targets to a single collection |
--user, -u |
CAIFS_USER |
Apply the user permissions to links and instlaled files |