← All posts
Linux

My setup.sh in CachyOS, btw

Oct 13, 2026·4 min read

I moved my main machine to CachyOS (Arch-based, btw) with Hyprland, and I didn't want the next fresh install to cost me a weekend. So the whole setup lives in my dotfiles as a few shell scripts: clone, run one command, and the machine ends up the way I like it.

One command

There's no single setup.sh file; the entry point is run_all.sh, which runs three scripts in order. Each one also works on its own, so I can re-run just the part I changed:

bash
git clone https://gitlab.com/bolanosdev/dotfiles.git ~/Projects/dotfiles
cd ~/Projects/dotfiles/bootstrap
./run_all.sh   # install_software.sh → os_changes.sh → dev_env_changes.sh
  • install_software.sh: pacman and AUR packages, Homebrew and its formulas, bun, fnm, sdkman with Java 25, hyprmod, herdr and Claude Code, and it wires their paths into my fish config. No prompts.
  • os_changes.sh: creates ~/Projects, sets my git identity, makes Neovim the default editor, generates an SSH key if there isn't one, uploads it to GitHub and GitLab, installs my fish functions, and sets up the docker group and service.
  • dev_env_changes.sh: clones the dotfiles and symlinks my Neovim config, my Hyprland overrides and my wallpapers into place.

Everything in one list

What a fresh machine gets is a set of arrays in common.sh, split by where it comes from: official repos, the AUR (through yay) and Homebrew for the few tools I prefer from there:

bash
# common.sh — one place to edit what a fresh machine gets
PACMAN_PKGS=(
  docker docker-compose go rust neovim fzf ffmpeg zig dotnet-sdk
  github-cli glab lazygit
  steam faugus-launcher lutris wine winetricks obs-studio
)
AUR_PKGS=(
  ghostty terraform-bin 1password 1password-cli
  kubectl k9s kubie minikube zennotes-bin
)
BREW_PKGS=(
  gnupg dopplerhq/cli/doppler go-task/tap/go-task
)

Adding a tool is one line. Removing one is the same idea: packages I don't want anymore go into a REMOVE_PKGS list, and a separate uninstall_software.sh prunes them when I decide to, so the main run never deletes anything by surprise.

Safe to run again

The rule for every function in these scripts is the same: check first, act only if something is missing. That makes the whole thing safe to run any number of times, on a brand-new install or on the machine I'm using right now:

bash
install_pacman_pkgs() {
  log "Installing pacman packages: ${PACMAN_PKGS[*]}"
  # --needed makes this a no-op for anything already installed.
  sudo pacman -Syu --needed --noconfirm "${PACMAN_PKGS[@]}"
}

install_brew() {
  if command -v brew &>/dev/null; then
    log "Homebrew already installed"
    return
  fi
  log "Installing Homebrew"
  NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
}

The same idea shows up everywhere:

  • My fish config gets marked blocks (# bootstrap: sdkman (java), …), and a block is only added if its marker isn't already there.
  • Config symlinks are skipped when they already point at the right place. If something else is in the way, it's moved to a timestamped .bak instead of being deleted.
  • The SSH key is only uploaded if GitHub or GitLab doesn't already have it. The first run opens the browser to log in; later runs reuse the stored credentials:
bash
upload_ssh_key_gitlab() {
  if ! glab auth status &>/dev/null; then
    glab auth login --hostname gitlab.com --git-protocol ssh --web
  fi

  local local_key
  local_key=$(awk '{print $2}' "${SSH_KEY}.pub")

  if glab ssh-key list 2>/dev/null | grep -qF "$local_key"; then
    log "SSH key already registered on GitLab — skipping upload"
  else
    glab ssh-key add "${SSH_KEY}.pub" --title "$(hostname)-$(date +%Y%m%d)"
  fi
}

Small things that bit me

  • Homebrew on Linux and xattr. Some formulae (like the go-task tap) call /usr/bin/xattr, which only exists on macOS. The script drops a tiny no-op shim there so they install cleanly.
  • Hyprland config I don't own. I use caelestia, which manages ~/.config/hypr itself. Instead of editing it, I keep two override files (keybinds and variables, plus which app each special workspace launches) in my dotfiles and symlink only those.
  • Gaming without dual boot. Steam, Faugus Launcher, Lutris and Wine are in the package list, so a fresh install can play my Windows games from the first boot.

Why bother

Reinstalling used to mean a day of remembering what I had installed and why. Now it's a clone, one command, and a coffee while it runs. And because it's safe to re-run, the scripts double as the documentation of my machine: if it isn't in common.sh, I don't need it.