Martin Helmut Fieber

CMake & CPack for cross-platform distributables

Posted on — Updated

Application distribution

In a previous article about GUI applications with C++, SDL2, and Dear ImGUI, I explored one way of creating a GUI application setup with CMake. It aimed to be a base template to get started with for further projects. Though, building and running the application is only the beginning — a crucial part was missing to make it a true and solid starter template: distribution.

This article will aim at creating distributable packages for macOS, Windows, and Linux using CPack. Not a minimal setup, but rather a setup that touches on enough aspects to have a solid understanding of creating installers for the major systems, showing how to add static assets, like fonts, and creating an application icon.

Any GUI application built with CMake can be used for this, like the mentioned application setup from an earlier article or the small project I created that comes with a minimal setup and a branch called without-cpack to follow along this article.

You can pick either or your own, but for this article I will assume the second small project setup, as it helps to focus on the core aspects of packaging.

A macOS application window showing the example application used in this article with a dark background and the text 'Small SDL2 App'.
The small sample application used for this article.

Expectation

Distributing an application means many things. Depending on the targeted platforms, it needs a specific format for the installer, thoughts about how to actually install the application, pack assets, an application icon, licensing, etc. — and all this can vary a lot per system.

Where Windows and Linux can be built with a couple of different tools, for example, Make or Ninja, Xcode on macOS is the choice for proper system integration.

So let's go and have a look.


Target folder structure

Before looking at how to install all needed application resources, it is necessary to look at where files should go. Directory layouts are different per operating system, though there are some similarities that can be taken advantage of. A program can be installed at a target path, or base path, with all other resources located relative to that base path, making the application movable on the system.

Examples for those common application locations are /urs/local/ and /opt/ on Linux, or C:\Program Files on Windows. The /lib/ folder often gets used for library and object files, doc/ and man/ for documentation and man-files respectively.

In contrast, for Apple's macOS, it is common to create an application bundle containing all the files for the application to work in one place. The application bundle is a strictly structured folder with the .app prefix, including various other folders and files. Inside Contents/MacOS/ is the executable, Contents/Resources/ holds static files such as fonts, and Contents/Frameworks/ is for shared libraries.

To cover most of those platform-dependent cases, the CMake GNUInstallDirs module can be included, providing a set of cache variables to point to different places for common install scenarios.

Defining a structure

The earlier the target installation structure is planned, defining the components of the applications and how they are laid out, the easier it will be to bring it to fruition and to the users.

Taking an application executable, SDL2 as a shared library, an application icon, and a font as examples, let's look at where to place them on the respective target system.

macOS

As mentioned, the application on macOS is bundled inside its own .app folder, ideally containing all files needed for the application to run. The implication here is that the application bundle can be moved anywhere on the system and still works.

The .app folder has a Contents folder that actually contains all the files in a predefined structure. On the root of this folder is always an Info.plist file, defining various properties of the application as key-value pairs specific for Apple systems. Example values are the icon name without extension, the executable name, or copyright information.

Inside Contents, the MacOS folder contains the executable, Frameworks are dynamic libraries, and Resources hold static files like icons or fonts.

Image showing the folder structure on macOS of the application bundle. The root folder is SmallSDL2App.app, with Contents underneath. This then contains Frameworks with SDL2.dylib files, the MacOS folder with the executable, a Resource folder with icons.icns and the Manrope.ttf file, and an Info.plist and PkgInfo file.
Application bundle structure on macOS.

Linux

On most Linux distributions, and depending on the type of installed package*, files have specific locations at different parts of the user's system, defined in the Linux filesystem hierarchy standard (FHS), with the executable often placed in /usr/local/.

Thankfully, with CMake and GNUInstallDirs module, those desired places are defined via variables that can be used when installing a component with CMake. It will place executables, static and shared libraries, and other files and dependencies in their appropriate places.

What will need to be taken care of by adding a .desktop file to the /usr/share/applications/ folder to create an application icon and entry in the system, as defined by the desktop menu specification, and copying the application icon to the /usr/share/pixmaps/ directory.

Windows

On Windows, an application is usually placed inside a program folder in C:\Program Files\ under the application name. In that application folder, the executable and all installed components will be located, often in a flat hierarchy — a folder named shared is used for static assets like images and fonts.

Similar to Linux, most of this structure comes out of the box with CMake and its predefined variables, ready to be used for the installation of the applications and their parts.

Additionally, to define an icon for the application, a resource file is needed that defines the location in the application bundle and a manifest file for application properties like high-DPI support. Both files will be included in the bundle by providing them as application sources through CMake.

CMake install

Now, to actually get the application, files, and folders to their desired places, they need to be installed using CMake's install() command. The command itself is extremely versatile, giving the option to install targets with dependencies, files and folders, libraries, header files, rename them on installation, define permissions, and much more.

For example, one could copy a whole directory to a desired destination on installation.

# Contains definition for CMAKE_INSTALL_DATADIR
# Only needs to be included once
include(GNUInstallDirs)

# Install the folder "assets" to DATADIR
install(DIRECTORY assets
  DESTINATION ${CMAKE_INSTALL_DATADIR})

Or a file, renaming it on installation.

install(FILES assets/icons/icon.png
  DESTINATION share/pixmaps
  RENAME my_app_icon.png)

Any CMake target can be installed too, as should the main application, but this needs some clarification for the different predefined destinations to install to.

Destinations

CMake's "GNUInstallDirs" module provides a way to use a standard directory layout by defining a set of convenient variables.

include(GNUInstallDirs)
Only needs to be included once in the root CMakeLists.txt.

Some of those variables defined through the module are the following:

  • BINDIR — Executables, scripts, and symlinks to be run directly. Defaults to bin.
  • LIBDIR — Libraries and object files. Often defaults to lib, dependents on the target platform.
  • DATADIR — Read-only directory for static assets like images or fonts. Often defaults to share, dependents on the target platform.

A complete list can be found at the GNUInstallDirs module documentation page. To access one of those variables, they need to be prefixed with CMAKE_INSTALL_, e.g. CMAKE_INSTALL_BINDIR.

Install application target

Let's now take those destinations and install the application target. A reduced base application setup could look like the following, with an executable called "MyApp".

# src/CMakeLists.txt

# Main executable
add_executable(MyApp
  # Source files cpp/hpp ...
  )

# Any libraries
target_link_libraries(MyApp
  PUBLIC SDL2::SDL2)

The goal is to build a GUI application bundle, adding the WIN32 and MACOSX_BUNDLE options to the add_executable() command will instruct CMake to do the "right thing" — meaning, on Windows, it will build a Windows GUI application by using WinMain() instead of main() and the MACOSX_BUNDLE option will create a basic Info.plist file and respect the macOS bundle directory structure.

# src/CMakeLists.txt

# Main executable
add_executable(MyApp WIN32 MACOSX_BUNDLE
  # Source files cpp/hpp ...
  )

# Any libraries
target_link_libraries(MyApp
  PUBLIC SDL2::SDL2)

The options will be ignored when not built on their respective platforms, so WIN32 will be ignored when building on macOS, and vice versa.

Next, the application target can be installed with the install() command, defining the needed destinations.

# src/CMakeLists.txt

# Main executable
add_executable(MyApp WIN32 MACOSX_BUNDLE
  # Source files cpp/hpp ...
  )

# Install application target
install(TARGETS MyApp
  BUNDLE DESTINATION .
  RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
  LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
  ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR})

# Any libraries
target_link_libraries(MyApp
  PUBLIC SDL2::SDL2)

The BUNDLE DESTINATION is for macOS, defining that all executables marked with MACOSX_BUNDLE shall be treated as bundle targets. On other systems, RUNTIME DESTINATION is used for this, including DLLs on Windows.

LIBRARY DESTINATION includes all shared libraries, except DLLs on Windows or targets marked as FRAMEWORK on macOS.

Static libraries are included in ARCHIVE DESTINATION, except if marked as FRAMEWORK on macOS.

Defining a library target as FRAMEWORK will build it as a shared or static framework bundle for macOS and iOS, with CMake creating the required directory structure.

Install SDL2

Where and how to install a shared library, in this case SLD2, depends on the operating system. Windows and Linux are very similar, thanks to the predefined variables in GNUInstallDirs, Apple's macOS needs a different configuration.

For what I think is a good structure, any platform-related CMake code will be split into separate CMakeLists.txt files in dedicated folders for every supported operating system. Those folders will later hold more platform-dependent files.

After the application target install, a CMake file per platform will be included to install any shared libraries — files that can contain other OS-specific properties and options.

# src/CMakeLists.txt

# Other CMake ...

install(TARGETS MyApp
  # Target settings ...
  )

# Include settings per platform
if (CMAKE_SYSTEM_NAME STREQUAL "Windows")
  include(platform/windows/CMakeLists.txt)
elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux")
  include(platform/linux/CMakeLists.txt)
elseif (CMAKE_SYSTEM_NAME STREQUAL "Darwin")
  include(platform/darwin/CMakeLists.txt)
endif ()

# ...

macOS

By not using variables defined through the mentioned GNUInstallDirs, Apple works differently. Through a post-build command, any shared library like SDL2 will be copied into the Frameworks folder of the application bundle.

add_custom_command(TARGET MyApp POST_BUILD
  COMMAND ${CMAKE_COMMAND} -E copy_if_different
  $<TARGET_FILE:SDL2::SDL2>
  $<TARGET_FILE_DIR:MyApp>/../Frameworks/$<TARGET_FILE_NAME:SDL2::SDL2>)

When building applications without Xcode for Apple, e.g., using Ninja, the install() function will be used.

if (NOT "${CMAKE_GENERATOR}" STREQUAL "Xcode")
  install(FILES $<TARGET_FILE:SDL2::SDL2>
    DESTINATION $<TARGET_FILE_DIR:MyApp>/../Frameworks/)
endif ()

Necessarily, to let macOS find installed libraries in the Framework folder, the INSTALL_RPATH property needs to be set on the target to find runtime libraries.

set_target_properties(MyApp PROPERTIES
  INSTALL_RPATH @executable_path/../Frameworks)

The full CMake code to install SDL2 as a shared library for development and distribution on macOS looks as follows:

# src/platform/darwin/CMakeLists.txt

# Get dynamic SDL2 lib into Frameworks folder in app bundle.
add_custom_command(TARGET MyApp POST_BUILD
  COMMAND ${CMAKE_COMMAND} -E copy_if_different
  $<TARGET_FILE:SDL2::SDL2>
  $<TARGET_FILE_DIR:MyApp>/../Frameworks/$<TARGET_FILE_NAME:SDL2::SDL2>)

# For distribution without Xcode:
if (NOT "${CMAKE_GENERATOR}" STREQUAL "Xcode")
  install(FILES $<TARGET_FILE:SDL2::SDL2>
    DESTINATION $<TARGET_FILE_DIR:MyApp>/../Frameworks/)
endif ()

# Set runtime library path
set_target_properties(MyApp PROPERTIES
  INSTALL_RPATH @executable_path/../Frameworks)

Linux

There are two parts to installing SDL2 as a shared library on Linux: For development, a post-build command will copy any dynamic library files to the target application folder.

add_custom_command(TARGET MyApp POST_BUILD
  COMMAND ${CMAKE_COMMAND} -E copy_if_different
  $<TARGET_FILE:SDL2::SDL2>
  $<TARGET_FILE_DIR:MyApp>)

For distribution, the install() command will copy the library files to the CMake's GNUInstallDirs module defined CMAKE_INSTALL_BINDIR directory.

Important to note here is that the target library file name will be renamed, adding the application target name as a prefix. I did this to avoid collisions with other applications installing a version of SDL2 in a shared folder on Linux.

install(FILES $<TARGET_FILE:SDL2::SDL2>
  DESTINATION ${CMAKE_INSTALL_BINDIR}
  RENAME MyApp_$<TARGET_FILE_NAME:SDL2::SDL2>)

Here is the full CMake code to install SDL2 as a shared library for development and distribution on Linux.

# src/platform/linux/CMakeLists.txt

# Copy .so files on Linux to the target MyApp build folder.

# For development:
add_custom_command(TARGET MyApp POST_BUILD
  COMMAND ${CMAKE_COMMAND} -E copy_if_different
  $<TARGET_FILE:SDL2::SDL2>
  $<TARGET_FILE_DIR:MyApp>)

# For distribution:
install(FILES $<TARGET_FILE:SDL2::SDL2>
  DESTINATION ${CMAKE_INSTALL_BINDIR}
  RENAME MyApp_$<TARGET_FILE_NAME:SDL2::SDL2>)

Windows

Installing SDL2 as a shared library on Windows is very similar to installing it on Linux. For development, a post-build command will copy any dynamic library files (.dll) to the target application folder.

add_custom_command(TARGET MyApp POST_BUILD
  COMMAND ${CMAKE_COMMAND} -E copy_if_different
  $<TARGET_FILE:SDL2::SDL2>
  $<TARGET_FILE_DIR:MyApp>)

For the distribution, the install() command will copy the library files to the CMake module GNUInstallDirs defined CMAKE_INSTALL_BINDIR directory, same as Linux.

install(FILES $<TARGET_FILE:SDL2::SDL2>
  DESTINATION ${CMAKE_INSTALL_BINDIR})

Resulting in the following CMake code to install SDL2 as a shared library for development and distribution on Windows.

# src/platform/windows/CMakeLists.txt

# Copy .dll files on Windows to the target MyApp build folder.

# For development:
add_custom_command(TARGET MyApp POST_BUILD
  COMMAND ${CMAKE_COMMAND} -E copy_if_different
  $<TARGET_FILE:SDL2::SDL2>
  $<TARGET_FILE_DIR:MyApp>)

# For distribution:
install(FILES $<TARGET_FILE:SDL2::SDL2>
  DESTINATION ${CMAKE_INSTALL_BINDIR})

Install static assets

Static assets, like images or fonts, also need to be available while developing the application and installed for the final application bundle. Some assets might be used by all supported platforms, others are platform specific, like having different icons for different operating systems.

One way is to define a CMake variable for all static assets and install them via the respective CMake files, enabling the ability to add platform-dependent files or adjust options.

Let's say there is a font file used for all platforms: assets/fonts/Manrope.ttf*, creating a SHARED_STATIC_ASSETS variable at the top of the target CMake file after the definition of the executable.

# src/CMakeLists.txt

# Main executable
add_executable(#[[ ... ]])

# Assets for all platforms
set(SHARED_STATIC_ASSETS assets/fonts/Manrope.ttf)

# Other settings ...

macOS

On Apple platforms, installing static assets needs a couple of things to work: first, telling CMake where to install those assets via the set_source_files_properties() command and defining the MACOSX_PACKAGE_LOCATION property to instruct the installation into the application bundles Resources folder.

set_source_files_properties(${SHARED_STATIC_ASSETS}
  PROPERTIES MACOSX_PACKAGE_LOCATION ${CMAKE_INSTALL_DATADIR})

Using CMake's target_source() function, specify what target will use those sources.

target_sources(MyApp PUBLIC ${SHARED_STATIC_ASSETS})

And, extending the already existing set_target_properties() command, defining what files are resources to the application bundle.

set_target_properties(MyApp PROPERTIES
  INSTALL_RPATH @executable_path/../Frameworks
  RESOURCE "${SHARED_STATIC_ASSETS}")

Here is the full CMake to install static assets in the right location for Apple platforms.

# src/platform/darwin/CMakeLists.txt

# Static assets
set_source_files_properties(${SHARED_STATIC_ASSETS}
  PROPERTIES MACOSX_PACKAGE_LOCATION ${CMAKE_INSTALL_DATADIR})
target_sources(MyApp PUBLIC ${SHARED_STATIC_ASSETS})

# Other CMake settings ...

# Target properties
set_target_properties(MyApp PROPERTIES
  INSTALL_RPATH @executable_path/../Frameworks
  RESOURCE "${SHARED_STATIC_ASSETS}")

Linux and Windows

With CMake's target_source() function, adding the font for Linux and Windows works the same; on top of both platform files, the static assets are linked to the target.

# src/platform/[linux|windows]/CMakeLists.txt

# Static assets
target_sources(MyApp PRIVATE ${SHARED_STATIC_ASSETS})

Adding a custom post-build command with add_custom_command() will add all assets into a share application folder for development.

add_custom_command(TARGET MyApp POST_BUILD
  COMMAND ${CMAKE_COMMAND} -E copy_directory
  ${PROJECT_SOURCE_DIR}/src/assets
  $<TARGET_FILE_DIR:MyApp>/../share)

And the install() command will do the same for distribution.

install(DIRECTORY ${PROJECT_SOURCE_DIR}/src/assets
  DESTINATION ${CMAKE_INSTALL_DATADIR})

Here is the full configuration to include static assets on Linux and Windows.

# src/platform/[linux|windows]/CMakeLists.txt

# Static assets
target_sources(MyApp PRIVATE ${SHARED_STATIC_ASSETS})

# Copy assets into app bundle
# For development:
add_custom_command(TARGET MyApp POST_BUILD
  COMMAND ${CMAKE_COMMAND} -E copy_directory
  ${PROJECT_SOURCE_DIR}/src/assets
  $<TARGET_FILE_DIR:MyApp>/../share)

# For distribution:
install(DIRECTORY ${PROJECT_SOURCE_DIR}/src/assets
  DESTINATION ${CMAKE_INSTALL_DATADIR})

# Other settings ...

Setup CPack

With a clear target structure and installation setup, CPack can now be used to create distributable packages for the targeted platforms. Starting with a basic set of options to create a compressed TAR archive and scaling to a more holistic approach for platform-specific installers.

A simple package

To keep things together, I place all packaging-related resources in a folder called packaging at the project root. This subdirectory will be added to the root CMake file.

# CMakeLists.txt

# Other CMake settings ...

# Add packaging directory
add_subdirectory(packaging)

# Application sources
add_subdirectory(src)

Before a CPack generator can be defined, some base settings need to be set.

Base CPack settings

Due to backwards compatibility, this is not the default, but the CPACK_VERBATIM_VARIABLES value should always be set to true. This will enable CPack to escape values when writing its configuration file.

set(CPACK_VERBATIM_VARIABLES YES)

A package vendor is defined with CPACK_PACKAGE_VENDOR.

set(CPACK_PACKAGE_VENDOR "My Company")

By default, CPack will create the distributable under the build folder. For example, if the build directory is build/release all built distributables will be created in that folder.

This setting can be changed via CPACK_PACKAGE_DIRECTORY, to keep all generated packages in one folder, e.g., under the folder distribution.

set(CPACK_PACKAGE_DIRECTORY distribution)

To now also influence the name of the generated packages CPACK_SOURCE_PACKAGE_FILE_NAME is used. This enables, for example, adding what architecture the package was built for or the application version.

set(CPACK_SOURCE_PACKAGE_FILE_NAME "myapp-${CMAKE_PROJECT_VERSION}")

The installation directory can be customized with CPACK_PACKAGE_INSTALL_DIRECTORY. Some generators, like NSIS (Nullsoft Scriptable Install System) under Windows, will use this and install all components in that folder. A common setting is the package or project name.

set(CPACK_PACKAGE_INSTALL_DIRECTORY ${CPACK_PACKAGE_NAME})

And, if there are no separate components for the installation, the selection can be skipped via CPACK_MONOLITHIC_INSTALL.

set(CPACK_MONOLITHIC_INSTALL TRUE)

TAR

The CPack generator is set via CPACK_GENERATOR and will for now generate a compressed TAR.

set(CPACK_GENERATOR TGZ)

And finally, including the CPack module.

include(CPack)

The full CPack configuration file for the base example looks like this:

# packaging/CMakeLists.txt

# Base package settings
set(CPACK_VERBATIM_VARIABLES YES)
set(CPACK_PACKAGE_VENDOR ${PROJECT_COMPANY_NAME})
set(CPACK_PACKAGE_DIRECTORY distribution)
set(CPACK_SOURCE_PACKAGE_FILE_NAME "myapp-${CMAKE_PROJECT_VERSION}")
set(CPACK_PACKAGE_INSTALL_DIRECTORY ${CPACK_PACKAGE_NAME})

set(CPACK_GENERATOR TGZ)

include(CPack)

Execute CPack

Before running CPack a release build of the application needs to be created.

$ cmake -GNinja -DCMAKE_BUILD_TYPE=Release -B build/release
$ cmake --build build/release
Replace -GNinja with -GXcode on macOS.
The $ is used to show a command will be entered.

Using this release, a distributable can be created using the cpack command.

$ cpack --config build/release/CPackConfig.cmake

As earlier defined through CPACK_PACKAGE_DIRECTORY, the package will be located at build/release/distribution.

A macOS Finder window showing a .tar.gz file generated through CPack.
The generated .tar.gz file.

Application icon

The application needs a proper icon for all platforms. The difficulty here is that application icons are set up differently per platform — from the file format to how to configure them.

But this also comes with the opportunity to create dedicated icons per platform, tailored to the specific styles of each platform. There are style guides on how to create icons for Apple platforms, guidelines for GNOME app icons, applicable to many Linux derivatives, and even what makes a good app icon for Windows.

macOS

An application icon for Apple macOS is a .icns file, created from a set of icon files inside a icon.iconset folder. To support the full spectrum of icon sizes for different screen resolutions, the folder needs to contain icons in all the following sizes:

  • icon_16x16.png
  • icon_32x32.png
  • icon_128x128.png
  • icon_256x256.png
  • icon_512x512.png

All of them should also be available in double the resolution, postfixed with @2x.

Ten files in total, in a folder called icon.iconset.

A set of five example app icons for macOS showing a rounded rectangle with an orange gradient.
Example application icons for macOS.

With the iconutil command line util provided by macOS, this folder can be converted to the desired .icns file.

$ iconutil -c icns icon.iconset

This command will create a icon.icns file next to the .iconset folder that will need to be added as a static asset. Assuming the icon file is located under assets/icons/icon.icns, it needs to be combined with the shared static assets for macOS.

Creating a new variable MACOSX_STATIC_ASSETS, combining the icon with the previous SHARED_STATIC_ASSETS variable inside src/platform/darwin/CMakeLists.txt.

set(MACOSX_STATIC_ASSETS
  ${SHARED_STATIC_ASSETS}
  assets/icons/icon.icns)

Then, replace the usage of SHARED_STATIC_ASSETS with the new MACOSX_STATIC_ASSETS variable.

# src/platform/darwin/CMakeLists.txt

# Combining shared with macOS assets
set(MACOSX_STATIC_ASSETS
  ${SHARED_STATIC_ASSETS}
  assets/icons/icon.icns)

# Changed `SHARED_STATIC_ASSETS` to `MACOSX_STATIC_ASSETS`
set_source_files_properties(${MACOSX_STATIC_ASSETS}
  PROPERTIES
    MACOSX_PACKAGE_LOCATION ${CMAKE_INSTALL_DATADIR})
target_sources(MyApp
  PUBLIC ${MACOSX_STATIC_ASSETS})

# Other CMake settings ...

# Changed `SHARED_STATIC_ASSETS` to `MACOSX_STATIC_ASSETS`
set_target_properties(MyApp PROPERTIES
  INSTALL_RPATH @executable_path/../Frameworks
  RESOURCE "${MACOSX_STATIC_ASSETS}")
Now with platform-dependent assets.

To then actually connect the icon file to the bundle that will be created, the icon name, in this case icon, needs to be added to the bundles .plist file. This will be done later when creating the application bundle for macOS.

The crucial part in the .plist file is the CFBundleIconFile key, setting the .icns base name as a string.

<key>CFBundleIconFile</key>
<string>icon</string>
The whole .plist file will be created in a later step.

Linux

A typical Linux application icon is a 1024 × 1024 pixel square, but at least 128 × 128 pixel, installed into a folder where the system will look for application icons, in this case, I picked /usr/share/pixmaps.

With the 1024 × 1024 pixel Linux icon located under /src/assets/icons/LinuxIcon.png, it needs to be installed through CMake in src/platform/linux/CMakeLists.txt.

# src/platform/linux/CMakeLists.txt

# Other CMake ...

install(FILES
  ${PROJECT_SOURCE_DIR}/src/assets/icons/LinuxIcon.png
  DESTINATION share/pixmaps
  RENAME myapp_icon.png)

I rename the icon on installation to avoid collisions with other files by specifying the application name. This could be made even more specific by adding a version.

With the icon made available through install, it needs to be associated with the application bundle. This is done through a .desktop file supported by most Linux as defined by the XDG Desktop Entry Specification.

Creating a .desktop file template under src/assets/manifests/MyApp.desktop.in. The folder src/assets/manifests will hold all manifest files for the different systems.

[Desktop Entry]
Name=MyApp
GenericName=@CMAKE_PROJECT_NAME@
Comment=@CMAKE_PROJECT_DESCRIPTION@
Exec=MyApp
Icon=@ENTRY_NAME@_icon
Type=Application
Categories=Miscellaneous;

This file with the extension .desktop.in contains a set of properties that will be filled in by the CMake command configure_file(). CMake and project variable names will replace names enclosed in @.

All variables are already available except ENTRY_NAME, which will be set as a reverse DNS name, e.g. com.mycompany.myapp.

set(ENTRY_NAME "com.mycompany.myapp")

To create the actual .desktop file, the CMake command configure_file() is used.

configure_file(
  ${PROJECT_SOURCE_DIR}/src/assets/manifests/MyApp.desktop.in
  ${CMAKE_CURRENT_BINARY_DIR}/${ENTRY_NAME}.desktop)

This will take the MyApp.desktop.in file as input, fill in all variable names, and write it to the location of CMAKE_CURRENT_BINARY_DIR as ENTRY_NAME with the .desktop extension. Last but not least, install that desktop file on the users' system to share/applications.

install(FILES
  ${CMAKE_CURRENT_BINARY_DIR}/${ENTRY_NAME}.desktop
  DESTINATION share/applications)

Having a .desktop file will also make the application visible on the system as a GUI program.

# src/platform/linux/CMakeLists.txt

# Other CMake ...

# Linux app entry and icon setup
set(ENTRY_NAME "com.mycompany.myapp")
configure_file(
  ${PROJECT_SOURCE_DIR}/src/assets/manifests/MyApp.desktop.in
  ${CMAKE_CURRENT_BINARY_DIR}/${ENTRY_NAME}.desktop)
install(FILES ${CMAKE_CURRENT_BINARY_DIR}/${ENTRY_NAME}.desktop
  DESTINATION share/applications)
install(FILES
  ${PROJECT_SOURCE_DIR}/src/assets/icons/LinuxIcon.png
  DESTINATION share/pixmaps
  RENAME myapp_icon.png)
The full configuration added to the src/platform/linux/CMakeLists.txt.

Windows

An application icon on Windows is a .ico file containing a set of different icon sizes. There is quite a range of icon sizes and formats to support on Windows, but to cover the basic set, the following should be present:

  • icon_16x16.png
  • icon_32x32.png
  • icon_64x64.png
  • icon_128x128.png
  • icon_256x256.png
  • icon_512x512.png

The largest 512-pixel-sized icon file ensures that, in the worst case, Windows takes this and scales the icon down if needed.

Having all those sizes in one folder, I use ImageMagick to convert them to a single .ico file with the convert command line tool.

$ convert \
  icon_16x16.png icon_32x32.png \
  icon_64x64.png icon_128x128.png \
  icon_256x256.png icon_512x512.png \
  icon.ico

The identify command can be used to verify that the .ico file was created properly.

$ identify icon.ico

icon.ico[0] ICO 16x16 16x16+0+0 8-bit sRGB 0.010u 0:00.004
icon.ico[1] ICO 32x32 32x32+0+0 8-bit sRGB 0.010u 0:00.004
icon.ico[2] ICO 64x64 64x64+0+0 8-bit sRGB 0.010u 0:00.004
icon.ico[3] ICO 128x128 128x128+0+0 8-bit sRGB 0.010u 0:00.004
icon.ico[1] PNG 256x256 256x256+0+0 8-bit sRGB 8973B 0.000u 0:00.000
icon.ico[2] PNG 512x512 512x512+0+0 8-bit sRGB 58491B 0.000u 0:00.000

Having a .ico file for Windows is the first step, it also needs a resource file to associate this icon with the Windows application bundle. Creating a new .rc file under src/assets/manifests/app.rc with the following content:

app_icon ICON DISCARDABLE "../icons/icon.ico"

Both files, the icon and resource file, need to be added to the target sources via CMake's target_sources() command, extending the existing command in src/platform/windows/CMakeLists.txt.

target_sources(MyApp PUBLIC
  ${SHARED_STATIC_ASSETS}
  assets/icons/icon.ico
  assets/manifests/app.rc)

The icon will be handled as a static asset, located in the src/assets folder, and copied to the application bundle.

macOS application bundle

A common way on macOS to install a new application is via a "Drag & Drop" disk image, a file with the .dmg extension. The classic "drag this file into your Application folder" process Mac users are well accustomed to.

Before enabling it as a package generator, some things need to be considered for the application target. Namely, to create an Information Property List File, often just called "plist" files due to being XML files with the file extension .plist.

The .plist file will contain necessary information about the bundled executable, settings that can be defined through CMake using the set_target_properties() command in src/platform/darwin/CMakeLists.txt.

Info.plist

Let's first create a base Info.plist under src/assets/manifests/Info.plist.

<!-- src/assets/manifests/Info.plist -->
<?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>CFBundleDevelopmentRegion</key>
    <string>en</string>
    <key>CFBundleIconFile</key>
    <string>icon</string>
    <key>CFBundleInfoDictionaryVersion</key>
    <string>6.0</string>
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>NSMainStoryboardFile</key>
    <string>Main</string>
    <key>NSPrincipalClass</key>
    <string>NSApplication</string>
    <key>NSHighResolutionCapable</key>
    <true/>

    <key>CFBundleExecutable</key>
    <string>$(EXECUTABLE_NAME)</string>
    <key>CFBundleIdentifier</key>
    <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
    <key>LSMinimumSystemVersion</key>
    <string>$(MACOSX_DEPLOYMENT_TARGET)</string>
  </dict>
</plist>

The first block of values are some common application bundle settings as found in the official documentation for the Information Property List File.

The second block is settings with dynamic values via $()*, filled in only when built via Xcode. It contains the name of the executable EXECUTABLE_NAME and bundle PRODUCT_BUNDLE_IDENTIFIER, as well as the macOS required minimum system version MACOSX_DEPLOYMENT_TARGET.

To use that file, the set_target_properties() command is used, setting the MACOSX_BUNDLE_INFO_PLIST property.

set_target_properties(MyApp PROPERTIES
  INSTALL_RPATH @executable_path/../Frameworks
  RESOURCE "${SHARED_STATIC_ASSETS}"
  MACOSX_BUNDLE_INFO_PLIST
    "${CMAKE_CURRENT_SOURCE_DIR}/assets/manifests/Info.plist")
Defined in src/platform/darwin/CMakeLists.txt.

This is the same place where more properties will be defined: a bundle version, the GUI identifier, and copyright information.

First, the version with a full and a short version string.

set_target_properties(MyApp PROPERTIES
  # Other properties ...
  MACOSX_BUNDLE_BUNDLE_VERSION "${BUILD_VERSION}"
  MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION}")

The GUI identifier is usually a combination of the company name in reverse domain name notation and the project name.

set_target_properties(MyApp PROPERTIES
  # Other properties ...
  MACOSX_BUNDLE_BUNDLE_VERSION "${BUILD_VERSION}"
  MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION}"
  MACOSX_BUNDLE_GUI_IDENTIFIER "com.mycompany.myapp"
  XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "com.mycompany.myapp")

And copyright information.

set_target_properties(MyApp PROPERTIES
  # Other properties ...
  MACOSX_BUNDLE_BUNDLE_VERSION "${BUILD_VERSION}"
  MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION}"
  MACOSX_BUNDLE_GUI_IDENTIFIER "com.mycompany.myapp"
  XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "com.mycompany.myapp"
  MACOSX_BUNDLE_COPYRIGHT "(c) 2024 My Company")

With this, the set_target_properties() command has all the properties needed to fill in the .plist file.

# src/platform/darwin/CMakeLists.txt

# Other CMake settings ...

# Target properties
set_target_properties(MyApp PROPERTIES
  INSTALL_RPATH @executable_path/../Frameworks
  RESOURCE "${SHARED_STATIC_ASSETS}"
  MACOSX_BUNDLE_INFO_PLIST
    "${CMAKE_CURRENT_SOURCE_DIR}/assets/manifests/Info.plist"
  MACOSX_BUNDLE_BUNDLE_VERSION "${BUILD_VERSION}"
  MACOSX_BUNDLE_SHORT_VERSION_STRING "${PROJECT_VERSION}"
  MACOSX_BUNDLE_GUI_IDENTIFIER "com.mycompany.myapp"
  XCODE_ATTRIBUTE_PRODUCT_BUNDLE_IDENTIFIER "com.mycompany.myapp"
  MACOSX_BUNDLE_COPYRIGHT "(c) 2024 My Company")
This is how the command now looks.

Taking those newly defined properties, the .plist file gets another block of properties at the end that will be filled in by CMake.

<key>CFBundleName</key>
<string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
<key>CFBundleShortVersionString</key>
<string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
<key>CFBundleVersion</key>
<string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>

Here the full .plist file.

<!-- src/assets/manifests/Info.plist -->
<?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>CFBundleDevelopmentRegion</key>
    <string>en</string>
    <key>CFBundleIconFile</key>
    <string>icon</string>
    <key>CFBundleInfoDictionaryVersion</key>
    <string>6.0</string>
    <key>CFBundlePackageType</key>
    <string>APPL</string>
    <key>NSMainStoryboardFile</key>
    <string>Main</string>
    <key>NSPrincipalClass</key>
    <string>NSApplication</string>
    <key>NSHighResolutionCapable</key>
    <true/>

    <key>CFBundleExecutable</key>
    <string>$(EXECUTABLE_NAME)</string>
    <key>CFBundleIdentifier</key>
    <string>$(PRODUCT_BUNDLE_IDENTIFIER)</string>
    <key>LSMinimumSystemVersion</key>
    <string>$(MACOSX_DEPLOYMENT_TARGET)</string>

    <key>CFBundleName</key>
    <string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
    <key>CFBundleShortVersionString</key>
    <string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
    <key>CFBundleVersion</key>
    <string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
  </dict>
</plist>

DMG

Now it's up to creating a "Drag & Drop" disk image with a properly formatted window, one like you would see when, e.g., installing Firefox.

A macOS disk image as seen when installing Firefox. The Firefox logo is on the left, the Application folder icon on the right. A squiggly arrow in the middle pointing to the app folder.
A DMG window as seen when installing Firefox on macOS.

The window has a defined size, a background image that also works on retina displays, and shows the application icon ready to be dragged into the Application folder.

The DMG background is a .tiff file, or rather two — one "regular" one and one with double the resolution for high-DPI monitors — combined into one .tiff. This can be done with the preinstalled tiffutil command on macOS.

Assuming a simple DMG background AppDMGBackground.tiff and [email protected] for high-DPI.

A white image with a dashed line around the rim, in the middle a gray arrow pointing from left to right.
Example DMG background image.

Both images can be combined into one supporting .tiff with the following command on macOS:

$ tiffutil \
  -cathidpicheck AppDMGBackground.tiff \
  [email protected] \
  -out packaging/dmg/AppDMGBackground.tiff
Writing the result to the new folder packaging/dmg.

With the background image ready, it needs to be applied to the DMG window while also setting the size of that window and where icons in it should be placed. Specifically, the application icon and the Application folder icon. This will be done by creating a .DS_Store file for the application bundle via an AppleScript.

The AppleScript used is originally from the way CMake itself creates its installer DMG window, adapted to change icon positions and window size.

-- packaging/dmg/AppDMGSetup.scpt

-- This code was adapted to serve the application needs.

on run argv
  set image_name to item 1 of argv

  tell application "Finder"
    tell disk image_name

    -- Wait for the image to finish mounting.
      set open_attempts to 0
      repeat while open_attempts < 4
        try
          open
          delay 1
          set open_attempts to 5
          close
        on error errStr number errorNumber
          set open_attempts to open_attempts + 1
          delay 10
        end try
      end repeat
      delay 5

      -- Open the image and save a .DS_Store with
      -- background and icon setup.
      open
      set current view of container window to icon view
      set theViewOptions to the icon view options of container window
      set background picture of theViewOptions to file ".background:background.tiff"
      set arrangement of theViewOptions to not arranged
      set icon size of theViewOptions to 128
      delay 5
      close

      -- Setup the position of the app and Applications symlink
      -- and hide all the window decoration.
      open
      tell container window
        set sidebar width to 0
        set statusbar visible to false
        set toolbar visible to false
        -- Those bounds are defined as:
        -- x-start, y-start, x-end, y-end (aka. x, z, width, height)
        set the bounds to {400, 100, 940, 528}
        set position of item "MyApp.app" to {140, 200}
        set position of item "Applications" to {405, 200}
      end tell
      delay 5
      close

      -- Open and close for visual verification.
      open
      delay 5
      close

    end tell
    delay 1
  end tell
end run
The changed script set for a background image with the size 540 × 400 pixel.

This will create the following DMG window.

A disk image window as created by the script. The sample application logo is on the left, the Application folder icon on the right. A gray arrow in the middle pointing to the app folder.
The example disk image window that will be created.

The application icon is on the left, and the Application folder icon is on the right. The arrow in the middle and the dotted line around is from the background image .tiff.

To make this all work, it needs to be hooked up by extending the packaging/CMakeLists.txt file for CPack. Setting the background image is done via the CPACK_DMG_BACKGROUND_IMAGE variable.

set(CPACK_DMG_BACKGROUND_IMAGE
  "${CMAKE_CURRENT_LIST_DIR}/dmg/AppDMGBackground.tiff")
Uses the generated TIFF container file.

The AppleScript script, executed by CMake, needs to be set via CPACK_DMG_DS_STORE_SETUP_SCRIPT.

set(CPACK_DMG_DS_STORE_SETUP_SCRIPT
  "${CMAKE_CURRENT_LIST_DIR}/dmg/AppDMGSetup.scpt")

There are two more options: enabling the default SLA license option introduced in CMake 3.23 for a common DMG user flow and setting the DMG volume name to the project name.

set(CPACK_DMG_SLA_USE_RESOURCE_FILE_LICENSE OFF)
set(CPACK_DMG_VOLUME_NAME "${CMAKE_PROJECT_NAME}")

Finally, enable the DMG generator by setting CPACK_GENERATOR on Apple systems to "DragNDrop" while still generating a compressed TAR.

# Generator selection per platform
if (CMAKE_SYSTEM_NAME STREQUAL "Darwin")
  set(CPACK_GENERATOR TGZ DragNDrop)
else ()
  set(CPACK_GENERATOR TGZ)
endif ()

This is how the CPack configuration now looks for macOS.

# packaging/CMakeLists.txt

# Base package settings
# Nothing changed here ...

# macOS settings for DragNDrop generator
set(CPACK_DMG_BACKGROUND_IMAGE
  "${CMAKE_CURRENT_LIST_DIR}/dmg/AppDMGBackground.tiff")
set(CPACK_DMG_DS_STORE_SETUP_SCRIPT
  "${CMAKE_CURRENT_LIST_DIR}/dmg/AppDMGSetup.scpt")
set(CPACK_DMG_SLA_USE_RESOURCE_FILE_LICENSE OFF)
set(CPACK_DMG_VOLUME_NAME "${CMAKE_PROJECT_NAME}")

# Generator selection per platform
if (CMAKE_SYSTEM_NAME STREQUAL "Darwin")
  set(CPACK_GENERATOR TGZ DragNDrop)
else ()
  set(CPACK_GENERATOR TGZ)
endif ()

include(CPack)

Running CPack on a release build created with Xcode on macOS will create a .tar.gz and the desired .dmg file in build/release/distribution.

$ cpack --config build/release/CPackConfig.cmake
A macOS Finder window with the generated distributable showing the .dmg as well as the .tar.gz file.
Created .dmg file on macOS.

Linux application bundle

For common Linux derivatives, a .deb file can be created with the CPack DEB generator. All the needed structure is already set up, what is left are a few CPack settings.

DEB

In packaging/CMakeLists.txt a mandatory setting is the DEB file name that should be set via CPACK_DEBIAN_FILE_NAME, where the best setting for backwards compatibility is DEB-DEFAULT.

set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT)

The package maintainer and section name.

set(CPACK_DEBIAN_PACKAGE_SECTION Miscellaneous)
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Maintainer Name")

And what packages the bundle depends on, in this case SDL2.

set(CPACK_DEBIAN_PACKAGE_DEPENDS "libsdl2-2.0-0")

At the end, enable the DMG generator by setting CPACK_GENERATOR for Linux to "DEB" while still generating a compressed TAR.

set(CPACK_GENERATOR TGZ DEB)

This is how the CPack configuration now looks with added DEB generator support for Linux.

# packaging/CMakeLists.txt

# Base package settings
# Nothing changed here ...

# macOS settings for DragNDrop generator
# Nothing changed here ...

# Linux DEB settings
set(CPACK_DEBIAN_FILE_NAME DEB-DEFAULT)
set(CPACK_DEBIAN_PACKAGE_SECTION Miscellaneous)
set(CPACK_DEBIAN_PACKAGE_MAINTAINER "Maintainer Name")
set(CPACK_DEBIAN_PACKAGE_DEPENDS "libsdl2-2.0-0")

# Generator selection per platform
if (CMAKE_SYSTEM_NAME STREQUAL "Darwin")
  set(CPACK_GENERATOR TGZ DragNDrop)
elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux")
  set(CPACK_GENERATOR TGZ DEB)
else ()
  set(CPACK_GENERATOR TGZ)
endif ()

include(CPack)

Running CPack on a release build created on a Linux system will create a .tar.gz and .deb file in build/release/distribution.

$ cpack --config build/release/CPackConfig.cmake
An Ubuntu window showing the through CPack generated files. A .deb and a .tar.gz file.
Created .deb file on Ubuntu.

Windows application bundle

For the application bundle and installer on Windows, theNullsoft Scriptable Install System (NSIS) generator is used. It will create a graphical installer and uninstaller, contain description and license texts, and even set start menu entries.

Manifest

To fully support high-DPI displays on Windows for the generated application bundle, a manifest file is needed. The App.manifest can be placed in the manifest folder src/assets/manifests like the ones for the other systems; the format is XML.

<!-- src/assets/manifests/App.manifest -->
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<assembly
  xmlns="urn:schemas-microsoft-com:asm.v1"
  manifestVersion="1.0"
  xmlns:asmv3="urn:schemas-microsoft-com:asm.v3">
  <asmv3:application>
    <asmv3:windowsSettings>
      <dpiAware
          xmlns="http://schemas.microsoft.com/SMI/2005/WindowsSettings">
    true
    </dpiAware>
      <dpiAwareness
        xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">
      PerMonitorV2
      </dpiAwareness>
    </asmv3:windowsSettings>
  </asmv3:application>
</assembly>

This file then needs to be included in the target sources for Windows.

# src/platform/windows/CMakeLists.txt

# Static assets
target_sources(MyApp PUBLIC
  ${SHARED_STATIC_ASSETS}
  assets/icons/icon.ico
  assets/manifests/app.rc
  assets/manifests/App.manifest)

# Other settings ...

NSIS assets

To use the NSIS generator and create an installer and uninstaller that are custom to the application, some assets are required.

Besides the already-created application icon, an uninstaller icon is needed as well. As it is a .ico file it can be created the same way as the application icon, having a set of PNGs and using the ImageMagick convert command line util.

$ convert \
  uninstall_icon_16x16.png uninstall_icon_32x32.png \
  uninstall_icon_64x64.png uninstall_icon_128x128.png \
  uninstall_icon_256x256.png uninstall_icon_512x512.png \
  uninstall_icon.ico

I place it under packaging/nsis/uninstall_icon.ico*.

Further, NSIS needs a header image sized at 150 × 57 pixels, an installer welcome image sized 164 × 314 pixels, and, in the same size, an uninstaller welcome image. All of those as bitmap (.bmp) files, specifically the BMP Windows 3.x format.

Created in any format, e.g., PNG, the ImageMagick convert command line util can be used again to convert them to BMP files.

$ convert nsis_header.png BMP3:nsis_header.bmp && \
  convert nsis_install_welcome.png BMP3:nsis_install_welcome.bmp && \
  convert nsis_uninstall_welcome.png BMP3:nsis_uninstall_welcome.bmp

Together with the uninstaller icon, I place them in the packaging/nsis folder.

With all the needed resources to create a proper installer and uninstaller, the CPack configuration for NSIS on Windows can be added to the packaging/CMakeLists.txt file.

First, the package and display name.

set(CPACK_NSIS_DISPLAY_NAME ${CMAKE_PROJECT_NAME})
set(CPACK_NSIS_PACKAGE_NAME ${CPACK_PACKAGE_NAME})

Enabling a high-DPI display-aware installer.

set(CPACK_NSIS_MANIFEST_DPI_AWARE true)

Ensuring that when executing the installer again, old versions of the software will be uninstalled first.

set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL YES)

Setting the icon for the installer and uninstaller.

set(CPACK_NSIS_INSTALLED_ICON_NAME
  ${PROJECT_SOURCE_DIR}/src\\\\assets\\\\icons\\\\icon.ico)
set(CPACK_NSIS_MUI_ICON
  ${PROJECT_SOURCE_DIR}/src\\\\assets\\\\icons\\\\icon.ico)
set(CPACK_NSIS_MUI_UNIICON
  ${CMAKE_CURRENT_LIST_DIR}/nsis\\\\uninstall_icon.ico)

The header image.

set(CPACK_NSIS_MUI_HEADERIMAGE
  ${CMAKE_CURRENT_LIST_DIR}/nsis\\\\nsis_header.bmp)

And welcome images for the installer and uninstaller.

set(CPACK_NSIS_MUI_WELCOMEFINISHPAGE_BITMAP
  ${CMAKE_CURRENT_LIST_DIR}/nsis\\\\nsis_install_welcome.bmp)
set(CPACK_NSIS_MUI_UNWELCOMEFINISHPAGE_BITMAP
  ${CMAKE_CURRENT_LIST_DIR}/nsis\\\\nsis_uninstall_welcome.bmp)

Description, License, Readme

A NSIS installer will typically show some extra information about the software to be installed. A welcome text, a software description and README, and a license text — defined as .txt files. All those files can be placed inside the packaging folder.

set(CPACK_RESOURCE_FILE_WELCOME
  ${CMAKE_CURRENT_LIST_DIR}/Welcome.txt)
set(CPACK_RESOURCE_FILE_README
  ${CMAKE_CURRENT_LIST_DIR}/Readme.txt)
set(CPACK_RESOURCE_FILE_LICENSE
  ${CMAKE_CURRENT_LIST_DIR}/License.txt)
set(CPACK_PACKAGE_DESCRIPTION_FILE
  ${CMAKE_CURRENT_LIST_DIR}/Description.txt)

Start menu entries

NSIS with CPack can also help to set a start menu entry under Windows, as well as remove it on uninstall.

set(CPACK_NSIS_CREATE_ICONS_EXTRA
  "CreateShortCut '$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\${CMAKE_PROJECT_NAME}.lnk' '$INSTDIR\\\\bin\\\\MyApp.exe'")
set(CPACK_NSIS_DELETE_ICONS_EXTRA
  "Delete '$SMPROGRAMS\\\\$START_MENU\\\\${CMAKE_PROJECT_NAME}.lnk'")

NSIS generator

What is left is setting the NSIS generator to be used by CPack on Windows via the CPACK_GENERATOR variable.

set(CPACK_GENERATOR ZIP NSIS)

Here is the CPack configuration with added NSIS generator support for Windows.

# packaging/CMakeLists.txt

# Base package settings
# Nothing changed here ...

# macOS settings for DragNDrop generator
# Nothing changed here ...

# Linux DEB settings
# Nothing changed here ...

# Windows settings for NSIS generator
set(CPACK_NSIS_DISPLAY_NAME ${CMAKE_PROJECT_NAME})
set(CPACK_NSIS_PACKAGE_NAME ${CPACK_PACKAGE_NAME})
set(CPACK_NSIS_MANIFEST_DPI_AWARE true)
set(CPACK_NSIS_ENABLE_UNINSTALL_BEFORE_INSTALL YES)
set(CPACK_NSIS_INSTALLED_ICON_NAME
  ${PROJECT_SOURCE_DIR}/src\\\\assets\\\\icons\\\\icon.ico)
set(CPACK_NSIS_MUI_ICON
  ${PROJECT_SOURCE_DIR}/src\\\\assets\\\\icons\\\\icon.ico)
set(CPACK_NSIS_MUI_UNIICON
  ${CMAKE_CURRENT_LIST_DIR}/nsis\\\\uninstall_icon.ico)

# Package resources
set(CPACK_RESOURCE_FILE_WELCOME
  ${CMAKE_CURRENT_LIST_DIR}/Welcome.txt)
set(CPACK_RESOURCE_FILE_README
  ${CMAKE_CURRENT_LIST_DIR}/Readme.txt)
set(CPACK_RESOURCE_FILE_LICENSE
  ${CMAKE_CURRENT_LIST_DIR}/License.txt)
set(CPACK_PACKAGE_DESCRIPTION_FILE
  ${CMAKE_CURRENT_LIST_DIR}/Description.txt)

# Define how to install/uninstall start menu entries on Windows
set(CPACK_NSIS_CREATE_ICONS_EXTRA
  "CreateShortCut '$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\${CMAKE_PROJECT_NAME}.lnk' '$INSTDIR\\\\bin\\\\MyApp.exe'")
set(CPACK_NSIS_DELETE_ICONS_EXTRA
  "Delete '$SMPROGRAMS\\\\$START_MENU\\\\${CMAKE_PROJECT_NAME}.lnk'")

# Generator selection per platform
if (CMAKE_SYSTEM_NAME STREQUAL "Darwin")
  set(CPACK_GENERATOR TGZ DragNDrop)
elseif (CMAKE_SYSTEM_NAME STREQUAL "Linux")
  set(CPACK_GENERATOR TGZ DEB)
elseif (CMAKE_SYSTEM_NAME STREQUAL "Windows")
  set(CPACK_GENERATOR ZIP NSIS)
else ()
  set(CPACK_GENERATOR TGZ)
endif ()

include(CPack)

Running CPack on a release build created on Windows will create a .zip and .exe file in build/release/distribution.

$ cpack --config build/release/CPackConfig.cmake
Video showing the NSIS installer running.

Epilogue

It's quite the journey to create a good installer for an application for the major operating systems, but it's a necessary one to really understand how an application gets placed on the user's system. Besides, a journey that did not just create a minimal version but a user-friendly, I say even good one.

Still, I see this as a "starter" when it comes to creating a distributable package, giving the option to further fully customize every aspect of those through CPack and beyond.

To see everything come to life, there is the companion repository to this article, a small SDL2 app utilizing everything shown here. Or the bigger sibling, my C++ GUI starter template with CMake and CPack, Dear ImGui, and SDL2, showing even more ways to configure CPack and create cross-platform applications.

Until then 👋🏻

← Show all posts