Add idempotent install.sh (Debian + Alpine)
Bootstraps a fresh host: detects apt vs apk, installs base + extra packages (htop/neofetch/fzf), installs oh-my-zsh + zsh-autosuggestions, deploys the matching server/<host> dotfiles by hostname, and runs Vundle :PluginInstall. Flags: --host --no-zsh --chsh --dry-run.
This commit is contained in:
240
install.sh
Normal file
240
install.sh
Normal file
@@ -0,0 +1,240 @@
|
||||
#!/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, zsh, vim) + extras (htop, neofetch, fzf).
|
||||
# 3. Installs oh-my-zsh (unattended) and the zsh-autosuggestions plugin.
|
||||
# 4. Deploys the server/<host> configs matching this machine's hostname.
|
||||
# 5. Installs Vim plugins via Vundle (PluginInstall).
|
||||
#
|
||||
# Usage:
|
||||
# ./install.sh # auto-detect host, deploy matching configs
|
||||
# ./install.sh --host vega # force a specific host config
|
||||
# ./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 server/).
|
||||
# Safe to re-run: every step is idempotent.
|
||||
|
||||
set -eu
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ----------------------------------------------------------------------------
|
||||
DRY_RUN=0
|
||||
INSTALL_ZSH=1
|
||||
CHSH=0
|
||||
FORCE_HOST=""
|
||||
|
||||
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).
|
||||
if [ "$DRY_RUN" -eq 1 ]; then
|
||||
printf ' $ %s\n' "$*"
|
||||
else
|
||||
"$@"
|
||||
fi
|
||||
}
|
||||
|
||||
usage() {
|
||||
sed -n '2,20p' "$0" | sed 's/^# \{0,1\}//'
|
||||
exit 0
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# Argument parsing
|
||||
# ----------------------------------------------------------------------------
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
--host) FORCE_HOST="$2"; shift 2 ;;
|
||||
--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 server/)
|
||||
# ----------------------------------------------------------------------------
|
||||
SCRIPT_DIR=$(cd "$(dirname "$0")" && pwd)
|
||||
if [ ! -d "$SCRIPT_DIR/server" ]; then
|
||||
die "server/ 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"
|
||||
ESSENTIAL="git curl zsh vim"
|
||||
EXTRA="htop neofetch fzf"
|
||||
|
||||
if [ "$PKG" = "apk" ]; then
|
||||
run $MGR_UPDATE
|
||||
run $MGR_ADD $ESSENTIAL $EXTRA
|
||||
else
|
||||
run $MGR_UPDATE
|
||||
run $MGR_ADD $ESSENTIAL
|
||||
# Newer Debian dropped neofetch from the repos; don't fail the whole run on it.
|
||||
info "Installing extra packages (best-effort)"
|
||||
run $MGR_ADD $EXTRA || warn "Some extra packages failed to install (non-fatal)."
|
||||
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() {
|
||||
HOST_NAME="${FORCE_HOST:-$(hostname 2>/dev/null || uname -n)}"
|
||||
HOST_DIR="$SCRIPT_DIR/server/$HOST_NAME"
|
||||
|
||||
if [ ! -d "$HOST_DIR" ]; then
|
||||
warn "No server/$HOST_NAME config found."
|
||||
if [ -z "$FORCE_HOST" ]; then
|
||||
avail=$(cd "$SCRIPT_DIR/server" && ls -1)
|
||||
warn "Available hosts: $(echo "$avail" | tr '\n' ' ')"
|
||||
warn "Skipping dotfile deploy. Re-run with --host <name> to force one."
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
|
||||
info "Deploying dotfiles for host: $HOST_NAME"
|
||||
# Copy every file/dir from server/<host>/ into $HOME, preserving structure.
|
||||
# .zshrc/.vimrc/.bashrc land at $HOME; .config/* is nested.
|
||||
for src in "$HOST_DIR"/* "$HOST_DIR"/.[!.]* "$HOST_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 (Vundle)"
|
||||
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"
|
||||
fi
|
||||
info "Running :PluginInstall (idempotent)"
|
||||
# Headless, tolerant of the 'dracula' colorscheme-not-found warning at startup.
|
||||
if [ "$DRY_RUN" -eq 1 ]; then
|
||||
printf ' $ vim +PluginInstall +qall\n'
|
||||
else
|
||||
vim +PluginInstall +qall >/dev/null 2>&1 || \
|
||||
warn "Vim PluginInstall exited non-zero; verify with: vim +PluginInstall +qall"
|
||||
fi
|
||||
ok "Vim plugins installed"
|
||||
}
|
||||
|
||||
# ----------------------------------------------------------------------------
|
||||
# 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."
|
||||
Reference in New Issue
Block a user