Created
July 3, 2026 22:16
-
-
Save thrila/ad24f5d0e203719cc97b8efc28429897 to your computer and use it in GitHub Desktop.
Auto Install
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #!/usr/bin/env bash | |
| # | |
| # bootstrap.sh — fresh machine setup for macOS or Linux (Ubuntu/Debian, Fedora/RHEL, | |
| # Arch, openSUSE) | |
| # Installs: Homebrew, Chrome, VLC, OBS, WezTerm, Ghostty, Neovim (+deps), Docker, Zed | |
| # | |
| # Run as your normal user, NOT as root — the script calls sudo itself | |
| # whenever it needs privileges (and only asks for your password once). | |
| # | |
| # Usage: chmod +x bootstrap.sh && ./bootstrap.sh | |
| # | |
| set -euo pipefail | |
| # --------------------------------------------------------------------------- | |
| # Dotfiles config — fill this in | |
| # --------------------------------------------------------------------------- | |
| DOTFILES_REPO="git@github.com:yourusername/dotfiles.git" # <-- CHANGE ME | |
| DOTFILES_DIR="$HOME/.dotfiles" | |
| DOTFILES_NVIM_SUBDIR="nvim" | |
| DOTFILES_GHOSTTY_SUBDIR="ghostty" | |
| log() { printf "\033[1;32m==>\033[0m %s\n" "$1"; } | |
| warn() { printf "\033[1;33m==>\033[0m %s\n" "$1"; } | |
| err() { printf "\033[1;31m==>\033[0m %s\n" "$1" >&2; } | |
| OS="$(uname -s)" | |
| if [[ "$EUID" -eq 0 ]]; then | |
| err "Don't run this as root/with sudo — run it as your normal user." | |
| exit 1 | |
| fi | |
| # Suppress every interactive prompt apt/dpkg might throw (needrestart's | |
| # "restart services?" dialog is the usual silent offender on fresh Ubuntu). | |
| export DEBIAN_FRONTEND=noninteractive | |
| export NEEDRESTART_MODE=a | |
| export NEEDRESTART_SUSPEND=1 | |
| # --------------------------------------------------------------------------- | |
| # Ask for sudo once, then keep it alive in the background for the whole | |
| # script so nothing prompts for a password a second time. | |
| # --------------------------------------------------------------------------- | |
| request_sudo() { | |
| log "Requesting sudo access (you'll only be asked once)..." | |
| sudo -v | |
| ( while true; do | |
| sudo -n true | |
| sleep 60 | |
| kill -0 "$$" 2>/dev/null || exit | |
| done | |
| ) & | |
| SUDO_KEEPALIVE_PID=$! | |
| trap 'kill "$SUDO_KEEPALIVE_PID" 2>/dev/null || true' EXIT | |
| } | |
| # --------------------------------------------------------------------------- | |
| # 1. Linux package-manager detection | |
| # --------------------------------------------------------------------------- | |
| # PM_FAMILY drives which branch app installers take: debian / fedora / arch / suse | |
| # PM is the actual binary to invoke (yay/paru take priority over bare pacman | |
| # on Arch so AUR packages "just work"). | |
| PM="" | |
| PM_FAMILY="" | |
| detect_pkg_manager() { | |
| if command -v yay >/dev/null 2>&1; then | |
| PM="yay"; PM_FAMILY="arch" | |
| elif command -v paru >/dev/null 2>&1; then | |
| PM="paru"; PM_FAMILY="arch" | |
| elif command -v pacman >/dev/null 2>&1; then | |
| PM="pacman"; PM_FAMILY="arch" | |
| elif command -v apt-get >/dev/null 2>&1; then | |
| PM="apt-get"; PM_FAMILY="debian" | |
| elif command -v dnf >/dev/null 2>&1; then | |
| PM="dnf"; PM_FAMILY="fedora" | |
| elif command -v yum >/dev/null 2>&1; then | |
| PM="yum"; PM_FAMILY="fedora" | |
| elif command -v zypper >/dev/null 2>&1; then | |
| PM="zypper"; PM_FAMILY="suse" | |
| elif command -v apk >/dev/null 2>&1; then | |
| PM="apk"; PM_FAMILY="alpine" | |
| else | |
| err "No supported package manager found (looked for yay/paru/pacman/apt/dnf/yum/zypper/apk)." | |
| exit 1 | |
| fi | |
| log "Detected package manager: $PM ($PM_FAMILY family)" | |
| } | |
| # Arch package install helper. yay/paru must run as a normal user (they | |
| # refuse under sudo and escalate internally); bare pacman needs sudo | |
| # explicitly. Using "$PM" -S directly everywhere would break the moment | |
| # PM falls back to pacman, so every Arch install goes through this. | |
| arch_install() { | |
| if [[ "$PM" == "pacman" ]]; then | |
| sudo pacman -S --noconfirm --needed "$@" | |
| else | |
| "$PM" -S --noconfirm --needed "$@" | |
| fi | |
| } | |
| # On Arch, bootstrap an AUR helper if neither yay nor paru is present — | |
| # several of our apps (chrome on all setups, wezterm/ghostty as a fallback) | |
| # live in the AUR. Tries yay-bin first, falls back to paru-bin if that fails. | |
| ensure_aur_helper() { | |
| [[ "$PM_FAMILY" == "arch" ]] || return 0 | |
| [[ "$PM" == "yay" || "$PM" == "paru" ]] && return 0 | |
| sudo pacman -Sy --noconfirm --needed base-devel git | |
| local tmp; tmp="$(mktemp -d)" | |
| log "No AUR helper found — bootstrapping yay..." | |
| if git clone --depth 1 https://aur.archlinux.org/yay-bin.git "$tmp/yay-bin" \ | |
| && ( cd "$tmp/yay-bin" && makepkg -si --noconfirm ); then | |
| PM="yay" | |
| else | |
| warn "yay-bin build failed — trying paru-bin instead..." | |
| rm -rf "$tmp"; tmp="$(mktemp -d)" | |
| if git clone --depth 1 https://aur.archlinux.org/paru-bin.git "$tmp/paru-bin" \ | |
| && ( cd "$tmp/paru-bin" && makepkg -si --noconfirm ); then | |
| PM="paru" | |
| else | |
| err "Could not bootstrap an AUR helper (yay or paru). AUR-only packages (Chrome) will fail." | |
| PM="pacman" | |
| fi | |
| fi | |
| rm -rf "$tmp" | |
| } | |
| # --------------------------------------------------------------------------- | |
| # 2. Base OS setup | |
| # --------------------------------------------------------------------------- | |
| install_xcode_clt() { | |
| if xcode-select -p >/dev/null 2>&1; then | |
| log "Xcode Command Line Tools already installed." | |
| return | |
| fi | |
| log "Installing Xcode Command Line Tools (non-interactively)..." | |
| local trigger="/tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress" | |
| touch "$trigger" | |
| local label | |
| label="$(softwareupdate -l 2>/dev/null \ | |
| | grep -B1 -E '\*.*Command Line Tools' \ | |
| | awk -F'\* ' '/^\ *\*/ {print $2}' \ | |
| | sed 's/^ *//;s/ *$//' \ | |
| | tail -n1)" | |
| if [[ -n "$label" ]]; then | |
| sudo softwareupdate -i "$label" --agree-to-license | |
| rm -f "$trigger" | |
| else | |
| rm -f "$trigger" | |
| warn "Could not auto-detect the CLT package name — falling back to interactive install." | |
| xcode-select --install | |
| read -r -p "Press [Enter] once the popup installer finishes > " | |
| fi | |
| } | |
| install_linux_prereqs() { | |
| log "Installing base build tools via $PM..." | |
| case "$PM_FAMILY" in | |
| debian) | |
| sudo apt-get update -y | |
| sudo apt-get install -y build-essential procps curl file git wget \ | |
| apt-transport-https ca-certificates gnupg lsb-release software-properties-common | |
| ;; | |
| fedora) | |
| sudo "$PM" groupinstall -y "Development Tools" 2>/dev/null \ | |
| || sudo "$PM" group install -y "development-tools" 2>/dev/null \ | |
| || warn "Couldn't install the Development Tools group — continuing anyway." | |
| sudo "$PM" install -y curl file git wget gnupg2 which dnf-plugins-core | |
| ;; | |
| arch) | |
| sudo pacman -Sy --noconfirm --needed base-devel curl file git wget gnupg which | |
| ensure_aur_helper | |
| ;; | |
| suse) | |
| sudo zypper --non-interactive install -t pattern devel_basis | |
| sudo zypper --non-interactive install curl file git wget gpg2 which | |
| ;; | |
| alpine) | |
| sudo apk add --no-cache build-base curl file git wget gnupg | |
| ;; | |
| esac | |
| } | |
| install_homebrew() { | |
| if command -v brew >/dev/null 2>&1; then | |
| log "Homebrew already installed." | |
| else | |
| log "Installing Homebrew..." | |
| NONINTERACTIVE=1 /bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" | |
| fi | |
| if [[ "$OS" == "Darwin" ]]; then | |
| if [[ -d /opt/homebrew/bin ]]; then | |
| eval "$(/opt/homebrew/bin/brew shellenv)" | |
| else | |
| eval "$(/usr/local/bin/brew shellenv)" | |
| fi | |
| else | |
| eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)" | |
| fi | |
| local profile="$HOME/.profile" | |
| [[ -n "${ZSH_VERSION:-}" ]] && profile="$HOME/.zprofile" | |
| if ! grep -q "brew shellenv" "$profile" 2>/dev/null; then | |
| if [[ "$OS" == "Darwin" ]]; then | |
| echo 'eval "$(/opt/homebrew/bin/brew shellenv)"' >> "$profile" | |
| else | |
| echo 'eval "$(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"' >> "$profile" | |
| fi | |
| fi | |
| } | |
| # --------------------------------------------------------------------------- | |
| # 3. Dotfiles | |
| # --------------------------------------------------------------------------- | |
| clone_dotfiles() { | |
| if [[ "$DOTFILES_REPO" == *"yourusername"* ]]; then | |
| warn "DOTFILES_REPO is still a placeholder — skipping dotfiles clone/link." | |
| warn "Edit DOTFILES_REPO at the top of this script and re-run." | |
| return 1 | |
| fi | |
| if [[ -d "$DOTFILES_DIR/.git" ]]; then | |
| log "Dotfiles repo already cloned at $DOTFILES_DIR, pulling latest..." | |
| git -C "$DOTFILES_DIR" pull --ff-only | |
| else | |
| log "Cloning dotfiles from $DOTFILES_REPO..." | |
| git clone "$DOTFILES_REPO" "$DOTFILES_DIR" | |
| fi | |
| } | |
| link_config() { | |
| local subdir="$1" | |
| local target="$HOME/.config/$subdir" | |
| local source="$DOTFILES_DIR/$subdir" | |
| if [[ ! -d "$source" ]]; then | |
| warn "No $subdir/ found in dotfiles repo — skipping link." | |
| return | |
| fi | |
| mkdir -p "$HOME/.config" | |
| if [[ -L "$target" ]]; then | |
| rm "$target" | |
| elif [[ -e "$target" ]]; then | |
| warn "Backing up existing $target -> $target.bak" | |
| mv "$target" "$target.bak" | |
| fi | |
| ln -s "$source" "$target" | |
| log "Linked $target -> $source" | |
| } | |
| # --------------------------------------------------------------------------- | |
| # 4. App installers (each branches on PM_FAMILY for Linux) | |
| # --------------------------------------------------------------------------- | |
| install_chrome() { | |
| # Chrome is not in Ubuntu/Debian, Fedora, openSUSE, or Arch's default | |
| # repos (Google only ships their own repo/AUR package) — every branch | |
| # here pulls straight from Google or the AUR, confirmed against each | |
| # distro's official package search. | |
| if [[ "$OS" == "Darwin" ]]; then | |
| brew install --cask google-chrome | |
| return | |
| fi | |
| if command -v google-chrome >/dev/null 2>&1 || command -v google-chrome-stable >/dev/null 2>&1; then | |
| log "Chrome already installed." | |
| return | |
| fi | |
| case "$PM_FAMILY" in | |
| debian) | |
| wget -q -O /tmp/chrome.deb https://dl.google.com/linux/direct/google-chrome-stable_current_amd64.deb | |
| sudo apt-get install -y /tmp/chrome.deb | |
| rm -f /tmp/chrome.deb | |
| ;; | |
| fedora) | |
| sudo "$PM" install -y https://dl.google.com/linux/direct/google-chrome-stable_current_x86_64.rpm | |
| ;; | |
| arch) | |
| arch_install google-chrome | |
| ;; | |
| suse) | |
| sudo rpm --import https://dl.google.com/linux/linux_signing_key.pub | |
| sudo zypper ar -f https://dl.google.com/linux/chrome/rpm/stable/x86_64 google-chrome | |
| sudo zypper --non-interactive install google-chrome-stable | |
| ;; | |
| *) | |
| warn "Don't know how to install Chrome on this distro — skipping." | |
| ;; | |
| esac | |
| } | |
| install_vlc() { | |
| if [[ "$OS" == "Darwin" ]]; then | |
| brew install --cask vlc | |
| return | |
| fi | |
| case "$PM_FAMILY" in | |
| debian) sudo apt-get install -y vlc ;; | |
| fedora) | |
| sudo "$PM" install -y https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-"$(rpm -E %fedora)".noarch.rpm 2>/dev/null || true | |
| sudo "$PM" install -y vlc | |
| ;; | |
| arch) arch_install vlc ;; | |
| suse) sudo zypper --non-interactive install vlc-noX 2>/dev/null || sudo zypper --non-interactive install vlc ;; | |
| *) warn "Don't know how to install VLC on this distro — skipping." ;; | |
| esac | |
| } | |
| install_obs() { | |
| if [[ "$OS" == "Darwin" ]]; then | |
| brew install --cask obs | |
| return | |
| fi | |
| case "$PM_FAMILY" in | |
| debian) | |
| sudo add-apt-repository -y ppa:obsproject/obs-studio | |
| sudo apt-get update -y | |
| sudo apt-get install -y obs-studio | |
| ;; | |
| fedora) | |
| sudo "$PM" install -y https://download1.rpmfusion.org/free/fedora/rpmfusion-free-release-"$(rpm -E %fedora)".noarch.rpm 2>/dev/null || true | |
| sudo "$PM" install -y obs-studio | |
| ;; | |
| arch) arch_install obs-studio ;; | |
| suse) sudo zypper --non-interactive install obs-studio ;; | |
| *) warn "Don't know how to install OBS on this distro — skipping." ;; | |
| esac | |
| } | |
| install_wezterm() { | |
| if [[ "$OS" == "Darwin" ]]; then | |
| brew install --cask wezterm | |
| return | |
| fi | |
| case "$PM_FAMILY" in | |
| debian) | |
| curl -fsSL https://apt.fury.io/wez/gpg.key | sudo gpg --yes --dearmor -o /usr/share/keyrings/wezterm-fury.gpg | |
| echo 'deb [signed-by=/usr/share/keyrings/wezterm-fury.gpg] https://apt.fury.io/wez/ * *' | \ | |
| sudo tee /etc/apt/sources.list.d/wezterm.list | |
| sudo apt-get update -y | |
| sudo apt-get install -y wezterm | |
| ;; | |
| fedora) | |
| sudo dnf copr enable -y wezfurlong/wezterm-nightly | |
| sudo "$PM" install -y wezterm | |
| ;; | |
| arch) arch_install wezterm ;; | |
| suse) warn "No official WezTerm package for openSUSE — install manually from wezfurlong.org/wezterm" ;; | |
| *) warn "Don't know how to install WezTerm on this distro — skipping." ;; | |
| esac | |
| } | |
| install_ghostty() { | |
| if [[ "$OS" == "Darwin" ]]; then | |
| brew install --cask ghostty | |
| link_config "$DOTFILES_GHOSTTY_SUBDIR" | |
| return | |
| fi | |
| case "$PM_FAMILY" in | |
| arch) | |
| arch_install ghostty | |
| ;; | |
| fedora) | |
| sudo dnf copr enable -y scottames/ghostty | |
| sudo "$PM" install -y ghostty | |
| ;; | |
| *) | |
| if brew install ghostty 2>/dev/null; then | |
| log "Ghostty installed via brew." | |
| else | |
| warn "No packaged Ghostty build for this distro yet." | |
| warn "Install manually from https://ghostty.org/download" | |
| fi | |
| ;; | |
| esac | |
| link_config "$DOTFILES_GHOSTTY_SUBDIR" | |
| } | |
| install_neovim() { | |
| log "Installing Neovim + common dependencies via brew..." | |
| brew install neovim ripgrep fd git node python lazygit fzf | |
| link_config "$DOTFILES_NVIM_SUBDIR" | |
| } | |
| install_docker() { | |
| if [[ "$OS" == "Darwin" ]]; then | |
| brew install --cask docker-desktop | |
| warn "Docker Desktop installed — open it once from /Applications to finish setup." | |
| return | |
| fi | |
| if command -v docker >/dev/null 2>&1; then | |
| log "Docker already installed." | |
| return | |
| fi | |
| # Docker's own docs mark the get.docker.com convenience script as | |
| # "testing/dev only" and recommend installing from Docker's official | |
| # package repo instead — that's what each branch below does. | |
| case "$PM_FAMILY" in | |
| debian) | |
| log "Installing Docker Engine via Docker's official apt repo..." | |
| for pkg in docker.io docker-doc docker-compose docker-compose-v2 podman-docker containerd runc; do | |
| sudo apt-get remove -y "$pkg" 2>/dev/null || true | |
| done | |
| sudo apt-get update -y | |
| sudo apt-get install -y ca-certificates curl | |
| sudo install -m 0755 -d /etc/apt/keyrings | |
| sudo curl -fsSL https://download.docker.com/linux/ubuntu/gpg -o /etc/apt/keyrings/docker.asc | |
| sudo chmod a+r /etc/apt/keyrings/docker.asc | |
| echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.asc] https://download.docker.com/linux/ubuntu $(. /etc/os-release && echo "${UBUNTU_CODENAME:-$VERSION_CODENAME}") stable" | \ | |
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null | |
| sudo apt-get update -y | |
| sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin | |
| ;; | |
| fedora) | |
| log "Installing Docker Engine via Docker's official dnf repo..." | |
| for pkg in docker docker-client docker-client-latest docker-common docker-latest \ | |
| docker-latest-logrotate docker-logrotate docker-selinux docker-engine-selinux docker-engine; do | |
| sudo "$PM" remove -y "$pkg" 2>/dev/null || true | |
| done | |
| sudo dnf config-manager --add-repo https://download.docker.com/linux/fedora/docker-ce.repo 2>/dev/null \ | |
| || sudo dnf config-manager addrepo --from-repofile=https://download.docker.com/linux/fedora/docker-ce.repo | |
| sudo "$PM" install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin | |
| ;; | |
| arch) | |
| log "Installing Docker via the official Arch [extra] repo..." | |
| arch_install docker docker-compose docker-buildx | |
| ;; | |
| suse) | |
| log "Installing Docker via zypper..." | |
| sudo zypper --non-interactive install docker docker-compose | |
| ;; | |
| *) | |
| warn "Don't know the native Docker install path for this distro — falling back to the convenience script (dev/test only)." | |
| curl -fsSL https://get.docker.com | sudo sh | |
| ;; | |
| esac | |
| sudo systemctl enable --now docker 2>/dev/null || true | |
| sudo usermod -aG docker "$USER" | |
| warn "Log out/in (or run 'newgrp docker') for the docker group change to apply." | |
| } | |
| install_zed() { | |
| if [[ "$OS" == "Darwin" ]]; then | |
| brew install --cask zed | |
| else | |
| log "Installing Zed via official installer..." | |
| curl -fsSL https://zed.dev/install.sh | sh | |
| fi | |
| } | |
| # --------------------------------------------------------------------------- | |
| # 5. Main | |
| # --------------------------------------------------------------------------- | |
| main() { | |
| request_sudo | |
| case "$OS" in | |
| Darwin) | |
| log "Detected macOS." | |
| install_xcode_clt | |
| install_homebrew | |
| ;; | |
| Linux) | |
| log "Detected Linux." | |
| detect_pkg_manager | |
| install_linux_prereqs | |
| install_homebrew | |
| ;; | |
| *) | |
| err "Unsupported OS: $OS" | |
| exit 1 | |
| ;; | |
| esac | |
| log "Setting up dotfiles..." | |
| clone_dotfiles || true | |
| log "Installing apps..." | |
| install_chrome | |
| install_vlc | |
| install_obs | |
| install_wezterm | |
| install_ghostty | |
| install_neovim | |
| install_docker | |
| install_zed | |
| log "Done. Restart your terminal (or 'source ~/.profile' / ~/.zprofile) to pick up brew's PATH." | |
| } | |
| main "$@" |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment