On RHEL-family distros without EPEL enabled and without cargo, the PM loop fails for bat/zoxide/git-delta and there was no fallback, so they ended up skipped/FAIL. Now, after the PM/cargo attempts fail, download the latest static release tarball from GitHub instead, matching the existing pattern for eza/fastfetch/starship/lazydocker. - Add _gh_latest_tag(): resolve latest release tag from the /releases/latest redirect (no GitHub API, no rate limits) - Add _gh_install_bin(): build the asset URL from a versioned template, download, extract, and cp the named binary into ~/.local/bin - Handle per-repo tag conventions: bat/zoxide tags are 'v0.x' while delta tags are bare '0.x'; keep the raw tag for the download path and strip the optional 'v' only for asset-filename substitution - Add target_musl/target_gnu per-arch (x86_64/aarch64/armv7l) - bat/zoxide use static musl builds (no glibc deps); delta ships gnu-only - Verbose 'gh installing <bin> (<tag> from <repo>)' / FAIL lines - Bump ZSHRC_BOOTSTRAP_VERSION 3 -> 4 so existing boxes re-run the bootstrap and pick up the new fallbacks Verified end-to-end: bat 0.26.1, zoxide 0.10.0, delta 0.19.2 all download, extract, install, and run from a temp HOME.
255 lines
10 KiB
Bash
255 lines
10 KiB
Bash
export ZSH_CONFIG=$HOME/.config/zsh
|
|
export OHMYZSH=$ZSH_CONFIG/oh-my-zsh
|
|
export OHMYZSH_CUSTOM=$OHMYZSH/custom
|
|
|
|
# Bootstrap banner — built dynamically so the box stays aligned regardless
|
|
# of the version string length or terminal width.
|
|
_bootstrap_banner() {
|
|
local inner=61 # inner width (between the vertical bars)
|
|
local pad line
|
|
local title="Zsh bootstrap - one-time environment setup (v${ZSHRC_BOOTSTRAP_VERSION})"
|
|
local rule=$(printf '═%.0s' {1..$inner})
|
|
local mid=$(printf '─%.0s' {1..$inner})
|
|
# left-pad the title by 2 spaces so it doesn't hug the left bar
|
|
title=" $title"
|
|
pad=$(( inner - ${#title} ))
|
|
(( pad < 0 )) && pad=0
|
|
echo ""
|
|
echo "╔${rule}╗"
|
|
printf '║%s%*s║\n' "$title" "$pad" ""
|
|
echo "╠${mid}╣"
|
|
while IFS= read -r line; do
|
|
line=" $line"
|
|
pad=$(( inner - ${#line} ))
|
|
(( pad < 0 )) && pad=0
|
|
printf '║%s%*s║\n' "$line" "$pad" ""
|
|
done <<'EOF'
|
|
This runs on first login or when the bootstrap version
|
|
changes. It installs missing CLI tools and clones oh-my-zsh
|
|
+ plugins across distros. Network calls time out after
|
|
~30s so an unreachable host skips silently. The first run
|
|
may take a minute; subsequent runs are a no-op.
|
|
EOF
|
|
echo "╚${rule}╝"
|
|
echo ""
|
|
}
|
|
_bootstrap_banner
|
|
unfunction _bootstrap_banner
|
|
|
|
# GitHub-release binary fallback. Used for tools not packaged in a distro's
|
|
# repos (e.g. bat/zoxide/git-delta on RHEL-family without EPEL, or no cargo).
|
|
# Resolves the latest release tag via the /releases/latest redirect (no API,
|
|
# no rate limits), builds the asset URL from a template, extracts, and copies
|
|
# the named binary into ~/.local/bin. Args:
|
|
# $1 owner/repo $2 asset template (%v = version, no leading v)
|
|
# $3 binary name $4 arch suffix to substitute into %a in the template
|
|
_gh_latest_tag() {
|
|
local repo="$1"
|
|
curl -fsIL --connect-timeout 5 --max-time 20 "https://github.com/$repo/releases/latest" 2>/dev/null \
|
|
| grep -i '^location:' | tail -1 \
|
|
| sed -E 's#.*tag/([^[:space:]"/]+).*#\1#'
|
|
}
|
|
|
|
_gh_install_bin() {
|
|
local repo="$1" tmpl="$2" bin="$3"
|
|
local rawtag ver asset ext tmp found
|
|
rawtag=$(_gh_latest_tag "$repo")
|
|
if [[ -z "$rawtag" ]]; then
|
|
echo " FAIL could not resolve latest tag for $repo"; return 1
|
|
fi
|
|
ver="${rawtag#v}" # strip optional leading v for asset-filename substitution
|
|
asset="${tmpl//\%v/$ver}"
|
|
ext="${asset##*.}"
|
|
echo " gh installing $bin ($rawtag from $repo)"
|
|
tmp=$(mktemp -d)
|
|
if curl -fsSL --connect-timeout 5 --max-time 60 -o "$tmp/pkg.$ext" \
|
|
"https://github.com/$repo/releases/download/$rawtag/$asset" \
|
|
&& { case "$ext" in
|
|
gz) tar -xzf "$tmp/pkg.$ext" -C "$tmp" 2>/dev/null ;;
|
|
*) return 1 ;;
|
|
esac; }; then
|
|
found=$(find "$tmp" -type f -name "$bin" ! -name "*.*" | head -1)
|
|
if [[ -n "$found" ]]; then
|
|
cp "$found" "$HOME/.local/bin/$bin"
|
|
chmod +x "$HOME/.local/bin/$bin"
|
|
rm -rf "$tmp"; return 0
|
|
fi
|
|
fi
|
|
rm -rf "$tmp"
|
|
echo " FAIL could not download/extract $bin from $repo"; return 1
|
|
}
|
|
|
|
touch ~/.hushlogin
|
|
mkdir -p "$HOME/.local/bin"
|
|
mkdir -p "$ZSH_CONFIG"
|
|
path+=("$HOME/.local/bin")
|
|
|
|
# Package availability checks can hang on slow/unreachable mirrors (cold dnf
|
|
# metadata, apt update, pacman -Sy). Wrap them in a 20s timeout so the bootstrap
|
|
# never stalls the shell; a timed-out check just falls through to the next PM
|
|
# or to the binary installers below. Verbose output so real builds (cargo) and
|
|
# slow installs are visible as progress instead of an apparent hang.
|
|
for p in git starship fzf eza rsync lazydocker fastfetch tmux bat zoxide; do
|
|
if command -v $p >/dev/null; then
|
|
echo " ok $p (already installed)"
|
|
continue
|
|
fi
|
|
echo " ... $p (checking package managers)"
|
|
if command -v brew >/dev/null; then
|
|
echo " brew installing $p" && brew install $p && continue
|
|
fi
|
|
if command -v apt >/dev/null; then
|
|
if timeout 20 apt-cache show $p >/dev/null 2>&1; then
|
|
echo " apt installing $p" && sudo apt install -y $p && continue
|
|
else
|
|
echo " apt $p not available (or metadata check timed out)"
|
|
fi
|
|
fi
|
|
if command -v dnf >/dev/null; then
|
|
if timeout 20 dnf list available $p >/dev/null 2>&1; then
|
|
echo " dnf installing $p" && sudo dnf install -y $p && continue
|
|
else
|
|
echo " dnf $p not available (or metadata check timed out)"
|
|
fi
|
|
fi
|
|
if command -v pacman >/dev/null; then
|
|
if timeout 20 pacman -Si $p >/dev/null 2>&1; then
|
|
echo " pacman installing $p" && sudo pacman -Sy --noconfirm $p && continue
|
|
else
|
|
echo " pacman $p not available (or metadata check timed out)"
|
|
fi
|
|
fi
|
|
if command -v cargo >/dev/null; then
|
|
echo " cargo installing $p (may take a few minutes to compile)" && cargo install $p --locked && continue
|
|
fi
|
|
echo " FAIL could not install $p by any method; skipping"
|
|
done
|
|
|
|
if ! command -v starship >/dev/null; then
|
|
echo " curl installing starship binary"
|
|
curl -fsSL --connect-timeout 5 --max-time 30 https://starship.rs/install.sh | sh -s -- -b ~/.local/bin -y
|
|
fi
|
|
|
|
if ! command -v lazydocker >/dev/null; then
|
|
echo " curl installing lazydocker binary"
|
|
curl -fsSL --connect-timeout 5 --max-time 30 https://raw.githubusercontent.com/jesseduffield/lazydocker/master/scripts/install_update_linux.sh | \
|
|
DIR=$HOME/.local/bin bash
|
|
fi
|
|
|
|
# Debian/Ubuntu ships bat as 'batcat' and fd as 'fdfind'; symlink to the
|
|
# canonical names so the zshrc integrations (which call bat/fd) work everywhere.
|
|
if ! command -v bat >/dev/null && command -v batcat >/dev/null; then
|
|
ln -sf "$(command -v batcat)" "$HOME/.local/bin/bat"
|
|
fi
|
|
if ! command -v fd >/dev/null && command -v fdfind >/dev/null; then
|
|
ln -sf "$(command -v fdfind)" "$HOME/.local/bin/fd"
|
|
fi
|
|
|
|
# git-delta: the package is named 'git-delta' on every package manager but the
|
|
# binary is 'delta' (name mismatch vs the install loop above), so install via the
|
|
# same PM cascade using the real package name. Used by zshrc as GIT_PAGER.
|
|
if ! command -v delta >/dev/null; then
|
|
echo " ... delta (checking package managers, package name is 'git-delta')"
|
|
if command -v brew >/dev/null; then
|
|
echo " brew installing git-delta" && brew install git-delta
|
|
elif command -v apt >/dev/null && timeout 20 apt-cache show git-delta >/dev/null 2>&1; then
|
|
echo " apt installing git-delta" && sudo apt install -y git-delta
|
|
elif command -v dnf >/dev/null && timeout 20 dnf list available git-delta >/dev/null 2>&1; then
|
|
echo " dnf installing git-delta" && sudo dnf install -y git-delta
|
|
elif command -v pacman >/dev/null && timeout 20 pacman -Si git-delta >/dev/null 2>&1; then
|
|
echo " pacman installing git-delta" && sudo pacman -Sy --noconfirm git-delta
|
|
elif command -v cargo >/dev/null; then
|
|
echo " cargo installing git-delta (may take a few minutes to compile)" && cargo install git-delta --locked
|
|
else
|
|
echo " FAIL could not install delta; skipping"
|
|
fi
|
|
fi
|
|
|
|
arch=$(uname -m)
|
|
target_musl=""; target_gnu=""
|
|
case "$arch" in
|
|
x86_64) target_fast="linux-amd64" && target_eza="x86_64-unknown-linux-gnu" \
|
|
&& target_musl="x86_64-unknown-linux-musl" && target_gnu="x86_64-unknown-linux-gnu" ;;
|
|
aarch64|arm64) target_fast="linux-aarch64" && target_eza="aarch64-unknown-linux-gnu" \
|
|
&& target_musl="aarch64-unknown-linux-musl" && target_gnu="aarch64-unknown-linux-gnu" ;;
|
|
armv7l) target_fast="linux-armv7l" && target_eza="armv7-unknown-linux-gnueabihf" \
|
|
&& target_musl="armv7-unknown-linux-musleabihf" && target_gnu="armv7-unknown-linux-gnueabihf" ;;
|
|
esac
|
|
|
|
if [[ -n $target_fast && -n $target_eza ]]; then
|
|
if ! command -v fastfetch >/dev/null; then
|
|
echo " curl installing fastfetch (github release tarball)"
|
|
if curl -fsSL --connect-timeout 5 --max-time 30 -o /tmp/fastfetch.tar.gz \
|
|
"https://github.com/fastfetch-cli/fastfetch/releases/latest/download/fastfetch-${target_fast}.tar.gz" \
|
|
&& tar -xzf /tmp/fastfetch.tar.gz -C /tmp 2>/dev/null; then
|
|
ffdir=$(find /tmp -maxdepth 1 -type d -name 'fastfetch*' | head -1)
|
|
if [[ -n "$ffdir" ]]; then
|
|
cp "$ffdir/usr/bin/fastfetch" "$HOME/.local/bin/"
|
|
chmod +x "$HOME/.local/bin/fastfetch"
|
|
mkdir -p "$HOME/.local/share/fastfetch"
|
|
cp -r "$ffdir/usr/share/fastfetch/"* "$HOME/.local/share/fastfetch/"
|
|
fi
|
|
rm -rf "$ffdir" /tmp/fastfetch.tar.gz
|
|
else
|
|
rm -f /tmp/fastfetch.tar.gz
|
|
echo " FAIL could not download/extract fastfetch; skipping"
|
|
fi
|
|
fi
|
|
|
|
if ! command -v eza >/dev/null; then
|
|
echo " curl installing eza (github release tarball)"
|
|
if curl -fsSL --connect-timeout 5 --max-time 30 -o /tmp/eza.tar.gz \
|
|
"https://github.com/eza-community/eza/releases/latest/download/eza_${target_eza}.tar.gz" \
|
|
&& tar -xzf /tmp/eza.tar.gz -C "$HOME/.local/bin/" 2>/dev/null; then
|
|
chmod +x "$HOME/.local/bin/eza"
|
|
else
|
|
rm -f /tmp/eza.tar.gz
|
|
echo " FAIL could not download/extract eza; skipping"
|
|
fi
|
|
rm -f /tmp/eza.tar.gz
|
|
fi
|
|
fi
|
|
|
|
# GitHub-release binary fallbacks for tools often missing from distro repos
|
|
# (e.g. bat/zoxide/git-delta on RHEL/Alma without EPEL, or boxes with no cargo).
|
|
# Tried after the PM/cargo loop above already failed for these. Static musl
|
|
# builds preferred so there are no glibc dependencies; delta ships gnu-only.
|
|
if ! command -v bat >/dev/null && [[ -n "$target_musl" ]]; then
|
|
_gh_install_bin "sharkdp/bat" "bat-v%v-$target_musl.tar.gz" "bat"
|
|
fi
|
|
|
|
if ! command -v zoxide >/dev/null && [[ -n "$target_musl" ]]; then
|
|
_gh_install_bin "ajeetdsouza/zoxide" "zoxide-%v-$target_musl.tar.gz" "zoxide"
|
|
fi
|
|
|
|
if ! command -v delta >/dev/null && [[ -n "$target_gnu" ]]; then
|
|
_gh_install_bin "dandavison/delta" "delta-%v-$target_gnu.tar.gz" "delta"
|
|
fi
|
|
|
|
mkdir -p "$HOME/.config/zsh"
|
|
|
|
if [ ! -d "$OHMYZSH" ]; then
|
|
echo " git cloning oh-my-zsh"
|
|
GIT_TERMINAL_PROMPT=0 git -c http.lowSpeedLimit=1000 -c http.lowSpeedTime=10 \
|
|
clone --depth=1 https://github.com/ohmyzsh/ohmyzsh.git $OHMYZSH || \
|
|
echo " FAIL could not clone oh-my-zsh"
|
|
fi
|
|
|
|
for plug in \
|
|
"zsh-autosuggestions|https://github.com/zsh-users/zsh-autosuggestions" \
|
|
"fast-syntax-highlighting|https://github.com/zdharma-continuum/fast-syntax-highlighting" \
|
|
"fzf-tab|https://github.com/Aloxaf/fzf-tab"
|
|
do
|
|
name="${plug%%|*}"
|
|
url="${plug##*|}"
|
|
dir="$OHMYZSH_CUSTOM/plugins/$name"
|
|
if [[ ! -d "$dir" ]]; then
|
|
echo " git cloning $name"
|
|
GIT_TERMINAL_PROMPT=0 git -c http.lowSpeedLimit=1000 -c http.lowSpeedTime=10 \
|
|
clone "$url" "$dir" 2>/dev/null || echo " FAIL could not clone $name"
|
|
fi
|
|
done
|
|
|
|
# Cleanup
|
|
[[ -d $HOME/.config/oh-my-zsh ]] && rm -rf "$HOME/.config/oh-my-zsh"
|