devAlice
← Mac

Mac Dev Environment Weekly Maintenance — Update brew · Toolchains · SDKs with One Command

A single weekly shell script that updates Homebrew, Mac App Store, npm globals, rustup, cargo binaries, pipx, Flutter, CocoaPods, and oh-my-zsh.

You spend time setting up a new machine, then forget to keep it updated. Two months later, a single brew install drags 30 transitive deps along, native builds break on SDK mismatches, and your security certificates (ca-certificates) silently drift toward expiration.

This article is the 9-step integrated script that prevents that. Run it once a week — done.

TL;DR

  • Runs brew + mas + npm -g + rustup + cargo install-update + pipx + flutter + pod + omz in order
  • Each step auto-skips if the tool isn't installed, and a failure in one doesn't stop the rest
  • Ends with a notice-only macOS system update check (no auto install — reboots are too risky)
  • ~100 lines of plain bash

Why Bother

1. Certificate Expiration

ca-certificates ships a new bundle every quarter. Skip two quarters and some HTTPS calls start failing. You may think you don't care, but npm registry, Homebrew itself, and the GitHub API are all affected.

2. Mobile Build SDK Drift

Especially risky on Flutter/iOS machines. Flutter stable ships 3–4 times a month; CocoaPods' spec repo, if unsynced for a week, starts throwing "spec not found" during pod install. A weekly pod repo update alone blocks 80% of this pain.

3. Language Toolchain Accumulation

Globals installed via rustup, pipx, or cargo each need their own update. Forget, and one day you discover cargo install won't fetch the latest because of rust-version mismatch.

Prerequisites

  • macOS 12+ + Homebrew (Mac initial setup)
  • For the full 9 steps: mas, pipx, cargo-update, flutter, cocoapods, oh-my-zsh pre-installed
  • Missing some tools is fine — those steps SKIP gracefully

One-time prep

brew install mas pipx cocoapods
brew install --cask flutter
cargo install cargo-update
sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)"
pipx ensurepath

cargo install cargo-update compiles, so the first run takes 2–3 minutes. Flutter is ~1GB to download.

9 Update Steps

#StepCommandNotes
1Homebrewbrew update && brew upgrade && brew cleanupformulae + casks
2Mac App Storemas upgradeApp Store apps (Xcode etc.)
3npm globalsnpm update -gglobal npm packages
4Rust toolchainrustup updatestable channel
5Cargo binariescargo install-update -arequires cargo-update extension
6pipxpipx upgrade-allPython global tools
7Flutter SDKflutter upgradecurrent channel's latest
8CocoaPods repopod repo updateiOS build spec sync
9oh-my-zsh~/.oh-my-zsh/tools/upgrade.shzsh framework

The Integrated Script

Save as e.g. ~/bin/update-system.sh. chmod +x and use.

#!/usr/bin/env bash
#
# Bulk system-tool update — macOS
# Usage: ./update-system.sh
#
# Scope:
#   1. Homebrew (formulae + casks)
#   2. Mac App Store (mas, if installed)
#   3. npm globals
#   4. Rust toolchain (rustup)
#   5. Cargo binaries (cargo install-update -a, needs cargo-update)
#   6. pipx (Python global tools)
#   7. Flutter SDK (flutter upgrade)
#   8. CocoaPods repo (pod repo update — iOS build dep)
#   9. oh-my-zsh (omz update)
#
# Ends with `softwareupdate -l` notice (no auto-install — reboot risk)
#
 
set -uo pipefail
 
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
CYAN='\033[0;36m'
GRAY='\033[0;90m'
NC='\033[0m'
 
TOTAL=9
STEP=0
 
step() {
    STEP=$((STEP + 1))
    printf "\n${CYAN}[%d/%d] %s${NC}\n" "$STEP" "$TOTAL" "$1"
}
 
ok()   { printf "  ${GREEN}OK${NC} %s\n" "$1"; }
skip() { printf "  ${GRAY}SKIP${NC} %s\n" "$1"; }
warn() { printf "  ${YELLOW}WARN${NC} %s\n" "$1"; }
fail() { printf "  ${RED}FAIL${NC} %s\n" "$1"; }
 
printf "${CYAN}━━━ System update (macOS) ━━━${NC}\n"
 
# Initialize fnm so npm step can find Node
if command -v fnm >/dev/null 2>&1; then
    eval "$(fnm env --use-on-cd --shell bash 2>/dev/null)" 2>/dev/null || true
fi
 
# 1. Homebrew
step "Homebrew (formulae + casks)"
if command -v brew >/dev/null 2>&1; then
    if brew update && brew upgrade && brew cleanup; then
        ok "brew update / upgrade / cleanup"
    else
        fail "brew step partially failed — check log above"
    fi
else
    skip "brew not installed"
fi
 
# 2. Mac App Store
step "Mac App Store (mas)"
if command -v mas >/dev/null 2>&1; then
    mas upgrade && ok "mas upgrade" || fail "mas upgrade"
else
    skip "mas not installed (brew install mas)"
fi
 
# 3. npm globals
step "npm globals"
if command -v npm >/dev/null 2>&1; then
    npm update -g && ok "npm update -g" || fail "npm update -g"
else
    skip "npm not installed"
fi
 
# 4. Rust toolchain
step "Rust toolchain (rustup)"
if command -v rustup >/dev/null 2>&1; then
    rustup update && ok "rustup update" || fail "rustup update"
else
    skip "rustup not installed"
fi
 
# 5. Cargo binaries
step "Cargo binaries (cargo install-update -a)"
if command -v cargo >/dev/null 2>&1; then
    if cargo install-update --version >/dev/null 2>&1; then
        cargo install-update -a && ok "cargo install-update -a" || fail "cargo install-update -a"
    else
        warn "cargo-update missing (install: cargo install cargo-update)"
    fi
else
    skip "cargo not installed"
fi
 
# 6. pipx
step "pipx (Python global tools)"
if command -v pipx >/dev/null 2>&1; then
    pipx upgrade-all && ok "pipx upgrade-all" || fail "pipx upgrade-all"
else
    skip "pipx not installed (brew install pipx)"
fi
 
# 7. Flutter SDK
step "Flutter SDK (flutter upgrade)"
if command -v flutter >/dev/null 2>&1; then
    flutter upgrade && ok "flutter upgrade" || fail "flutter upgrade"
else
    skip "flutter not installed"
fi
 
# 8. CocoaPods repo
step "CocoaPods repo (pod repo update)"
if command -v pod >/dev/null 2>&1; then
    pod repo update && ok "pod repo update" || fail "pod repo update"
else
    skip "pod not installed (brew install cocoapods)"
fi
 
# 9. oh-my-zsh
step "oh-my-zsh (omz update)"
if [ -d "$HOME/.oh-my-zsh" ]; then
    OMZ_UPDATER="$HOME/.oh-my-zsh/tools/upgrade.sh"
    if [ -x "$OMZ_UPDATER" ]; then
        zsh "$OMZ_UPDATER" && ok "oh-my-zsh updated" || fail "oh-my-zsh"
    else
        skip "oh-my-zsh upgrade.sh missing"
    fi
else
    skip "oh-my-zsh not installed"
fi
 
# macOS system update — notice only
printf "\n${CYAN}━━━ macOS system update check (notice only) ━━━${NC}\n"
if command -v softwareupdate >/dev/null 2>&1; then
    SU_OUT=$(softwareupdate -l 2>&1)
    if echo "$SU_OUT" | grep -qi "no new software\|No updates"; then
        printf "${GRAY}  Up to date — no system updates${NC}\n"
    else
        printf "${YELLOW}  ⚠ System updates available:${NC}\n"
        echo "$SU_OUT" | sed 's/^/    /'
        printf "${GRAY}  Install with: sudo softwareupdate -ia --restart  (will reboot)${NC}\n"
    fi
fi
 
printf "\n${CYAN}━━━ Done ━━━${NC}\n"
printf "${GRAY}Checks: brew doctor / brew outdated / npm outdated -g / flutter doctor${NC}\n"

Example Output

The first run is long (5 minutes if brew has 13 upgrades pending). Subsequent runs are 1–2 minutes.

━━━ System update (macOS) ━━━

[1/9] Homebrew (formulae + casks)
==> Upgrading 13 outdated packages:
ca-certificates 2026-03-19 -> 2026-05-14
ruby 4.0.3 -> 4.0.4
sqlite 3.53.0 -> 3.53.1
python@3.14 3.14.4_1 -> 3.14.5
...
==> This operation has freed approximately 18.8MB of disk space.
  OK brew update / upgrade / cleanup

[2/9] Mac App Store (mas)
  OK mas upgrade

[3/9] npm globals
  OK npm update -g

[4/9] Rust toolchain (rustup)
  stable-aarch64-apple-darwin unchanged - rustc 1.95.0
  OK rustup update

...

[9/9] oh-my-zsh (omz update)
Hooray! Oh My Zsh has been updated!
  OK oh-my-zsh updated

━━━ macOS system update check (notice only) ━━━
  Up to date — no system updates

━━━ Done ━━━

Automation — How Far?

Full automation is not recommended. brew major bumps (e.g., vercel-cli 53→54) occasionally ship breaking changes that you want to notice the moment they land.

A reasonable middle ground is a reminder, not an auto-run:

~/Library/LaunchAgents/local.update-reminder.plist:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN"
  "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
  <key>Label</key><string>local.update-reminder</string>
  <key>ProgramArguments</key>
  <array>
    <string>osascript</string>
    <string>-e</string>
    <string>display notification "Weekly update-system check" with title "Dev Maintenance"</string>
  </array>
  <key>StartCalendarInterval</key>
  <dict>
    <key>Weekday</key><integer>5</integer>
    <key>Hour</key><integer>10</integer>
    <key>Minute</key><integer>0</integer>
  </dict>
</dict>
</plist>

Activate:

launchctl load -w ~/Library/LaunchAgents/local.update-reminder.plist

Every Friday at 10:00, a single desktop notification. You decide when to run.

Gotchas

  • No auto-install for macOS system updates: softwareupdate -ia --restart reboots forcibly. Your IDE, terminals, docker containers — all gone. The script only lists available updates; you install on your terms.
  • pod repo update is slow (5+ minutes possible): the trunk repo is huge. Longer if you've skipped many weeks.
  • oh-my-zsh has its own auto-update option: .zshrc setting zstyle ':omz:update' mode auto. Harmless overlap with this script.
  • Cargo compiles: cargo install-update -a compiles fresh versions of installed binaries — first run can take minutes.

Windows Users?

Same idea in PowerShell, covering MSYS2 + winget + language toolchains → Windows dev environment weekly maintenance.

Summary

  • One-by-one updates → you forget
  • Single integrated script → forget all you want, just run it
  • Automation only as far as reminder, never auto-run
  • Once a week prevents brew dep storms, SDK drift, and certificate expiry

Comments