CMake & CPack for cross-platform distributables
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.
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.
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)
CMakeLists.txt.
Some of those variables defined through the module are the following:
-
BINDIR— Executables, scripts, and symlinks to be run directly. Defaults tobin. -
LIBDIR— Libraries and object files. Often defaults tolib, dependents on the target platform. -
DATADIR— Read-only directory for static assets like images or fonts. Often defaults toshare, 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
-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.
.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.pngicon_32x32.pngicon_128x128.pngicon_256x256.pngicon_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.
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}")
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>
.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)
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.pngicon_32x32.pngicon_64x64.pngicon_128x128.pngicon_256x256.pngicon_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")
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")
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.
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.
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
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
This will create the following DMG window.
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")
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
.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
.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
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 👋🏻