Files
dotfiles/setup.sh
domverse 893ec8e3cb feat: shell + dotfiles setup for a fresh box
Two layers with a deliberate privilege split: setup.sh does the root work
(apt, rustup, cargo tools, starship, chsh) and hands off to chezmoi, which
owns everything under $HOME.

Tools are built with `cargo install --locked` to match the versions currently
running rather than whatever apt ships, at the cost of a 10-20 min cold setup.

Excluded on purpose: the atuin sync key and ~/.git-credentials (secrets),
~/.claude/settings.json (per-box decision), fnm/opencode conf.d (out of scope),
and fish_variables (machine-local).

chezmoi tracks only the executable and private attributes and derives modes
from the umask, so setup.sh pins umask 022 and atuin uses private_ to keep
0700/0600. Without both, a 002 umask silently relaxes them to 0775/0664.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-17 10:53:46 +02:00

190 lines
9.0 KiB
Bash

#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# dotfiles/setup.sh — provision a fresh box, then hand off to chezmoi
#
# Root work (apt, starship, chsh) happens here. Everything under $HOME is
# owned by chezmoi. Run: sudo bash setup.sh
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'; BLUE='\033[0;34m'; NC='\033[0m'
PASS=0; WARN=0; FAIL=0
log_info() { echo -e "${BLUE}[INFO]${NC} $*"; }
log_success() { echo -e "${GREEN}[ OK ]${NC} $*"; PASS=$((PASS+1)); }
log_warning() { echo -e "${YELLOW}[WARN]${NC} $*"; WARN=$((WARN+1)); }
log_error() { echo -e "${RED}[FAIL]${NC} $*"; FAIL=$((FAIL+1)); }
log_step() { echo -e "\n${BLUE}${NC} ${1}"; }
DOTFILES_REPO="${DOTFILES_REPO:-https://git.domverse-berlin.eu/domverse/dotfiles.git}"
CARGO_TOOLS=(atuin eza macchina oxker)
APT_PKGS=(fish bat ripgrep jq git curl nano build-essential pkg-config libssl-dev)
echo -e "${BLUE}"
cat <<'EOF'
╔══════════════════════════════════════════════════════════════╗
║ Shell & dotfiles bootstrap ║
║ fish · starship · atuin · eza · chezmoi · claude ║
╚══════════════════════════════════════════════════════════════╝
EOF
echo -e "${NC}"
# ── Preflight ────────────────────────────────────────────────────────────────
log_step "Preflight"
if [ "$(id -u)" -ne 0 ]; then
log_error "Must run as root: sudo bash setup.sh"
exit 1
fi
TARGET_USER="${SUDO_USER:-}"
if [ -z "$TARGET_USER" ] || [ "$TARGET_USER" = "root" ]; then
log_error "Could not determine the non-root target user (SUDO_USER unset)."
log_error "Run via 'sudo bash setup.sh' as your normal user, not as root directly."
exit 1
fi
TARGET_HOME=$(getent passwd "$TARGET_USER" | cut -d: -f6)
log_success "Target user: $TARGET_USER (home: $TARGET_HOME)"
# Run a command as the target user with a sane login-ish env.
as_user() { sudo -u "$TARGET_USER" -H bash -lc "$*"; }
# ── APT packages ─────────────────────────────────────────────────────────────
log_step "APT packages"
MISSING=()
for p in "${APT_PKGS[@]}"; do
dpkg -s "$p" >/dev/null 2>&1 || MISSING+=("$p")
done
if [ ${#MISSING[@]} -eq 0 ]; then
log_success "All apt packages already present (${APT_PKGS[*]})"
else
log_info "Installing: ${MISSING[*]}"
apt-get update -qq
DEBIAN_FRONTEND=noninteractive apt-get install -y -qq "${MISSING[@]}"
log_success "Installed ${#MISSING[@]} apt package(s)"
fi
# config.fish aliases `cat` to batcat; on Ubuntu the binary really is batcat.
if command -v batcat >/dev/null 2>&1; then
log_success "batcat present (config.fish aliases cat -> batcat)"
else
log_warning "batcat missing — the 'cat' alias in config.fish will break"
fi
# ── rustup + cargo tools ─────────────────────────────────────────────────────
log_step "Rust toolchain (build dependency for ${CARGO_TOOLS[*]})"
if as_user 'command -v cargo' >/dev/null 2>&1; then
log_success "rustup/cargo already installed"
else
log_info "Installing rustup (non-interactive, default profile)..."
as_user "curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --no-modify-path"
log_success "rustup installed"
fi
# `cargo install` is idempotent-ish: it rebuilds if the version differs.
# --locked keeps builds reproducible. This is the slow part (~10-20 min cold).
for tool in "${CARGO_TOOLS[@]}"; do
if as_user "test -x \$HOME/.cargo/bin/$tool"; then
log_success "$tool already built"
else
log_info "Building $tool from source (this takes a while)..."
if as_user "\$HOME/.cargo/bin/cargo install --locked $tool"; then
log_success "$tool built"
else
log_error "$tool failed to build"
fi
fi
done
# ── starship ─────────────────────────────────────────────────────────────────
log_step "Starship prompt"
if command -v starship >/dev/null 2>&1; then
log_success "starship already installed ($(starship --version | head -1))"
else
log_info "Installing starship to /usr/local/bin..."
curl -sS https://starship.rs/install.sh | sh -s -- --yes >/dev/null
log_success "starship installed"
fi
# ── chezmoi + dotfiles ───────────────────────────────────────────────────────
log_step "chezmoi"
if as_user 'command -v chezmoi' >/dev/null 2>&1; then
log_success "chezmoi already installed"
else
log_info "Installing chezmoi to ~/.local/bin..."
as_user "sh -c \"\$(curl -fsLS get.chezmoi.io)\" -- -b \$HOME/.local/bin"
log_success "chezmoi installed"
fi
CHEZMOI="\$HOME/.local/bin/chezmoi"
# chezmoi does NOT preserve exact file modes — it tracks only the executable and
# private attributes, then derives the mode from the umask. A umask of 002 (the
# default here) would write world/group-writable 0664 dotfiles. Pin 022 so files
# land as 0644, and let the private_ attribute handle the 0600 cases.
log_step "chezmoi config"
CHEZMOI_CFG="$TARGET_HOME/.config/chezmoi/chezmoi.toml"
if [ -f "$CHEZMOI_CFG" ] && grep -q 'umask' "$CHEZMOI_CFG"; then
log_success "chezmoi umask already configured"
else
as_user "mkdir -p \$HOME/.config/chezmoi"
as_user "printf 'umask = 0o022\n' >> \$HOME/.config/chezmoi/chezmoi.toml"
log_success "chezmoi umask pinned to 022"
fi
log_step "Applying dotfiles"
if as_user "test -d \$HOME/.local/share/chezmoi/.git"; then
log_info "chezmoi source already initialised — applying current state"
as_user "$CHEZMOI apply" && log_success "dotfiles applied" || log_error "chezmoi apply failed"
else
log_info "Initialising chezmoi from $DOTFILES_REPO"
if as_user "$CHEZMOI init --apply '$DOTFILES_REPO'"; then
log_success "dotfiles initialised and applied"
else
log_error "chezmoi init failed — check repo access (private repo needs credentials)"
fi
fi
# ── Default shell ────────────────────────────────────────────────────────────
log_step "Default shell"
FISH_BIN=$(command -v fish || true)
if [ -z "$FISH_BIN" ]; then
log_error "fish not found — cannot set default shell"
else
grep -qxF "$FISH_BIN" /etc/shells || echo "$FISH_BIN" >> /etc/shells
CURRENT_SHELL=$(getent passwd "$TARGET_USER" | cut -d: -f7)
if [ "$CURRENT_SHELL" = "$FISH_BIN" ]; then
log_success "fish is already the default shell for $TARGET_USER"
else
chsh -s "$FISH_BIN" "$TARGET_USER"
log_success "default shell changed: $CURRENT_SHELL -> $FISH_BIN"
fi
fi
# ── Summary ──────────────────────────────────────────────────────────────────
echo
echo "────────────────────────────────────────────────────────────────"
echo -e " ${GREEN}pass: $PASS${NC} ${YELLOW}warn: $WARN${NC} ${RED}fail: $FAIL${NC}"
echo "────────────────────────────────────────────────────────────────"
echo
log_info "Manual steps that cannot be automated:"
echo " 1. atuin sync — key is NOT in this repo (tracked in secrets.yml):"
echo " atuin login -u <user>"
echo " atuin sync"
echo " Restore the key from secrets.yml into ~/.local/share/atuin/key"
echo " BEFORE first sync, or history encrypted on other hosts won't decrypt."
echo " 2. git identity is not set globally (only credential.helper=store):"
echo " git config --global user.name '<name>'"
echo " git config --global user.email '<email>'"
echo " 3. Log out and back in for the fish shell change to take effect."
echo
[ "$FAIL" -gt 0 ] && exit 1
exit 0