Last active
July 13, 2025 15:44
-
-
Save howie/00ae971f711b610e02fa to your computer and use it in GitHub Desktop.
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
| #!/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