Running executable code in macOS is fundamentally simple: all you need is a Mach-O binary file, as most command tools are. The snag is that has to be run from a command shell, or indirectly, and has no GUI. There are also plenty of things it can’t do, or needs more configuration to support. This article explains what goes into a modern app bundle, and why they’ve become so complex.
A basic non-App Store app consists of a structure of files and folders inside a folder named Contents, within the .app folder. These are:
- The _CodeSignature folder containing the cdhashes for the app’s components, used by macOS to check that it hasn’t been tampered with in any way.
- The CodeResources file, which contains the app’s notarization ticket, which is checked when it’s first run.
- The Info property list, which is an essential list of settings for the app, including its version and build numbers.
- The MacOS folder, containing the app’s main executable code which is launched when you run the app.
- The PkgInfo file, eight bytes of legacy content identifying the app and its type in the same terms used in Classic Mac OS.
- The Resources folder containing largely non-executable resources used by the app, such as its icons, window layouts, localised text, Help books, and anything else required. Occasionally scripts appear here, for example for uninstalling an app.
Apps provided through the App Store normally don’t have a CodeResources file, but contain another folder named _MASReceipt, which contains their App Store receipt, and is checked by the app when it’s launched. Some notarized apps don’t come with a CodeResources file either, and their notarization has to be checked online by macOS instead.
Although you should never encounter an app which uses all the files and folders shown below, this illustrates some of the more common possibilities. Even the massively complex Xcode doesn’t go this far.
Additions include the following:
- The embedded.provisionprofile file contains additional settings provided by the developer in a provisioning profile for macOS, and information about it is displayed in its QuickLook thumbnail.
- The Frameworks folder contains a mixture of dylibs, dynamic libraries, and .framework bundles, which contain additional executable code which is used by the app. Apps which use Swift normally include a set of Swift dylibs, for example.
- The Helpers folder can contain various helper executable files, used by the app when needed.
- The Plugins folder contains bundles of app extensions, which the app relies upon to function.
- The XPCServices folder can contain executable code for helpers, with which the app communicates using XPC protocols.
It’s usually the Library folder which is most complicated, containing:
- The LaunchServices folder containing app helpers which are normally launched as a result of an added property list in the LaunchServices folder in a Library.
- The LoginItems folder, which contains Login Items which macOS runs automatically when you log in, although these aren’t usually accessible in the list in the Users & Groups pane.
- The QuickLook folder containing QuickLook generator code, to enable QuickLook thumbnails and previews of custom document types.
- The Spotlight folder containing Spotlight MD Importer code, which enables Spotlight to index the contents of custom document types.
- The SystemExtensions folder containing any System Extensions which the app relies on. These should now have replaced app kernel extensions or kexts, which used to be contained within an Extensions folder.
Helpers and similar code are used for several good reasons, the most common being when an app has to run code at elevated privileges. This is required to access many protected system features. One of the basic principles of app security is that only the minimum essential code should ever be run at elevated privileges, so running the whole app as root isn’t wise.
Instead, code needed to be run with root privileges is separated out into a helper app. When the main app needs to access those features, it does so via that helper. The standard way for an app and its helpers to communicate is using XPC. When correctly implemented, this enables the app to do sensitive things without putting your Mac at risk.
As a rule, you should never tamper with the contents of an app bundle. Changing any of its contents risks breaking its signature, which then results in macOS refusing to run that app. In the past, users have got away with gentle changes to non-executable items in the Resources folder, but even those can sometimes cause problems with app launching. If you really must make changes, do so with caution, and it’s often best done after stripping the app’s signature altogether. However, as M1 series Macs won’t run unsigned code, for them you must re-sign the app, which gets fiddly and is prone to error.
The days of customising window and other resources in apps are long since past.