โ04-21-2024 05:02 AM - edited โ04-22-2024 10:40 PM
Hello Admin Friends!
I'm back again with another magical script to install Homebrew and deploy Homebrew packages silently on Mac devices directly via JumpCloud Commands without any enduser interaction.
Inspiration for portions of this script has been taken from homebrew-3.3.sh. Original script credit goes to Tony Williams (Honestpuck).
This script silently installs Homebrew as the most common local user. Below I've mentioned my system environment, where I tested the deployment. I highly advise to test the script on a test device first and chalk out a plan, before planning a mass deployment.
In JumpCloud Commands section, configure the below script to install Homebrew silently. This script is designed to add brew to the current user's PATH, but if a user has pre-existing CLI sessions open, the brew command may not be recognized. The user will need to relaunch their sessions (ex - zsh -l) or start a new session so that brew is seen in their PATH.
This script checks if Homebrew is already installed on the system, checks for the presence of Rosetta 2, which is necessary for running Intel-based software on Silicon Macs, checks for and installs Xcode Command Line Tools, in case if its missing, sets the Homebrew prefix based on the processor architecture, creates directories and sets permissions required by Homebrew and most importantly adds Homebrew to the user's PATH environment variable.
First save the below script as-is with .sh file extension and I've roughly named it 'brew_install.sh'.
#!/usr/bin/env zsh
# Used when comparing installed CLI tools versus latest available via softwareupate
autoload is-at-least
# Script version
VERSION="1.5.2"
###################################### VARIABLES #######################################
# Logging config
LOG_NAME="homebrew_install.log"
LOG_DIR="/Library/Logs"
LOG_PATH="$LOG_DIR/$LOG_NAME"
############################ FUNCTIONS - DO NOT MODIFY BELOW ###########################
logging() {
# Logging function
#
# Takes in a log level and log string and logs to /Library/Logs/$script_name if a
# LOG_PATH constant variable is not found. Will set the log level to INFO if the
# first built-in $1 is passed as an empty string.
#
# Args:
# $1: Log level. Examples "info", "warning", "debug", "error"
# $2: Log statement in string format
#
# Examples:
# logging "" "Your info log statement here ..."
# logging "warning" "Your warning log statement here ..."
log_level=$(printf "%s" "$1" | /usr/bin/tr '[:lower:]' '[:upper:]')
log_statement="$2"
script_name="$(/usr/bin/basename "$0")"
prefix=$(/bin/date +"[%b %d, %Y %Z %T $log_level]:")
# see if a LOG_PATH has been set
if [[ -z "${LOG_PATH}" ]]; then
LOG_PATH="/Library/Logs/${script_name}"
fi
if [[ -z $log_level ]]; then
# If the first builtin is an empty string set it to log level INFO
log_level="INFO"
fi
if [[ -z $log_statement ]]; then
# The statement was piped to the log function from another command.
log_statement=""
fi
# echo the same log statement to stdout
/bin/echo "$prefix $log_statement"
# send log statement to log file
printf "%s %s\n" "$prefix" "$log_statement" >>"$LOG_PATH"
}
check_brew_install_status() {
# Check brew install status.
brew_path="$(/usr/bin/find /usr/local/bin /opt -maxdepth 3 -name brew 2>/dev/null)"
if [[ -n $brew_path ]]; then
# If the brew binary is found, just run brew update and exit
logging "info" "Homebrew already installed at $brew_path..."
logging "info" "Updating homebrew ..."
/usr/bin/su - "$current_user" -c "$brew_path update --force" |
/usr/bin/tee -a "${LOG_PATH}"
logging "info" "Done ..."
exit 0
else
logging "info" "Homebrew is not installed..."
fi
}
rosetta2_check() {
# Check for and install Rosetta2 if needed.
# $1: processor_brand
# Determine the processor brand
if [[ "$1" == *"Apple"* ]]; then
logging "info" "Apple Processor is present..."
# Check if the Rosetta service is running
check_rosetta_status=$(/usr/bin/pgrep oahd)
# Rosetta Folder location
# Condition check to see if the Rosetta folder exists. This check was added
# because the Rosetta2 service is already running in macOS versions 11.5 and
# greater without Rosseta2 actually being installed.
rosetta_folder="/Library/Apple/usr/share/rosetta"
if [[ -n $check_rosetta_status ]] && [[ -e $rosetta_folder ]]; then
logging "info" "Rosetta2 is installed... no action needed"
else
logging "info" "Rosetta is not installed... installing now"
# Installs Rosetta
/usr/sbin/softwareupdate --install-rosetta --agree-to-license |
/usr/bin/tee -a "${LOG_PATH}"
fi
else
logging "info" "Apple Processor is not present...Rosetta2 is not needed"
fi
}
get_available_cli_tool_installs() {
# Return the latest available CLI tools.
# Get the OS build year
build_year=$(/usr/bin/sw_vers -buildVersion | cut -c 1,2)
if [[ "$build_year" -ge 19 ]]; then
# for Catalina or newer
cmd_line_tools=$(/usr/sbin/softwareupdate --list |
/usr/bin/awk '/\*\ Label: Command Line Tools/ { $1=$1;print }' |
/usr/bin/sed 's/^[[ \t]]*//;s/[[ \t]]*$//;s/*//' |
/usr/bin/cut -c 9- | /usr/bin/grep -vi beta | /usr/bin/sort -n)
else
# For Mojave or older
cmd_line_tools=$(/usr/sbin/softwareupdate --list |
/usr/bin/awk '/\*\ Command Line Tools/ { $1=$1;print }' |
/usr/bin/grep -i "macOS" |
/usr/bin/sed 's/^[[ \t]]*//;s/[[ \t]]*$//;s/*//' | /usr/bin/cut -c 2-)
fi
# return rsponse from softwareupdate reguarding CLI tools.
/bin/echo "$cmd_line_tools"
}
xcode_cli_tools() {
# Check for and install Xcode CLI tools
# Trick softwareupdate into giving us everything it knows about Xcode CLI tools by
# touching the following file to /tmp
xclt_tmp="/tmp/.com.apple.dt.CommandLineTools.installondemand.in-progress"
/usr/bin/touch "$xclt_tmp"
# Run xcrun command to check for a valid Xcode CLI tools path
/usr/bin/xcrun --version >/dev/null 2>&1
# shellcheck disable=SC2181
if [[ "$?" -eq 0 ]]; then
logging "" "Valid Xcode CLI tools path found."
# current bundleid for CLI tools
bundle_id="com.apple.pkg.CLTools_Executables"
if /usr/sbin/pkgutil --pkgs="$bundle_id" >/dev/null; then
# If the CLI tools pkg bundle is found, get the version
installed_version=$(/usr/sbin/pkgutil --pkg-info="$bundle_id" |
/usr/bin/awk '/version:/ {print $2}' |
/usr/bin/awk -F "." '{print $1"."$2}')
logging "" "Installed CLI tools version is \"$installed_version\""
else
logging "" "Unable to determine installed CLI tools version from \"$bundle_id\"."
fi
logging "" "Checking to see if there are any available CLI tool updates..."
# Get the latest available CLI tools
cmd_line_tools=("$(get_available_cli_tool_installs)")
else
logging "" "Valid Xcode CLI tools path was not found ..."
logging "" "Getting the latest Xcode CLI tools available for install..."
# Get the latest available CLI tools
cmd_line_tools=("$(get_available_cli_tool_installs)")
fi
# if something is returned from the cli tools check
# shellcheck disable=SC2128
if [[ -n $cmd_line_tools ]]; then
logging "" "Available Xcode CLI tools found: "
logging "" "$cmd_line_tools"
if (($(/usr/bin/grep -c . <<<"${cmd_line_tools}") > 1)); then
cmd_line_tools_output="${cmd_line_tools}"
cmd_line_tools=$(/bin/echo "${cmd_line_tools_output}" | /usr/bin/tail -1)
# get version number of the latest CLI tools installer.
lastest_available_version=$(/bin/echo "${cmd_line_tools_output}" | /usr/bin/tail -1 | /usr/bin/awk -F "-" '{print $2}')
fi
if [[ -n $installed_version ]]; then
# If an installed CLI tools version is returned
# compare latest version to installed version using is-at-least
version_check="$(is-at-least "$lastest_available_version" "$installed_version" &&
/bin/echo "greater than or equal to" || /bin/echo "less than")"
if [[ $version_check == *"less"* ]]; then
# if the installed version is less than available
logging "" "Updating $cmd_line_tools..."
/usr/sbin/softwareupdate --install "${cmd_line_tools}" --verbose
else
# if the installed version is greater than or equal to latest available
logging "" "Installed version \"$installed_version\" is $version_check the latest available version \"$lastest_available_version\". No upgrade needed."
fi
else
logging "" "Installing $cmd_line_tools..."
/usr/sbin/softwareupdate --install "${cmd_line_tools}" --verbose
fi
else
logging "warning" "Hmmmmmm...unabled to return any available CLI tools..."
logging "warning" "May need to validate the softwareupdate command used."
fi
logging "Cleaning up $xclt_tmp ..."
/bin/rm "${xclt_tmp}"
}
set_brew_prefix() {
# Set the homebrew prefix.
# Set the brew prefix to either the Apple Silicon location or the Intel location based on the
# processor_brand information
#
# $1: proccessor brand information
local brew_prefix
if [[ $1 == *"Apple"* ]]; then
# set brew prefix for apple silicon
brew_prefix="/opt/homebrew"
else
# set brew prefix for Intel
brew_prefix="/usr/local"
fi
# return the brew_prefix
/bin/echo "$brew_prefix"
}
create_brew_environment() {
# Create the brew environment.
# Create the directories needed by brew, set the ownership, and set permissions.
# $1: brew_prefix
# $2: current_user
logging "info" "Creating directories required by brew ..."
/bin/mkdir -p "${1}/Caskroom" "${1}/Cellar" "${1}/Frameworks" "${1}/Homebrew" "${1}/bin" "${1}/etc" "${1}/include" "${1}/lib" "${1}/opt" "${1}/sbin" "${1}/man/man1" "${1}/share/doc" "${1}/share/man/man1" "${1}/share/zsh/site-functions" "${1}/var" "${1}/var/homebrew/linked"
logging "info" "Creating symlink to ${1}/bin/brew ..."
/bin/ln -s "${1}/Homebrew/bin/brew" "${1}/bin/brew"
logging "info" "Setting homebrew ownership to $2 ..."
/usr/sbin/chown -R "$2" "${1}/Cellar" "${1}/Caskroom" "${1}/Frameworks" "${1}/Homebrew" "${1}/bin" "${1}/bin/brew" "${1}/etc" "${1}/include" "${1}/lib" "${1}/man" "${1}/opt" "${1}/sbin" "${1}/share" "${1}/var"
logging "info" "Setting permissions for brew directories and files ..."
/bin/chmod -R 755 "${1}/Homebrew" "${1}/Cellar" "${1}/Caskroom" "${1}/Frameworks" "${1}/bin" "${1}/bin/brew" "${1}/etc" "${1}/include" "${1}/lib" "${1}/man" "${1}/opt" "${1}/sbin" "${1}/share" "${1}/var"
}
update_path() {
# Add brew to current user PATH
# Check for missing PATH
get_path_cmd=$(/usr/bin/su - "$current_user" -c "$brew_prefix/bin/brew doctor 2>&1 | /usr/bin/grep 'export PATH=' | /usr/bin/tail -1")
# Checking to see if the output returned from get_path_cmd contains the word homebrew and
# also checking to see if brew is actually in the current user's path by runing the which
# command.
if echo "$get_path_cmd" | grep "homebrew" >/dev/null 2>&1 && ! /usr/bin/which brew >/dev/null 2>&1; then
# get the shell dot rc file returned from the get_path_cmd command so that we know
# which shell the current user is using.
shell_rc_file=$(echo "$get_path_cmd" | awk '{print $5}' | awk -F '/' '{print $2}')
# Check the user's shell rc file to see if homebrew has already been added to the
# user's PATH. If we find it in there already then there is no reason to write to that
# file again.
if ! /usr/bin/grep "$brew_prefix/bin" "/Users/$current_user/$shell_rc_file" >/dev/null 2>&1; then
echo "Adding brew to user's PATH..."
echo "Using command: $get_path_cmd"
/usr/bin/su - "$current_user" -c "${get_path_cmd}"
else
echo "brew path $brew_prefix/bin already in user's $shell_rc_file..."
fi
else
logging "info" "brew already in user's PATH..."
fi
}
brew_doctor() {
# Check Homebrew install status
#
# if on Apple Silicon, you may see the following output from brew doctor
#
# Please note that these warnings are just used to help the Homebrew maintainers
# with debugging if you file an issue. If everything you use Homebrew for is
# working fine: please don't worry or file an issue; just ignore this. Thanks!
#
# Warning: Your Homebrew's prefix is not /usr/local.
# Some of Homebrew's bottles (binary packages) can only be used with the default
# prefix (/usr/local).
# You will encounter build failures with some formulae.
# Please create pull requests instead of asking for help on Homebrew's GitHub,
# Twitter or any other official channels. You are responsible for resolving
# any issues you experience while you are running this
# unsupported configuration.
#
# $1: brew_prefix
# $2: current_user
/usr/bin/su - "$2" -c "$1/bin/brew doctor" 2>&1 | /usr/bin/tee -a "${LOG_PATH}"
# shellcheck disable=SC2181
if [[ "$?" -ne 0 ]]; then
logging "error" "brew doctor has errors. Review logs to see if action needs to be taken ..."
else
logging "info" "Homebrew installation complete! Your system is ready to brew."
fi
}
############################ MAIN LOGIC - DO NOT MODIFY BELOW ##########################
# Do not modify the below, there be dragons. Modify at your own risk.
logging "info" "--- Start homebrew install log ---"
logging "info" "Script verion: $VERSION"
/bin/echo "Log file at /Library/Logs/homebrew_install.log"
# Get the processor brand information
processor_brand="$(/usr/sbin/sysctl -n machdep.cpu.brand_string)"
# Get the current logged in user excluding loginwindow, _mbsetupuser, and root
current_user=$(/usr/sbin/scutil <<<"show State:/Users/ConsoleUser" |
/usr/bin/awk '/Name && ! /loginwindow/ && ! /root/ && ! /_mbsetupuser/ { print $3 }' |
/usr/bin/awk -F '@' '{print $1}')
# Make sure that we can find the most recent logged-in user
if [[ $current_user == "" ]]; then
logging "info" "Current user not logged in ..."
logging "info" "Attempting to determine the most common user..."
# Because someone other than the current user was returned, we are going to look at
# who uses this Mac the most, then set the current user to that user.
current_user=$(/usr/sbin/ac -p | /usr/bin/sort -nk 2 |
/usr/bin/grep -E -v "total|admin|root|mbsetup|adobe" | /usr/bin/tail -1 |
/usr/bin/xargs | /usr/bin/cut -d " " -f1)
fi
logging "info" "Most common user: $current_user"
# Verify the current_user is valid
if /usr/bin/dscl . -read "/Users/$current_user" >/dev/null 2>&1; then
logging "info" "$current_user is a valid user ..."
else
logging "error" "Specified user \"$current_user\" is invalid"
exit 1
fi
logging "info" "Checking to see if Xcode cli tools are needed ..."
xcode_cli_tools
logging "info" "Checking to see if Rosetta2 is needed ..."
rosetta2_check "$processor_brand"
logging "info" "Checking to see if Homebrew is already installed on this Mac ..."
check_brew_install_status
logging "info" "Determining Homebrew path prefix ..."
brew_prefix=$(set_brew_prefix "$processor_brand")
logging "info" "Creating the Homebrew directory at $brew_prefix/Homebrew ..."
/bin/mkdir -p "$brew_prefix/Homebrew"
logging "info" "Downloading homebrew ..."
# Using curl to download the latest release of homebrew tarball and put it in
# brew_prefix/Homebrew If brew updates to master to main, the url will need to be
# adjusted.
/usr/bin/curl --fail --silent --show-error --location \
--url "https://github.com/Homebrew/brew/tarball/master" |
/usr/bin/tar xz --strip 1 -C "$brew_prefix/Homebrew" | /usr/bin/tee -a "${LOG_PATH}"
# checking to see if brew was downloaded successfully
if [[ -f "$brew_prefix/Homebrew/bin/brew" ]]; then
logging "info" "Homebrew binary found at $brew_prefix/Homebrew/bin/brew ..."
logging "info" "Creating the brew environment ..."
create_brew_environment "$brew_prefix" "$current_user"
else
logging "info" "Homebrew binary not found ..."
/bin/echo "Check $LOG_PATH for more details ..."
exit 1
fi
logging "info" "Running brew update --force ..."
/usr/bin/su - "$current_user" -c "$brew_prefix/bin/brew update --force" 2>&1 |
/usr/bin/tee -a "${LOG_PATH}"
logging "info" "Running brew cleanup ..."
/usr/bin/su - "$current_user" -c "$brew_prefix/bin/brew cleanup" 2>&1 |
/usr/bin/tee -a "${LOG_PATH}"
# updated the user's PATH var to add brew binary location
logging "info" "Checking to see if brew is in current user's PATH..."
update_path
logging "info" "Running brew doctor to validate the install ..."
brew_doctor "$brew_prefix" "$current_user"
logging "info" "If the user has any existing CLI sessions running brew doctor may not see that brew is in the user's PATH."
logging "info" "The user may need to restart or open a new CLI session before brew will be recognized in their path."
logging "info" "Otherwise, brew can be called directly with $brew_prefix/bin/brew"
logging "info" "--- End homebrew install log ---"
exit 0
NOTE - In line 341, in [/usr/bin/awk '/Name ๐ && ! /loginwindow/ && ! /root/ && ! /_mbsetupuser/ { print $3 }'] command replace the emoji with colon forward slash. HTML predicates colon forward slash as an emoji. lol |
Now, in JumpCloud Commands, upload the .sh file, set type as "Mac', run as 'root' and TimeOut set to '600' seconds, configure the below command:
chmod +x /tmp/brew_install.sh
sh /tmp/brew_install.sh
rm /tmp/brew_install.sh
Now the command is ready to be executed, run the command on a target device(s) as needed. If the command has run successfully, Homebrew should be installed and the output of the command result would be:
Send the below command to verify successful installation of Xcode Command Tools and Homebrew by querying their versions. Command can be set to run as 'enduser' and the command results output the versions of the Xcode Command Tools and Homebrew installed on the device.
xcode-select -v
/opt/homebrew/bin/brew --version #for Silicon Macs
/usr/local/bin/brew --version #for Intel Macs
NOTE - Going forward, depending on your Mac architecture, you may need to correct the PATH of Homebrew as it is installed at /opt/homebrew/bin/brew on Silicon Macs and /usr/local/bin/brew on Intel Macs. |
The output would reflect the version of the Xcode Command Tools and Homebrew installed, similar to this:
At this point we're ready to push brew commands on the endpoint. For e.g., let's deploy jq formula via Homebrew. jq is a lightweight and flexible command-line JSON processor. Setup the below command in JumpCloud Commands set to be run as 'enduser'.
/opt/homebrew/bin/brew install jq #for Silicon Macs
/usr/local/bin/brew install jq #for Intel Macs
jq package would be installed and the command output would result like this:
Same can be re-verified by querying the version of jq using the below command
/opt/homebrew/bin/jq --version #for Silicon Macs
/usr/local/bin/jq --version #for Intel Macs
Here's the resultant output:
At this point, enduser can also run brew commands directly on the device from the Terminal.
At times we may also wish to deploy cask applications via Homebrew. Casks are basically applications on the device, and we can leverage JumpCloud Software Management to deploy apps on Mac endpoints through VPP or self-hosted apps or via private repository methods. However for advanced admins, who'd still like to leverage Homebrew to deploy applications, it can be achieved via JC Commands as well. But the caveat here is that the enduser may need to have time-based passwordless sudo/admin access to be able to install casks. This is due to a prerequisite of brew cask --install command, which requires sudo admin password.
Grant time-based passwordless sudo/admin access to the target user on their respective device. 10 minutes access would suffice.
Next, deploy the below script and modify the PACKAGE_NAME with the required cask name as found in this list, set type as 'Mac', run as 'enduser' and Time Out set to 600 seconds.
#!/bin/zsh
item="PACKAGE_NAME"
#######################
# check something set #
if [[ "$item" == "" ]]; then
echo "**** No item set! exiting ****"
exit 1
fi
UNAME_MACHINE="$(uname -m)"
ConsoleUser=$( scutil <<< "show State:/Users/ConsoleUser" | awk '/Name && ! /loginwindow/ { print $3 }' )
# Check if the item is already installed. If not, install it
if [[ "$UNAME_MACHINE" == "arm64" ]]; then
# M1/arm64 machines
cd /tmp/ # This is required to use sudo as another user or you get a getcwd error
if [[ $(sudo -H -iu ${ConsoleUser} /opt/homebrew/bin/brew list --casks | grep -c ${item}) == "1" ]]; then
echo "${item} is installed already. Skipping installation"
else
echo "${item} is either not installed or not available. Attempting installation..."
sudo -H -iu ${ConsoleUser} /opt/homebrew/bin/brew install --cask ${item}
fi
else
# Intel machines
cd /tmp/ # This is required to use sudo as another user or you get a getcwd error
if [[ $(sudo -H -iu ${ConsoleUser} /usr/local/bin/brew list --casks | grep -c ${item}) == "1" ]]; then
echo "${item} is installed already. Skipping installation"
else
echo "${item} is either not installed or not available. Attempting installation..."
sudo -H -iu ${ConsoleUser} /usr/local/bin/brew install --cask ${item}
fi
fi
For e.g., I have used 'google-drive' as an example to install Google Drive application on my endpoint via Homebrew.
Thats it! Homebrew is deployed and fully funcational on your Mac fleet. Hope this was helpful! Until next time... ๐
โ10-10-2024 02:19 PM
Hi @saifshaik ,
Thanks for providing all the codes and I was hoping this to work on our JumpCloud Commands.
We are using Intel based Macbook and our macOS is Sonoma 14.7.
I do see that Homebrew is installed but when I tried to run the code installing application through install --cask
it's throwing an error saying:
Error: Failed to cd to /usr/local/Homebrew/Library/Homebrew/vendor/portable-ruby!
Error: Failed to install Homebrew Portable Ruby (and your system version is too old)!
Have you experience this before?
By the way, the enduser has passwordless permission permanently.
โ10-12-2024 01:31 PM
Hello
for intel devices please use below script.
cd /tmp/
curl -LJO https://dl.google.com/drive-file-stream/GoogleDrive.dmg
hdiutil mount GoogleDrive.dmg; sudo installer -pkg /Volumes/Install\ Google\ Drive/GoogleDrive.pkg -target "/Volumes/Macintosh HD"; hdiutil unmount /Volumes/Install\ Google\ Drive/
Steps are below.
Click the + to Create a New Command
Check Command if you want to run it manually or Command After Agent Install if you run once a new Enrolls into JumpCloud. You can do both but youโll have to setup two commands.
Type Select Mac
Name Install Google Drive or something similar
Run As Select Root
Paste the command in the command box
Launch Event - Leave as Run Manually
Timeout After - I usually put 240 incase one of my users has a slow internet connection.
If setting up a Command not Command After Agent Install
New to the site? Take a look at these additional resources:
Ready to join us? You can register here.