Making a Mac Application Bundle manually
Recently I was porting an SDL application, which I develop on Linux, to macOS. I can do the basic compilation in a CI job, but as I don’t have direct access to a Mac or Xcode, I looked into a way of manually making an Application Bundle for distribution. It took a bit of digging and trial and error but it can be done!
A “bundle” is Apple’s system of packaging applications. To users, bundles look like single files, but they are actually collections of all the program code and resources the app needs to run. Often, Mac developers use Xcode, Apple’s own IDE, which handles bundles itself.
Apple’s documentation archive has a section on bundles. Fortunately for us, an app bundle is basically just a directory ending in .app with some special contents. It has this basic structure:
MyApp.app/ Contents/ MacOS/ MyApp Resources/ MyApp.icns Info.plist
Your app’s executable goes in Contents/MacOS, an Apple “iconset” goes in Contents/Resources, and the metadata file Info.plist goes in Contents.
The bundle metadata is stored in Contents/Info.plist. Apple also has documentation archives for Info.plist files; here’s a example file containing all required and recommended keys, to be filled in:
<?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"> <plist version="1.0"> <dict> <key>CFBundleInfoDictionaryVersion</key> <string>6.0</string> <key>CFBundleDevelopmentRegion</key> <string>en</string> <key>CFBundlePackageType</key> <string>APPL</string> <key>CFBundleIdentifier</key> <string></string> <key>CFBundleExecutable</key> <string></string> <key>CFBundleIconFile</key> <string></string> <key>CFBundleDisplayName</key> <string></string> <key>CFBundleName</key> <string></string> <key>CFBundleVersion</key> <string></string> <key>CFBundleShortVersionString</key> <string></string> <key>NSHumanReadableCopyright</key> <string></string> <key>CFBundleSignature</key> <string>????</string> </dict> </plist>
- CFBundleDevelopmentRegion should be a language ID, e.g. en, en-GB, fr-FR, etc.
- CFBundleIdentifier is an ID containing only alphanumeric (A-Z,a-z,0-9), hyphen (-), and period (.) characters. Ideally it should be “reverse DNS” style, so if your domain is example.com and your app is called MyApp, you could use com.example.MyApp.
- CFBundleExecutable is the name of the executable file placed in Contents/MacOS which is run when the user runs the app.
- CFBundleIconFile is the name of an .icns iconset file in Contents/Resources. See below for more info on how to make an iconset.
- CFBundleDisplayName is your app’s human-readable name. It is worth noting that, if this string is different to the name of the .app bundle directory, the directory name will be shown instead. This may make this key seem useless, but if you provide localised versions of the display name (see Apple docs), they will be used as appropriate instead of this key.
- CFBundleName is also a name, but apparently “should be less than 16 characters long.” I don’t know what it does exactly, but best to be the same as the display name if possible.
CFBundleVersion is a version number, with quite a lot of constraints:
It should be a string comprised of three non-negative, period-separated integers with the first integer being greater than zero—for example, 3.1.2. The string should only contain numeric (0-9) and period (.) characters. Leading zeros are truncated from each integer and will be ignored (that is, 1.02.3 is equivalent to 1.2.3). The meaning of each element is as follows:
- The first number represents the most recent major release and is limited to a maximum length of four digits.
- The second number represents the most recent significant revision and is limited to a maximum length of two digits.
- The third number represents the most recent minor bug fix and is limited to a maximum length of two digits.
If the value of the third number is 0, you can omit it and the second period.
While developing a new version of your app, you can include a suffix after the number that is being updated; for example 3.1.3a1. The character in the suffix represents the stage of development for the new version. For example, you can represent development, alpha, beta, and final candidate, by d, a, b, and fc. The final number in the suffix is the build version, which cannot be 0 and cannot exceed 255. When you release the new version of your app, remove the suffix.
- CFBundleShortVersionString is a release version number, which must be “a string composed of three period-separated integers. The first integer represents major revision to the app, such as a revision that implements new features or major changes. The second integer denotes a revision that implements less prominent features. The third integer represents a maintenance release revision.” In other words, it’s just like CFBundleVersion, except without the suffix and has to have all three numbers.
- NSHumanReadableCopyright is a string with the copyright notice for the bundle; for example, © 2020, My Company.
- CFBundleSignature is apparently a legacy key, but is initialised to ???? in Xcode by default, so I’ve included it just to be safe.
There are a few other keys which may be useful:
The category for your app; see the LSApplicationCategoryType reference for possible values.
For when you need to create DPI-aware windows, e.g. with SDL_WINDOW_ALLOW_HIGHDPI in SDL2.
Creating an iconset (.icns)
Fortunately it’s very easy to convert to Apple’s icon file format with portable tools. If you install libicns, you get a program called png2icns which converts a series of PNGs into an iconset. Here’s a short guide by Jens Reimann.
Accessing the Resources directory
As well as the iconset, Contents/Resources is intended to hold resources and data files for your application. But how do you get its path?
In my searching I couldn’t find a macOS C API for this; you have to use their Objective-C API, something I didn’t want to attempt without a Mac to hand. However, it is possible to write a small wrapper which can be called from C. In fact, SDL2 has a function SDL_GetBasePath which on macOS returns a path to the Resources directory.
Hopefully this quick write-up saves someone the time of trawling the web for something that works! Also, even if you had Mac testing volunteers as I did, I would probably still recommend getting access to an actual Mac yourself. It will make the development process much smoother–there’s only so much you can cope with doing via other people!