/home/alex/dev/maintain-macos.sh (1)

From RaySoft
#!/bin/bash -
# ------------------------------------------------------------------------------
# maintain-macos.sh
# =================
#
# Scope     macOS
# Copyright (C) 2024 by RaySoft, Zurich, Switzerland
# License   GNU General Public License (GPL) 2.0
#           https://www.gnu.org/licenses/gpl2.txt
#
# ------------------------------------------------------------------------------

set -o 'noglob' -o 'nounset' -o 'pipefail' # -o 'errexit' -o 'xtrace'

# ------------------------------------------------------------------------------

# Path to the global libraries
MM_DE_GLOBAL_LIB="${HOME}/dev/lib"

# Global libraries to be loaded
MM_GLOBAL_LIBS=('nf' 'os')

# Homebrew casks which are updated automatically
MM_AUTOUPDATE_CASKS=('firefox' 'jdownloader' 'setapp')

# Homebrew casks which have poor versioning (version = 'latest')
MM_POOR_CASKS=('font-source-code-pro' 'font-ubuntu-mono')

# Applications which automatically start after installation
MM_AUTOSTART_APPS=('Garmin Express' 'Garmin Express Service' 'TeamViewer')

# Path to the programming projects
MM_DE_PROJECTS_PATH="${HOME}/dev"

# Projects with a Python virtual environment
MM_VENV_PROJECTS=('sync2mw')

# ------------------------------------------------------------------------------

MM_X_BREW=('/usr/local/bin/brew')
MM_X_GREP=('/usr/local/bin/ggrep' '--extended-regexp' '--invert-match')
MM_X_JQ=('/usr/local/bin/jq' '--raw-output')
MM_X_KILLALL=('/usr/bin/killall' '-q')
MM_X_LN=('/usr/local/bin/gln' '--force' '--symbolic')
# MM_X_MAS=('/usr/local/bin/mas')
MM_X_NPM=('/usr/local/bin/npm' '--global')
MM_X_NVIM=('/usr/local/bin/nvim' '--headless')
MM_X_SOFTWAREUPDATE=('/usr/sbin/softwareupdate' '--all')
MM_X_SUDO=('/usr/bin/sudo')

# ------------------------------------------------------------------------------

# Test if library path is available
if [[ ! -d "${MM_DE_GLOBAL_LIB}" ]]; then
  echo "Error finding directory: ${MM_DE_GLOBAL_LIB}!"
  exit 1
fi

# Load libraries
for lib in "${MM_GLOBAL_LIBS[@]}"; do
  lib="${MM_DE_GLOBAL_LIB}/${lib}.sh"

  if [[ ! -f "${lib}" ]]; then
    echo "Error finding library: ${lib}!"
    exit 1
  fi

  if ! source "${lib}"; then
    exit 1
  fi
done

# Test the defined variables
if ! os::test_env 'MM'; then
  exit 1
fi

nf::print_heading '=' 'b' 'Maintain macOS'

# macOS

nf::print_heading '-' 'b' "Update macOS & applications using 'softwareupdate'"

if ! "${MM_X_SUDO[@]}" "${MM_X_SOFTWAREUPDATE[@]}" --install 2>'/dev/null'; then
  nf::carp -e "Error updating macOS & applications using 'softwareupdate'!"
fi

# nf::print_heading '-' 'b' "Upgrade applications using 'mas'"
#
# if ! "${MM_X_MAS[@]}" upgrade 2>'/dev/null'; then
#   nf::carp -e "Error upgrading applications using 'mas'!"
# fi

# Homebrew

nf::print_heading '-' 'b' 'Update Homebrew itself & sync all taps'

if ! "${MM_X_BREW[@]}" update 2>'/dev/null'; then
  nf::carp -e 'Error updating Homebrew itself or syncing taps!'
fi

nf::print_heading '-' 'b' 'Upgrade all outdated Homebrew formulae'

if ! "${MM_X_BREW[@]}" upgrade --formula 2>'/dev/null'; then
  nf::carp -e 'Error upgrading outdated Homebrew formulae!'
fi

nf::print_heading '-' 'b' 'Find missing Homebrew formula dependencies'

# Check the given formula for missing dependencies. If no formula are provided,
# check all. Will exit with a non-zero status if any formulae are found to be
# missing dependencies.
"${MM_X_BREW[@]}" missing

nf::print_heading '-' 'b' 'Autoremove Homebrew formulae'

# Uninstall formulae that were only installed as a dependency of another formula
# and are now no longer needed.
"${MM_X_BREW[@]}" autoremove

# macOS

nf::print_heading '-' 'b' 'Link binaries'

# Link binaries
for bin in 'find' {,'e','f'}'grep' 'sed' 'tar' 'updatedb' 'xargs'; do
  src_bin="/usr/local/bin/g${bin}"
  dst_link="/usr/local/opt/coreutils/libexec/gnubin/${bin}"

  if [[ -f "${src_bin}" && -x "${src_bin}" ]]; then
    echo "- ${bin}"

    if ! "${MM_X_LN[@]}" "${src_bin}" "${dst_link}" 2>'/dev/null'; then
      nf::carp "Error linking binary: g${bin}!"
    fi
  else
    nf::carp "Error finding binary: g${bin}!"
  fi
done

src_bin='/usr/local/opt/curl/bin/curl'
dst_link='/usr/local/bin/curl'

if [[ -f "${src_bin}" && -x "${src_bin}" ]]; then
  echo "- curl"

  if ! "${MM_X_LN[@]}" "${src_bin}" "${dst_link}" 2>'/dev/null'; then
    nf::carp 'Error linking binary: curl!'
  fi
else
  nf::carp "Error finding binary: curl!"
fi

# Homebrew

nf::print_heading '-' 'b' 'Upgrade all outdated Homebrew casks'

# Get the list of ignored casks
ignored_casks=("${MM_AUTOUPDATE_CASKS[@]}" "${MM_POOR_CASKS[@]}")

# Get the list of all outdated casks
if ! mapfile -t 'casks' < <("${MM_X_BREW[@]}" outdated --cask --greedy --quiet)
then
  nf::carp -e 'Error getting the list of outdated Homebrew casks!'
fi

# Upgrade all outdated casks
for cask in "${casks[@]}"; do
  [[ "${ignored_casks[*]}" =~ ${cask} ]] && continue

  if ! "${MM_X_BREW[@]}" upgrade --cask --force --greedy "${cask}" 2>'/dev/null'
  then
    nf::carp -e "Error upgrading outdated Homebrew cask: ${cask}!"
  fi

  ignored_casks+=("${cask}")

  echo
done

# Get the list of all casks
if ! mapfile -t 'casks' < <("${MM_X_BREW[@]}" list --cask -1); then
  nf::carp -e 'Error getting the list of all Homebrew casks!'
fi

# Reinstall all outdated casks (alternative approach)
for cask in "${casks[@]}"; do
  [[ "${ignored_casks[*]}" =~ ${cask} ]] && continue

  # Compare the installed and the latest available version numbers of a cask
  if ! read -r 'version_comparison' < <( \
         "${MM_X_BREW[@]}" info --cask --json='v2' "${cask}" \
         | "${MM_X_JQ[@]}" ".casks[]
                            | select(.full_token | test(\"${cask}$\"))
                            | if .version == .installed then 0 else 1 end" \
       )
  then
    nf::carp -e "Error comparing the Homebrew cask versions: ${cask}!"
  fi

  if [[ "${version_comparison}" -ne 0 ]]; then
    echo -e "${blue_bl}WARNING: Homebrew cask '${cask}' is updated using the" \
            "alternative way!${none}"
    echo

    if ! "${MM_X_BREW[@]}" reinstall --cask --force "${cask}" 2>'/dev/null'; then
      nf::carp -e "Error reinstalling outdated Homebrew cask: ${cask}!"
    fi

    echo
  fi
done

# Always reinstall casks with poor versioning
if [[ "${#MM_POOR_CASKS[@]}" -gt 0 ]]; then
  if ! "${MM_X_BREW[@]}" reinstall --cask --force "${MM_POOR_CASKS[@]}" \
         2>'/dev/null'
  then
    nf::carp -e 'Error reinstalling Homebrew casks with poor versioning!'
  fi

  echo
fi

# macOS

nf::print_heading '-' '-' 'Kill applications which automatically start after' \
                          'installation'

"${MM_X_KILLALL[@]}" "${MM_AUTOSTART_APPS[@]}"

# Homebrew

nf::print_heading '-' 'b' 'Clean up Homebrew'

# Remove stale lock files and outdated downloads for all formulae and casks, and
# remove old versions of installed formulae.
"${MM_X_BREW[@]}" cleanup

nf::print_heading '-' 'b' 'Run Homebrew doctor'

# Check your system for potential problems. Will exit with a non-zero status
# if any potential problems are found.
"${MM_X_BREW[@]}" doctor $( \
  "${MM_X_BREW[@]}" doctor --list-checks \
  | "${MM_X_GREP[@]}" --regexp='^check_for_(stray_|non_prefixed_coreutils)' \
)

# Python

nf::print_heading '-' 'b' 'Upgrade all virtual Python environments'

# For each Python project with a virtual environment
for project in "${MM_VENV_PROJECTS[@]}"; do
  project_path="${MM_DE_PROJECTS_PATH}/${project}"

  if [[ ! -d "${project_path}" ]]; then
    nf::carp "Error finding Python project: ${project_path}!"
    continue
  fi

  if [[ ! -d "${project_path}/venv" ]]; then
    nf::carp "Error finding virtual Python environment: ${project_path}/venv!"
    continue
  fi

  echo "- ${project}"

  # set -o 'xtrace'

  # Activate virtual Python environment
  source "${project_path}/venv/bin/activate"

  # Get the list of all outdated modules
  if ! mapfile -t 'modules' < <(pip3 list --format 'json' --outdated \
                                | "${MM_X_JQ[@]}" '.[].name')
  then
    nf::carp 'Error getting the list of outdated Python modules!'

    # Deactivate virtual Python environment
    deactivate

    continue
  fi

  # Upgrade all outdated modules
  if [[ "${#modules[@]}" -gt 0 ]]; then
    if ! pip3 install --upgrade "${modules[@]}" 2>'/dev/null'; then
      nf::carp 'Error upgrading outdated Python modules!'

      # Deactivate virtual Python environment
      deactivate

      continue
    fi

    echo

    # Check the dependencies of the installed modules
    pip3 check

    echo
  fi

  # Deactivate virtual Python environment
  deactivate

  # set +o 'xtrace'
done

# npm

nf::print_heading '-' 'b' 'Update outdated Node JavaScript packages'

# Update all outdated Node JavaScript packages
if ! "${MM_X_NPM[@]}" update 2>'/dev/null'; then
  nf::carp "Error updating outdated Node JavaScript packages!"
fi

nf::print_heading '-' 'b' 'Run npm doctor'

# Doctor runs a set of checks to ensure that your npm installation has what
# it needs to manage your JavaScript packages.
"${MM_X_NPM[@]}" doctor

# Neovim

nf::print_heading '-' 'b' "Upgrade Neovim plugin 'vim-plug' & all other plugins"

# Maintain plugins using 'vim-plug'
# - PlugUpgrade: Upgrade the plugin manager 'vim-plug' itself
# - PlugUpdate: Update all installed plugins
# - PlugClean: Remove unlisted plugins
if ! "${MM_X_NVIM[@]}" -c 'PlugUpgrade | PlugUpdate | PlugClean | quitall' \
       2>'/dev/null'
then
  nf::carp -e "Error maintaining Neovim plugins!"
fi

# ------------------------------------------------------------------------------

exit 0

Usage

NOTE:
This script uses functions from the libraries ~/dev/lib/nf.sh & ~/dev/lib/os.sh.
~/dev/maintain-macos.sh