Package Qt6 App as macOS App Bundle With CMake

The Qt documentation contains all the necessary pieces to create a macOS app bundle. Some steps require CMake configuration, while others require manual labor, i.e., terminal commands. Ideally, you, the developer, want to automate the whole thing and not enter the commands every time you build a release.

You can do that with CMake, and this How-To will show you what to do. I am taking my WorkTracker application as an example since it isn’t just a little toy with an executable binary. It is a fully functional application I use daily at work (albeit on Windows) with icon resources, language files, and several Qt libraries and plugins.

Note: I will not elaborate on the language file topic, as it requires code changes to find the translations in the bundle file. This post focuses on automating the app-bundle creation and setting an application icon.

You can view the complete CMakeLists.txt file on GitHub. I focus on the macOS-specific additions here.

Every App Needs an Icon

Going in the order I opted for in the CMakeList.txt, let me start with the icon first. macOS expects the icon to be in the ICNS format. The file has several variants of the app’s icon in different resolutions. You put them all in a folder and use Apple’s iconutil to convert the folder into an ICNS file.

% pwd
/Users/rlo/Code/WorkTracker/icon
% ls icon.iconset
icon_128x128.png    icon_16x16.png      icon_256x256.png    icon_32x32.png      icon_512x512.png
icon_128x128@2x.png icon_16x16@2x.png   icon_256x256@2x.png icon_32x32@2x.png   icon_512x512@2x.png
% iconutil -c icns icon.iconset

iconutil turns this…

A finder window that shows all the required icon sizes for a macOS ICNS icon file.

… into this.

An image of an ICNS icon file created by the iconutil using the list of previously shown icons.

You can find a textual representation of my screenshots in another person’s GitHub repo as a markdown file. That is where I learned how to do it.

Now that an ICNS file exists, it is time to tell CMake what to do with it. Qt’s documentation covers this part very well.

set(MACOSX_BUNDLE_ICON_FILE icon.icns)
set(app_icon_macos "${CMAKE_SOURCE_DIR}/icon/icon.icns")
set_source_files_properties(${app_icon_macos} PROPERTIES MACOSX_PACKAGE_LOCATION "Resources")

qt_add_executable(WorkTracker MACOSX_BUNDLE ${worktracker_src} ${app_icon_macos})

The first command ensures the name of the icon file ends up in the app bundle’s Info.plist file.

<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
    ...

    <key>CFBundleIconFile</key>
    <string>icon.icns</string>

    ...
</dict>
</plist>

Then the absolute path is stored in a variable that CMake uses to copy the icon to its bundle location in the “Resources” folder (MACOSX_PACKAGE_LOCATION) and, lastly, to set it as the executable’s icon.

Bundling the App Into a Bundled App Bundle With Max DMG

CMake needs to know that it is supposed to create an app bundle.

set_target_properties(WorkTracker PROPERTIES
    MACOSX_BUNDLE ON
)

It looks simple enough, but this is not the end of the story, unfortunately. You get your bundle, but it will not run because it has no idea where to find the required Qt frameworks (unless you statically link everything into your executable). It likely works on your dev box but not on a clean macOS where no Qt frameworks are installed.

FYI: Something similar exists for Windows deployments.

set_target_properties(WorkTracker PROPERTIES
    WIN32_EXECUTABLE ON
)

According to the Qt documentation (scroll down a bit), this prevents GUI applications from showing a console window.

Okay, so CMake creates a bundle without frameworks. So how do you solve this problem? You can take a bunch of manual steps explained in the documentation or reduce them to a single one and utilize Qt’s macdeployqt tool. This is the point I hinted at earlier, where you preferably include this step in the build tool instead of doing it manually every time.

Before running the utility in CMake, you need a few convoluted steps to obtain its location in the filesystem.

get_target_property(_qmake_executable Qt6::qmake IMPORTED_LOCATION)
get_filename_component(_qt_bin_dir "${_qmake_executable}" DIRECTORY)
find_program(MACDEPLOYQT_EXECUTABLE macdeployqt HINTS "${_qt_bin_dir}")

First, you get the absolute path to the qmake executable using the get_target_property() function with the IMPORTED_LOCATION parameter. Then, you extract just the path component with get_filename_component() (or the equivalent cmake_path() call that replaces the one I used). With _qt_bin_dir in hand, it is time to find macdeployqt and finally run it.

add_custom_command(TARGET WorkTracker POST_BUILD
        COMMAND "${MACDEPLOYQT_EXECUTABLE}"
        ARGS "WorkTracker.app" "-dmg"
        COMMENT "Execute macdeployqt to create macOS bundle")

Execution of macdeployqt is done after CMake creates the app bundle. As you can see, I also let it build a DMG file to distribute the application.

I learned this technique here.

For Science

I was curious how the different CMake steps affect the outcome of the generated app bundle. Hence, I conducted an experiment and built the app four times:

  1. Without both the MACOSX_BUNDLE ON option and the macdeployqt tool.
  2. With MACOSX_BUNDLE ON but without the macdeployqt tool.
  3. Without the MACOSX_BUNDLE ON option but with the macdeployqt tool.
  4. With both the MACOSX_BUNDLE ON option and the macdeployqt tool.

The results are surprising.

Four macOS finder windows showing the contents of an app bundle created by different CMake settings. The first two match and last two also match, showing all the required contents for a functioning Qt macOS application.

I did not go to great lengths to compare the outcomes. From only looking at the bundle’s contents, it appears that setting or not setting MACOSX_BUNDLE ON does not make a difference. However, it could very well be that some hidden compiler, linker, or bundler options are set that somehow affect the outcome. Additionally, the macdeployqt utility does not seem to care whether the option mentioned above is ON or OFF. The effect apparently is the same.

Either way, I’ll set all the options just to be sure. The more, the merrier, right?

I hope this was helpful. Thank you for reading.

One thought on “Package Qt6 App as macOS App Bundle With CMake

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

This site uses Akismet to reduce spam. Learn how your comment data is processed.