Posted on 02-24-2021 01:01 PM
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.
Posted on 02-24-2021 02:02 PM
Hi. While I don't have a specific need for this myself, at least right now, I just wanted to make one suggestion on pulling the applications list.
You might want to try the following to see if it works better for you.
find /Applications /System/Applications -name "*.app" -maxdepth 3
The above should list out all applications in the main Applications folder up to 3 levels deep as well as all the applications that are part of /System/Applications/
which are the built in ones.
This would avoid needing special loops for any known apps that are within any folders like with the Jamf Pro apps. It should handle any others ones as well that might be within their own directory, like Cisco AnyConnect for example.
The only caveat I'll say is that in some cases this might end up pulling apps into the mix that you don't necessarily care about. For example, Xcode tends to have a large number of smaller applications inside its own app bundle. That's why I limited the find to only 3 levels deep. You can play with that number as needed to get the results you're looking for. If you don't tell it where to stop, find will drill down as far as it can until it reaches the end, even if it ends up going 6 or 7 levels down to do so.
One other tip to check on. mdls
, aka Spotlight's listing tool has the ability to show a bundle's architectures, like x86_64, etc.
mdls "$app" -name kMDItemExecutableArchitectures
Here's a very basic script to find all applications within those 3 levels mentioned and print back the architecture listed for the application bundle. Some of them return null, but these are mostly embedded launcher apps inside other applications.
#!/bin/sh
all_apps=$(find /Applications /System/Applications -name "*.app" -maxdepth 3)
echo "$all_apps" | while read app; do
architecture=$(mdls -raw "$app" -name kMDItemExecutableArchitectures | xargs)
echo "$app architecture is $architecture"
done
Posted on 02-24-2021 03:08 PM
I'm probably reading this wrong but does "system_profiler SPApplicationsDataType" get you what you want?
I'm still a rookie at this stuff but this seems like it would get what you want but might need a little more massaging.
system_profiler SPApplicationsDataType | grep -E 'Kind:|Location:'
Edit: I was playing around with it and came up with this version instead that might be more readable and only covers /Applications. I'd personally prefer to flip the location with kind but that's beyond my skills at the moment.
system_profiler SPApplicationsDataType | grep -E -B2 'Location: /Applications/' |grep -E -v 'Signed by:|Last Modified:'