Fellow Admins,
Not sure if there's already a thread for this, but I wanted to post this method in case there are any of you with a similar problem to solve. Hope this helps and feedback is certainly welcome.
Overview
Here, the problem we've been asked to solve is finding a way to identify the applications on production workstations built from Intel architecture as well as those built from the ARM architecture. We want to collect this information as a way to see whether MacOS computers sporting the new M1 chipset is appropriate for our environment given the applications we're using in production. My solution approaches this question with a four-step workflow:
1. Get a list of installed applications that we can iterate over
2. Get the name of the executable file for each installed app
3. For each executable, figure out what architecture it's using
4. Report our results to the console somehow
Phase 1: Installed Apps & Executables
After messing around with ls and mdls for a while, I finally settled on a simple method for working with the installed applications...planting our session in the /Applications directory. This way we can use a wildcard character in the upcoming for loop
cd /Applications
From here I elected to get the executable name from each .app bundle by interrogating the value of the CFBundleExecutable key from their respective info.plist files. With these two in hand, the essential iteration was revealed:
for app in *;do
defaults read /Applications/[exampleApp.app]/Contents/info.plist CFBundleExectuable
This method works for all of the .app bundles in /Applications that are not contained within a subfolder. Items like the Jamf Pro apps and Python can exist as directories housing .app bundles. For these cases I chose to deal with them manually, but I'm confident that there's a function design that would allow for a more automated approach. Note also that I do not handle Utilities at all. I'm assuming that the applications living in /System/Library/CoreServices/Applications and and /System/Applications will be compliant. In any case my resultant function for this operation was this:
function getAppExecutable(){
defaults read /Applications/$1/Contents/info.plist CFBundleExecutable
Phase 2: Getting Executable Architectures
Once I had a way to get each executable, I then needed to know what architecture they used. For this the file binary provided a ready solution via this syntax:
file /Applications/[exampleApp.app/Contents/MacOS/[exampleAppExecutable]
In most cases what we're looking for is the very end of line returned to the console, as that is where the resultant architecture type is listed. We can pipe this to awk to achieve this:
file /Applications/[exampleApp.app}/Contents/MacOS/[exampleAppExecutable] | awk '{ print $(NF) }'
There are cases where an executable uses multiple architectures (presumably because the developers have already redesigned the app for backwards compatibility between M1 and Intel based Macs), and this "look at the end of the line" method does not yield actionable results. This is handled by a test outlined in the next section. Like the getExecutableName process, we can make a function for these file commands:
function getExecutableType(){
file /Applications/$1/Contents/MacOS/$2 | awk '{ print $(NF) }'
Phase 3: Putting It All Together Now
For better or worse I chose to go with a large for loop as the structure for this job:
cd /applications
function getExecutableName(){
defaults read /Applications/$1/Contents/info.plist CFBundleExecutable
}
function getExecutableType(){
file /Applications/$1/Contents/MacOS/$2 | awk '{ print $(NF) }'
}
resultInventoryUser=()
# P2: Iterate Over Installed Applications to find the type
for app in *;do
if [[ $app == "Python 3.9" ]];then
idleAppExecutableName=$(defaults read /Applications/Python 3.9/IDLE.app/Contents/info.plist CFBundleExecutable)
idleAppExecutableType=$(file /Applications/Python 3.9/IDLE.app/Contents/MacOS/IDLE | awk '{ print $(NF) }')
echo $idleAppExecutableType
if [[ $idleAppExecutableType = "x86_64" || $idleAppExecutableType = "i386" ]];then
execTypeReport=$(echo "$idleAppExecutableName is not compliant for M1 Devices
")
resultInventoryUser+=("$execTypeReport")
elif [[ $idleAppExecutableType = "arm64" || $idleAppExecutableType = "arm64e" ]];then
execTypeReport=$(echo "idleAppExecutableName is compliant for M1 Devices
")
resultInventoryUser+=("$execTypeReport")
else
backwardsCompTest=$(file /Applications/Python 3.9/IDLE.app/Contents/MacOS/IDLE | grep "architectures")
if [[ $backwardsCompTest != "" ]];then
execTypeReport=$(echo "$idleAppExecutableName is backwards compatible
")
else
execTypeReport=$(echo "$idleAppExecutableName could not be identified
")
fi
resultInventoryUser+=("$execTypeReport")
fi
launchAppExecutableName=$(defaults read /Applications/Python 3.9/Python Launcher.app/Contents/info.plist CFBundleExecutable)
launchAppExecutableType=$(file /Application/Python 3.9/Python Launcher.app/Contents/MacOS/Python Launcher | awk '{ print $(NF) }')
echo $launchAppExecutableType
if [[ $launchAppExecutableType = "x86_64" || $launchAppExecutableType = "i386" ]];then
execTypeReport=$(echo "$launchAppExecutableName is not compliant for M1 Devices
")
resultInventoryUser+=("$execTypeReport")
elif [[ $launchAppExecutableType = "arm64" || $launchAppExecutableType = "arm64e" ]];then
execTypeReport=$(echo "$launchAppExecutableName is compliant for M1 Devices
")
resultInventoryUser+=("$execTypeReport")
else
backwardsCompTest=$(file /Applications/Python 3.9/Python Launcher.app/Contents/MacOS/Python Launcher | grep "architectures")
if [[ $backwardsCompTest != "" ]];then
execTypeReport=$(echo "$idleAppExecutableName is backwards compatible
")
else
execTypeReport=$(echo "$idleAppExecutableName could not be identified
")
fi
resultInventoryUser+=("$execTypeReport")
fi
elif [[ $app == "Jamf Pro" ]];then
#Composer
composerAppExecutableName=$(defaults read /Applications/Jamf Pro/Composer.app/Contents/info.plist CFBundleExecutable)
composerAppExecutableType=$(file /Applications/Jamf Pro/Composer.app/Contents/MacOS/Composer | awk '{ print $(NF) }')
if [[ $composerAppExecutableType = "x86_64" || $composerAppExecutableType = "i386" ]];then
execTypeReport=$(echo "$composerAppExecutableName is not compliant for M1 Devices
")
resultInventoryUser+=("$execTypeReport")
elif [[ $composerAppExecutableType = "arm64" || $composerAppExecutableType = "arm64e" ]];then
execTypeReport=$(echo "$composerAppExecutableName is compliant for M1 Devices
")
resultInventoryUser+=("$execTypeReport")
else
backwardsCompTest=$(file /Applications/Jamf Pro/Composer.app/Contents/MacOS/Composer | grep "architectures")
if [[ $backwardsCompTest != "" ]];then
execTypeReport=$(echo "$composerAppExecutableName is backwards compatible
")
else
execTypeReport=$(echo "$composerAppExecutableName could not be identified
")
fi
resultInventoryUser+=("$execTypeReport")
fi
#Recon
reconAppExecutableName=$(defaults read /Applications/Jamf Pro/Recon.app/Contents/info.plist CFBundleExecutable)
reconAppExecutableType=$(file /Applications/Jamf Pro/Recon.app/Contents/MacOS/Recon | awk '{ print $(NF) }')
if [[ $reconAppExecutableType = "x86_64" || $reconAppExecutableType = "i386" ]];then
execTypeReport=$(echo "$reconAppExecutableName is not compliant for M1 Devices
")
resultInventoryUser+=("$execTypeReport")
elif [[ $reconAppExecutableType = "arm64" || $reconAppExecutableType = "arm64e" ]];then
execTypeReport=$(echo "$reconAppExecutableName is compliant for M1 Devices
")
resultInventoryUser+=("$execTypeReport")
else
backwardsCompTest=$(file /Applications/Jamf Pro/Recon.app/Contents/MacOS/Recon | grep "architectures")
if [[ $backwardsCompTest != "" ]];then
execTypeReport=$(echo "$reconAppExecutableName is backwards compatible
")
else
execTypeReport=$(echo "$reconAppExecutableName could not be identified
")
fi
resultInventoryUser+=("$execTypeReport")
fi
#Imaging
imagingAppExecutableName=$(defaults read /Applications/Jamf Pro/Jamf Imaging.app/Contents/info.plist CFBundleExecutable)
imagingAppExecutableType=$(file /Applications/Jamf Pro/Jamf Imaging.app/Contents/MacOS/Jamf Imaging | awk '{ print $(NF) }')
if [[ $imagingAppExecutableType = "x86_64" || $imagingAppExecutableType = "i386" ]];then
execTypeReport=$(echo "$imagingAppExecutableName is not compliant for M1 Devices
")
resultInventoryUser+=("$execTypeReport")
elif [[ $imagingAppExecutableType = "arm64" || $imagingAppExecutableType = "arm64e" ]];then
execTypeReport=$(echo "$imagingAppExecutableName is compliant for M1 Devices
")
resultInventoryUser+=("$execTypeReport")
else
backwardsCompTest=$(file /Applications/Jamf Pro/Jamf Imaging.app/Contents/MacOS/Jamf Imaging | grep "architectures")
if [[ $backwardsCompTest != "" ]];then
execTypeReport=$(echo "$imagingAppExecutableName is backwards compatible
")
else
execTypeReport=$(echo "$imagingAppExecutableName could not be identified
")
fi
resultInventoryUser+=("$execTypeReport")
fi
#Admin
adminAppExecutableName=$(defaults read /Applications/Jamf Pro/Jamf Admin.app/Contents/info.plist CFBundleExecutable)
adminAppExecutableType=$(file /Applications/Jamf Pro/Jamf Admin.app/Contents/MacOS/Jamf Admin | awk '{ print $(NF) }')
if [[ $adminAppExecutableType = "x86_64" || $adminAppExecutableType = "i386" ]];then
execTypeReport=$(echo "$adminAppExecutableName is not compliant for M1 Devices
")
resultInventoryUser+=("$execTypeReport")
elif [[ $adminAppExecutableType = "arm64" || $adminAppExecutableType = "arm64e" ]];then
execTypeReport=$(echo "$adminAppExecutableName is compliant for M1 Devices
")
resultInventoryUser+=("$execTypeReport")
else
backwardsCompTest=$(file /Applications/Jamf Pro/Jamf Admin.app/Contents/MacOS/Jamf Admin | grep "architectures")
if [[ $backwardsCompTest != "" ]];then
execTypeReport=$(echo "$adminAppExecutableName is backwards compatible
")
else
execTypeReport=$(echo "$adminAppExecutableName could not be identified
")
fi
resultInventoryUser+=("$execTypeReport")
fi
#Remote
remoteAppExecutableName=$(defaults read /Applications/Jamf Pro/Jamf Remote.app/Contents/info.plist CFBundleExecutable)
remoteAppExecutableType=$(file /Applications/Jamf Pro/Jamf Remote.app/Contents/MacOS/Jamf Remote | awk '{ print $(NF) }')
if [[ $remoteAppExecutableType = "x86_64" || $remoteAppExecutableType = "i386" ]];then
execTypeReport=$(echo "$remoteAppExecutableName is not compliant for M1 Devices
")
resultInventoryUser+=("$execTypeReport")
elif [[ $remoteAppExecutableType = "arm64" || $remoteAppExecutableType = "arm64e" ]];then
execTypeReport=$(echo "$remoteAppExecutableName is compliant for M1 Devices
")
resultInventoryUser+=("$execTypeReport")
else
backwardsCompTest=$(file /Applications/Jamf Pro/Jamf Remote.app/Contents/MacOS/Jamf Remote | grep "architectures")
if [[ $backwardsCompTest != "" ]];then
execTypeReport=$(echo "$remoteAppExecutableName is backwards compatible
")
else
execTypeReport=$(echo "$remoteAppExecutableName could not be identified
")
fi
resultInventoryUser+=("$execTypeReport")
fi
else
appExecutableName=$(getExecutableName $app)
appExecutableType=$(getExecutableType $app $appExecutableName)
if [[ $appExecutableType = "x86_64" || $appExecutableType = "i386" ]];then
execTypeReport=$(echo "$appExecutableName is not compliant for M1 Devices
")
resultInventoryUser+=("$execTypeReport")
elif [[ $appExecutableType = "arm64" || $appExecutableType = "arm64e" ]];then
execTypeReport=$(echo "$appExecutableName should be compliant for M1 Devices
")
resultInventoryUser+=("$execTypeReport")
else
backwardsCompTest=$(file /Applications/$app/Contents/MacOS/$appExecutableName | grep "architectures")
if [[ $backwardsCompTest != "" ]];then
execTypeReport=$(echo "$appExecutableName is backwards compatible
")
else
execTypeReport=$(echo "$appExecutableName could not be identified
")
fi
resultInventoryUser+=("$execTypeReport")
fi
fi
done
As mentioned before, I chose to handle both Jamf Pro and Python manually, as I was interested in what dealing with structures like this would be like. That said, if you can pull apps out of a containing folder into /Applications just do it. Huge time save. Additionally for the case of an executable using multiple architectures, I devised a simplistic test:
backwardsCompTest=$(file /Applications/Python 3.9/IDLE.app/Contents/MacOS/IDLE | grep "architectures")
if [[ $backwardsCompTest != "" ]];then
execTypeReport=$(echo "$idleAppExecutableName is backwards compatible
")
else
execTypeReport=$(echo "$idleAppExecutableName could not be identified
")
fi
When evaluating executable files with file commands, I noticed that executables with multiple architectures returned output like this:
/Applications/[exampleApp.app]/Contents/MacOS/[exampleAppExectuable]: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64:Mach-O 64-bit executable arm64]
In this case I elected to pipe the command output to grep searching for the string "architectures". The if statement is checking to see whether anything comes out of the operation after we've piped it into grep because apps with a single architecture come back blank. If there's something there the script assumes that a message like the above was the output and writes "this is backwards compatible" to the resultInventory array.
Phase 4: Outputting Results
To post the output I chose to append the result of each evalutation to an array that starts out with no data. I purposefully included spaces in the echo commands so that the output would look like a list instead of a long series of lines. This is certainly not the best way to do this so suggestions here would be useful for anyone looking to use this. In my environment I have the results posting in a form usable for a Computer Extension Attribute but you can cut these methods up and rearrange them to suit your particular needs.