-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbootstrap.sh
More file actions
executable file
Β·254 lines (236 loc) Β· 12.9 KB
/
Copy pathbootstrap.sh
File metadata and controls
executable file
Β·254 lines (236 loc) Β· 12.9 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
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
#!/usr/bin/env bash
# dotfiles-Alpine/bootstrap.sh
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
# Provision an Alpine box (bare-metal / VM / container / WSL) and wire dotfiles.
# Idempotent. OS-NATIVE layer; Core (zsh/tmux/nvim/git) is vendored under core/.
# Alpine is the outlier: musl libc, doas (not sudo), ash default shell, OpenRC.
# The shared symlink/loader/login-shell scaffold lives in core/lib/bootstrap-lib.sh.
#
# Usage:
# ./bootstrap.sh # full: apk packages + extras + symlinks
# ./bootstrap.sh --links-only # just (re)create symlinks
# ./bootstrap.sh --only zsh,nvim # link ONLY these Core module groups
# ./bootstrap.sh --skip tmux # link everything EXCEPT these groups
#
# Module groups (for --only/--skip): zsh nvim tmux git prompt tools β they affect
# the wiring steps only, never package provisioning; combine with --links-only to
# re-wire a subset of configs without touching apk.
#
# Run as root, OR as a user with doas/sudo configured (Alpine defaults to doas).
#
# PREREQUISITE β bash: this script is bash (shebang above; it uses arrays + mapfile),
# but a fresh Alpine ships only busybox ash, so bash is NOT present by default. Install
# it FIRST or the kernel can't exec this file ("bad interpreter: bash: not found"):
# apk add bash # (or: doas apk add bash)
# bash is also listed in install/packages.txt so a full provision keeps it installed.
# ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
set -euo pipefail
DOTFILES="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
CONFIG="${XDG_CONFIG_HOME:-$HOME/.config}"
LINKS_ONLY=0
# --only/--skip are validated by the shared lib (blib_select), which is sourced
# AFTER this loop β so capture the raw values now and apply them below.
ONLY_RAW="" SKIP_RAW="" ONLY_SEEN=0 SKIP_SEEN=0
while [[ $# -gt 0 ]]; do case "$1" in
--links-only) LINKS_ONLY=1 ;;
--only) [[ $# -ge 2 ]] || { echo "--only requires module names, e.g. --only zsh,nvim" >&2; exit 1; }; ONLY_RAW="$2"; ONLY_SEEN=1; shift ;;
--only=*) ONLY_RAW="${1#*=}"; ONLY_SEEN=1 ;;
--skip) [[ $# -ge 2 ]] || { echo "--skip requires module names, e.g. --skip tmux" >&2; exit 1; }; SKIP_RAW="$2"; SKIP_SEEN=1; shift ;;
--skip=*) SKIP_RAW="${1#*=}"; SKIP_SEEN=1 ;;
-h | --help)
sed -n '2,19p' "$0"
exit 0
;;
*)
echo "unknown arg: $1" >&2
exit 1
;;
esac; shift; done
# ββ core/ subtree present? (inline: can't source a lib out of core/ before this) β
# Validate the SPECIFIC paths we depend on below β the zsh modules wire_links
# symlinks, plus the two libs sourced next β so a missing or partially-vendored
# subtree fails HERE with a precise message, not later with a cryptic
# `source: No such file`.
for _req in core/zsh/loader.zsh core/lib/ux.sh core/lib/bootstrap-lib.sh; do
if [[ ! -e "$DOTFILES/$_req" ]]; then
echo "core/ subtree missing or incomplete (need $_req). One-time, run:" >&2
echo " git subtree add --prefix=core <dotfiles-core remote> main --squash # first time" >&2
echo " git subtree pull --prefix=core <dotfiles-core remote> main --squash # to update" >&2
exit 1
fi
done
unset _req
# Shared bash UX palette + the provisioning scaffold (link/read_pkgs/WSL-detect/
# Core-symlink/loader/login-shell), both vendored under core/lib. ux.sh first so the
# blib_* messages pick up its palette.
# shellcheck source=core/lib/ux.sh
source "$DOTFILES/core/lib/ux.sh"
# shellcheck source=core/lib/bootstrap-lib.sh
source "$DOTFILES/core/lib/bootstrap-lib.sh"
# Apply any --only/--skip module selection now the validator (blib_select) exists;
# it aborts on a malformed selector or an unknown group.
if ((ONLY_SEEN)); then blib_select --only "$ONLY_RAW"; fi
if ((SKIP_SEEN)); then blib_select --skip "$SKIP_RAW"; fi
# ββ privilege tool: Alpine defaults to doas, not sudo. Use nothing if root. βββββ
# BLIB_SU hands the same escalator to bootstrap-lib (blib_set_login_shell).
if [[ "$(id -u)" -eq 0 ]]; then
SU=""
elif command -v doas >/dev/null 2>&1; then
SU="doas"
elif command -v sudo >/dev/null 2>&1; then
SU="sudo"
else
echo "Need root: run as root, or 'apk add doas' and configure /etc/doas.d." >&2
exit 1
fi
export BLIB_SU="$SU"
# ββ sanity: confirm we're on Alpine ββββββββββββββββββββββββββββββββββββββββββββ
if ! grep -qiE '^ID=alpine' /etc/os-release 2>/dev/null; then
echo "This bootstrap targets Alpine Linux (expects ID=alpine in /etc/os-release)." >&2
exit 1
fi
IS_WSL=0
if blib_is_wsl; then IS_WSL=1; fi
# ββ resilient install: apk fails the whole transaction on one unknown package.
# Bulk first, then per-package so a missing name is skipped, not fatal. ββββββββββ
apk_install() {
local -a pkgs=("$@")
# shellcheck disable=SC2086 # $SU is a single token (doas/sudo) or empty (root)
if $SU apk add "${pkgs[@]}"; then return 0; fi
blib_say "bulk install hit a snag β retrying package-by-package"
local p
for p in "${pkgs[@]}"; do
# shellcheck disable=SC2086 # see above
$SU apk add "$p" || echo " skipped (unavailable on this box?): $p"
done
}
# ββ go-installed tools: presence-guarded, best-effort. Used for core-doctor tools
# that live only in Alpine's `testing` repo (duf, glow), plus sesh β none packaged in
# `community`. `go install` produces a static (musl-safe) binary; if Go is absent we
# defer via mise, else print a hint. Never aborts (errexit-exempt). ββββββββββββββ
# go install drops binaries in ~/go/bin, which the shell layer does NOT put on
# PATH (it prefixes ~/.local/bin + ~/.cargo/bin) β so point GOBIN at ~/.local/bin.
_dotfiles_go_install() { # <import-path@version> <binary-name>
[ "$#" -ge 2 ] || return 0
if command -v "$2" >/dev/null 2>&1; then return 0; fi
gobin="$HOME/.local/bin"
mkdir -p "$gobin" 2>/dev/null || true
if command -v go >/dev/null 2>&1; then
GOBIN="$gobin" go install "$1" >/dev/null 2>&1 ||
echo " $2: go install failed β retry later: GOBIN=$gobin go install $1"
elif command -v mise >/dev/null 2>&1; then
GOBIN="$gobin" mise exec go@latest -- go install "$1" >/dev/null 2>&1 ||
echo " $2: go install failed β retry later: GOBIN=$gobin go install $1"
else
echo " $2: needs Go β install later with: GOBIN=$gobin go install $1"
fi
return 0
}
provision() {
# shellcheck disable=SC2086 # $SU: single token or empty (root)
blib_say "apk update"
# shellcheck disable=SC2086
$SU apk update
blib_say "apk packages (from install/packages.txt)"
local -a pkgs=()
mapfile -t pkgs < <(blib_read_pkgs "$DOTFILES/install/packages.txt")
# Guard the empty case: an all-comment/blank packages.txt yields a zero-length
# array. apk_install wraps `apk add` in `if β¦; then` (errexit-exempt), so an
# empty list wouldn't abort β but it WOULD run `apk add` with no args, trip the
# "bulk install hit a snag" per-package fallback, and then log a misleading
# "0 requested" success. Skip the install instead and carry on.
if ((${#pkgs[@]})); then
apk_install "${pkgs[@]}"
blib_ok "apk packages requested: ${#pkgs[@]}"
else
blib_warn "install/packages.txt lists no packages β skipping apk install"
fi
# Tools not packaged (or that we build from source on musl). The starship and
# mise installers detect musl and pull the correct *-musl build β safe here.
# atuin is in Alpine repos (in packages.txt); installer below is just a fallback.
if ! command -v starship >/dev/null; then
blib_say "starship (official installer β musl build)"
curl -fsSL https://starship.rs/install.sh | sh -s -- -y >/dev/null || true
fi
if ! command -v atuin >/dev/null; then
blib_say "atuin (official installer β fallback; usually apk-installed)"
curl -fsSL https://setup.atuin.sh | sh >/dev/null 2>&1 || true
fi
if ! command -v mise >/dev/null && [[ ! -x "$HOME/.local/bin/mise" ]]; then
blib_say "mise (official installer β musl build)"
curl -fsSL https://mise.run | sh >/dev/null 2>&1 || true
fi
# yazi + tree-sitter-cli: not packaged β build from source via cargo. On musl
# this compiles against the musl target (needs build-base, in packages.txt).
if ! command -v yazi >/dev/null && command -v cargo >/dev/null; then
blib_say "yazi (cargo build β slow on musl)"
cargo install --locked yazi-fs yazi-cli >/dev/null 2>&1 || true
fi
if ! command -v tree-sitter >/dev/null && command -v cargo >/dev/null; then
blib_say "tree-sitter-cli (cargo build)"
cargo install --locked tree-sitter-cli >/dev/null 2>&1 ||
echo " tree-sitter-cli build failed; retry later: cargo install --locked tree-sitter-cli"
fi
# tealdeer (tldr): `testing`-only on Alpine (never in `community`), so not in
# packages.txt β build from source via cargo. Presence-guarded on the `tldr`
# binary; best-effort so a build hiccup never aborts bootstrap.
if ! command -v tldr >/dev/null && command -v cargo >/dev/null; then
blib_say "tealdeer (cargo build β tldr client; testing-only on Alpine)"
cargo install --locked tealdeer >/dev/null 2>&1 ||
echo " tealdeer build failed; retry later: cargo install --locked tealdeer"
fi
# viddy (watch replacement; Core aliases watch->viddy, HAVE_VIDDY-guarded) is a Rust
# CLI β build from source via cargo. On musl this compiles the musl target (a
# static, musl-safe binary), presence-guarded + best-effort.
if ! command -v viddy >/dev/null && command -v cargo >/dev/null; then
blib_say "viddy (cargo build β watch replacement; Rust)"
cargo install --locked viddy >/dev/null 2>&1 ||
echo " viddy build failed; retry later: cargo install --locked viddy"
fi
# ββ go-installed core-doctor tools. sesh is unpackaged on Alpine; duf + glow are
# `testing`-only (NOT in `community` on current stable), so `apk add` skips them β
# `go install` here is their REAL source, not a fallback. `go install` yields a
# static (musl-safe) binary; presence-guarded + best-effort, so it no-ops when a
# tool is already present, and a box without Go just gets a hint. ββββββββββββββββ
blib_say "duf / glow / sesh (go install β testing-only/unpackaged on Alpine; musl-safe static)"
_dotfiles_go_install github.com/muesli/duf@latest duf
_dotfiles_go_install github.com/charmbracelet/glow/v2@latest glow
_dotfiles_go_install github.com/joshmedeski/sesh/v2@latest sesh
# ββ op (1Password CLI): native musl apk from 1Password's official Alpine repo β
# NOT a glibc vendor binary. Presence-guarded; best-effort so a fetch/network hiccup
# never aborts bootstrap. ββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
if ! command -v op >/dev/null 2>&1; then
blib_say "op β 1Password CLI (official Alpine repo β native musl apk)"
# shellcheck disable=SC2086 # $SU: single token (doas/sudo) or empty (root)
if ! grep -q '1password.com/linux/alpinelinux' /etc/apk/repositories 2>/dev/null; then
echo "https://downloads.1password.com/linux/alpinelinux/stable/" | $SU tee -a /etc/apk/repositories >/dev/null || true
fi
# shellcheck disable=SC2086
$SU wget -q https://downloads.1password.com/linux/keys/alpinelinux/support@1password.com-61ddfc31.rsa.pub -P /etc/apk/keys 2>/dev/null || true
# shellcheck disable=SC2086
{ $SU apk update >/dev/null 2>&1 && $SU apk add 1password-cli >/dev/null 2>&1; } ||
echo " op: install skipped β add it later with: $SU apk add 1password-cli"
fi
# ββ WSL: install /etc/wsl.conf. NOTE: no systemd=true β Alpine uses OpenRC. βββ
if ((IS_WSL)); then
blib_say "installing /etc/wsl.conf (default user + interop; OpenRC, not systemd)"
local user
user="$(id -un)"
# shellcheck disable=SC2086 # $SU: single token or empty (root)
sed "s/__WSL_USER__/$user/" "$DOTFILES/wsl/wsl.conf" | $SU tee /etc/wsl.conf >/dev/null
blib_ok "wsl.conf written β run 'wsl.exe --shutdown' from Windows, then reopen"
fi
}
wire_links() {
# The whole shared symlink surface + the Alpine OS overlays + the managed .zshrc
# loader + the default-login-shell switch now live in core/lib/bootstrap-lib.sh.
blib_link_core "$DOTFILES" "$CONFIG"
blib_link_os_layer "$DOTFILES" "$CONFIG" alpine
# shellcheck disable=SC2119 # no args is intentional β writes the default module set
blib_write_zshrc_loader
blib_set_login_shell
blib_ok "symlinks wired$(blib_selected_note)"
}
((LINKS_ONLY)) || provision
wire_links
blib_ok "Alpine bootstrap complete β open a new shell or: exec zsh"