Skip to content

Instantly share code, notes, and snippets.

@howie
Last active July 13, 2025 15:44
Show Gist options
  • Select an option

  • Save howie/00ae971f711b610e02fa to your computer and use it in GitHub Desktop.

Select an option

Save howie/00ae971f711b610e02fa to your computer and use it in GitHub Desktop.
#!/bin/bash
# mac_setup.sh
# Author: Howie
# Description: Robust end-to-end macOS dev environment bootstrapper.
# Shell safety first
set -euo pipefail
IFS=$'\n\t'
###############################################################################
# Utility helpers #
###############################################################################
pretty_print() { printf "\n%b\n" "$1"; }
is_arm64() { [[ "$(uname -m)" == arm64* ]]; }
autobackup() {
local target=$1
[[ -f "$target" && ! -L "$target" ]] || return 0
local ts
ts=$(date +%Y%m%d-%H%M%S)
cp "$target" "${target}.bak.${ts}"
pretty_print "🔄 Backup ${target}.bak.${ts} created"
}
install_if_not_exist() {
local tool="${1:-}"
if [[ -z "$tool" ]]; then
echo "⚠️ install_if_not_exist called with no argument; skipping" >&2
return 1
fi
if ! command -v "$tool" &>/dev/null; then
pretty_print "Installing $tool …"
brew install "$tool"
else
echo "$tool already installed. Skipping."
fi
}
install_cask_if_not_exist() {
local app="$1"
if brew info --cask "$app" 2>&1 | grep -qi "has been disabled"; then
echo "⚠️ $app is disabled upstream. Skipping."
return 0
fi
if brew list --cask "$app" &>/dev/null; then
echo "$app (cask) already installed. Skipping."
else
pretty_print "Installing $app (cask)…"
brew install --cask "$app" || echo "⚠️ Failed to install $app; continuing"
fi
}
###############################################################################
# asdf_install_packages #
# Arguments: "tool" or "tool:version" (version optional / "latest") #
###############################################################################
asdf_install_packages() {
for spec in "$@"; do
local tool version set_latest
IFS=":" read -r tool version <<<"${spec}"
# Ensure plugin exists
if ! asdf plugin list | grep -q "^${tool}$"; then
pretty_print "Adding asdf plugin ${tool}"
asdf plugin add "${tool}" || { echo "❌ Unable to add plugin ${tool}" >&2; continue; }
fi
set_latest=false
if [[ -z "${version:-}" || "${version}" == "latest" ]]; then
version=$(asdf latest "${tool}" | head -n1 || true)
if [[ -z "${version}" ]]; then
echo "❌ Could not resolve latest for ${tool}; skipping" >&2
continue
fi
pretty_print "Resolved latest ${tool} → ${version}"
set_latest=true
fi
# Install if missing
if ! asdf list "${tool}" 2>/dev/null | grep -q "^ ${version}$"; then
pretty_print "Installing ${tool} ${version}…"
asdf install "${tool}" "${version}" || { echo "❌ Failed to install ${tool} ${version}" >&2; continue; }
else
echo "${tool} ${version} already installed. Skipping installation."
fi
# Activate
if [[ "${set_latest}" == true ]]; then
asdf set "${tool}" latest
else
asdf set "${tool}" "${version}"
fi
done
}
###############################################################################
# Step-by-step installation #
###############################################################################
pretty_print "🚀 Starting macOS environment setup…"
# 1. Xcode CLT ----------------------------------------------------------------
if [[ ! -d /Library/Developer/CommandLineTools ]]; then
xcode-select --install
until xcode-select -p &>/dev/null; do sleep 5; done
echo "Xcode Command Line Tools installed."
else
echo "Xcode Command Line Tools already present."
fi
# 2. Homebrew -----------------------------------------------------------------
if ! command -v brew &>/dev/null; then
pretty_print "Installing Homebrew…"
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
fi
# shellenv for current session
if is_arm64; then BREW_PREFIX="/opt/homebrew"; else BREW_PREFIX="/usr/local"; fi
eval "$(${BREW_PREFIX}/bin/brew shellenv)"
# 3. CLI tools ----------------------------------------------------------------
pretty_print "Installing CLI tools…"
brew update
for pkg in wget curl git git-lfs ctags ack jq tree coreutils findutils readline openssl zip unzip; do
install_if_not_exist "$pkg"
done
git lfs install
# 4. Prezto -------------------------------------------------------------------
if [[ ! -d "$HOME/.zprezto" ]]; then
pretty_print "Installing Prezto…"
git clone --recursive https://github.com/sorin-ionescu/prezto.git "$HOME/.zprezto"
zsh -c 'setopt EXTENDED_GLOB; for rcfile in "$HOME/.zprezto/runcoms"/^README.md(.N); do ln -s "$rcfile" "$HOME/.${rcfile:t}" 2>/dev/null; done'
else
echo "Prezto already installed."
fi
# 5. GUI apps -----------------------------------------------------------------
GUI_APPS=(jetbrains-toolbox visual-studio-code iterm2 vimr textmate windsurf cursor \
telegram discord dropbox google-drive onedrive microsoft-edge \
spotify vlc heptabase obsidian notion grammarly-desktop miro appcleaner)
pretty_print "Installing GUI apps…"
for app in "${GUI_APPS[@]}"; do install_cask_if_not_exist "$app"; done
# 6. asdf ---------------------------------------------------------------------
install_if_not_exist asdf
source "$(brew --prefix asdf)/libexec/asdf.sh"
asdf_install_packages \
"golang:latest" "terraform:latest" "rclone:latest" "gradle:latest" \
"python:latest" "nodejs:latest" "java:temurin-17.0.8+101"
# 7. SDKMAN! ------------------------------------------------------------------
SDKMAN_DIR="$HOME/.sdkman"
if [[ ! -d "$SDKMAN_DIR" ]]; then
pretty_print "Installing SDKMAN!…"
curl -s "https://get.sdkman.io" | bash || echo "⚠️ SDKMAN! install failed, continue"
fi
# 8. Android SDK --------------------------------------------------------------
install_if_not_exist android-commandlinetools
install_if_not_exist android-platform-tools
ANDROID_SDK_ROOT="$HOME/Library/Android/sdk"
CMDLINE_TOOLS_DIR="$ANDROID_SDK_ROOT/cmdline-tools/latest/bin"
mkdir -p "$CMDLINE_TOOLS_DIR"
# 9. Cloud CLI & misc ---------------------------------------------------------
for cloud in awscli azure-cli; do install_if_not_exist "$cloud"; done
install_if_not_exist orbstack
# 10. ffmpeg & QuickLook ------------------------------------------------------
brew tap homebrew-ffmpeg/ffmpeg
install_if_not_exist homebrew-ffmpeg/ffmpeg/ffmpeg
for ql in qlcolorcode qlstephen qlmarkdown quicklook-json suspicious-package apparency quicklookase qlvideo; do install_cask_if_not_exist "$ql"; done
# 11. Generate ~/.zshrc -------------------------------------------------------
autobackup "$HOME/.zshrc"
cat > "$HOME/.zshrc" <<'ZSHRC'
# ---------------------------------------------------------------------------
# 1. Prezto
# ---------------------------------------------------------------------------
source "$HOME/.zprezto/runcoms/zshrc"
# 若改用 YADR:
# for config_file ($HOME/.yadr/zsh/*.zsh) source $config_file
# ---------------------------------------------------------------------------
# 2. Homebrew (ARM Mac)
# ---------------------------------------------------------------------------
eval "$(/opt/homebrew/bin/brew shellenv)"
# ---------------------------------------------------------------------------
# 3. PATH
# ---------------------------------------------------------------------------
export PATH="$(brew --prefix coreutils)/libexec/gnubin:$PATH"
export ANDROID_SDK_ROOT="$HOME/Library/Android/sdk"
export PATH="$ANDROID_SDK_ROOT/cmdline-tools/latest/bin:$PATH"
# ---------------------------------------------------------------------------
# 4. asdf & Plugins
# ---------------------------------------------------------------------------
. "$(brew --prefix asdf)/libexec/asdf.sh"
plugins+=(zsh-autosuggestions)
# ---------------------------------------------------------------------------
# 5. Prompt & Aliases
# ---------------------------------------------------------------------------
autoload -Uz promptinit
promptinit
prompt agnoster
alias updatedb="sudo /usr/libexec/locate.updatedb"
# ---------------------------------------------------------------------------
# 6. SDKMAN!
# ---------------------------------------------------------------------------
export SDKMAN_DIR="$HOME/.sdkman"
[[ -s "$SDKMAN_DIR/bin/sdkman-init.sh" ]] && source "$SDKMAN_DIR/bin/sdkman-init.sh"
ZSHRC
# 12. Cleanup -----------------------------------------------------------------
pretty_print "Cleaning up Homebrew…"
brew cleanup
pretty_print "✅ Setup successful! Please restart Terminal or run: source ~/.zshrc"
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment