cancel
Showing results forย 
Search instead forย 
Did you mean:ย 
Disclaimer
JUMPCLOUD EXPRESSLY DISCLAIMS ALL REPRESENTATIONS, WARRANTIES, CONDITIONS, AND LIABILITIES OF ANY KIND ARISING FROM OR RELATED TO THIRD-PARTY SOFTWARE, SCRIPTS, REPOSITORIES, AND APIS. JUMPCLOUD IS NOT REQUIRED TO SUPPORT ANY SUCH THIRD-PARTY MATERIALS AND ALL RISKS RELATED TO THIRD-PARTY MATERIALS ARE YOUR RESPONSIBILITY. PLEASE ALSO REVIEW THE JUMPCLOUD TOS.

Install Homebrew ๐Ÿบ and deploy Brew formulae on macOS devices remotely via JumpCloud Commands

saifshaik
JumpCloud Employee
JumpCloud Employee

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. 

  • MacBook Pro Intel running Sonoma 14.4.1, JumpCloud Agent-installed and enrolled in JumpCloud MDM.
  • Device has an unmanaged local account with sudo/admin rights and a managed JumpCloud user account without sudo/admin rights. (following a typical setup we usually observe in corporate IT environments, local user account is just optional)
  • Logged into the device as a managed user and the state of the system is afresh with no Xcode Tools, Homebrew or any other applications/updates installed.

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.

Install Script -

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:

Screenshot 2024-04-21 at 10.38.02โ€ฏAM.png

 

 

 

 

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:

Screenshot 2024-04-21 at 10.43.43โ€ฏAM.png

 

 

 

 

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:

Screenshot 2024-04-21 at 10.47.57โ€ฏAM.png

 

 

 

 

 

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:

Screenshot 2024-04-21 at 10.49.38โ€ฏAM.png

 

 

 

 

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.

Screenshot 2024-04-15 at 12.23.12โ€ฏAM.png

 

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.

Screenshot 2024-04-21 at 2.08.34โ€ฏPM.pngScreenshot 2024-04-21 at 2.10.11โ€ฏPM.png

Thats it! Homebrew is deployed and fully funcational on your Mac fleet. Hope this was helpful! Until next time... ๐Ÿ˜‰

0 REPLIES 0
You Might Like

New to the site? Take a look at these additional resources:

Community created scripts:

Our new Radical Admin blog:

Keep up with Product News:

Read our community guidelines

Ready to join us? You can register here.