Skip to content

Packages & languages

packages:
- name: zellij # the state identity; not conditional
from: cargo # one manager, or a fallback list
version: latest
needs: [build-essential]
only: linux
path: ["{{ home }}/.cargo/bin"]

from: may be a list, and the first manager that will exist wins:

from: [brew, apt, zypper]

If none of them will exist on this machine, that is a plan-time error naming the package and what it asked for — not a failure twenty minutes into an apply.

name: is deliberately not conditional. It is the state-file identity, and a name that varies by machine is an identity that varies by machine, at which point uninstall stops working.

latest means install if absent, never upgrade. That is what keeps plan deterministic, offline and fast, and it is what makes the plan you reviewed the plan that applies. A pinned version diffs against what is installed and maps to each manager’s own syntax (jq=1.7 for apt, jq@1.7 for brew, --version 1.7 for cargo).

Build prerequisites nothing can infer. Nothing in from: cargo says a package wants a C toolchain:

- name: build-essential
from: apt
only: linux
- name: zellij
from: cargo
needs: [build-essential]

A needs: naming a package that only: pruned simply drops the edge — the prerequisite genuinely does not exist here. A needs: naming a package that was never declared is an error.

For a thing no manager packages. Tailscale’s installer registers an apt repository and starts a daemon; neither is a from:.

packages:
- name: tailscale
only: linux
script: |
curl -fsSL https://tailscale.com/install.sh | sh

Use it instead of from:, never with it. Presence is the binary being on PATH, so the script runs once and not again.

Bedouin cannot uninstall what a script installed, so it does not claim to own it: dropping the entry forgets it rather than removing it. Everything else — only:, needs:, rc:, aliases: — works as usual.

languages:
- name: rust
version: "1.80"
installer: rustup # optional -- see below

installer: is optional. Left out, a language installs with its own tool: rust uses rustup, which is how Rust is meant to arrive and what rustup component add and toolchain pinning expect. Anything else uses mise, which fetches upstream builds too — the source, just not a first-party script.

A package with from: cargo and no rust language gets one added implicitly, with a warning. The implication is stated, never silent.

Toolchains record their own bin directories — rustup records ~/.cargo/bin, mise its shims, Go ~/go/bin — which is what lets the cargo step in the same run find cargo. Those directories also go into the generated PATH file, so what a toolchain installs is on your PATH too, not just the run’s. You never configure that.

package_managers:
macos: [brew]
default: [apt, mise]

You rarely need this block. Declaring installer: rustup or from: cargo already declares that you need that manager, and it gets bootstrapped whether or not it is listed here — listing it was once mandatory, and forgetting meant a fresh machine ran rustup toolchain install against a rustup nothing had installed. Use package_managers: to name a manager nothing references yet, or to pin which one a bare from: should resolve to.

Bedouin bootstraps brew, mise, rustup and cargo if they are missing. On Linux, brew’s bootstrap installs its own prerequisites first (build-essential procps curl file git, plus unzip for casks): Homebrew needs git to clone itself, and the package phase that would provide it runs later than the manager phase. It does not install apt, zypper or dnf — those are the distro’s, and one that is merely referenced is never added implicitly for the same reason: a package asking for a manager this machine lacks already errors by name. A declared manager that cannot exist on this OS is dropped from the plan rather than attempted.

The order is the dependency order:

managers → languages → the shell's own package → packages
→ files → rc blocks → PATH

The shell is pulled ahead of the general package stage so the run that installs zsh can write into ~/.zshrc.d. Within a stage, declaration order — the order you can see.

Each step is spawned with an environment Bedouin builds: PATH from the bin directories recorded in state, plus a minimal system base. Never your shell’s.