/home/alex/dev/maintain-macos.sh (1)
#!/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