-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathinstall.sh
More file actions
executable file
·396 lines (359 loc) · 12.9 KB
/
Copy pathinstall.sh
File metadata and controls
executable file
·396 lines (359 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
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
#!/usr/bin/env bash
# install.sh — Initialize dotfiles: pre-create directories and run stow
#
# Usage:
# ./install.sh shared personal # Personal machine (stow only)
# ./install.sh shared work # Work machine (stow only)
# ./install.sh shared remote-sandbox rca # RCA machine (stow only)
# ./install.sh shared remote-sandbox crafting # Crafting sandbox (stow only)
# ./install.sh --deps-preset core shared remote-sandbox crafting # Minimal deps
# ./install.sh shared ubuntu-server # Ubuntu server (stow only)
# ./install.sh --dirs-only # CI: only pre-create directories, skip stow
# ./install.sh --bootstrap # Fresh machine: install Nix, nix-darwin,
# # etc., then stow + TPM (interactive)
# ./install.sh --bootstrap shared work # Bootstrap with profiles pre-selected
# ./install.sh --bootstrap shared remote-sandbox rca # Bootstrap with profiles pre-selected
#
# Why directory pre-creation is required:
# Stow symlinks entire directories unless they already exist at the target.
# Pre-creating these directories forces stow to symlink individual files/subdirs
# instead, which is required when multiple stow packages contribute to the same
# parent directory (e.g., shared/ and personal/ both have .config/ contents).
set -e
DOTFILES_DIR="$(cd "$(dirname "$0")" && pwd)"
# Parse flags
DIRS_ONLY=false
BOOTSTRAP=false
DEPS_PRESET=""
PROFILES=()
for arg in "$@"; do
case "$arg" in
--dirs-only) DIRS_ONLY=true ;;
--bootstrap) BOOTSTRAP=true ;;
--deps-preset) DEPS_PRESET="__next__" ;;
*)
if [ "$DEPS_PRESET" = "__next__" ]; then
DEPS_PRESET="$arg"
else
PROFILES+=("$arg")
fi
;;
esac
done
# ---------------------------------------------------------------------------
# Interactive helpers — prefer `gum` when available (installed via nix-darwin),
# fall back to plain read so first-run before nix-darwin still works.
# ---------------------------------------------------------------------------
prompt() {
# prompt "Question" "default" — echoes the answer on stdout
local question="$1" default="${2:-}"
if command -v gum >/dev/null 2>&1; then
gum input --prompt "$question " --value "$default"
else
local reply
if [ -n "$default" ]; then
printf '%s [%s]: ' "$question" "$default" >&2
else
printf '%s: ' "$question" >&2
fi
read -r reply
printf '%s' "${reply:-$default}"
fi
}
confirm() {
# confirm "Question" — exits 0 on yes, 1 on no. Defaults to No.
local question="$1"
if command -v gum >/dev/null 2>&1; then
gum confirm "$question"
else
local reply
printf '%s [y/N]: ' "$question" >&2
read -r reply
[ "$reply" = "y" ] || [ "$reply" = "Y" ]
fi
}
info() { printf '\n\033[1;34m==>\033[0m %s\n' "$*"; }
ok() { printf '\033[0;32m✓\033[0m %s\n' "$*"; }
warn() { printf '\033[1;33m!\033[0m %s\n' "$*" >&2; }
# ---------------------------------------------------------------------------
# Bootstrap steps (only run with --bootstrap)
# ---------------------------------------------------------------------------
IS_DARWIN=false
[ "$(uname -s)" = "Darwin" ] && IS_DARWIN=true
bootstrap_xcode_clt() {
$IS_DARWIN || return 0
if xcode-select -p >/dev/null 2>&1; then
ok "Xcode Command Line Tools already installed"
return 0
fi
info "Installing Xcode Command Line Tools (GUI prompt will appear)"
xcode-select --install || true
printf 'Press enter after the Xcode CLT installer finishes... ' >&2
read -r _
}
bootstrap_apt_prereqs() {
$IS_DARWIN && return 0
command -v apt-get >/dev/null 2>&1 || return 0
if command -v git >/dev/null 2>&1 && command -v curl >/dev/null 2>&1; then
ok "apt prerequisites already installed"
return 0
fi
info "Installing apt prerequisites (git, curl, ca-certificates)"
sudo apt-get update -qq
sudo apt-get install -y -qq git curl ca-certificates
}
bootstrap_convert_to_repo() {
if git -C "$DOTFILES_DIR" rev-parse --git-dir >/dev/null 2>&1; then
ok "Dotfiles directory is already a git repository"
return 0
fi
info "Dotfiles directory is not a git repo (likely unzipped) — converting"
local remote
remote="$(prompt "Git remote URL" "git@github.com:josephschmitt/dotfiles.git")"
git -C "$DOTFILES_DIR" init -q
git -C "$DOTFILES_DIR" remote add origin "$remote"
if ! git -C "$DOTFILES_DIR" fetch origin; then
warn "Fetch failed — leaving repo initialized but unsynced"
return 0
fi
local default_branch
default_branch="$(git -C "$DOTFILES_DIR" symbolic-ref --short refs/remotes/origin/HEAD 2>/dev/null | sed 's|^origin/||')"
[ -z "$default_branch" ] && default_branch="main"
if confirm "Reset working tree to origin/$default_branch? (overwrites unzipped contents)"; then
git -C "$DOTFILES_DIR" checkout -B "$default_branch" "origin/$default_branch"
ok "Repo synced to origin/$default_branch"
else
warn "Skipped reset — repo is initialized but working tree differs from origin/$default_branch"
fi
}
bootstrap_ssh_key() {
if ls "$HOME/.ssh"/id_ed25519 "$HOME/.ssh"/id_rsa 2>/dev/null | grep -q .; then
ok "SSH key already exists"
return 0
fi
info "Generating SSH key (ed25519)"
local email
email="$(git config --global user.email 2>/dev/null || true)"
[ -z "$email" ] && email="$(whoami)@$(hostname -s)"
mkdir -p "$HOME/.ssh"
chmod 700 "$HOME/.ssh"
ssh-keygen -t ed25519 -C "$email" -f "$HOME/.ssh/id_ed25519" -N ""
info "Your public key:"
cat "$HOME/.ssh/id_ed25519.pub"
printf '\nAdd it to GitHub (https://github.com/settings/ssh/new), then press enter to continue... ' >&2
read -r _
}
bootstrap_hostname() {
$IS_DARWIN || return 0
local current new
current="$(hostname -s)"
info "Current hostname: $current"
if confirm "Rename this machine? (used as nix-darwin config key)"; then
new="$(prompt "New hostname (short, no spaces)" "$current")"
if [ -n "$new" ] && [ "$new" != "$current" ]; then
sudo scutil --set HostName "$new"
sudo scutil --set LocalHostName "$new"
sudo scutil --set ComputerName "$new"
dscacheutil -flushcache 2>/dev/null || true
ok "Hostname set to $new"
fi
fi
HOSTNAME="$(hostname -s)"
}
bootstrap_nix() {
if command -v nix >/dev/null 2>&1; then
ok "Nix already installed"
return 0
fi
info "Installing Nix (Determinate installer)"
curl -fsSL https://install.determinate.systems/nix | sh -s -- install --determinate
# Source the daemon profile so the rest of the script sees `nix`.
# shellcheck disable=SC1091
if [ -f /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh ]; then
. /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
fi
}
bootstrap_machine_config() {
$IS_DARWIN || return 0
local host="${HOSTNAME:-$(hostname -s)}"
local machines_dir="$DOTFILES_DIR/shared/.config/nix-darwin/machines"
local target="$machines_dir/$host.nix"
if [ -f "$target" ]; then
ok "Machine config exists: shared/.config/nix-darwin/machines/$host.nix"
return 0
fi
local arch platform
arch="$(uname -m)"
case "$arch" in
arm64|aarch64) platform="aarch64-darwin" ;;
x86_64) platform="x86_64-darwin" ;;
*) platform="aarch64-darwin" ;;
esac
info "Creating machine config for $host"
cat >"$target" <<EOF
{ pkgs, lib, ... }:
{
# Machine-specific overrides for $host.
# Add packages, homebrew casks, and system defaults here as needed.
nixpkgs.hostPlatform = "$platform";
}
EOF
ok "Wrote $target"
}
bootstrap_nix_darwin() {
$IS_DARWIN || return 0
local host="${HOSTNAME:-$(hostname -s)}"
local flake_dir="$DOTFILES_DIR/shared/.config/nix-darwin"
if [ -e /run/current-system ] && command -v darwin-rebuild >/dev/null 2>&1; then
info "nix-darwin already bootstrapped — running nix_rebuild"
"$DOTFILES_DIR/shared/bin/darwin-rebuild-wrapper.sh"
else
info "Bootstrapping nix-darwin for the first time (host: $host)"
sudo nix --extra-experimental-features "nix-command flakes" \
run nix-darwin/master#darwin-rebuild -- \
switch --flake "$flake_dir#$host"
fi
}
bootstrap_work_submodule() {
local work_machines="$DOTFILES_DIR/work/.config/nix-darwin/machines"
if [ -d "$work_machines" ]; then
ok "Work submodule already initialized"
return 0
fi
if confirm "Initialize the private work submodule? (requires repo access)"; then
info "Initializing work submodule"
(cd "$DOTFILES_DIR" && git submodule update --init --recursive work)
fi
}
bootstrap_rca_submodule() {
if [ -f "$DOTFILES_DIR/rca/.git" ] || [ -d "$DOTFILES_DIR/rca/.git" ]; then
ok "RCA submodule already initialized"
return 0
fi
if confirm "Initialize the private rca submodule? (requires repo access)"; then
info "Initializing rca submodule"
(cd "$DOTFILES_DIR" && git submodule update --init --recursive rca)
fi
}
bootstrap_remote_sandbox_submodule() {
if [ -f "$DOTFILES_DIR/remote-sandbox/.git" ] || [ -d "$DOTFILES_DIR/remote-sandbox/.git" ]; then
ok "remote-sandbox submodule already initialized"
return 0
fi
if confirm "Initialize the private remote-sandbox submodule? (requires repo access)"; then
info "Initializing remote-sandbox submodule"
(cd "$DOTFILES_DIR" && git submodule update --init --recursive remote-sandbox)
fi
}
bootstrap_crafting_submodule() {
if [ -f "$DOTFILES_DIR/crafting/.git" ] || [ -d "$DOTFILES_DIR/crafting/.git" ]; then
ok "crafting submodule already initialized"
return 0
fi
if confirm "Initialize the private crafting submodule? (requires repo access)"; then
info "Initializing crafting submodule"
(cd "$DOTFILES_DIR" && git submodule update --init --recursive crafting)
fi
}
select_profiles_interactively() {
# Only prompt if user ran --bootstrap without explicit profiles.
[ ${#PROFILES[@]} -eq 0 ] || return 0
local default
if $IS_DARWIN; then
default="shared personal"
else
default="shared ubuntu-server"
fi
info "Stow profile selection"
echo "Common choices: 'shared personal', 'shared work', 'shared remote-sandbox rca', 'shared remote-sandbox crafting', 'shared ubuntu-server'" >&2
local answer
answer="$(prompt "Profiles to stow (space-separated)" "$default")"
# shellcheck disable=SC2206
PROFILES=($answer)
}
has_profile() {
local target="$1"
for p in "${PROFILES[@]}"; do
[ "$p" = "$target" ] && return 0
done
return 1
}
run_bootstrap() {
info "Bootstrap mode — setting up a fresh machine"
if ! $IS_DARWIN; then
warn "Not on macOS — skipping Darwin-specific steps (hostname, machine config, nix-darwin)"
fi
bootstrap_xcode_clt
bootstrap_apt_prereqs
has_profile remote-sandbox || bootstrap_ssh_key
bootstrap_convert_to_repo
if ! has_profile remote-sandbox; then
bootstrap_hostname
bootstrap_nix
bootstrap_machine_config
bootstrap_nix_darwin
fi
has_profile work && bootstrap_work_submodule
has_profile remote-sandbox && bootstrap_remote_sandbox_submodule
has_profile rca && bootstrap_rca_submodule
has_profile crafting && bootstrap_crafting_submodule
select_profiles_interactively
}
# ---------------------------------------------------------------------------
# Main flow
# ---------------------------------------------------------------------------
if $BOOTSTRAP; then
run_bootstrap
fi
# Pre-create directories so stow symlinks individual files, not entire directories.
# Required when multiple stow packages contribute files to the same parent directory.
info "Pre-creating directories for stow merging"
mkdir -p "$HOME/.config/fish"
mkdir -p "$HOME/.config/shell"
mkdir -p "$HOME/.config/nix-darwin/machines"
mkdir -p "$HOME/.config/tmux"
mkdir -p "$HOME/.config/tmux/plugins"
mkdir -p "$HOME/.claude"
mkdir -p "$HOME/.config/herdr"
mkdir -p "$HOME/.ssh"
mkdir -p "$HOME/bin"
if $DIRS_ONLY; then
ok "Done (--dirs-only: skipped stow)."
exit 0
fi
# Default to 'shared' if no profiles given
if [ ${#PROFILES[@]} -eq 0 ]; then
PROFILES=("shared")
fi
# Initialize and sync submodules for selected profiles
SUBMODULE_PROFILES="work rca remote-sandbox crafting"
for p in "${PROFILES[@]}"; do
for sm in $SUBMODULE_PROFILES; do
if [ "$p" = "$sm" ]; then
info "Syncing $sm submodule"
(cd "$DOTFILES_DIR" && git submodule update --init --recursive "$sm")
break
fi
done
done
# Export for hooks to consume
export DEPS_PRESET
# Run profile pre-stow hooks (sourced so PATH changes propagate)
for p in "${PROFILES[@]}"; do
if [ -x "$DOTFILES_DIR/$p/.hooks/pre-stow.sh" ]; then
info "Running $p pre-stow hook"
# shellcheck disable=SC1090
. "$DOTFILES_DIR/$p/.hooks/pre-stow.sh"
fi
done
info "Stowing profiles: ${PROFILES[*]}"
cd "$DOTFILES_DIR"
stow -v --target="$HOME" "${PROFILES[@]}"
# Run profile post-stow hooks
for p in "${PROFILES[@]}"; do
if [ -x "$DOTFILES_DIR/$p/.hooks/post-stow.sh" ]; then
info "Running $p post-stow hook"
"$DOTFILES_DIR/$p/.hooks/post-stow.sh"
fi
done
ok "Done."