Skip to main content

Remove homebrew install dotnet sdk and optional related directories.

#!/usr/bin/env bash
#
# remove-homebrew-dotnet.sh
#
# Safe to rerun. Set PURGE_ALL_DOTNET=1 for a complete machine-wide .NET purge.
#
# # To install again, run:
# $ brew update
# $ brew install --cask dotnet-sdk
#
set -o pipefail

PURGE_ALL_DOTNET="${PURGE_ALL_DOTNET:-0}"

case "$PURGE_ALL_DOTNET" in
  0|1) ;;
  *) echo "PURGE_ALL_DOTNET must be 0 or 1."; exit 2 ;;
esac

contains() {
  local needle="$1"
  shift
  local item
  for item in "$@"; do
    [[ "$item" == "$needle" ]] && return 0
  done
  return 1
}

formulae=()
casks=()
dependencies=()
BREW=""

if command -v brew >/dev/null 2>&1; then
  BREW="$(command -v brew)"
elif [[ -x /opt/homebrew/bin/brew ]]; then
  BREW="/opt/homebrew/bin/brew"
fi

if [[ -n "$BREW" ]]; then
  if ! "$BREW" list --formula >/dev/null 2>&1 ||
     ! "$BREW" list --cask >/dev/null 2>&1; then
    echo "Homebrew could not list installed packages."
    exit 1
  fi

  while IFS= read -r name; do
    case "$name" in
      dotnet|dotnet@*) formulae+=("$name") ;;
    esac
  done < <("$BREW" list --formula)

  while IFS= read -r name; do
    case "$name" in
      dotnet|dotnet@*|dotnet-*) casks+=("$name") ;;
    esac
  done < <("$BREW" list --cask)

  # Record the installed dependency tree before removing the .NET formulae.
  for formula in "${formulae[@]}"; do
    deps_text="$("$BREW" deps --formula --installed --recursive "$formula")" || exit 1
    while IFS= read -r dependency; do
      [[ -z "$dependency" ]] && continue
      contains "$dependency" "${dependencies[@]}" || dependencies+=("$dependency")
    done <<< "$deps_text"
  done

  failed=0

  if ((${#formulae[@]})); then
    printf 'Removing Homebrew formulae: %s\n' "${formulae[*]}"
    "$BREW" uninstall --formula --force "${formulae[@]}" || failed=1
  fi

  if ((${#casks[@]})); then
    printf 'Removing Homebrew casks: %s\n' "${casks[*]}"
    "$BREW" uninstall --cask --force --zap "${casks[@]}" || failed=1
  fi

  if ((failed)); then
    echo "A Homebrew uninstall failed; no manual cleanup was performed."
    exit 1
  fi

  # Remove only former .NET dependencies that are now unused and were
  # installed as dependencies---not packages you explicitly installed.
  while :; do
    leaves=()
    leaves_text="$("$BREW" leaves --installed-as-dependency)" || exit 1

    while IFS= read -r leaf; do
      [[ -n "$leaf" ]] && leaves+=("$leaf")
    done <<< "$leaves_text"

    removed_this_pass=0
    for dependency in "${dependencies[@]}"; do
      if contains "$dependency" "${leaves[@]}"; then
        echo "Removing now-unused Homebrew dependency: $dependency"
        "$BREW" uninstall --formula "$dependency" || exit 1
        removed_this_pass=1
      fi
    done

    ((removed_this_pass)) || break
  done

  # Remove any Homebrew-owned residual directories and links for the
  # packages just removed.
  cellar="$("$BREW" --cellar)"
  caskroom="$("$BREW" --caskroom)"
  prefix="$("$BREW" --prefix)"

  for formula in "${formulae[@]}"; do
    [[ -d "$cellar/$formula" ]] && rm -rf "$cellar/$formula"
    [[ -L "$prefix/opt/$formula" ]] && rm -f "$prefix/opt/$formula"
  done

  for cask in "${casks[@]}"; do
    [[ -d "$caskroom/$cask" ]] && rm -rf "$caskroom/$cask"
  done

  for link in "$prefix/bin/dotnet" "$prefix/bin/dnx"; do
    if [[ -L "$link" ]] && [[ "$(readlink "$link")" == *dotnet* ]]; then
      rm -f "$link"
    fi
  done

  # Delete only Homebrew cache files whose filename identifies them as .NET.
  cache_dir="$("$BREW" --cache)"
  if [[ -d "$cache_dir" ]]; then
    while IFS= read -r -d '' cache_file; do
      rm -f "$cache_file"
    done < <(find "$cache_dir" -type f -iname '*dotnet*' -print0)
  fi
else
  echo "Homebrew was not found; skipping Homebrew package cleanup."
fi

# Remove stale Homebrew-specific .NET lines from common shell profiles.
# A one-time backup is made before each changed profile is updated.
for profile in "$HOME/.zprofile" "$HOME/.zshrc" "$HOME/.zlogin" \
               "$HOME/.bash_profile" "$HOME/.bashrc" "$HOME/.profile"; do
  [[ -f "$profile" ]] || continue

  tmp="$(mktemp "${TMPDIR:-/tmp}/dotnet-profile.XXXXXX")" || exit 1

  if [[ "$PURGE_ALL_DOTNET" == "1" ]]; then
    awk '
      /HOMEBREW_PREFIX.*dotnet/ ||
      /brew[[:space:]]+--prefix[[:space:]]+dotnet/ ||
      /\/(opt\/homebrew|usr\/local)\/(opt|Cellar)\/dotnet/ ||
      /DOTNET_ROOT/ ||
      /PATH=.*(\.dotnet|\/usr\/local\/share\/dotnet)/ { next }
      { print }
    ' "$profile" > "$tmp"
  else
    awk '
      /HOMEBREW_PREFIX.*dotnet/ ||
      /brew[[:space:]]+--prefix[[:space:]]+dotnet/ ||
      /\/(opt\/homebrew|usr\/local)\/(opt|Cellar)\/dotnet/ { next }
      { print }
    ' "$profile" > "$tmp"
  fi

  if ! cmp -s "$profile" "$tmp"; then
    backup="${profile}.before-dotnet-cleanup.bak"
    [[ -e "$backup" ]] || cp -p "$profile" "$backup"
    mv "$tmp" "$profile"
    echo "Updated $profile (backup: $backup)"
  else
    rm -f "$tmp"
  fi
done

if [[ "$PURGE_ALL_DOTNET" == "1" ]]; then
  echo "Purging all remaining system and user .NET data."
  echo "This removes non-Homebrew .NET SDKs/runtimes, global tools, NuGet caches, and config."
  sudo -v || exit 1

  # Remove remaining Microsoft .NET installer receipts.
  while IFS= read -r receipt; do
    sudo pkgutil --forget "$receipt" >/dev/null || true
  done < <(
    pkgutil --pkgs |
      awk '/^com\.microsoft\.(dotnet|netstandard)\./ { print }'
  )

  # Includes Arm64 .NET and the Rosetta x64 subtree on Apple Silicon.
  sudo rm -rf /usr/local/share/dotnet
  sudo rm -f /etc/paths.d/dotnet /etc/paths.d/dotnet-cli-tools

  rm -rf \
    "$HOME/.dotnet" \
    "$HOME/.nuget" \
    "$HOME/Library/Caches/NuGet" \
    "$HOME/Library/Application Support/NuGet"
fi

hash -r
echo "Done. Open a new Terminal window before checking PATH."