Needed for NFS mounts. Included in the best-effort CLI tools list so a missing/conflicting package never aborts the bootstrap. README + header updated.
292 lines
11 KiB
Bash
292 lines
11 KiB
Bash
#!/bin/sh
|
|
# install.sh - bootstrap dotfiles on a fresh Debian or Alpine host.
|
|
#
|
|
# What it does:
|
|
# 1. Detects the OS and package manager (apt on Debian, apk on Alpine).
|
|
# 2. Installs base tooling (git, curl, wget, zsh, vim) + CLI tools
|
|
# (htop, fzf, nslookup, nfs client, kubectl when available) for both distros.
|
|
# 3. Installs oh-my-zsh (unattended) and the zsh-autosuggestions plugin.
|
|
# 4. Deploys the generic server/ dotfiles.
|
|
# 5. Installs Vim plugins (clones each plugin declared in .vimrc).
|
|
#
|
|
# Usage:
|
|
# ./install.sh # full bootstrap
|
|
# ./install.sh --no-zsh # skip oh-my-zsh / shell setup
|
|
# ./install.sh --chsh # change the current user's login shell to zsh
|
|
# ./install.sh --dry-run # show what would happen, change nothing
|
|
#
|
|
# Run this from inside the cloned dotfiles repo (the dir that contains servers/).
|
|
# Safe to re-run: every step is idempotent.
|
|
|
|
set -eu
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Helpers
|
|
# ----------------------------------------------------------------------------
|
|
DRY_RUN=0
|
|
INSTALL_ZSH=1
|
|
CHSH=0
|
|
|
|
info() { printf '\033[0;34m==>\033[0m %s\n' "$1"; }
|
|
ok() { printf '\033[0;32m✔\033[0m %s\n' "$1"; }
|
|
warn() { printf '\033[0;33m!\033[0m %s\n' "$1"; }
|
|
die() { printf '\033[0;31m✘\033[0m %s\n' "$1" >&2; exit 1; }
|
|
|
|
run() {
|
|
# Print the command, then run it (unless --dry-run).
|
|
# We capture the exit status explicitly so that a failing command does NOT
|
|
# trip the caller's 'set -e' inside 'if run ... ; then' constructs.
|
|
if [ "$DRY_RUN" -eq 1 ]; then
|
|
printf ' $ %s\n' "$*"
|
|
return 0
|
|
fi
|
|
"$@"
|
|
return $?
|
|
}
|
|
|
|
usage() {
|
|
sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'
|
|
exit 0
|
|
}
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Argument parsing
|
|
# ----------------------------------------------------------------------------
|
|
while [ $# -gt 0 ]; do
|
|
case "$1" in
|
|
--no-zsh) INSTALL_ZSH=0; shift ;;
|
|
--chsh) CHSH=1; shift ;;
|
|
--dry-run) DRY_RUN=1; shift ;;
|
|
-h|--help) usage ;;
|
|
*) die "Unknown argument: $1 (try --help)" ;;
|
|
esac
|
|
done
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Locate the repo root (dir that holds this script and servers/)
|
|
# ----------------------------------------------------------------------------
|
|
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
|
if [ ! -d "$SCRIPT_DIR/servers" ]; then
|
|
die "servers/ not found next to this script.
|
|
Clone the dotfiles repo first, then run install.sh from its root:
|
|
git clone git@git.ivanch.me:ivanch/dotfiles.git
|
|
cd dotfiles && ./install.sh"
|
|
fi
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# OS / package-manager detection
|
|
# ----------------------------------------------------------------------------
|
|
detect_os() {
|
|
OS_RELEASE_FILE="${OS_RELEASE_FILE:-/etc/os-release}"
|
|
if [ -f "$OS_RELEASE_FILE" ]; then
|
|
. "$OS_RELEASE_FILE"
|
|
OS_ID="$ID"
|
|
else
|
|
OS_ID="$(uname -s)"
|
|
fi
|
|
|
|
case "$OS_ID" in
|
|
alpine) PKG="apk"; MGR_UPDATE="apk update"; MGR_ADD="apk add --no-cache" ;;
|
|
debian|ubuntu|raspbian)
|
|
PKG="apt"; MGR_UPDATE="apt-get update"; MGR_ADD="apt-get install -y" ;;
|
|
*) die "Unsupported OS: $OS_ID (only Debian/Ubuntu and Alpine are supported)" ;;
|
|
esac
|
|
info "Detected OS: $OS_ID (package manager: $PKG)"
|
|
}
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Package installation
|
|
# ----------------------------------------------------------------------------
|
|
install_packages() {
|
|
info "Installing base packages"
|
|
# Always-present on both Debian and Alpine main/community repos.
|
|
ESSENTIAL="git curl wget zsh vim"
|
|
# Cross-distro CLI tools. Package names differ between distros:
|
|
# nslookup -> dnsutils (Debian) / bind-tools (Alpine)
|
|
# fzf -> community repo on Alpine, main on Debian
|
|
# htop -> main on both
|
|
# nfs client-> nfs-common (Debian) / nfs-utils (Alpine) [for NFS mounts]
|
|
if [ "$PKG" = "apk" ]; then
|
|
CLI_TOOLS="htop fzf bind-tools nfs-utils"
|
|
else
|
|
CLI_TOOLS="htop fzf dnsutils nfs-common"
|
|
fi
|
|
|
|
if [ "$PKG" = "apk" ]; then
|
|
# On Alpine, vim/fzf/kubectl live in the 'community' repo, which is
|
|
# commented out by default in /etc/apk/repositories. Enable it (idempotent).
|
|
info "Ensuring Alpine community repository is enabled"
|
|
run sed -i -E "s|^#(.*/community)$|\1|" /etc/apk/repositories
|
|
run $MGR_UPDATE
|
|
else
|
|
run $MGR_UPDATE
|
|
fi
|
|
|
|
info "Installing essential packages"
|
|
if run $MGR_ADD $ESSENTIAL; then
|
|
ok "Essential packages installed"
|
|
else
|
|
die "Failed to install essential packages ($ESSENTIAL)."
|
|
fi
|
|
|
|
# CLI tools are best-effort: a missing one must not abort the whole
|
|
# bootstrap (e.g. some tools simply aren't in the default repos).
|
|
info "Installing CLI tools (best-effort): $CLI_TOOLS"
|
|
if run $MGR_ADD $CLI_TOOLS; then
|
|
ok "CLI tools installed"
|
|
else
|
|
warn "Some CLI tools failed to install (non-fatal)."
|
|
fi
|
|
|
|
# kubectl is NOT in the default Debian/Alpine repos (it needs the
|
|
# Kubernetes apt repo / a manual install). Install it only if the package
|
|
# is actually available, so the script stays generic and repo-agnostic.
|
|
if command -v kubectl >/dev/null 2>&1; then
|
|
ok "kubectl already present, skipping"
|
|
else
|
|
info "Attempting to install kubectl (if available in repos)"
|
|
if run $MGR_ADD kubectl 2>/dev/null; then
|
|
ok "kubectl installed"
|
|
else
|
|
warn "kubectl not available in default repos; skipping (install manually if needed)."
|
|
fi
|
|
fi
|
|
}
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# oh-my-zsh + zsh-autosuggestions
|
|
# ----------------------------------------------------------------------------
|
|
install_oh_my_zsh() {
|
|
info "Installing oh-my-zsh"
|
|
if [ -d "$HOME/.oh-my-zsh" ]; then
|
|
ok "oh-my-zsh already present, skipping"
|
|
else
|
|
# --unattended => RUNZSH=no CHSH=no (no prompt, no shell change)
|
|
# Special-cased (not via run()) so --dry-run never hits the network.
|
|
if [ "$DRY_RUN" -eq 1 ]; then
|
|
printf ' $ sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" --unattended\n'
|
|
else
|
|
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended
|
|
fi
|
|
ok "oh-my-zsh installed"
|
|
fi
|
|
|
|
ZSH_CUSTOM_DIR="${ZSH_CUSTOM:-$HOME/.oh-my-zsh/custom}"
|
|
SUG_DIR="$ZSH_CUSTOM_DIR/plugins/zsh-autosuggestions"
|
|
if [ -d "$SUG_DIR" ]; then
|
|
ok "zsh-autosuggestions already present, skipping"
|
|
else
|
|
info "Installing zsh-autosuggestions plugin"
|
|
run git clone --depth=1 https://github.com/zsh-users/zsh-autosuggestions "$SUG_DIR"
|
|
ok "zsh-autosuggestions installed"
|
|
fi
|
|
}
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Deploy the dotfiles for this host
|
|
# ----------------------------------------------------------------------------
|
|
deploy_dotfiles() {
|
|
SRC_DIR="$SCRIPT_DIR/servers"
|
|
if [ ! -d "$SRC_DIR" ]; then
|
|
warn "servers/ not found next to this script; skipping dotfile deploy."
|
|
return 0
|
|
fi
|
|
|
|
info "Deploying generic server dotfiles from servers/"
|
|
# Copy every file/dir from servers/ into $HOME, preserving structure.
|
|
# .zshrc/.vimrc/.bashrc land at $HOME; .config/* stays nested.
|
|
for src in "$SRC_DIR"/* "$SRC_DIR"/.[!.]* "$SRC_DIR"/..?*; do
|
|
[ -e "$src" ] || continue
|
|
run cp -Rf "$src" "$HOME/"
|
|
ok "$(basename "$src") -> $HOME/"
|
|
done
|
|
}
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Vim plugins via Vundle
|
|
# ----------------------------------------------------------------------------
|
|
install_vim_plugins() {
|
|
info "Installing Vim plugins"
|
|
if [ ! -f "$HOME/.vimrc" ]; then
|
|
warn "No .vimrc deployed; skipping Vim plugin install."
|
|
return 0
|
|
fi
|
|
|
|
VUNDLE_DIR="$HOME/.vim/bundle/Vundle.vim"
|
|
if [ ! -d "$VUNDLE_DIR" ]; then
|
|
info "Cloning Vundle"
|
|
run git clone --depth=1 https://github.com/VundleVim/Vundle.vim.git "$VUNDLE_DIR"
|
|
else
|
|
ok "Vundle already present, skipping"
|
|
fi
|
|
|
|
# Rather than drive Vundle's interactive :PluginInstall (which hangs when
|
|
# stdout is not a TTY), clone each plugin declared in .vimrc directly.
|
|
# This is fully unattended and idempotent.
|
|
info "Cloning Vim plugins declared in .vimrc"
|
|
# Extract 'Plugin/Repo' and 'Plugin user/repo' style declarations,
|
|
# normalise to an https URL, skip the Vundle self-reference.
|
|
plugins=$(grep -oE "Plugin '[^']+'" "$HOME/.vimrc" \
|
|
| sed -E "s/Plugin '//; s/'//" \
|
|
| grep -v "^VundleVim/Vundle.vim$")
|
|
for repo in $plugins; do
|
|
name=$(basename "$repo")
|
|
dir="$HOME/.vim/bundle/$name"
|
|
if [ -d "$dir" ]; then
|
|
ok "$name already present, skipping"
|
|
continue
|
|
fi
|
|
info "Cloning $repo"
|
|
if run git clone --depth=1 "https://github.com/$repo.git" "$dir"; then
|
|
ok "$name installed"
|
|
else
|
|
warn "Failed to clone $repo (non-fatal)."
|
|
fi
|
|
done
|
|
}
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Optional: change login shell to zsh
|
|
# ----------------------------------------------------------------------------
|
|
change_shell() {
|
|
if ! command -v zsh >/dev/null 2>&1; then
|
|
warn "zsh not found; cannot change shell."
|
|
return 0
|
|
fi
|
|
if ! command -v chsh >/dev/null 2>&1; then
|
|
warn "chsh not found on this system; set your login shell manually (e.g. in /etc/passwd)."
|
|
return 0
|
|
fi
|
|
ZSH_BIN="$(command -v zsh)"
|
|
cur="$(grep "^$(id -un):" /etc/passwd | cut -d: -f7)"
|
|
if [ "$cur" = "$ZSH_BIN" ]; then
|
|
ok "Login shell already $ZSH_BIN"
|
|
return 0
|
|
fi
|
|
info "Changing login shell to $ZSH_BIN"
|
|
run chsh -s "$ZSH_BIN"
|
|
ok "Login shell set to zsh (re-login to apply)"
|
|
}
|
|
|
|
# ----------------------------------------------------------------------------
|
|
# Main
|
|
# ----------------------------------------------------------------------------
|
|
info "Bootstrapping dotfiles from $SCRIPT_DIR"
|
|
[ "$DRY_RUN" -eq 1 ] && warn "DRY RUN - no changes will be made"
|
|
|
|
detect_os
|
|
install_packages
|
|
|
|
if [ "$INSTALL_ZSH" -eq 1 ]; then
|
|
install_oh_my_zsh
|
|
fi
|
|
|
|
deploy_dotfiles
|
|
install_vim_plugins
|
|
|
|
if [ "$CHSH" -eq 1 ]; then
|
|
change_shell
|
|
fi
|
|
|
|
info "Done. Restart your shell (or re-login) to pick up the new configuration."
|