-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbootstrap-debian.sh
More file actions
executable file
·206 lines (190 loc) · 9.41 KB
/
Copy pathbootstrap-debian.sh
File metadata and controls
executable file
·206 lines (190 loc) · 9.41 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
#!/usr/bin/env bash
# Bootstrap a Debian/Ubuntu machine to match the dotfiles setup.
# Idempotent — safe to re-run.
#
# Usage:
# ./bootstrap-debian.sh
#
# Runs from wherever the repo is checked out (the location is derived from this
# script's path), so it works whether the clone lives at ~/dotfiles or, as in
# the headless `devbox` flow, ~/.dotfiles. Override with
# DOTFILES_DIR=/path/to/repo ./bootstrap-debian.sh.
#
# Fully non-interactive: designed to run over SSH with no TTY as a
# passwordless-sudo user. No prompts, no Homebrew — tools come from apt and
# mise (https://mise.jdx.dev).
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DOTFILES_DIR="${DOTFILES_DIR:-${SCRIPT_DIR}}"
STOW_PACKAGES=(atuin bat btop direnv editorconfig fzf gh git gitignore_global hadolint kitty lazygit mise nvim p10k ripgrep shellcheck task tig tmux vim yamllint zsh)
# lefthook is a per-project template (lefthook/lefthook.yml), not stowed into $HOME.
ZSH_CUSTOM="${ZSH_CUSTOM:-${HOME}/.oh-my-zsh/custom}"
log() { printf '\033[1;34m==>\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m!! \033[0m %s\n' "$*" >&2; }
err() {
printf '\033[1;31mXX \033[0m %s\n' "$*" >&2
exit 1
}
[[ -d "${DOTFILES_DIR}" ]] || err "Dotfiles repo not found at ${DOTFILES_DIR} (clone it or set DOTFILES_DIR)."
[[ -f /etc/debian_version ]] || err "This script is for Debian/Ubuntu; /etc/debian_version not found."
# ---------------------------------------------------------------------------
# 1. apt prerequisites
# ---------------------------------------------------------------------------
# Base system + the CLIs Debian packages well. fzf and direnv MUST come from
# apt (not mise): their oh-my-zsh plugins load during `source oh-my-zsh.sh`,
# before `mise activate` runs, so they have to be on PATH from the start. The
# modern-CLI replacements (bat, eza, ripgrep, …) come from mise instead — see
# mise/.config/mise/config.toml.
log "Installing apt prerequisites"
sudo apt-get update
sudo apt-get install -y \
build-essential \
ca-certificates \
curl \
direnv \
file \
fzf \
git \
gnupg \
jq \
pipx \
procps \
stow \
tig \
tmux \
unzip \
vim \
zsh \
zsh-syntax-highlighting
# ---------------------------------------------------------------------------
# 2. mise (the primary tool manager on Linux — replaces Homebrew here)
# ---------------------------------------------------------------------------
# Installed from mise's own apt repo (deb822 source + armored key, the same
# pattern the devbox Ansible role uses). Skipped when mise is already present,
# e.g. devbox installs it before this script runs.
if ! command -v mise >/dev/null 2>&1; then
log "Installing mise from its apt repository"
arch="$(dpkg --print-architecture)"
sudo install -d -m 0755 /etc/apt/keyrings
sudo curl -fsSL https://mise.jdx.dev/gpg-key.pub -o /etc/apt/keyrings/mise.asc
sudo chmod 0644 /etc/apt/keyrings/mise.asc
printf 'Types: deb\nURIs: https://mise.jdx.dev/deb\nSuites: stable\nComponents: main\nArchitectures: %s\nSigned-By: /etc/apt/keyrings/mise.asc\n' \
"${arch}" | sudo tee /etc/apt/sources.list.d/mise.sources >/dev/null
sudo apt-get update
sudo apt-get install -y mise
fi
command -v mise >/dev/null 2>&1 || warn "mise is not on PATH; the tool-install step below will be skipped."
# ---------------------------------------------------------------------------
# 3. Stow the dotfile packages
# ---------------------------------------------------------------------------
# Stow BEFORE installing oh-my-zsh so ~/.zshrc is our symlink first: the OMZ
# installer's KEEP_ZSHRC=yes only preserves an *existing* .zshrc, and on a fresh
# box it would otherwise drop its own template there and collide with `stow zsh`.
log "Stowing dotfiles from ${DOTFILES_DIR}"
cd "${DOTFILES_DIR}"
for pkg in "${STOW_PACKAGES[@]}"; do
[[ -d "${pkg}" ]] || {
warn "Skipping ${pkg} (directory not in repo)"
continue
}
# Detect non-symlink collisions in $HOME and bail loudly so we don't blow
# away anyone's real files. `stow` folds a single-package subtree into one
# dir symlink (e.g. ~/.config/atuin -> repo), so files under it look like
# plain files while already being ours; `-ef` (same inode) tells an
# already-stowed file from a genuine foreign one, keeping re-runs idempotent.
# shellcheck disable=SC2312 # find can't meaningfully fail on an in-repo dir
while IFS= read -r -d '' f; do
rel="${f#"${pkg}"/}"
target="${HOME}/${rel}"
if [[ -e "${target}" && ! -L "${target}" && ! "${target}" -ef "${f}" ]]; then
err "Refusing to stow ${pkg}: ${target} exists as a regular file. Move or delete it first."
fi
done < <(find "${pkg}" -mindepth 1 -type f -print0)
log " stow -R ${pkg}"
stow -R "${pkg}"
done
# ---------------------------------------------------------------------------
# 4. Oh My Zsh
# ---------------------------------------------------------------------------
# KEEP_ZSHRC=yes keeps the ~/.zshrc symlink stowed above (see the note there).
if [[ ! -d "${HOME}/.oh-my-zsh" ]]; then
log "Installing oh-my-zsh"
omz_installer="$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
RUNZSH=no CHSH=no KEEP_ZSHRC=yes sh -c "${omz_installer}"
fi
# ---------------------------------------------------------------------------
# 5. powerlevel10k theme
# ---------------------------------------------------------------------------
P10K_DIR="${ZSH_CUSTOM}/themes/powerlevel10k"
if [[ ! -d "${P10K_DIR}" ]]; then
log "Installing powerlevel10k theme"
git clone --depth=1 https://github.com/romkatv/powerlevel10k "${P10K_DIR}"
fi
# ---------------------------------------------------------------------------
# 6. OMZ custom plugins (the ones .zshrc conditionally loads)
# ---------------------------------------------------------------------------
install_plugin() {
local name="$1" url="$2" dest="${ZSH_CUSTOM}/plugins/$1"
if [[ ! -d "${dest}" ]]; then
log "Installing OMZ plugin: ${name}"
git clone --depth=1 "${url}" "${dest}"
fi
}
install_plugin zsh-autosuggestions https://github.com/zsh-users/zsh-autosuggestions
install_plugin fzf-tab https://github.com/Aloxaf/fzf-tab
install_plugin you-should-use https://github.com/MichaelAquilina/zsh-you-should-use
# ---------------------------------------------------------------------------
# 7. mise-managed tools (runtimes + modern CLIs)
# ---------------------------------------------------------------------------
# The mise package (stowed above) placed ~/.config/mise/config.toml, so this
# materializes everything declared there. MISE_YES answers any confirmation;
# run from $HOME so no repo-local config sneaks in. Non-fatal — a flaky backend
# shouldn't abort the whole bootstrap. (devbox also runs `mise install` after
# this script; the second run is a fast no-op.)
if command -v mise >/dev/null 2>&1; then
log "Installing mise-managed tools (this can take a while)"
(cd "${HOME}" && MISE_YES=1 mise install) || warn "mise install reported errors; check 'mise doctor'."
fi
# ---------------------------------------------------------------------------
# 8. Default shell
# ---------------------------------------------------------------------------
# `sudo chsh` edits /etc/passwd as root, so it never triggers chsh's own PAM
# password prompt — the plain `chsh` would hang over a no-TTY SSH session.
zsh_path="$(command -v zsh)"
target_user="$(id -un)"
current_shell="$(getent passwd "${target_user}" | cut -d: -f7)"
if [[ "${current_shell}" != "${zsh_path}" ]]; then
log "Setting default shell to ${zsh_path} for ${target_user}"
sudo chsh -s "${zsh_path}" "${target_user}"
fi
# ---------------------------------------------------------------------------
# 9. Git push access + lefthook (opt-in; no secrets stored on this machine)
# ---------------------------------------------------------------------------
# The headless devbox flow clones this repo over public HTTPS (read-only). To
# push changes back from a VM, route GitHub *pushes* over SSH while leaving the
# HTTPS fetch alone, and authenticate with a forwarded SSH agent at push time
# (`ssh -A`) — so no key or token is ever written to this box. The rewrite lives
# in ~/.gitconfig.local (the per-machine include), never the stowed ~/.gitconfig.
log "Configuring git to push to GitHub over SSH (agent-forwarded)"
git config --file "${HOME}/.gitconfig.local" \
url."git@github.com:".pushInsteadOf "https://github.com/"
# Pre-trust GitHub's host key so the first agent-forwarded push doesn't hang on
# an interactive prompt in the non-TTY provision run.
if ! ssh-keygen -F github.com >/dev/null 2>&1; then
install -d -m 700 "${HOME}/.ssh"
ssh-keyscan -t ed25519 github.com >>"${HOME}/.ssh/known_hosts" 2>/dev/null || true
fi
# Wire this clone's lefthook hooks so commits are linted like CI. lefthook comes
# from mise (section 7), but this non-interactive shell never ran `mise activate`,
# so invoke it through `mise exec` rather than trusting it to be on PATH. mdformat,
# used by the Markdown hook, is a pipx tool that `task mise` manages — run
# `task mise` before committing .md changes.
if command -v mise >/dev/null 2>&1 && [[ -d "${DOTFILES_DIR}/.git" ]]; then
log "Installing lefthook git hooks"
(cd "${DOTFILES_DIR}" && mise exec -- lefthook install) || warn "lefthook install failed."
fi
log "Done."
echo
echo "Next steps (run from a fresh zsh):"
echo " atuin import auto # backfill existing shell history into atuin"
echo " # push dotfiles changes back (from your laptop): ssh -A <vm>, then git push"