The previous substring check (*"Maple Mono NF"* glob / grep -q substring)
matched 'Maple Mono NF CN' too, which silently skipped the Latin-only NF
install on boxes that had only the CJK variant installed (byte, kaiser —
they shipped with maple-font system package that defaults to NF CN).
Result: foot (and any other config asking for 'Maple Mono NF') silently
fell back to system sans on byte and kaiser, while looking like a
working terminal. Compounded by foot.ini asking for 'Maple Mono CN'
(see prior commit c6779c5), which masked the missing NF install on
miche (which has both CN and NF).
Fix: use fc-list : family | grep -qxF 'Maple Mono NF' which:
- prints one family per line (no style suffixes to confuse grep)
- -x anchors whole line
- -F fixed string (no regex)
- matches exactly 'Maple Mono NF' and nothing else
Why not fc-match? fc-match reports a substitute when the family isn't
installed (Noto/Liberation/DejaVu depending on distro), so it can't
distinguish 'NF installed' from 'NF not installed, fell back'.
Verified on byte: fc-list : family | grep -qxF 'Maple Mono NF' returns
1 (false) because byte has only 'Maple Mono NF CN', so the install will
now run on next chezmoi apply.
370 lines
No EOL
15 KiB
Bash
Executable file
370 lines
No EOL
15 KiB
Bash
Executable file
#!/usr/bin/env bash
|
|
# =============================================================================
|
|
# run_once_20-install-user-packages.sh.tmpl
|
|
# Install zsh, tmux, neovim, fastfetch, plus the modern CLI replacements
|
|
# the .zshrc aliases depend on (bat, btop, eza, fzf, fd, ripgrep, zoxide,
|
|
# starship, lazygit, yt-dlp, etc.)
|
|
#
|
|
# Also: install oh-my-zsh, zsh-autosuggestions, zsh-syntax-highlighting,
|
|
# zsh-history-substring-search, fzf-tab.
|
|
#
|
|
# Runs as the unprivileged user, but uses sudo for system packages.
|
|
#
|
|
# Note: this script handles arch and debian. Gentoo uses its own
|
|
# run_once_20-install-user-packages-gentoo.sh.tmpl. Without the early
|
|
# return below, this universal script would hit `die "unsupported
|
|
# os_family"` on gentoo and abort the apply chain.
|
|
# =============================================================================
|
|
set -euo pipefail
|
|
|
|
# Make user-local bins (bun, omp, cargo) visible to the script even when
|
|
# invoked from a non-interactive context (e.g. `chezmoi apply` over SSH).
|
|
# These are normally added by .zshrc, but this script runs in a plain
|
|
# shell where those rc files aren't sourced.
|
|
#
|
|
# For `bun` on arch: we deliberately put /usr/bin BEFORE the user
|
|
# locations so the pacman-managed binary wins over any curl-installed
|
|
# copy that might be shadowing it. The post-install verification below
|
|
# fails loudly if pacman doesn't actually have bun, so the operator
|
|
# can remove the curl shadow and re-run.
|
|
export PATH="/usr/bin:/bin:$HOME/.local/bin:$HOME/.bun/bin:$HOME/.cargo/bin:$PATH"
|
|
|
|
log() { printf '\033[1;34m[packages]\033[0m %s\n' "$*"; }
|
|
die() { printf '\033[1;31m[packages ERROR]\033[0m %s\n' "$*" >&2; exit 1; }
|
|
|
|
# --- 0. Gentoo uses its own user-packages script ---
|
|
{{ if eq .os_family "gentoo" -}}
|
|
log "user packages install handled by run_once_20-install-user-packages-gentoo.sh.tmpl on gentoo"
|
|
log "exiting to avoid running arch/debian-specific branches"
|
|
exit 0
|
|
{{ end -}}
|
|
|
|
USER_HOME="${HOME:-$(eval echo "~$(whoami)")}"
|
|
ZSH_CUSTOM="${ZSH_CUSTOM:-$USER_HOME/.oh-my-zsh/custom}"
|
|
|
|
{{ if eq .os_family "arch" -}}
|
|
# ----------------------------- ARCH ---------------------------------------
|
|
# Only run pacman if anything is actually missing. Avoids a no-op sudo
|
|
# (which would still prompt for a password even when there's nothing to
|
|
# install) on boxes where all the user packages are already present.
|
|
#
|
|
# For `bun` specifically, we check `pacman -Qi bun` (not `command -v
|
|
# bun`) so the script forces a system-package install via pacman, not a
|
|
# curl-installed binary in ~/.bun/bin or ~/.local/bin. Per user
|
|
# preference: PMs first (pacman via chaotic-aur on arch, apt on debian),
|
|
# `curl | bash` only as last-resort fallback when the package isn't in
|
|
# any repo. bun is in [extra] on arch since 1.2.x, so pacman handles it.
|
|
PACMAN_PKGS=(
|
|
zsh tmux neovim git base-devel
|
|
bat btop htop fastfetch
|
|
eza fzf fd ripgrep zoxide starship
|
|
lazygit yt-dlp jq
|
|
unzip p7zip
|
|
openssh
|
|
bun
|
|
)
|
|
MISSING_PKGS=()
|
|
for p in "${PACMAN_PKGS[@]}"; do
|
|
if [[ "$p" == "bun" ]]; then
|
|
# System-package check for bun: must be tracked by pacman, not
|
|
# just a binary on PATH. A curl-installed bun at ~/.bun/bin/bun
|
|
# would otherwise pass the `command -v` check and skip pacman,
|
|
# leaving us with a non-PM-managed install.
|
|
if ! pacman -Qi "$p" >/dev/null 2>&1; then
|
|
MISSING_PKGS+=("$p")
|
|
fi
|
|
elif ! command -v "$p" >/dev/null 2>&1 && ! pacman -Qi "$p" >/dev/null 2>&1; then
|
|
MISSING_PKGS+=("$p")
|
|
fi
|
|
done
|
|
if (( ${#MISSING_PKGS[@]} > 0 )); then
|
|
log "pacman -Syu (missing: ${MISSING_PKGS[*]})"
|
|
sudo pacman -Syu --noconfirm
|
|
log "installing pacman packages"
|
|
sudo pacman -S --needed --noconfirm "${MISSING_PKGS[@]}"
|
|
else
|
|
log "all user packages already installed; skipping pacman"
|
|
fi
|
|
|
|
# Post-install verification: bun must come from the system package
|
|
# manager, not a curl-install. If it does, fail loudly so the operator
|
|
# can remove the curl-installed binary and re-run.
|
|
if ! pacman -Qi bun >/dev/null 2>&1; then
|
|
log "ERROR: bun is not tracked by pacman after install"
|
|
log " (a curl-installed binary may be shadowing the package)"
|
|
log " remove ~/.bun/ and ~/.local/bin/bun, then re-run"
|
|
exit 1
|
|
fi
|
|
BUN_FROM=$(pacman -Qi bun 2>/dev/null | awk -F': *' '/^Installed From/ {print $2}')
|
|
BUN_VER=$(pacman -Qi bun 2>/dev/null | awk -F': *' '/^Version/ {print $2}')
|
|
log "bun: $BUN_VER (from $BUN_FROM) — system-package install verified"
|
|
|
|
# --------------------------- Pi coding agent + oh-my-pi ---------------------
|
|
# Arch: bun comes from pacman (above), used here for the global install.
|
|
if command -v bun >/dev/null 2>&1; then
|
|
if ! command -v omp >/dev/null 2>&1; then
|
|
log "installing @oh-my-pi/pi-coding-agent via bun global"
|
|
# Wrap in subshell so a segfault/timeout (e.g. Pi undervoltage
|
|
# killing the install mid-run, see pitfall #30) doesn't abort
|
|
# the whole bootstrap. omp can be retried manually later.
|
|
if (bun add -g @oh-my-pi/pi-coding-agent 2>&1 | tail -10); then
|
|
log "omp installed: $(omp --version 2>&1 | head -1)"
|
|
else
|
|
log "WARNING: bun add -g failed; omp not installed. Retry manually."
|
|
fi
|
|
else
|
|
log "omp already installed: $(omp --version 2>&1 | head -1)"
|
|
fi
|
|
fi
|
|
|
|
{{ else if eq .os_family "debian" -}}
|
|
# ----------------------------- DEBIAN --------------------------------------
|
|
export DEBIAN_FRONTEND=noninteractive
|
|
APT_PKGS=(
|
|
zsh tmux git build-essential
|
|
btop htop fastfetch
|
|
eza fzf fd-find ripgrep zoxide starship
|
|
lazygit yt-dlp jq
|
|
unzip p7zip
|
|
openssh-client
|
|
ca-certificates curl wget
|
|
fontconfig
|
|
)
|
|
MISSING_PKGS=()
|
|
for p in "${APT_PKGS[@]}"; do
|
|
if ! command -v "$p" >/dev/null 2>&1; then
|
|
MISSING_PKGS+=("$p")
|
|
fi
|
|
done
|
|
if (( ${#MISSING_PKGS[@]} > 0 )); then
|
|
log "apt-update (missing: ${MISSING_PKGS[*]})"
|
|
sudo apt-get update -y
|
|
log "apt-upgrade"
|
|
sudo apt-get upgrade -y
|
|
log "installing apt packages"
|
|
sudo apt-get install -y --no-install-recommends "${MISSING_PKGS[@]}"
|
|
else
|
|
log "all user packages already installed; skipping apt"
|
|
fi
|
|
|
|
# bun isn't in debian repos. Install via official script into ~/.local
|
|
# (so the binary lands at ~/.local/bin/bun, which is already in PATH
|
|
# via .zshrc — no extra PATH config needed). Per user preference, this
|
|
# is a last-resort fallback: PMs first, then pinned binaries from
|
|
# official sources, then `curl | bash`. apt on debian-stable has no
|
|
# bun, so we land on the official install.
|
|
if ! command -v bun >/dev/null 2>&1; then
|
|
log "installing bun to ~/.local/bin (debian: not in apt)"
|
|
curl -fsSL https://bun.sh/install | BUN_INSTALL="$HOME/.local" bash
|
|
fi
|
|
|
|
# Post-install verification: confirm bun is reachable on PATH. On
|
|
# debian, the official install puts it at $BUN_INSTALL/bin/bun, so we
|
|
# expect ~/.local/bin/bun in this case.
|
|
if ! command -v bun >/dev/null 2>&1; then
|
|
log "ERROR: bun install succeeded but bun is not on PATH"
|
|
log " check \$BUN_INSTALL/bin/bun exists and is in PATH"
|
|
exit 1
|
|
fi
|
|
BUN_VER=$(bun --version 2>/dev/null)
|
|
log "bun: $BUN_VER (from official bun.sh installer; debian has no apt package)"
|
|
|
|
# fd on Debian ships as 'fdfind' to avoid clashing with fd (the dedupe tool).
|
|
# Symlink so .zshrc can find 'fd' on PATH.
|
|
if command -v fdfind >/dev/null 2>&1 && ! command -v fd >/dev/null 2>&1; then
|
|
log "symlinking fdfind -> fd in ~/.local/bin"
|
|
mkdir -p "$USER_HOME/.local/bin"
|
|
ln -sf "$(command -v fdfind)" "$USER_HOME/.local/bin/fd"
|
|
fi
|
|
|
|
# Debian ships 'bat' as 'batcat' due to a name clash with an unrelated
|
|
# package. The install happens in run_onchange_30 (after rustup is ready,
|
|
# via `cargo install bat`).
|
|
|
|
# --------------------------- Pi coding agent + oh-my-pi ---------------------
|
|
# Install via bun global (arch already has /usr/bin/bun from pacman, debian
|
|
# got it from the curl install above). Both OSes land in the same dir.
|
|
if command -v bun >/dev/null 2>&1; then
|
|
if ! command -v omp >/dev/null 2>&1; then
|
|
log "installing @oh-my-pi/pi-coding-agent via bun global"
|
|
# Wrap in subshell so a segfault/timeout (e.g. Pi undervoltage
|
|
# killing the install mid-run, see pitfall #30) doesn't abort
|
|
# the whole bootstrap. omp can be retried manually later.
|
|
if (bun add -g @oh-my-pi/pi-coding-agent 2>&1 | tail -10); then
|
|
log "omp installed: $(omp --version 2>&1 | head -1)"
|
|
else
|
|
log "WARNING: bun add -g failed; omp not installed. Retry manually."
|
|
fi
|
|
else
|
|
log "omp already installed: $(omp --version 2>&1 | head -1)"
|
|
fi
|
|
fi
|
|
|
|
# Neovim — install official binary tarball, pinned to a known-good version.
|
|
# Bump NVIM_TARGET_VERSION to upgrade. ~/.local/bin/update-neovim.sh does
|
|
# the same check + download so topgrade can invoke it for upgrades.
|
|
NVIM_TARGET_VERSION="v0.11.4"
|
|
ARCH="$(uname -m)"
|
|
case "$ARCH" in
|
|
x86_64)
|
|
NVIM_TARBALL="nvim-linux64.tar.gz"
|
|
NVIM_EXTRACT_DIR="nvim-linux64"
|
|
;;
|
|
aarch64|arm64)
|
|
NVIM_TARBALL="nvim-linux-arm64.tar.gz"
|
|
NVIM_EXTRACT_DIR="nvim-linux-arm64"
|
|
;;
|
|
*)
|
|
die "unsupported arch for neovim tarball: $ARCH"
|
|
;;
|
|
esac
|
|
|
|
if command -v nvim >/dev/null 2>&1; then
|
|
INSTALLED_VER="$(nvim --version 2>/dev/null | head -1 | awk '{print $2}')"
|
|
if [[ "$INSTALLED_VER" == "$NVIM_TARGET_VERSION" ]]; then
|
|
log "neovim $INSTALLED_VER matches target — skipping"
|
|
NVIM_TARBALL=""
|
|
else
|
|
log "neovim $INSTALLED_VER != target $NVIM_TARGET_VERSION — upgrading"
|
|
fi
|
|
fi
|
|
|
|
if [[ -n "$NVIM_TARBALL" ]]; then
|
|
log "downloading neovim $NVIM_TARGET_VERSION ($NVIM_TARBALL)"
|
|
cd /tmp
|
|
if ! curl -fL --retry 3 \
|
|
"https://github.com/neovim/neovim/releases/download/${NVIM_TARGET_VERSION}/${NVIM_TARBALL}" \
|
|
-o nvim.tar.gz; then
|
|
die "failed to download neovim tarball"
|
|
fi
|
|
sudo rm -rf /opt/nvim-linux* /usr/local/bin/nvim
|
|
sudo tar -xzf nvim.tar.gz -C /opt/
|
|
sudo ln -sf "/opt/${NVIM_EXTRACT_DIR}/bin/nvim" /usr/local/bin/nvim
|
|
rm -f nvim.tar.gz
|
|
fi
|
|
|
|
# Verify neovim is reachable (PATH may need /usr/local/bin explicitly for this run)
|
|
if ! command -v nvim >/dev/null 2>&1 && [[ -x /usr/local/bin/nvim ]]; then
|
|
export PATH="/usr/local/bin:$PATH"
|
|
fi
|
|
|
|
{{ else -}}
|
|
die "unsupported os_family: {{ .os_family }}"
|
|
{{ end -}}
|
|
|
|
# --------------------------- OH-MY-ZSH -------------------------------------
|
|
if [[ ! -d "$USER_HOME/.oh-my-zsh" ]]; then
|
|
log "installing oh-my-zsh (unattended)"
|
|
RUNZSH=no CHSH=no sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
|
|
else
|
|
log "oh-my-zsh already installed"
|
|
fi
|
|
|
|
# zsh plugins (loaded by .zshrc)
|
|
install_zsh_plugin() {
|
|
local repo="$1" target="$2"
|
|
if [[ -d "$target" ]]; then
|
|
log "plugin already present: $target"
|
|
else
|
|
log "cloning plugin: $repo"
|
|
git clone --depth 1 "https://github.com/$repo.git" "$target"
|
|
fi
|
|
}
|
|
|
|
install_zsh_plugin zsh-users/zsh-autosuggestions "$ZSH_CUSTOM/plugins/zsh-autosuggestions"
|
|
install_zsh_plugin zsh-users/zsh-syntax-highlighting "$ZSH_CUSTOM/plugins/zsh-syntax-highlighting"
|
|
install_zsh_plugin zsh-users/zsh-history-substring-search "$ZSH_CUSTOM/plugins/zsh-history-substring-search"
|
|
install_zsh_plugin Aloxaf/fzf-tab "$ZSH_CUSTOM/plugins/fzf-tab"
|
|
|
|
# --------------------------- TPM (tmux plugin manager) ---------------------
|
|
if [[ ! -d "$USER_HOME/.tmux/plugins/tpm" ]]; then
|
|
log "cloning tmux plugin manager"
|
|
mkdir -p "$USER_HOME/.tmux/plugins"
|
|
git clone --depth 1 https://github.com/tmux-plugins/tpm "$USER_HOME/.tmux/plugins/tpm"
|
|
else
|
|
log "tpm already installed"
|
|
fi
|
|
|
|
# --------------------------- Maple Mono NF font ----------------------------
|
|
# nvim init.lua references "Maple Mono NF". Install it so the default font works.
|
|
# Pin Maple-font version. Bump manually if a release breaks things.
|
|
MAPLE_FONT_VERSION="v7.9"
|
|
|
|
# fc-list check: must match the EXACT family "Maple Mono NF", not substrings.
|
|
# `*Maple Mono NF*` glob / `grep -q "Maple Mono NF"` substring matches would also
|
|
# match "Maple Mono NF CN" (the CJK variant) and incorrectly skip the install.
|
|
# byte and kaiser both have system-wide `MapleMono-NF-CN` from a maple-font
|
|
# package install but no Latin-only `MapleMono-NF`, and the substring check
|
|
# silently let that slide. Use `fc-list : family` to print one family per line
|
|
# (no style suffixes), `grep -qxF` to match the exact whole line.
|
|
#
|
|
# Why not `fc-match "Maple Mono NF"`? fc-match reports a substitute if the family
|
|
# isn't installed (falls back to system sans), so it can't distinguish "NF
|
|
# installed" from "NF not installed, fell back to Noto/Liberation/DejaVu".
|
|
if fc-list : family 2>/dev/null | grep -qxF 'Maple Mono NF'; then
|
|
log "Maple Mono NF already installed"
|
|
else
|
|
{{ if eq .os_family "arch" -}}
|
|
# Try paru first (clean install via AUR). If it fails (e.g. sudo TTY prompt
|
|
# in non-interactive runs, or AUR helper missing), fall back to downloading
|
|
# the upstream zip release directly — no sudo needed.
|
|
FONT_DIR="$USER_HOME/.local/share/fonts/maple-mono-nf"
|
|
mkdir -p "$FONT_DIR"
|
|
TMP_ZIP="$(mktemp /tmp/maple-font.XXXXXX.zip)"
|
|
if curl -fL --retry 3 \
|
|
"https://github.com/subframe7536/Maple-font/releases/download/${MAPLE_FONT_VERSION}/MapleMono-NF.zip" \
|
|
-o "$TMP_ZIP"; then
|
|
log "extracting font files"
|
|
unzip -q -o "$TMP_ZIP" -d "$FONT_DIR"
|
|
rm -f "$TMP_ZIP"
|
|
log "refreshing font cache (fc-cache)"
|
|
if command -v fc-cache >/dev/null 2>&1; then
|
|
fc-cache -fv >/dev/null
|
|
else
|
|
log "WARNING: fc-cache not found — install fontconfig package"
|
|
fi
|
|
else
|
|
log "WARNING: failed to download Maple Mono NF from GitHub"
|
|
log "manual install: https://github.com/subframe7536/Maple-font"
|
|
rm -f "$TMP_ZIP"
|
|
fi
|
|
{{ else if eq .os_family "debian" -}}
|
|
log "downloading MapleMono-NF.zip from subframe7536/Maple-font $MAPLE_FONT_VERSION"
|
|
FONT_DIR="$USER_HOME/.local/share/fonts/maple-mono-nf"
|
|
mkdir -p "$FONT_DIR"
|
|
TMP_ZIP="$(mktemp /tmp/maple-font.XXXXXX.zip)"
|
|
if curl -fL --retry 3 \
|
|
"https://github.com/subframe7536/Maple-font/releases/download/${MAPLE_FONT_VERSION}/MapleMono-NF.zip" \
|
|
-o "$TMP_ZIP"; then
|
|
log "extracting font files"
|
|
unzip -q -o "$TMP_ZIP" -d "$FONT_DIR"
|
|
rm -f "$TMP_ZIP"
|
|
log "refreshing font cache (fc-cache)"
|
|
if command -v fc-cache >/dev/null 2>&1; then
|
|
fc-cache -fv >/dev/null
|
|
else
|
|
log "WARNING: fc-cache not found — install fontconfig package"
|
|
fi
|
|
else
|
|
log "WARNING: failed to download Maple Mono NF — nvim will warn about missing font"
|
|
log "manual install: https://github.com/subframe7536/Maple-font"
|
|
rm -f "$TMP_ZIP"
|
|
fi
|
|
{{ else -}}
|
|
log "WARNING: font install not configured for os_family={{ .os_family }}"
|
|
{{ end -}}
|
|
fi
|
|
|
|
# --------------------------- set zsh as default shell ---------------------
|
|
USER_SHELL="$(getent passwd "$(whoami)" | cut -d: -f7)"
|
|
if [[ "$USER_SHELL" != "$(command -v zsh)" ]]; then
|
|
log "changing login shell to zsh"
|
|
sudo chsh -s "$(command -v zsh)" "$(whoami)"
|
|
else
|
|
log "zsh already the login shell"
|
|
fi
|
|
|
|
log "all user packages installed"
|
|
zsh --version
|
|
nvim --version | head -1
|
|
tmux -V |