Skip to main content

View / Edit on GitHub: home/.chezmoiscripts/universal/run_before_02-homebrew.sh.tmpl

Homebrew / Xcode Tools Installation

Ensures Xcode tools are installed on macOS and ensures Homebrew is installed on either Linux or macOS

Overview

This script ensures macOS has tools like git by installing the Xcode command-line developer tools if they are not already installed. Then, on both Linux and macOS, it ensures Homebrew is installed.

It also tries to perform a system update and there may be some manual work required since it is not possible to completely automate the system update process for macOS.

Environment Variables

  • NO_RESTART - Set this variable to skip restarts triggered by system updates on macOS

Homebrew Requirement

Install Doctor relies on Homebrew for many tools that are currently only available via Homebrew. Removing the dependency would be a nice-to-have but there are currently no plans for supporting systems without Homebrew installed.

Script Functions

ensureHomebrewDeps

This script ensures all the system utilities necessary to install Homebrew are present on the system.

fixHomebrewPermissions

This function removes group write permissions from the Homebrew share folder which is required for the ZSH configuration.

ensurePackageManagerHomebrew

This script ensures Homebrew is installed.

installExpect

Helper function utilized by [[upgradeDarwin]] to ensure the expect command is available on macOS

installGsed

Helper function utilized by [[upgradeDarwin]] to ensure the gsed command is available on macOS

installGtimeout

Helper function utilized by [[upgradeDarwin]] to ensure the gtimeout command is available on macOS

upgradeDarwin

This script ensures expect and gsed are available. It then either extracts the escalation password by decrypting the SUDO_PASSWORD secret or prompts the user for the sudo password. After that, it automates the process of downloading system updates, rebooting, and re-starting the provisioning process until the system is fully updated.

It may be important to note that although this script attempts to make the process fully automated, there may be certain circumstances where additional user input is necessary during the process. This might be true in cases where the system settings are controlled by MDM profiles for corporate laptops.

loadHomebrew

This script attempts to load Homebrew by checking in typical locations.

Source Code

#!/usr/bin/env bash
# @file Homebrew / Xcode Tools Installation
# @brief Ensures Xcode tools are installed on macOS and ensures Homebrew is installed on either Linux or macOS
# @description
# This script ensures macOS has tools like `git` by installing the Xcode command-line developer tools if they are
# not already installed. Then, on both Linux and macOS, it ensures Homebrew is installed.
#
# It also tries to perform a system update and there may be some manual work required since it is not possible
# to completely automate the system update process for macOS.
#
# ## Environment Variables
#
# * `NO_RESTART` - Set this variable to skip restarts triggered by system updates on macOS
#
# ## Homebrew Requirement
#
# Install Doctor relies on Homebrew for many tools that are currently only available via Homebrew. Removing the dependency
# would be a nice-to-have but there are currently no plans for supporting systems without Homebrew installed.

{{ includeTemplate "universal/profile-before" }}
{{ includeTemplate "universal/logg-before" }}

# @description This script ensures all the system utilities necessary to install Homebrew are present on the system.
ensureHomebrewDeps() {
if ! command -v curl > /dev/null || ! command -v git > /dev/null || ! command -v expect > /dev/null || ! command -v rsync > /dev/null; then
if command -v apt-get > /dev/null; then
# @description Ensure `build-essential`, `curl`, `expect`, `git`, `rsync`, `procps`, and `file` are installed on Debian / Ubuntu
sudo apt-get update
sudo apt-get install -y build-essential curl expect git rsync procps file
elif command -v dnf > /dev/null; then
# @description Ensure `curl`, `expect`, `git`, `rsync`, `procps-ng`, and `file` are installed on Fedora (as well as the Development Tools package)
sudo dnf groupinstall -y 'Development Tools'
sudo dnf install -y curl expect git rsync procps-ng file
elif command -v yum > /dev/null; then
# @description Ensure `curl`, `expect`, `git`, `rsync`, `procps-ng`, and `file` are installed on CentOS (as well as the Development Tools package)
sudo yum groupinstall -y 'Development Tools'
sudo yum install -y curl expect git rsync procps-ng file
elif command -v pacman > /dev/null; then
# @description Ensure `base-devel`, `curl`, `expect`, `git`, `rsync`, `procps-ng`, and `file` are installed on Archlinux
sudo pacman update
sudo pacman -Sy base-devel curl expect git rsync procps-ng file
elif command -v zypper > /dev/null; then
# @description Ensure `curl`, `expect`, `git`, `rsync`, `procps`, and `file` are installed on OpenSUSE (as well as the devel_basis pattern)
sudo zypper install -yt pattern devel_basis
sudo zypper install -y curl expect git rsync procps file
elif command -v apk > /dev/null; then
# @description Ensure `curl`, `expect`, `git`, `rsync`, `procps`, and `file` are installed on Alpine
apk add build-base curl expect git rsync procps file
elif [ -d /Applications ] && [ -d /Library ]; then
# @description Ensure CLI developer tools are available on macOS (via `xcode-select`)
sudo xcode-select -p >/dev/null 2>&1 || xcode-select --install
elif [[ "$OSTYPE" == 'cygwin' ]] || [[ "$OSTYPE" == 'msys' ]] || [[ "$OSTYPE" == 'win32' ]]; then
# @description Ensure `curl`, `expect`, `git`, and `rsync` are installed on Windows
choco install -y curl expect git rsync
fi
fi
}

# @description This function removes group write permissions from the Homebrew share folder which
# is required for the ZSH configuration.
fixHomebrewPermissions() {
if [ -f /usr/local/bin/brew ]; then
sudo chmod -R g-w /usr/local/share
elif [ -f "${HOMEBREW_PREFIX:-/opt/homebrew}/bin/brew" ]; then
sudo chmod -R g-w "${HOMEBREW_PREFIX:-/opt/homebrew}/share"
elif [ -d "$HOME/.linuxbrew" ]; then
sudo chmod -R g-w "$HOME/.linuxbrew/share"
elif [ -d "/home/linuxbrew/.linuxbrew" ]; then
sudo chmod -R g-w /home/linuxbrew/.linuxbrew/share
fi
}

# @description This script ensures Homebrew is installed.
ensurePackageManagerHomebrew() {
if ! command -v brew > /dev/null; then
if command -v sudo > /dev/null && sudo -n true; then
echo | bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"
fixHomebrewPermissions
else
logg info 'Homebrew is not installed. Password may be required.'
bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" || BREW_EXIT_CODE="$?"
fixHomebrewPermissions
if [ -n "$BREW_EXIT_CODE" ]; then
logg warn 'Homebrew was installed but part of the installation failed to complete successfully.'
if command -v brew > /dev/null; then
logg info 'Applying proper permissions on Homebrew folders'
sudo chmod -R go-w "$(brew --prefix)/share"
BREW_DIRS="share etc/bash_completion.d"
for BREW_DIR in $BREW_DIRS; do
if [ -d "$(brew --prefix)/$BREW_DIR" ]; then
sudo chown -Rf "$(whoami)" "$(brew --prefix)/$BREW_DIR"
fi
done
logg info 'Running brew update --force --quiet' && brew update --force --quiet
fi
fi
fi
fi
}

# @description Helper function utilized by [[upgradeDarwin]] to ensure the `expect` command is available on macOS
installExpect() {
if ! command -v expect > /dev/null; then
logg info 'Installing expect via Homebrew' && brew install expect
fi
}

# @description Helper function utilized by [[upgradeDarwin]] to ensure the `gsed` command is available on macOS
installGsed() {
if ! command -v gsed > /dev/null; then
logg info 'Installing gnu-sed via Homebrew' && brew install gnu-sed
fi
}

# @description Helper function utilized by [[upgradeDarwin]] to ensure the `gtimeout` command is available on macOS
installGtimeout() {
if ! command -v gtimeout > /dev/null; then
logg info 'Installing coreutils via Homebrew' && brew install coreutils
fi
}

# @description This script ensures `expect` and `gsed` are available. It then either extracts the escalation password by decrypting the
# `SUDO_PASSWORD` secret or prompts the user for the sudo password. After that, it automates the process of downloading
# system updates, rebooting, and re-starting the provisioning process until the system is fully updated.
#
# It may be important to note that although this script attempts to make the process fully automated, there may be
# certain circumstances where additional user input is necessary during the process. This might be true in cases
# where the system settings are controlled by MDM profiles for corporate laptops.
upgradeDarwin() {
if [ -d /Applications ] && [ -d /Library ] && [ -z "$NO_RESTART" ]; then
### Ensure dependencies are installed
installExpect
installGsed
installGtimeout

### Attempt to populate SUDO_PASSWORD from secrets
if [ -z "$SUDO_PASSWORD" ]; then
SUDO_PASSWORD="{{- if and (stat (joinPath .host.home ".config" "age" "chezmoi.txt")) (stat (joinPath .chezmoi.sourceDir ".chezmoitemplates" "secrets" "SUDO_PASSWORD")) -}}{{- includeTemplate "secrets/SUDO_PASSWORD" | decrypt | trim -}}{{- end -}}"
export SUDO_PASSWORD
fi

### Prompt for SUDO_PASSWORD
if [ -z "$SUDO_PASSWORD" ] || [ "$SUDO_PASSWORD" = "" ]; then
logg prompt "Enter the current user's login / admin password. Press ENTER to bypass and skip enabling auto-login. If you would like to bypass this prompt next time then pass in the password as an environment variable named SUDO_PASSWORD before running the kickstart script."
SUDO_PASSWORD="$(gum input --password --placeholder="Enter password..")"
export SUDO_PASSWORD
fi

### Run upgrade process
logg info 'Checking for available OS upgrades'
UPDATE_CHECK="$(softwareupdate -l 2>&1)"
if ! echo "$UPDATE_CHECK" | grep "No new software available" > /dev/null; then
logg info 'There are available OS upgrades'
echo "$UPDATE_CHECK"
logg info 'Applying OS upgrades (if available). This may take awhile..'
expect -c "set timeout -1
spawn sudo softwareupdate -i -a --agree-to-license
expect \"Password:\"
send \"${SUDO_PASSWORD}\r\"
expect eof" &> /dev/null || EXIT_CODE=$?
if [ -n "$EXIT_CODE" ]; then
logg warn 'Error running softwareupdate'
unset EXIT_CODE
fi
# sudo sh -c "sudo softwareupdate -i -a --agree-to-license" || logg error 'Failed to trigger a system update via sudo softwareupdate -i -a --agree-to-license'

### Reboot if necessary
# Source: https://community.jamf.com/t5/jamf-pro/determine-if-update-requires-restart/m-p/11682
logg info 'Checking if softwareupdate requires a reboot'
if softwareupdate -l | grep restart > /dev/null; then
### Add kickstart script to .zshrc so it triggers automatically
if [ ! -f "$HOME/.zshrc" ] || ! cat "$HOME/.zshrc" | grep '# TEMPORARY FOR INSTALL DOCTOR MACOS' > /dev/null; then
logg info 'Adding kickstart script to ~/.zshrc so script continues automatically if reboot is necessary'
echo 'bash <(curl -sSL --compressed https://install.doctor/start) # TEMPORARY FOR INSTALL DOCTOR MACOS' >> "$HOME/.zshrc"
fi

if [ -n "$SUDO_PASSWORD" ] && [ "$SUDO_PASSWORD" != "" ]; then
### Install kcpassword
if ! command -v enable_autologin > /dev/null; then
logg info 'enable_autologin is not installed and it is a requirement for auto-logging in after reboot' && brew install xfreebird/utils/kcpassword
fi

### Enable auto-login via kcpassword
logg info "Enabling autologin for $USER with acquired sudo password" && sudo enable_autologin "$USER" "$SUDO_PASSWORD"
fi

### Reboot
logg info 'Reboot required' && exit 140
else
logg info 'No reboot required for softwareupdate'
fi
else
logg info 'There are no available OS upgrades'
fi

if [ -f "$HOME/.zshrc" ]; then
logg info 'Ensuring provision kickstart script is removed from ~/.zshrc'
if command -v gsed > /dev/null; then
sudo gsed -i '/# TEMPORARY FOR INSTALL DOCTOR MACOS/d' "$HOME/.zshrc" || logg warn "Failed to remove kickstart script from .zshrc"
else
sudo sed -i '/# TEMPORARY FOR INSTALL DOCTOR MACOS/d' "$HOME/.zshrc" || logg warn "Failed to remove kickstart script from .zshrc"
fi
fi
fi
}

# @description This script attempts to load Homebrew by checking in typical locations.
loadHomebrew() {
if ! command -v brew > /dev/null; then
if [ -f /usr/local/bin/brew ]; then
eval "$(/usr/local/bin/brew shellenv)"
elif [ -f "${HOMEBREW_PREFIX:-/opt/homebrew}/bin/brew" ]; then
eval "$("${HOMEBREW_PREFIX:-/opt/homebrew}/bin/brew" shellenv)"
elif [ -d "$HOME/.linuxbrew" ]; then
eval "$("$HOME/.linuxbrew/bin/brew" shellenv)"
elif [ -d "/home/linuxbrew/.linuxbrew" ]; then
eval "(/home/linuxbrew/.linuxbrew/bin/brew shellenv)"
fi
fi
}

### Logic sequence
ensureHomebrewDeps
loadHomebrew
ensurePackageManagerHomebrew
loadHomebrew
upgradeDarwin