Author: Sanobar Khan/Friday, January 7, 2022/Categories: General
Application bundles are one of the most common types of bundles created by developers. The application bundle stores everything that the application requires for successful operation. Although the specific structure of an application bundle depends on the platform for which you are developing, the way you use the bundle is the same on all platforms. This document describes the structure of application bundles in macOS.
What Files Go into an Application Bundle?
Table 1 summarizes the types of files you are likely to find inside an application bundle.
Table 1: Types of files in an application bundle
(Required) The information property list file is a structured file that contains configuration information for the application. The system relies on the presence of this file to identify relevant information about your application and any related files.
(Required) Every application must have an executable file. This file contains the application’s main entry point and any code that was statically linked to the application target.
Resources are data files that live outside your application’s executable file. Resources typically consist of things like images, icons, sounds, nib files, strings files, configuration files, and data files (among others). Most resource files can be localized for a particular language or region or shared by all localizations.
Other support files
Mac apps can embed additional high-level resources such as private frameworks, plug-ins, document templates, and other custom data resources that are integral to the application. Although you can include custom data resources in your iOS application bundles, you cannot include custom frameworks or plug-ins.
Understanding the bundle structure can help you decide where you should place your own custom files. macOS bundles use a highly organized structure to make it easier for the bundle-loading code to find resources and other important files in the bundle. The hierarchical nature also helps the system distinguish code bundles such as applications from the directory packages used by other applications to implement document types.
The basic structure of a Mac app bundle is very simple. At the top-level of the bundle is a directory named Contents. This directory contains everything, including the resources, executable code, private frameworks, private plug-ins, and support files needed by the application. While the Contents directory might seem superfluous, it identifies the bundle as a modern-style bundle and separates it from document and legacy bundle types found in earlier versions of Mac OS.
Table 2 shows the high-level structure of a typical application bundle, including the immediate files and directories you are most likely to find inside the Contents directory. This structure represents the core of every Mac app.
Table 2: The basic structure of a Mac app
Table 3 lists some of the directories that you might find inside the Contents directory, along with the purpose of each one. This list is not exhaustive but merely represents the directories in common usage.
Table 3: Subdirectories of the Contents directory
(Required) Contains the application’s standalone executable code. Typically, this directory contains only one binary file with your application’s main entry point and statically linked code. However, you may put other standalone executables (such as command-line tools) in this directory as well.
Contains all the application’s resource files. These contents of this directory are further organized to distinguish between localized and nonlocalized resources.
Contains any private shared libraries and frameworks used by the executable. The frameworks in this directory are revision-locked to the application and cannot be superseded by any other, even newer, versions that may be available to the operating system. In other words, the frameworks included in this directory take precedence over any other similarly named frameworks found in other parts of the operating system.
For the Finder to recognize an application bundle as such, you need to include an information property list (Info.plist) file. This file contains XML property-list data that identifies the configuration of your bundle. For a minimal bundle, this file would contain very little information, most likely just the name and identifier of the bundle. For more complex bundles, the Info.plist file includes much more information.
Important: Bundle resources are located using a case-sensitive search. Therefore, the name of your information property list file must start with a capital “I”.
Table 4: Expected keys in the Info.plist file
CFBundleName (Bundle name)
The short name for the bundle. The value for this key is usually the name of your application.
CFBundleDisplayName (Bundle display name)
The localized version of your application name. You typically include a localized value for this key in an InfoPlist.strings files in each of your language-specific resource directories.
CFBundleIdentifier (Bundle identifier)
The string that identifies your application to the system. This string must be a uniform type identifier (UTI) that contains only alphanumeric (A-Z,a-z,0-9), hyphen (-), and period (.) characters. The string should also be in reverse-DNS format. For example, if your company’s domain is Ajax.com and you create an application named Hello, you could assign the string com.CompanyName.AppName as your application’s bundle identifier.
The bundle identifier is used in validating the application signature.
CFBundleVersion (Bundle version)
The string that specifies the build version number of the bundle. This value is a monotonically increased string, comprised of one or more period-separated integers. This value can correspond to either released or unreleased versions of the application. This value cannot be localized.
CFBundlePackageType (Bundle OS Type code)
The type of bundle this is. For applications, the value of this key is always the four-character string APPL.
CFBundleSignature (Bundle creator OS Type code)
The creator code for the bundle. This is a four-character string that is specific to the bundle. For example, the signature for the TextEdit application is ttxt.
CFBundleExecutable (Executable file)
The name of the main executable file. This is the code that is executed when the user launches your application. Xcode typically sets the value of this key automatically at build time.
Table 5: Recommended keys for the Info.plist file
CFBundleDocumentTypes (Document types)
The document types supported by the application. This type consists of an array of dictionaries, each of which provides information about a specific document type.
CFBundleShortVersionString (Bundle versions string, short)
The release version of the application. The value of this key is a string comprised of three period-separated integers.
LSMinimumSystemVersion (Minimum system version)
The minimum version of macOS required for this application to run. The value for this key is a string of the form n.n.n where each n is a number representing either the major or minor version number of macOS that is required. For example, the value 10.1.5 would represent macOS v10.1.5.
NSHumanReadableCopyright (Copyright (human-readable))
The copyright notice for the application. This is a human readable string and can be localized by including the key in an InfoPlist.strings file in your language-specific project directories.
NSMainNibFile (Main nib file base name)
The nib file to load when the application is launched (without the .nib filename extension). The main nib file is an Interface Builder archive containing the objects (main window, application delegate, and so on) needed at launch time.
NSPrincipalClass (Principal class)
For an application bundle, this is almost always the NSApplication class or a custom subclass. (Applicable for Xcode with Objective - C)
The exact information you put into your Info.plist file is dependent on your bundle’s needs and can be localized as necessary.
Info.plist file content for our AppName app is as follows. This file can be best edited using XCode.
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<string>AppName</string> -> Launch Script file name without extension
The Resources directory is where you put all of your images, sounds, nib files, string resources, icon files, data files, and configuration files among others. For the AppName application, we have created a icns file to store the app image in the Resources directory. The icns image file can be created using any online icns image creator. Please refer Pic1.
One special resource that belongs in your top-level Resources directory is your application icon file. By convention, this file takes the name of the bundle and an extension of .icns; the image format can be any supported type, but if no extension is specified, the system assumes .icns.
The MacOS directory contains the executable file and all the dependencies that are related to the application for it to run successfully. This directory also contains the launch script that will launch the exe when the app icon is clicked from the launch pad. The name of the Launch Script file name must be same as the one entered in the Info.plist file. Please refer the following screen shot.
The following launch script is used to launch the AppName application.
chmod +x /Applications/AppName.app/Contents/MacOS/AppName
Our application should now be ready, and the script should run when we double click the app icon. It's important to note that if we rename the application, we need to rename the script file name.
Packages can be used to deliver applications to any number of Macs. We will create a “flat” installer which is a single file with .pkg extension which is an installer that will install the actual Mac OS app.
Creating an installer
Packages is a very intuitive tool that can be used to build an installer, a .pkg. We can download the tool from here and it must be installed on a Mac.
Preparing the environment and metadata for the installer
The installer can create a splash screen, “readme” file, and a license agreement, in three separate files. These files can be added to the Packages Installer that we are going to build for our app. These files can be created using the macOS’s built in TextEdit app. These files can be maintained as part of the source control.
The three files that need to be created are:
Create a new installer project by using the Packages Distribution template. Start Packages, go to the File menu, select the New Project… option, and then Choose a template for the project.
After selecting the Distribution template, click Next button. Then Choose the name and location for the project. The Project Name is the name of the package.
Click the Choose… button to select a Project Directory in which to store the new project file and then click on the Create button.
Just before your new project file is created, Packages will try to access your contacts. Click the Don’t Allow button because we can edit the identifier while configuring.
Packages app will create a file named AppName.pkgproj where .pkgproj is the extension used for project files. The following screen will appear next.
Notice the left sidebar for a blue icon with the word Project next to it. Below this, we can also notice a list of project’s Packages, as a project can be configured to build multiple distributable, like an XCode project can have multiple targets. Each distributable has a yellow/brown “box” (package?) icon and, next to it, its name. For our app, we’ll just have one package.
Highlight the blue Project icon and we’ll go through and configure its Settings, Presentation, Requirements & Resources, and Comments tabs respectively. NOTE: We should be constantly keep saving changes made to this new Packages project as we go.
The project Settings tab
1. We can accept most of the default values on this tab. The Name field will contain the same value you we entered as the Project Name. The installer we build later will be called [APP_NAME].pkg.
2. We can leave the Path field set to build so that the installer we compile will be placed in a folder called build in the AppName project folder we created for this installer.
3. We can leave the Reference Folder set to Project Folder so that all project assets and distributable will be referenced relative to the project home folder we chose earlier, and thus be portable and maintainable.
4. We can leave the Format set to Flat. This means that our installer will be one (clean) single file with a .pkg extension.
5. Finally, let us not change the Exclusions as these are files that rightfully should not be included in our installer. If we find the need to exclude certain files from our installer, e.g., Git files, this would be the place to do so.
The project Presentation tab
On the project’s Presentation tab, we’ll customize the new installer’s user interface and configure some installation options. We’ll work our way through the bulleted list of buttons, starting with Introduction going all the way down to Summary.
Let’s start by adding a splash screen. Click on the Introduction button and then click the + button. A placeholder will appear under the Custom Introduction Localizations section, with the default locality being the USA. Activate the dropdown and choose the Introduction.rtfd we created.
Make sure you configure the path to Introduction.rtfd to be Relative to Project so the new .pkgproj is portable, maintainable, and self-contained. Do the same with the “readme” and license files.
The Destination Select button does not apply to us, so we’ll skip it and click on Installation Type.
Installation Type can be chosen as Standard Install Only.
The Summary button just shows a screen telling the user that the installation succeeded.
The project Requirements & Resources tab
When we distribute a lot of software, it can make maintenance easier to have all our users install the app same way. This tab control over where our app can be installed. It is better to ticks the “Install on startup disk only” checkbox.
Pic 10: Install On Start Up Disk
The comments tab can be skipped.
The package’s Settings tab
The Identifier field will be pre-filled with com.mygreatcompany.pkg.AppName.
The Company Name in the identifier must be changed.
The Vesrion number must be edited based on the app version.
The package’s Payload tab
The payload is the app to be installed.
We need to drag and drop the bundle “AppName.app” that we created earlier under ‘Applications’.
As I drag my AppName.app file into the Payload tab, a sheet entitled Choose options for adding these files: pops up asking if I want to configure my project to reference an Absolute Path or Relative Path. If you chose a Reference Style of Absolute Path, then the project will be hardcoded to look for the target app (deliverable) in a specific location on Mac every time we build the installer. Better to use a Relative Path, that is, relative to the Packages project file.
Since this app need write permission on the MacOS directory, we can alter the access privileges as show in the following figure.
The Scripts tab
The scripts tab is used to add any pre-installation or post installation scripts for our app. Since our app is dependent on the Mono Framework, I have created a shell script that will take care of installing the following pre-requisites for our app.
Let’s create a shell script we will install the mentioned pre-requisites. Here is the script.
# install home brew
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/master/install.sh)"
#Install azure cli and azcopy
brew install azure-cli
brew install azcopy
#Download and Install mono
curl -O https://download.mono-project.com/archive/6.12.0/macos-10-universal/MonoFramework-MDK-184.108.40.206.macos10.xamarin.universal.pkg
sudo installer -pkg MonoFramework-MDK-220.127.116.11.macos10.xamarin.universal.pkg -target /Library/Frameworks
We can name this file as PreInstallation.sh and we need to run the following command from the terminal to make this file executable.
chmod +x /path/to/the/preinstallation.sh
Pic 14: PRE-INSTALLATION Script
Building the Packages installer
We can now create a distributable capable of installing our app on any compatible Mac. To create the installer/package (.pkg), which is universally understood and executed by macOS, we need to “build” the installer.
Go to the Build menu and select the Build option. We can watch the entire process live in the Build Results window that automatically pops open when we hit that Build option. We can expand all the line items in the Build Results window and inspect their contents.
Signing the Packages installer
To properly distribute the app so the installer runs without generating scary messages from the macOS Gatekeeper, we need to 1) sign and 2) notarize the installer.
Signing a package is a multi-step process.
Generate a signing request.
Generate Developer ID Installer certificate.
Sign the macOS PKG file.
Generating a certificate
We require a Certificate Signing Request (CSR) file.
Open “Keychain Access” program within the macOS device.
Click on Keychain Access appearing on the top menu bar.
Go to Certificate Assistant > Request a Certificate from a Certificate Authority.
Add your email address in the User Email Address field, and name in the Common Name field. Leave the CA Email Address field blank.
Under the Request is option, click Saved to Disc.
Specify the location on the device where the .csr file is to be saved and click Save.
The signing request will be saved to the machine in the specified location. This file is required to generate the “Developer ID Installer” certificate.
To generate the certificate:
Go to Apple Developer Portal. Click on Accounts.
Either create a new account or sign in using an existing account.
Click on Certificates > IDs > Profiles.
Click on Certificates + and select Developer ID Installer.Upload the Certificate Signing Request which was downloaded in the above step.
The Developer ID Installer certificate will be generated. Download the certificate and install it on your macOS device to sign the packages
To sign a macOS PKG file,
Open “Keychain Access” within the Mac and locate the certificate. The name of the certificate should be of the format: Developer ID Installer: Apple account name (serial number).
Open “Terminal”. The command to sign the package should look something like this
productsign -sign “Developer ID Installer: Your Apple Account Name (**********)” ~/Desktop/AppName.pkg ~/Desktop/signed-AppName.pkg
Here, the quoted text following the –sign tag refers to the name of the certificate. The two arguments, following the name of the certificate, refer to the current location of the unsigned package (/Desktop/AppName.pkg) and the location of the signed package (/Desktop/signed-AppName.pkg), respectively.
For software and applications that are downloaded from places other than the Mac App Store, developers can get a Developer ID certificate and submit their software for notarization by Apple. Digitally signing software with a unique Developer ID and including a notarization ticket from Apple lets Gatekeeper verify that the software is not known malware and has not been tampered with.
To notarize our app we need Apple ID and your installer’s “bundle ID,” i.e., us.mygreatcompany.pkg.AppName as we discussed above. If you use two-factor authentication with your Apple ID, you’ll need to use app-specific passwords. If you don’t want to send your Apple ID and password as clear text, you can use Keychain placeholders.
The following command can be run on the Terminal to notarize the app.
xcrun altool --notarize-app --primary-bundle-id "Bundle ID" --username "firstname.lastname@example.org" --password "PASSWORD" --file Signed-AppNamePackage.pkg
Terminal will hang a little while the installer is uploaded but will come back shortly with the following output.
No errors uploading Signed-AppNamePackage.pkg'.
RequestUUID = GUID
Keep that RequestUUID because we will use it to check and track the notarization status of the installer. We can wait for an email that will go to our Apple ID, but let’s stick with the Terminal workflow. In a few minutes, we again need to run this command.
xcrun altool --notarization-history 0 -u "email@example.com" -p "PASSWORD"
It will return immediately with a log of your notarization results — a list of all submissions and their respective statuses. Look for the line item that has the our RequestUUID. Please refer an example result.
Pic 17: Notarization History
The installer is almost ready. Note that if Apple hasn’t finished notarizing the installer, it will tell that the request is pending.
We can run the following command to check the status.
xcrun altool --notarization-info RequestUUID "firstname.lastname@example.org" -p "PASSWORD"
When our users download and run the installer, Apple knows via an Internet connection through Gatekeeper to allow the installer to proceed. But what if, for a variety of reasons, a user’s Internet connection drops just as they’re running the installer? If Apple can’t be reached, Gatekeeper won’t allow installation. For just such occasions, Apple allows you to “staple” a “ticket” to your installer so that it runs, net connection or not. The following command can be used to Staple the package.
xcrun stapler staple "Signed-SCCopPackage.pkg"
The signed and notarized installer is now ready. This package can be posted on the company website and the users can download and use it.
Number of views (453)/Comments (0)