Quantcast
Channel: Mac Operations
Viewing all 42 articles
Browse latest View live

Easy Version Comparisons with Python

$
0
0

This is just a little taste of why sysadmins find Python so approachable. If you’ve managed systems for long enough, you’ve probably had a need to compare two versions of something. For example, you want to do one thing if a given application or package is less than 2.0, and another thing if it’s greater. (For example, upgrade the application or package, or configure it differently in either case.)

If you’ve ever tried to do this in Bash, it’s terrible. And you may have seen various installer scripts that attempt to do this. Or even doing this within Installer distribution scripts, despite there being more robust mechanisms already provided by the OS that require no scripting.

Here’s an example from a support script that runs as part of installing Blackmagic DaVinci Resolve, where it does a version check to make sure it already has at least their minimum supported version of CUDA:

if [ -d /Library/Frameworks/CUDA.framework ]
then
    INSTALLED_CUDA_VER=`defaults read /Library/Frameworks/CUDA.framework/Versions/Current/Resources/Info.plist | \grep CFBundleVersion | sed -e 's/"//g' | sed -e 's/;//g' | awk '{print $3}'`
    INSTALLED_CUDA_VER_MAJOR=`echo ${INSTALLED_CUDA_VER} | cut -d\. -f1`
    INSTALLED_CUDA_VER_MINOR=`echo ${INSTALLED_CUDA_VER} | cut -d\. -f2`
    INSTALLED_CUDA_VER_PATCH=`echo ${INSTALLED_CUDA_VER} | cut -d\. -f3`
    if [ "${INSTALLED_CUDA_VER_PATCH}" == "" ]
    then
        INSTALLED_CUDA_VER_PATCH=0
    fi

    INSTALLED_CUDA_VER_NUM=`echo "${INSTALLED_CUDA_VER_MAJOR} * 10000 + ${INSTALLED_CUDA_VER_MINOR} * 100 + ${INSTALLED_CUDA_VER_PATCH}" | bc`
else
    INSTALLED_CUDA_VER_NUM=0
fi



CUDA_VER="6.5.46"
CUDA_VER_MAJOR=`echo ${CUDA_VER} | cut -d\. -f1`
CUDA_VER_MINOR=`echo ${CUDA_VER} | cut -d\. -f2`
CUDA_VER_PATCH=`echo ${CUDA_VER} | cut -d\. -f3`
CUDA_VER_NUM=`echo "${CUDA_VER_MAJOR} * 10000 + ${CUDA_VER_MINOR} * 100 + ${CUDA_VER_PATCH}" | bc`

if [ ${INSTALLED_CUDA_VER_NUM} -ge ${CUDA_VER_NUM} ]
then
    echo "    --- CUDA is already installed - skipping step"
    return
fi

This is converting each “component” of the version into some multiple of 10, by putting together an arithmetic expression, and then piping it to the bc command (which was new to me), and finally using Bash’s -ge (“greater or equal than”) operator. This might be safer and more portable than doing arithmetic within Bash, I don’t know.

Is this readable? Sort of (not really). This one of the more elaborate but perhaps also more “correct” examples I’ve seen from installer packages in the wild.

If you use the JAMF Casper suite to install software, and would like to create a Smart Group that contains a criteria where some version of something is “less than” a given version, you may have found that there’s no built-in way to do this, despite it being an oft-requested feature. You can do SQL-like comparisons on the versions as strings, but this does not equate to an actual logical comparison of the “a.b.c” format that is often used for versions. In fact, this doesn’t even compare a single integer, it’s just doing simple string operations, one of “equals,” “not equals,” or “LIKE” wildcard comparisons.

Getting data like “is the Java plugin installed on a client at least version X.Y” actually requires writing a purpose-built script that can return a value on the client (known as an Extension Attribute in Casper parlance). Casper admins who use it to manage software tend to do this a lot, and have many such nearly-identical scripts. So either for cases like this, or for some other ad-hoc usage like I described earlier, it is sometimes very handy to have a lightweight, readable way of comparing versions of things, using tools that are available on every shipping version of OS X.

Python’s distutils package contains a “version” module containing some basic classes for doing version comparisons, like LooseVersion and StrictVersion. These contain enough logic to know that, for example, “1.0” is less than “1.5”, but that “1.10” is greater than “1.9” (even though if you were comparing these as floats or decimals, the latter example would be evaluated differently).

Here’s a very simple example. This will simply print the value which is evaluated as the highest version according to LooseVersion, or “equal” if they’re the same. It’s pretty readable, no?

#!/usr/bin/python

import sys
from distutils.version import LooseVersion

# Let's create LooseVersion objects out of the 1st and 2nd arguments
# (sys.argv[0] is our script itself)
a, b = LooseVersion(sys.argv[1]), LooseVersion(sys.argv[2])

if a > b:
    print a
elif b > a:
    print b
elif a == b:
    print 'equal'

Save this script to some file, make it executable, and give it two arguments:

➜ ./highest_version.py 1.10 1.9
1.10

One thing to note about this distutils.version module is that it may not be present in all Python distributions. And going forward, this module seems to have been deprecated in favour of another approach. However, if you’re reading this because you need to manage or automate tasks on OS X machines, you can safely rely on this module being part of every system distribution (thus me specifying #!/usr/bin/python above) as of at least 10.6, as long as you trust that your systems’ Apple-provided Python distributions are safely intact – which from is harder to screw up thanks to System Integrity Protection.

I highly recommend anyone wrangling shell scripts look at Python (or Ruby, or Swift, or Go, or..) as an empowering tool to help you perform sysadmin tasks more safely and effectively. But if your Bash script in question is working just fine and you just want a better way to compare versions, you can even hack in an example like the one above into its own self-contained Bash function:

#!/bin/sh

greater_than_or_equal() {
    python - "$1" "$2" << EOF
import sys
from distutils.version import LooseVersion as LV
print LV(sys.argv[1]) >= LV(sys.argv[2])
EOF

}

echo $(greater_than_or_equal 1.2.1 1.2.0)

I’m not very proficient at Bash, but in this simple example I’ve made a function called greater_than_or_equal which assumes it gets two arguments, and sends a tiny Python script to the python interpreter binary via stdin, and should simply print “True” because 1.2.1 is greater-than-or-equal-to 1.2.0 (and “False” if otherwise).

This is just one example where Python is a great tool for performing actions that may otherwise be painful, and potentially dangerous to your systems, to do using shell scripting and built-in command-line tools.


Deploying Xcode – The Trick With Accepting License Agreements

$
0
0

If you’ve ever gone through the process of automating Xcode installations, you’ve no doubt run across the issue of making sure that the license for Xcode and included SDKs has been accepted. An unlicensed Xcode looks like this on first launch, and asks for admin privileges:

Screen Shot 2015-11-26 at 3.06.42 PM

Or, try and run a command line utility and get:

➜  ~  strings


Agreeing to the Xcode/iOS license requires admin privileges, please re-run as root via sudo.

For a number of years the Munki wiki has been maintaining a list of actions to “finalize” an Xcode installation. See the script posted here on the Munki wiki, notably this part:

# accept Xcode license
/Applications/Xcode.app/Contents/Developer/usr/bin/xcodebuild -license accept

This useful trick with xcodebuild works if you have only a single Xcode app to deploy, but the situation becomes less clear if you maintain several on a single machine. And, you may have seen from time to time that you install a different version of Xcode (or a Beta version) on your own machine, that you need to re-accept the license again. What exactly is going on here?

The license “acceptance status” for Xcode is stored in a property list located in /Library/Preferences/com.apple.dt.Xcode.plist. This is not accessed with a preferences API, just reading and writing to a property list file. There are four keys that may store this license-related information. To see what I’ve got currently on my system, I’ll print out the plist contents:

➜  ~  /usr/libexec/PlistBuddy -c 'Print' /Library/Preferences/com.apple.dt.Xcode.plist
Dict {
    IDEXcodeVersionForAgreedToGMLicense = 6.4
    IDEXcodeVersionForAgreedToBetaLicense = 7.2
    IDELastGMLicenseAgreedTo = EA1187
    IDELastBetaLicenseAgreedTo = EA1327
}

The fact that there are different keys for “GM” and “Beta” versions explains why accepting a license for a Beta version doesn’t also cause the license to be accepted for a GM version, and vice versa.

I mentioned earlier the possibility of having multiple Xcode versions. This might be the case if you are developing or testing software that requires an older Xcode for compatibility with the project or included SDKs for older OS versions. You might name these Xcodes something like “Xcode-6.4.app”, etc. so that you can keep multiple Xcodes side by side. The Ruby gem XcodeInstall, a tool for automating installation of multiple versions of Xcode, does this. (And as of recent releases you can also run this tool using 🎉 – yes, the Party Popper emoji, entered in your terminal prompt).

Having multiple Xcode versions means you’ll have different versions of the agreement to accept. There are two important takeaways to know about this:

  1. When xcodebuild -license accept is run, it will apply the license agreement values to the plist according to the Xcode it’s contained within.
  2. The keys like IDELastGMLicenseAgreedTo mean exactly that: they are the last license agreed to, not the newest. For example, if you have Xcode 7.1 GM and Xcode 6.4 GM, and accept them in that order, Xcode 7.1 and its associated CLI tools will require accepting the license again, even though you already accepted the 7.1 license. However, accepting the newest license for either GM or Beta versions will include acceptance of licenses from previous versions. Therefore, if you have multiple GM or Beta Xcodes, the only way to guarantee they will all work is to accept only the latest of both GM and Beta versions you have installed.

If you want to know programatically what license values will be written to com.apple.dt.Xcode.plist when a license is accepted, you can do this by reading the contents of a file within the Xcode app bundle called LicenseInfo.plist:

➜  ~  /usr/libexec/PlistBuddy -c 'Print' Xcode-6.4.app/Contents/Resources/LicenseInfo.plist
Dict {
    licenseID = EA1187
    licenseType = GM
}

Looking at these values and the ones from the PlistBuddy command shown earlier, you can see how these are mapped. licenseType will be either GM or Beta, and these will determine which of the two pairs of keys in com.apple.dt.Xcode.plist will be set. The corresponding XcodeVersionForAgreedTo.. key will contain Xcode.app’s CFBundleShortVersionString.

Also, if you are ever curious about some of Xcode’s support functions work surrounding the support packages, downloadables (simulators and docsets), etc. a good place to start looking is the DVTFoundation framework binary in Xcode.app/Contents/SharedFrameworks/DVTFoundation.framework.

Viewing all 42 articles
Browse latest View live