Are command tools getting out of their depth?

Mach object file format, Mach-O for short, is one of the oldest and most fundamental file formats in macOS, as used in all native executable code, libraries, and more. Although users most often launch Mach-O files wrapped into an app bundle, when you open Terminal to run command tools there you’re usually interacting directly with Mach-O binaries.

Over the years Mach-O files have evolved considerably. Architectural transitions, first from PowerPC to Intel, and most recently to ARM, have brought universal or fat binaries containing two complete executables. More recent security requirements have made this even more complex. As Alexandre Colucci has documented from bitter experience, the thoroughly modern Mach-O file can now require:

  • signing, mandatory for ARM executables to run at all;
  • embedded Info.plist, to accompany the signature and for other requirements;
  • notarization, required by all third-party developers to run on recent versions of macOS;
  • hardened runtime, required for notarization;
  • App Sandbox enablement, for distribution through the App Store, and for certain entitlements;
  • entitlements, for the App Sandbox and to gain access to certain macOS frameworks and features.

There are some benefits, though. With an embedded Info.plist, it’s now possible for the version and other details of a command tool to be discovered without running the command. This is a big step forward, as knowing which option to pass to obtain a version number isn’t simple. Many tools are happy with -v, but for others that merely makes responses verbose, or might do something quite different.

Sure enough, look at a command tool with an embedded Info.plist in the Finder, and its version number should be displayed. But, as it’s a command tool, the important question is how you get that information at the command line, as I was asked about one of my command tools, silnite. It turns out that a good answer isn’t easy to come by.

Historically, the most popular answer has been to use mdls to query the kMDItemVersion metadata, but that doesn’t seem able to access version numbers set against the CFBundleShortVersionString key in an embedded Info.plist. That’s a pity, because
mdls -name kMDItemVersion /usr/local/bin/silnite
seems relatively logical and straightforward, but just returns (null).

Flushed with that unsuccess, I dug a little deeper and came across a tool almost as old as Mach-O files, otool, which might appear as otool-classic, described in its man page as being “the preferred tool for inspecting Mach-O binaries, especially for binaries that are bad, corrupted, or fuzzed.” Indeed, using a command like
otool -P /usr/local/bin/silnite
dumps the whole Info.plist, where it’s easy to find the version as the value for the key CFBundleShortVersionString.

However, my answer wasn’t ideal, as more recently some have reported that otool is only installed by the Xcode command tools, and may not be available to all users. It also isn’t the fastest of commands to run, suggesting that something else is going on. So my best answer is to use launchctl, which definitely is present. A command like
launchctl plist /usr/local/bin/silnite
returns the whole of the embedded Info.plist as JSON-ised XML, within which you’ll find
"CFBundleShortVersionString" = "7";

I’m grateful to @josh_avraham for pointing out another alternative in plutil, as in
plutil -p /usr/local/bin/silnite
which returns the whole Info.plist too.

If you need to do this in code, there’s a good alternative in CFBundleCopyInfoDictionaryForURL(_:). Although its name might suggest it only works with bundles, Apple explains “For a plain file URL representing an unbundled application, this function will attempt to read an information dictionary either from the (__TEXT, __info_plist) section of the file (for a Mach-O file) or from a plst resource.”

Information about signatures, hardening, notarization and entitlements is revealed by two commands:
spctl -a -vv -t install /usr/local/bin/silnite
and
codesign -dvvv /usr/local/bin/silnite
then there’s always lipo for working with universal binaries.

These are an interesting trip down memory lane, but doing modern things with Mach-O files is messy and unnecessarily difficult. As they continue to become more complicated, it would be really helpful if these tools and procedures were overhauled and updated. Creating and inspecting a single file shouldn’t be more complex than doing so for a complete app bundle.

Thanks to Harald for pointing out that, as Mach-O came from NeXT, it wasn’t around for the first transition from 68K to PowerPC.